diff --git a/greg/api/serializers/person.py b/greg/api/serializers/person.py index 3d2998977e81ba7daa6938be796e3bf5d585bfaa..c7d1e085d633167ef0363836d0bd55f012f60ea7 100644 --- a/greg/api/serializers/person.py +++ b/greg/api/serializers/person.py @@ -4,7 +4,7 @@ from rest_framework.fields import BooleanField, CharField, SerializerMethodField from greg.api.serializers.consent import ConsentSerializerBrief from greg.api.serializers.identity import IdentitySerializer, SpecialIdentitySerializer from greg.api.serializers.role import RoleSerializer, SpecialRoleSerializer -from greg.models import Person, Identity +from greg.models import Person class PersonSerializer(serializers.ModelSerializer): diff --git a/gregui/api/urls.py b/gregui/api/urls.py index 008cc82854478699d39b8b43a0b647b9ad9a5bdb..5fed05cb774633c8709642b9d59bcedf1f86e55c 100644 --- a/gregui/api/urls.py +++ b/gregui/api/urls.py @@ -36,7 +36,10 @@ urlpatterns += [ name="invite-resend", ), path("invite/", InvitationView.as_view(), name="invitation"), - path("person/search/", PersonSearchViewSet.as_view({"get": "list"}), name="person-search", - ), + path( + "person/search/", + PersonSearchViewSet.as_view({"get": "list"}), + name="person-search", + ), path("userinfo/", UserInfoView.as_view(), name="userinfo"), ] diff --git a/gregui/api/views/person.py b/gregui/api/views/person.py index cc281a2b332de21b2dcf62193ef1801dcbaa4fdc..622d9ff7db37c8edf71499108abb5d9238285fc5 100644 --- a/gregui/api/views/person.py +++ b/gregui/api/views/person.py @@ -40,7 +40,7 @@ class PersonViewSet(mixins.RetrieveModelMixin, mixins.UpdateModelMixin, GenericV class PersonSearchViewSet(GenericViewSet): - """Search for persons using email or phone number""" + """Search for persons using name, email, phone number and birth date""" authentication_classes = [SessionAuthentication, BasicAuthentication] permission_classes = [IsAuthenticated, IsSponsor] @@ -56,12 +56,13 @@ class PersonSearchViewSet(GenericViewSet): ) 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", - }, - ) + 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) @@ -70,32 +71,52 @@ class PersonSearchViewSet(GenericViewSet): search = self.request.query_params["q"] split_search = search.split() - words_joined = '|'.join(map(str, split_search)) - search_regex = r'^(%s)' % words_joined + words_joined = "|".join(map(str, split_search)) + # Create a regex with the terms in the search or-ed together. This will trigger a match + # if one of the fields that are being searched contains one of the terms + 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] + # First look for hits on name and birth date + persons = Person.objects.filter( + Q(first_name__iregex=search_regex) + | Q(last_name__iregex=search_regex) + | Q(date_of_birth__iregex=search_regex) + )[:10] included_persons = [] for person in persons: - hits.append({"pid": person.id, "first": person.first_name, "last": person.last_name}) + hits.append( + {"pid": person.id, "first": person.first_name, "last": person.last_name} + ) included_persons.append(person.id) + if len(hits) == 10: + # Max number of hits, no need to search more + return hits + + # Look for hits in e-mail and mobile phone identities = Identity.objects.filter( value__iregex=search_regex, type__in=[ Identity.IdentityType.PRIVATE_EMAIL, Identity.IdentityType.PRIVATE_MOBILE_NUMBER, ], - )[:10] + )[: (10 - len(hits))] 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}) + hits.append( + { + "pid": identity.person_id, + "first": identity.person.first_name, + "last": identity.person.last_name, + "value": identity.value, + "type": identity.type, + } + ) return hits @@ -126,6 +147,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 29157589a5e1a425b64a33a27f3c49b74c6a0244..03422aec7fab70dfaddd71a9da36f8a80e4a1c8a 100644 --- a/gregui/tests/api/views/test_search.py +++ b/gregui/tests/api/views/test_search.py @@ -4,8 +4,8 @@ from rest_framework.reverse import reverse @pytest.mark.django_db -def test_name_search(client, log_in, user_sponsor, create_person): - person = create_person( +def test_no_search_parameter_fails(client, log_in, user_sponsor, create_person): + create_person( first_name="foo", last_name="bar", email="foo@bar.com", @@ -26,7 +26,7 @@ def test_date_of_birth_search(client, log_in, user_sponsor, create_person): first_name="foo", last_name="bar", email="foo@bar.com", - date_of_birth="2005-06-20" + date_of_birth="2005-06-20", ) url = reverse("gregui-v1:person-search") + "?q=2005-06-20" @@ -47,18 +47,37 @@ def test_multiple_words_search(client, log_in, user_sponsor, create_person): person = create_person( first_name="foo", last_name="bar", + email="example@company.com", + ) + person2 = create_person( + first_name="test", + last_name="test2", email="foo@bar.com", - date_of_birth="2005-06-20" + date_of_birth="2006-06-20", + ) + person3 = create_person( + first_name="Bob", + last_name="Smith", + email="bob@smith.com", + date_of_birth="2005-06-20", + ) + person4 = create_person( + first_name="Frank", + last_name="Paulsen", + email="frank@paulsen.com", ) - url = reverse("gregui-v1:person-search") + "?q=foo%20bar" + url = reverse("gregui-v1:person-search") + "?q=foo%20bar%202005-06-20" log_in(user_sponsor) response = client.get(url) - assert len(response.data) == 1 + assert len(response.data) == 3 person_ids = list(map(lambda x: x["pid"], response.data)) assert person.id in person_ids + assert person2.id in person_ids + assert person3.id in person_ids + assert person4.id not in person_ids assert response.status_code == status.HTTP_200_OK diff --git a/gregui/tests/conftest.py b/gregui/tests/conftest.py index 694e676980734c557ceffe264c1db83b103bde8f..565bebeee7f10b53429e37b01f4b7bac907c068b 100644 --- a/gregui/tests/conftest.py +++ b/gregui/tests/conftest.py @@ -326,7 +326,9 @@ def invitation_link_expired( @pytest.fixture -def create_person() -> Callable[[str, str, str, Optional[str], Optional[str], Optional[datetime.date]], 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,12 +336,10 @@ def create_person() -> Callable[[str, str, str, Optional[str], Optional[str], Op email: str = None, nin: str = None, feide_id: str = None, - date_of_birth: datetime.date = 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 + first_name=first_name, last_name=last_name, date_of_birth=date_of_birth ) if nin: