17 |
17 |
import datetime
|
18 |
18 |
import logging
|
19 |
19 |
import unicodedata
|
|
20 |
import uuid
|
20 |
21 |
|
21 |
22 |
from django.contrib.contenttypes.models import ContentType
|
22 |
23 |
from django.contrib.postgres.search import TrigramDistance
|
|
24 |
from django.core.exceptions import ValidationError
|
23 |
25 |
from django.db import models, transaction, connection
|
24 |
26 |
from django.db.models import F, Value, FloatField, Subquery, OuterRef
|
25 |
27 |
from django.db.models.functions import Lower, Coalesce
|
26 |
|
from django.utils import six
|
27 |
|
from django.utils import timezone
|
|
28 |
from django.utils import timezone, six
|
28 |
29 |
from django.contrib.auth.models import BaseUserManager
|
29 |
30 |
|
30 |
31 |
from authentic2 import app_settings
|
31 |
32 |
from authentic2.models import Attribute, AttributeValue, UserExternalId
|
32 |
33 |
from authentic2.utils.lookups import Unaccent, ImmutableConcat
|
|
34 |
from authentic2.utils.date import parse_date
|
|
35 |
from authentic2.attribute_kinds import clean_number
|
33 |
36 |
|
34 |
37 |
|
35 |
38 |
class UserQuerySet(models.QuerySet):
|
36 |
39 |
|
37 |
40 |
def free_text_search(self, search):
|
|
41 |
search = search.strip()
|
|
42 |
|
|
43 |
if '@' in search:
|
|
44 |
return self.filter(email__icontains=search).order('email')
|
|
45 |
|
|
46 |
try:
|
|
47 |
guid = uuid.UUID(search)
|
|
48 |
except ValueError:
|
|
49 |
pass
|
|
50 |
else:
|
|
51 |
return self.filter(uuid=guid.hex)
|
|
52 |
|
|
53 |
try:
|
|
54 |
phone_number = clean_number(search)
|
|
55 |
except ValidationError:
|
|
56 |
pass
|
|
57 |
else:
|
|
58 |
attribute_values = AttributeValue.objects.filter(
|
|
59 |
content__contains=phone_number, attribute__kind='phone_number')
|
|
60 |
qs = self.filter(attribute_values__in=attribute_values).order_by('last_name', 'first_name')
|
|
61 |
if qs.exists():
|
|
62 |
return qs
|
|
63 |
|
|
64 |
try:
|
|
65 |
date = parse_date(search)
|
|
66 |
except ValueError:
|
|
67 |
pass
|
|
68 |
else:
|
|
69 |
attribute_values = AttributeValue.objects.filter(
|
|
70 |
content__contains=date.isoformat(), attribute__kind='birthdate')
|
|
71 |
qs = self.filter(attribute_values__in=attribute_values).order_by('last_name', 'first_name')
|
|
72 |
if qs.exists():
|
|
73 |
return qs
|
|
74 |
|
|
75 |
qs = self.find_duplicates(fullname=search, limit=None)
|
|
76 |
if qs.exists():
|
|
77 |
return qs
|
|
78 |
|
|
79 |
qs = self.filter(username__istartswith=search)
|
|
80 |
if qs.exists():
|
|
81 |
return qs
|
|
82 |
return self.free_text_search_complex(search)
|
|
83 |
|
|
84 |
def free_text_search_complex(self, search):
|
38 |
85 |
terms = search.split()
|
39 |
86 |
|
40 |
87 |
if not terms:
|
... | ... | |
44 |
91 |
queries = []
|
45 |
92 |
for term in terms:
|
46 |
93 |
q = None
|
47 |
|
|
48 |
94 |
specific_queries = []
|
49 |
95 |
for a in searchable_attributes:
|
50 |
96 |
kind = a.get_kind()
|
... | ... | |
78 |
124 |
self = self.distinct()
|
79 |
125 |
return self
|
80 |
126 |
|
81 |
|
def find_duplicates(self, first_name=None, last_name=None, fullname=None, birthdate=None):
|
|
127 |
|
|
128 |
|
|
129 |
def find_duplicates(self, first_name=None, last_name=None, fullname=None, birthdate=None, limit=5):
|
82 |
130 |
with connection.cursor() as cursor:
|
83 |
131 |
cursor.execute(
|
84 |
132 |
"SET pg_trgm.similarity_threshold = %f" % app_settings.A2_DUPLICATES_THRESHOLD
|
... | ... | |
96 |
144 |
qs = qs.filter(name__trigram_similar=name)
|
97 |
145 |
qs = qs.annotate(dist=TrigramDistance('name', name))
|
98 |
146 |
qs = qs.order_by('dist')
|
99 |
|
qs = qs[:5]
|
|
147 |
if limit is not None:
|
|
148 |
qs = qs[:limit]
|
100 |
149 |
|
101 |
150 |
# alter distance according to additionnal parameters
|
102 |
151 |
if birthdate:
|
103 |
|
-
|