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