From e51b20fbe70896200ad6c27afa2967dd6ad19137 Mon Sep 17 00:00:00 2001 From: Benjamin Dauvergne Date: Sat, 14 Nov 2020 09:21:28 +0100 Subject: [PATCH 1/2] misc: specialize user search for phone numbers (#48352) --- src/authentic2/custom_user/managers.py | 41 ++++++++++++++++++++++---- tests/test_custom_user.py | 40 +++++++++++++++++++++++++ 2 files changed, 76 insertions(+), 5 deletions(-) diff --git a/src/authentic2/custom_user/managers.py b/src/authentic2/custom_user/managers.py index b1b9ffd3..ae5edd78 100644 --- a/src/authentic2/custom_user/managers.py +++ b/src/authentic2/custom_user/managers.py @@ -17,9 +17,11 @@ import datetime import logging import unicodedata +import re from django.contrib.contenttypes.models import ContentType from django.contrib.postgres.search import TrigramDistance +from django.core.exceptions import ValidationError from django.db import models, transaction, connection from django.db.models import F, Value, FloatField, Subquery, OuterRef from django.db.models.functions import Lower, Coalesce @@ -30,23 +32,52 @@ from django.contrib.auth.models import BaseUserManager from authentic2 import app_settings from authentic2.models import Attribute, AttributeValue, UserExternalId from authentic2.utils.lookups import Unaccent, ImmutableConcat +from authentic2.attribute_kinds import clean_phone_number class UserQuerySet(models.QuerySet): def free_text_search(self, search): - terms = search.split() + # clean search string + search = search.strip() + if not search: + return self + # normalize spaces + search = re.sub(r'\s+', ' ', search) + + # get searchable attributes + phone_attributes = [] + other_attributes = [] + for attribute in Attribute.objects.filter(searchable=True): + if attribute.kind == 'phone_number': + phone_attributes.append(attribute) + else: + other_attributes.append(attribute) + + # look directly for phone number if it's the sole search term + if phone_attributes and ' ' not in search: + try: + phone_number = clean_phone_number(search) + except ValidationError: + pass + else: + # look only for the phone number if it starts with any local prefix + if phone_number.startswith( + tuple(app_settings.A2_LOCAL_PHONE_PREFIXES)): + return self.filter( + attribute_values__content=phone_number, + attribute_values__attribute__in=phone_attributes) + terms = search.split() if not terms: return self - searchable_attributes = Attribute.objects.filter(searchable=True) queries = [] for term in terms: q = None specific_queries = [] - for a in searchable_attributes: + for a in other_attributes: kind = a.get_kind() free_text_search_function = kind.get('free_text_search') if free_text_search_function: @@ -66,7 +97,7 @@ class UserQuerySet(models.QuerySet): | models.query.Q(last_name__icontains=term) | models.query.Q(email__icontains=term) ) - for a in searchable_attributes: + for a in other_attributes: if a.name in ('first_name', 'last_name'): continue q = q | models.query.Q( @@ -74,7 +105,7 @@ class UserQuerySet(models.QuerySet): queries.append(q) self = self.filter(six.moves.reduce(models.query.Q.__and__, queries)) # search by attributes can match multiple times - if searchable_attributes: + if other_attributes: self = self.distinct() return self diff --git a/tests/test_custom_user.py b/tests/test_custom_user.py index 0496d637..33c92c4e 100644 --- a/tests/test_custom_user.py +++ b/tests/test_custom_user.py @@ -21,6 +21,8 @@ from django.core.exceptions import ValidationError from django_rbac.utils import get_permission_model, get_role_model +from authentic2.models import Attribute + Permission = get_permission_model() Role = get_role_model() User = get_user_model() @@ -58,3 +60,41 @@ class CustomUserTestCase(TestCase): self.assertIn(r.id, [rparent1.id, rparent2.id]) self.assertEqual(r.member, []) + + +def test_free_text_search_phone_number(db, django_assert_num_queries): + Attribute.objects.create(name='phone', label='phone', kind='phone_number', searchable=True) + Attribute.objects.create(name='mobile', label='mobile', kind='phone_number', searchable=True) + + user1 = User.objects.create( + first_name='John', + last_name='Doe', + email='john.doe@example.com') + user1.attributes.phone = '0166666666' + user1.attributes.mobile = '0666666666' + + user2 = User.objects.create( + first_name='Jane', + last_name='Doe', + email='jane.doe@example.com') + user2.attributes.phone = '0166666666' + user2.attributes.mobile = '0677777777' + + user3 = User.objects.create( + first_name='Joe', + last_name='Doe', + email='joe.doe@example.com') + user3.attributes.phone = '0166666666' + user3.attributes.mobile = '0699999999' + + with django_assert_num_queries(2): + assert set(User.objects.free_text_search('jo doe')) == set([user1, user3]) + + with django_assert_num_queries(2): + assert set(User.objects.free_text_search(' +33-6-77-77-77-77 ')) == set([user2]) + + with django_assert_num_queries(2): + assert set(User.objects.free_text_search(' 0033.6.99.99.99.99 ')) == set([user3]) + + with django_assert_num_queries(2): + assert set(User.objects.free_text_search(' 01.66.66.66.66 ')) == set([user1, user2, user3]) -- 2.29.2