Skip to content

Commit

Permalink
users: Remove autocomplete relying on gps models from ItouUserManager
Browse files Browse the repository at this point in the history
  • Loading branch information
tonial committed Jan 9, 2025
1 parent a325337 commit 1b08111
Show file tree
Hide file tree
Showing 3 changed files with 48 additions and 47 deletions.
33 changes: 1 addition & 32 deletions itou/users/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
from django.core.serializers.json import DjangoJSONEncoder
from django.core.validators import MaxLengthValidator, MinLengthValidator, RegexValidator
from django.db import models
from django.db.models import Count, Exists, OuterRef, Q
from django.db.models import Count, Q
from django.db.models.functions import Upper
from django.urls import reverse
from django.utils import timezone
Expand All @@ -35,7 +35,6 @@
from itou.common_apps.address.format import compute_hexa_address
from itou.common_apps.address.models import AddressMixin
from itou.companies.enums import CompanyKind
from itou.utils.db import or_queries
from itou.utils.models import UniqueConstraintWithErrorCode
from itou.utils.templatetags.str_filters import mask_unless
from itou.utils.urls import get_absolute_url
Expand All @@ -46,36 +45,6 @@


class ItouUserManager(UserManager):
def autocomplete(self, search_string, current_user):
"""
We started by using to_vector queries but it's not suitable for searching names because
it tries to lemmatize names so for example, henry becomes henri after lemmatization and
the search doesn't work.
Then we tried TrigramSimilarity methods but it's too random for accurate searching.
Fallback to unaccent / icontains for now
"""
from itou.gps.models import FollowUpGroup

search_terms = search_string.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))
queryset = (
self.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,
)
)
)
)
return queryset[:10]

def get_duplicated_pole_emploi_ids(self):
"""
Returns an array of `pole_emploi_id` used more than once:
Expand Down
32 changes: 30 additions & 2 deletions itou/www/autocomplete/views.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,19 @@
from datetime import datetime

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

from itou.asp.models import Commune
from itou.cities.models import City
from itou.gps.models import FollowUpGroup
from itou.jobs.models import Appellation
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 @@ -138,12 +141,37 @@ def gps_users_autocomplete(request):
users = []

if term:
# We started by using to_vector queries but it's not suitable for searching names because
# it tries to lemmatize names so for example, henry becomes henri after lemmatization and
# the search doesn't work.
# Then we tried TrigramSimilarity methods but it's too random for accurate searching.
# 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,
)
)
)
)[:10]

users = [
{
"text": user.get_full_name(),
"id": user.pk,
}
for user in User.objects.autocomplete(term, current_user)
for user in users_qs
]

return JsonResponse({"results": users})
30 changes: 17 additions & 13 deletions tests/gps/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,11 @@
import freezegun
import pytest
from django.urls import reverse
from pytest_django.asserts import assertContains, assertNotContains, assertQuerySetEqual
from pytest_django.asserts import assertContains, assertNotContains

from itou.gps.models import FollowUpGroup, FollowUpGroupMembership, FranceTravailContact
from itou.users.models import User
from tests.gps.factories import FollowUpGroupFactory
from tests.prescribers.factories import PrescriberOrganizationWithMembershipFactory
from tests.prescribers.factories import PrescriberMembershipFactory, PrescriberOrganizationWithMembershipFactory
from tests.users.factories import (
EmployerFactory,
JobSeekerFactory,
Expand Down Expand Up @@ -36,8 +35,9 @@ def test_job_seeker_cannot_use_gps(client):
assert response.status_code == 403


def test_user_autocomplete():
def test_user_autocomplete(client):
prescriber = PrescriberFactory(first_name="gps member Vince")
PrescriberMembershipFactory(user=prescriber, organization__authorized=True)
first_beneficiary = JobSeekerFactory(first_name="gps beneficiary Bob", last_name="Le Brico")
second_beneficiary = JobSeekerFactory(first_name="gps second beneficiary Martin", last_name="Pêcheur")
third_beneficiary = JobSeekerFactory(first_name="gps third beneficiary Foo", last_name="Bar")
Expand All @@ -46,18 +46,23 @@ def test_user_autocomplete():
FollowUpGroupFactory(beneficiary=third_beneficiary, memberships=3, memberships__member=prescriber)
FollowUpGroupFactory(beneficiary=second_beneficiary, memberships=2)

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

# Employers should get the 3 job seekers.
users = User.objects.autocomplete("gps", EmployerFactory())
assertQuerySetEqual(users, [first_beneficiary, second_beneficiary, third_beneficiary], ordered=False)
results = get_autocomplete_results(EmployerFactory(with_company=True))
assert results == {first_beneficiary.pk, second_beneficiary.pk, third_beneficiary.pk}

# Authorized prescribers should get the 3 job seekers.
org = PrescriberOrganizationWithMembershipFactory(authorized=True)
users = User.objects.autocomplete("gps", org.members.get())
assertQuerySetEqual(users, [first_beneficiary, second_beneficiary, third_beneficiary], ordered=False)
results = get_autocomplete_results(org.members.get())
assert 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
users = User.objects.autocomplete("gps", prescriber).all()
assertQuerySetEqual(users, [second_beneficiary])
results = get_autocomplete_results(prescriber)
assert 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 @@ -67,9 +72,8 @@ def test_user_autocomplete():

# 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)
users = User.objects.autocomplete("gps", prescriber)

assertQuerySetEqual(users, [first_beneficiary, second_beneficiary], ordered=False)
results = get_autocomplete_results(prescriber)
assert results == {first_beneficiary.pk, second_beneficiary.pk}


@pytest.mark.parametrize(
Expand Down

0 comments on commit 1b08111

Please sign in to comment.