Skip to content

Commit

Permalink
gps: Improve user autocomplete
Browse files Browse the repository at this point in the history
Match every term against the full name, and order the results
using the number of matched term.

The performance is bad compared to the previous autocomplete, but the
results are way better
  • Loading branch information
tonial committed Jan 9, 2025
1 parent 1b08111 commit 49f0de1
Show file tree
Hide file tree
Showing 2 changed files with 36 additions and 27 deletions.
45 changes: 25 additions & 20 deletions itou/www/autocomplete/views.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
from datetime import datetime
from functools import reduce
from operator import add

from django.contrib.auth.decorators import login_not_required
from django.db.models import Exists, F, OuterRef, Q, Value
from django.db.models.functions import Least, Lower, NullIf, StrIndex
from django.db.models import Exists, F, IntegerField, OuterRef, Q, Value
from django.db.models.functions import Cast, Least, Lower, NullIf, StrIndex
from django.http import JsonResponse
from unidecode import unidecode

Expand All @@ -13,7 +15,6 @@
from itou.users.enums import UserKind
from itou.users.models import User
from itou.utils.auth import check_user
from itou.utils.db import or_queries
from itou.www.gps.views import is_allowed_to_use_gps_advanced_features


Expand Down Expand Up @@ -148,30 +149,34 @@ def gps_users_autocomplete(request):
# Fallback to unaccent / icontains for now

search_terms = term.split(" ")
name_q = []
for term in search_terms:
name_q.append(Q(first_name__unaccent__istartswith=term))
name_q.append(Q(last_name__unaccent__istartswith=term))
users_qs = (
User.objects.filter(or_queries(name_q))
.filter(kind=UserKind.JOB_SEEKER)
.exclude(
Exists(
FollowUpGroup.objects.filter(
beneficiary_id=OuterRef("pk"),
memberships__member=current_user,
memberships__is_active=True,
)

users_qs = User.objects.filter(kind=UserKind.JOB_SEEKER).exclude(
Exists(
FollowUpGroup.objects.filter(
beneficiary_id=OuterRef("pk"),
memberships__member=current_user,
memberships__is_active=True,
)
)
)[:10]
)

def match_term(term):
return Cast(
Exists(
User.objects.filter(pk=OuterRef("pk")).filter(
Q(first_name__unaccent__icontains=term) | Q(last_name__unaccent__icontains=term)
),
),
IntegerField(),
)

users_qs = users_qs.annotate(rank=reduce(add, [match_term(term) for term in search_terms])).order_by("-rank")
users = [
{
"text": user.get_full_name(),
"text": f"{user.title}. {user.get_full_name()} ({user.jobseeker_profile.birthdate})",
"id": user.pk,
}
for user in users_qs
for user in users_qs[:10]
]

return JsonResponse({"results": users})
18 changes: 11 additions & 7 deletions tests/gps/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,23 +46,23 @@ def test_user_autocomplete(client):
FollowUpGroupFactory(beneficiary=third_beneficiary, memberships=3, memberships__member=prescriber)
FollowUpGroupFactory(beneficiary=second_beneficiary, memberships=2)

def get_autocomplete_results(user):
def get_autocomplete_results(user, term="gps"):
client.force_login(user)
response = client.get(reverse("autocomplete:gps_users") + "?term=gps")
return set(r["id"] for r in response.json()["results"])
response = client.get(reverse("autocomplete:gps_users") + f"?term={term}")
return [r["id"] for r in response.json()["results"]]

# Employers should get the 3 job seekers.
results = get_autocomplete_results(EmployerFactory(with_company=True))
assert results == {first_beneficiary.pk, second_beneficiary.pk, third_beneficiary.pk}
assert set(results) == {first_beneficiary.pk, second_beneficiary.pk, third_beneficiary.pk}

# Authorized prescribers should get the 3 job seekers.
org = PrescriberOrganizationWithMembershipFactory(authorized=True)
results = get_autocomplete_results(org.members.get())
assert results == {first_beneficiary.pk, second_beneficiary.pk, third_beneficiary.pk}
assert set(results) == {first_beneficiary.pk, second_beneficiary.pk, third_beneficiary.pk}

# We should not get ourself nor the first and third user user because we are a member of their group
results = get_autocomplete_results(prescriber)
assert results == {second_beneficiary.pk}
assert set(results) == {second_beneficiary.pk}

# Now, if we remove the first user from our group by setting the membership to is_active False
# The autocomplete should return it again
Expand All @@ -73,7 +73,11 @@ def get_autocomplete_results(user):
# We should not get ourself but we should get the first beneficiary (we are is_active=False)
# and the second one (we are not part of his group)
results = get_autocomplete_results(prescriber)
assert results == {first_beneficiary.pk, second_beneficiary.pk}
assert set(results) == {first_beneficiary.pk, second_beneficiary.pk}

# with "martin gps" Martin is the first result
results = get_autocomplete_results(prescriber, term="martin gps")
assert results == [second_beneficiary.pk, first_beneficiary.pk]


@pytest.mark.parametrize(
Expand Down

0 comments on commit 49f0de1

Please sign in to comment.