From 89b0ac621af3ba12236097fce4384c1a31f22faf Mon Sep 17 00:00:00 2001 From: Tore Brede <Tore.Brede@uib.no> Date: Wed, 5 Jan 2022 18:39:24 +0100 Subject: [PATCH] GREG-161: Working on search --- greg/api/serializers/person.py | 10 ---- gregui/api/views/person.py | 66 ++++++++++++++++++++------- gregui/tests/api/views/test_search.py | 47 ++++++++++++++++++- gregui/tests/conftest.py | 4 +- 4 files changed, 98 insertions(+), 29 deletions(-) diff --git a/greg/api/serializers/person.py b/greg/api/serializers/person.py index b2a2c3cd..3d299897 100644 --- a/greg/api/serializers/person.py +++ b/greg/api/serializers/person.py @@ -71,13 +71,3 @@ class SpecialPersonSerializer(serializers.ModelSerializer): "verified", "roles", ] - - -class PersonSearchSerializer(serializers.ModelSerializer): - pid = CharField(source="person.id") - first = CharField(source="person.first_name") - last = CharField(source="person.last_name") - - class Meta: - model = Identity - fields = ["pid", "first", "last", "value", "type"] diff --git a/gregui/api/views/person.py b/gregui/api/views/person.py index 101d9a4d..cc281a2b 100644 --- a/gregui/api/views/person.py +++ b/gregui/api/views/person.py @@ -1,11 +1,12 @@ +from django.db.models import Q from rest_framework import mixins +from rest_framework import status from rest_framework.authentication import SessionAuthentication, BasicAuthentication from rest_framework.permissions import IsAuthenticated +from rest_framework.response import Response from rest_framework.viewsets import GenericViewSet -from greg.api.serializers.person import PersonSearchSerializer, SpecialPersonSerializer -from rest_framework.response import Response -from rest_framework import status +from greg.api.serializers.person import SpecialPersonSerializer from greg.models import Identity, Person from greg.permissions import IsSponsor from gregui import validation @@ -14,7 +15,6 @@ from gregui.models import GregUserProfile class PersonViewSet(mixins.RetrieveModelMixin, mixins.UpdateModelMixin, GenericViewSet): - """ Fetch person info for any guest as long as you are a sponsor @@ -39,34 +39,66 @@ class PersonViewSet(mixins.RetrieveModelMixin, mixins.UpdateModelMixin, GenericV return super().perform_update(serializer) -class PersonSearchViewSet(mixins.ListModelMixin, GenericViewSet): +class PersonSearchViewSet(GenericViewSet): """Search for persons using email or phone number""" authentication_classes = [SessionAuthentication, BasicAuthentication] permission_classes = [IsAuthenticated, IsSponsor] - serializer_class = PersonSearchSerializer - def get_queryset(self): + def list(self, request, *args, **kwargs): if "q" not in self.request.query_params: - return Response(status=status.HTTP_400_BAD_REQUEST) - + return Response( + status=status.HTTP_400_BAD_REQUEST, + data={ + "code": "no_search_term", + "message": "No search query parameter given", + }, + ) + + if len(self.request.query_params["q"]) > 50: + return Response(status=status.HTTP_400_BAD_REQUEST, + data={ + "code": "search_term_too_large", + "message": "Search term is too large", + }, + ) + + hits = self.get_hits() + return Response(hits) + + def get_hits(self): search = self.request.query_params["q"] - # TODO Implement search - split_search = search.split() + words_joined = '|'.join(map(str, split_search)) + search_regex = r'^(%s)' % words_joined + + hits = [] + persons = Person.objects.filter(Q(first_name__iregex=search_regex) | + Q(last_name__iregex=search_regex) | + Q(date_of_birth__iregex=search_regex))[:10] - name_matches = Person.objects.filter(first_name__icontains=search, - last_name__icontains=search) + included_persons = [] + for person in persons: + hits.append({"pid": person.id, "first": person.first_name, "last": person.last_name}) + included_persons.append(person.id) - return Identity.objects.filter( - value__icontains=search, # icontains to include wrong case emails + identities = Identity.objects.filter( + value__iregex=search_regex, type__in=[ Identity.IdentityType.PRIVATE_EMAIL, Identity.IdentityType.PRIVATE_MOBILE_NUMBER, ], )[:10] + for identity in identities: + if identity.person_id in included_persons: + continue + + hits.append({"pid": identity.person_id, "first": identity.person.first_name, "last": identity.person.last_name, "value": identity.value, "type": identity.type}) + + return hits + class GuestInfoViewSet(mixins.ListModelMixin, GenericViewSet): """ @@ -94,6 +126,6 @@ class GuestInfoViewSet(mixins.ListModelMixin, GenericViewSet): units = user.sponsor.get_allowed_units() return ( Person.objects.filter(roles__orgunit__in=list(units)) - .distinct() - .order_by("id") + .distinct() + .order_by("id") ) diff --git a/gregui/tests/api/views/test_search.py b/gregui/tests/api/views/test_search.py index 04110d9a..29157589 100644 --- a/gregui/tests/api/views/test_search.py +++ b/gregui/tests/api/views/test_search.py @@ -11,9 +11,54 @@ def test_name_search(client, log_in, user_sponsor, create_person): email="foo@bar.com", ) - url = reverse("gregui-v1:person-search", kwargs={"searchstring": "foo bar"}) + url = reverse("gregui-v1:person-search") log_in(user_sponsor) response = client.get(url) + # No q-parameter was specified, so the request should return a 400 response + assert response.status_code == status.HTTP_400_BAD_REQUEST + + +@pytest.mark.django_db +def test_date_of_birth_search(client, log_in, user_sponsor, create_person): + person = create_person( + first_name="foo", + last_name="bar", + email="foo@bar.com", + date_of_birth="2005-06-20" + ) + + url = reverse("gregui-v1:person-search") + "?q=2005-06-20" + + log_in(user_sponsor) + response = client.get(url) + + assert len(response.data) == 1 + + person_ids = list(map(lambda x: x["pid"], response.data)) + + assert person.id in person_ids + assert response.status_code == status.HTTP_200_OK + + +@pytest.mark.django_db +def test_multiple_words_search(client, log_in, user_sponsor, create_person): + person = create_person( + first_name="foo", + last_name="bar", + email="foo@bar.com", + date_of_birth="2005-06-20" + ) + + url = reverse("gregui-v1:person-search") + "?q=foo%20bar" + + log_in(user_sponsor) + response = client.get(url) + + assert len(response.data) == 1 + + person_ids = list(map(lambda x: x["pid"], response.data)) + + assert person.id in person_ids assert response.status_code == status.HTTP_200_OK diff --git a/gregui/tests/conftest.py b/gregui/tests/conftest.py index 5fd153b0..694e6769 100644 --- a/gregui/tests/conftest.py +++ b/gregui/tests/conftest.py @@ -326,7 +326,7 @@ def invitation_link_expired( @pytest.fixture -def create_person() -> Callable[[str, str, str, Optional[str], Optional[str]], Person]: +def create_person() -> Callable[[str, str, str, Optional[str], Optional[str], Optional[datetime.date]], Person]: # TODO fix the typing... def create_person( first_name: str, @@ -334,10 +334,12 @@ def create_person() -> Callable[[str, str, str, Optional[str], Optional[str]], P email: str = None, nin: str = None, feide_id: str = None, + date_of_birth: datetime.date = None ) -> Person: person = Person.objects.create( first_name=first_name, last_name=last_name, + date_of_birth=date_of_birth ) if nin: -- GitLab