From 6ef2aa2786e5b02bcd79433a6494de195348334c Mon Sep 17 00:00:00 2001 From: Ben Silverman Date: Thu, 11 Apr 2024 15:14:02 -0400 Subject: [PATCH] Allow diacritic-insensitive searching in admin list views for People/Places (#1479) --- geniza/entities/admin.py | 27 +++++++++++++++++--- geniza/entities/tests/test_entities_admin.py | 14 ++++++++++ geniza/entities/tests/test_entities_views.py | 11 ++++++++ geniza/entities/views.py | 5 +++- 4 files changed, 53 insertions(+), 4 deletions(-) diff --git a/geniza/entities/admin.py b/geniza/entities/admin.py index 3cb9d8f2e..759a116c4 100644 --- a/geniza/entities/admin.py +++ b/geniza/entities/admin.py @@ -4,6 +4,7 @@ from django.contrib import admin, messages from django.contrib.contenttypes.admin import GenericTabularInline from django.contrib.contenttypes.forms import BaseGenericInlineFormSet +from django.contrib.postgres.aggregates import ArrayAgg from django.db.models.fields import CharField, TextField from django.forms import ModelChoiceField, ValidationError from django.forms.models import ModelChoiceIterator @@ -250,7 +251,7 @@ def get_formset(self, request, obj=None, **kwargs): class PersonAdmin(TabbedTranslationAdmin, SortableAdminBase, admin.ModelAdmin): """Admin for Person entities in the PGP""" - search_fields = ("names__name",) + search_fields = ("name_unaccented", "names__name") fields = ("gender", "role", "has_page", "description") inlines = ( NameInline, @@ -285,7 +286,15 @@ def get_form(self, request, obj=None, **kwargs): def get_queryset(self, request): """For autocomplete ONLY, remove self from queryset, so that Person-Person autocomplete does not include self in the list of options""" - qs = super().get_queryset(request) + # also add unaccented name to queryset so we can search on it + qs = ( + super() + .get_queryset(request) + .annotate( + # ArrayAgg to group together related values from related model instances + name_unaccented=ArrayAgg("names__name__unaccent", distinct=True), + ) + ) # only modify if this is the person-person autocomplete request is_autocomplete = request and request.path == "/admin/autocomplete/" @@ -448,7 +457,7 @@ def get_formset(self, request, obj=None, **kwargs): class PlaceAdmin(SortableAdminBase, admin.ModelAdmin): """Admin for Place entities in the PGP""" - search_fields = ("names__name",) + search_fields = ("name_unaccented", "names__name") fields = (("latitude", "longitude"), "notes") inlines = ( NameInline, @@ -470,6 +479,18 @@ class PlaceAdmin(SortableAdminBase, admin.ModelAdmin): "i", # FootnoteInline ) + def get_queryset(self, request): + """Modify queryset to add unaccented name annotation field, so that places + can be searched from admin list view without entering diacritics""" + return ( + super() + .get_queryset(request) + .annotate( + # ArrayAgg to group together related values from related model instances + name_unaccented=ArrayAgg("names__name__unaccent", distinct=True), + ) + ) + @admin.register(PlacePlaceRelationType) class PlacePlaceRelationTypeAdmin(TabbedTranslationAdmin, admin.ModelAdmin): diff --git a/geniza/entities/tests/test_entities_admin.py b/geniza/entities/tests/test_entities_admin.py index 669ba35ff..486a18348 100644 --- a/geniza/entities/tests/test_entities_admin.py +++ b/geniza/entities/tests/test_entities_admin.py @@ -17,6 +17,7 @@ PersonPersonRelationTypeChoiceField, PersonPersonReverseInline, PersonPlaceInline, + PlaceAdmin, ) from geniza.entities.models import ( Name, @@ -290,3 +291,16 @@ def test_get_min_num(self, admin_client, document): ) content = str(response.content) assert 'name="documenteventrelation_set-MIN_NUM_FORMS" value="0"' in content + + +@pytest.mark.django_db +class TestPlaceAdmin: + def test_get_queryset(self): + # create a place + place = Place.objects.create() + Name.objects.create(name="Fusṭāṭ", content_object=place, primary=True) + place_admin = PlaceAdmin(Place, admin_site=admin.site) + + # queryset should include name_unaccented field without diacritics + qs = place_admin.get_queryset(Mock()) + assert qs.filter(name_unaccented__icontains="fustat").exists() diff --git a/geniza/entities/tests/test_entities_views.py b/geniza/entities/tests/test_entities_views.py index de6d42534..8a4d166b8 100644 --- a/geniza/entities/tests/test_entities_views.py +++ b/geniza/entities/tests/test_entities_views.py @@ -124,6 +124,12 @@ def test_get_queryset(self): assert qs.count() == 1 assert qs.first().pk == person.pk + # should allow search by name WITH diacritics + person_autocomplete_view.request.GET = {"q": "Ḥayyim"} + qs = person_autocomplete_view.get_queryset() + assert qs.count() == 1 + assert qs.first().pk == person_2.pk + class TestPlaceAutocompleteView: @pytest.mark.django_db @@ -135,6 +141,11 @@ def test_get_queryset(self): # should filter on place name, case and diacritic insensitive place_autocomplete_view.request = Mock() + place_autocomplete_view.request.GET = {"q": "Fusṭāṭ"} + qs = place_autocomplete_view.get_queryset() + assert qs.count() == 1 + assert qs.first().pk == place.pk + place_autocomplete_view.request.GET = {"q": "fustat"} qs = place_autocomplete_view.get_queryset() assert qs.count() == 1 diff --git a/geniza/entities/views.py b/geniza/entities/views.py index bdc86b340..bca9f59e3 100644 --- a/geniza/entities/views.py +++ b/geniza/entities/views.py @@ -2,6 +2,7 @@ from django.contrib import messages from django.contrib.auth.mixins import PermissionRequiredMixin from django.contrib.postgres.aggregates import ArrayAgg +from django.db.models import Q from django.forms import ValidationError from django.http import HttpResponseRedirect from django.urls import reverse @@ -85,7 +86,9 @@ def get_queryset(self): name_unaccented=ArrayAgg("names__name__unaccent", distinct=True), ).order_by("name_unaccented") if q: - qs = qs.filter(name_unaccented__icontains=q) + qs = qs.filter( + Q(name_unaccented__icontains=q) | Q(names__name__icontains=q) + ).distinct() return qs