diff --git a/apps/api/urls.py b/apps/api/urls.py index b74efc88..e3eb2a83 100644 --- a/apps/api/urls.py +++ b/apps/api/urls.py @@ -4,14 +4,15 @@ from django.conf.urls import url, include from django.contrib.auth.models import User from django.contrib.contenttypes.models import ContentType +from django.db.models import Q from django_filters.rest_framework import DjangoFilterBackend from rest_framework import routers, serializers -from rest_framework.filters import SearchFilter from rest_framework.viewsets import ReadOnlyModelViewSet from activity.api.urls import register_activity_urls from api.viewsets import ReadProtectedModelViewSet from member.api.urls import register_members_urls from note.api.urls import register_note_urls +from note.models import Alias from treasury.api.urls import register_treasury_urls from logs.api.urls import register_logs_urls from permission.api.urls import register_permission_urls @@ -52,9 +53,47 @@ class UserViewSet(ReadProtectedModelViewSet): """ queryset = User.objects.all() serializer_class = UserSerializer - filter_backends = [DjangoFilterBackend, SearchFilter] + filter_backends = [DjangoFilterBackend] filterset_fields = ['id', 'username', 'first_name', 'last_name', 'email', 'is_superuser', 'is_staff', 'is_active', ] - search_fields = ['$username', '$first_name', '$last_name', '$note__alias__name', '$note__alias__normalized_name', ] + + def get_queryset(self): + queryset = super().get_queryset().order_by("username") + + if "search" in self.request.GET: + pattern = self.request.GET["search"] + + # We match first a user by its username, then if an alias is matched without normalization + # And finally if the normalized pattern matches a normalized alias. + queryset = queryset.filter( + username__iregex="^" + pattern + ).union( + queryset.filter( + Q(note__alias__name__iregex="^" + pattern) + & ~Q(username__iregex="^" + pattern) + ), all=True).union( + queryset.filter( + Q(note__alias__normalized_name__iregex="^" + Alias.normalize(pattern)) + & ~Q(note__alias__name__iregex="^" + pattern) + & ~Q(username__iregex="^" + pattern) + ), + all=True).union( + queryset.filter( + Q(note__alias__normalized_name__iregex="^" + pattern.lower()) + & ~Q(note__alias__normalized_name__iregex="^" + Alias.normalize(pattern)) + & ~Q(note__alias__name__iregex="^" + pattern) + & ~Q(username__iregex="^" + pattern) + ), + all=True).union( + queryset.filter( + (Q(last_name__iregex="^" + pattern) | Q(first_name__iregex="^" + pattern)) + & ~Q(note__alias__normalized_name__iregex="^" + pattern.lower()) + & ~Q(note__alias__normalized_name__iregex="^" + Alias.normalize(pattern)) + & ~Q(note__alias__name__iregex="^" + pattern) + & ~Q(username__iregex="^" + pattern) + ), + all=True) + + return queryset # This ViewSet is the only one that is accessible from all authenticated users! diff --git a/apps/member/views.py b/apps/member/views.py index 2a16168e..c97d15a3 100644 --- a/apps/member/views.py +++ b/apps/member/views.py @@ -180,9 +180,9 @@ class UserListView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView): """ Filter the user list with the given pattern. """ - qs = super().get_queryset().distinct("pk").annotate(alias=F("note__alias__name"))\ + qs = super().get_queryset().distinct("username").annotate(alias=F("note__alias__name"))\ .annotate(normalized_alias=F("note__alias__normalized_name"))\ - .filter(profile__registration_valid=True) + .filter(profile__registration_valid=True).order_by("username") if "search" in self.request.GET: pattern = self.request.GET["search"] @@ -190,13 +190,16 @@ class UserListView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView): return qs.none() qs = qs.filter( - Q(first_name__iregex=pattern) - | Q(last_name__iregex=pattern) - | Q(profile__section__iregex=pattern) - | Q(username__iregex=pattern) - | Q(alias__iregex=pattern) - | Q(normalized_alias__iregex=Alias.normalize(pattern)) - ) + username__iregex="^" + pattern + ).union( + qs.filter( + (Q(alias__iregex="^" + pattern) + | Q(normalized_alias__iregex="^" + Alias.normalize(pattern)) + | Q(last_name__iregex="^" + pattern) + | Q(first_name__iregex="^" + pattern) + | Q(email__istartswith=pattern)) + & ~Q(username__iregex="^" + pattern) + ), all=True) else: qs = qs.none() diff --git a/apps/note/api/views.py b/apps/note/api/views.py index 0821ab48..20818e1b 100644 --- a/apps/note/api/views.py +++ b/apps/note/api/views.py @@ -40,9 +40,19 @@ class NotePolymorphicViewSet(ReadOnlyProtectedModelViewSet): alias = self.request.query_params.get("alias", ".*") queryset = queryset.filter( - Q(alias__name__regex="^" + alias) - | Q(alias__normalized_name__regex="^" + Alias.normalize(alias)) - | Q(alias__normalized_name__regex="^" + alias.lower())) + name__iregex="^" + alias + ).union( + queryset.filter( + Q(normalized_name__iregex="^" + Alias.normalize(alias)) + & ~Q(name__iregex="^" + alias) + ), + all=True).union( + queryset.filter( + Q(normalized_name__iregex="^" + alias.lower()) + & ~Q(normalized_name__iregex="^" + Alias.normalize(alias)) + & ~Q(name__iregex="^" + alias) + ), + all=True) return queryset.distinct() @@ -85,9 +95,19 @@ class AliasViewSet(ReadProtectedModelViewSet): alias = self.request.query_params.get("alias", ".*") queryset = queryset.filter( - Q(name__regex="^" + alias) - | Q(normalized_name__regex="^" + Alias.normalize(alias)) - | Q(normalized_name__regex="^" + alias.lower())) + name__iregex="^" + alias + ).union( + queryset.filter( + Q(normalized_name__iregex="^" + Alias.normalize(alias)) + & ~Q(name__iregex="^" + alias) + ), + all=True).union( + queryset.filter( + Q(normalized_name__iregex="^" + alias.lower()) + & ~Q(normalized_name__iregex="^" + Alias.normalize(alias)) + & ~Q(name__iregex="^" + alias) + ), + all=True) return queryset @@ -108,13 +128,25 @@ class ConsumerViewSet(ReadOnlyProtectedModelViewSet): queryset = super().get_queryset() alias = self.request.query_params.get("alias", ".*") + queryset = queryset.order_by('name').prefetch_related('note') + # We match first an alias if it is matched without normalization, + # then if the normalized pattern matches a normalized alias. queryset = queryset.filter( - Q(name__regex="^" + alias) - | Q(normalized_name__regex="^" + Alias.normalize(alias)) - | Q(normalized_name__regex="^" + alias.lower()))\ - .order_by('name').prefetch_related('note') + name__iregex="^" + alias + ).union( + queryset.filter( + Q(normalized_name__iregex="^" + Alias.normalize(alias)) + & ~Q(name__iregex="^" + alias) + ), + all=True).union( + queryset.filter( + Q(normalized_name__iregex="^" + alias.lower()) + & ~Q(normalized_name__iregex="^" + Alias.normalize(alias)) + & ~Q(name__iregex="^" + alias) + ), + all=True) - return queryset + return queryset.distinct() class TemplateCategoryViewSet(ReadProtectedModelViewSet): diff --git a/note_kfet/static/js/base.js b/note_kfet/static/js/base.js index c12e3f0a..97ef7005 100644 --- a/note_kfet/static/js/base.js +++ b/note_kfet/static/js/base.js @@ -79,7 +79,7 @@ function refreshBalance () { * @param fun For each found note with the matched alias `alias`, fun(note, alias) is called. */ function getMatchedNotes (pattern, fun) { - $.getJSON("/api/note/alias/?format=json&alias=" + pattern + "&search=user|club&ordering=normalized_name", fun); + $.getJSON("/api/note/alias/?format=json&alias=" + pattern + "&search=user|club", fun); } /** @@ -261,9 +261,7 @@ function autoCompleteNote (field_id, note_list_id, notes, notes_display, alias_p return; } - $.getJSON("/api/note/consumer/?format=json&alias=" - + pattern - + "&search=user|club&ordering=normalized_name", + $.getJSON("/api/note/consumer/?format=json&alias=" + pattern + "&search=user|club", function (consumers) { // The response arrived too late, we stop the request if (pattern !== $("#" + field_id).val())