From 36c01902a1c055fda3c65027ebce9c438267ce5c Mon Sep 17 00:00:00 2001 From: Jonas Braathen Date: Sun, 5 Jul 2026 17:53:33 +0200 Subject: [PATCH] improve event organizer search --- dnscms/events/models.py | 2 +- dnscms/events/views.py | 44 ++++++++++++++++++++++++++++++++++ dnscms/events/wagtail_hooks.py | 5 +++- dnscms/tests/test_events.py | 29 ++++++++++++++++++++++ 4 files changed, 78 insertions(+), 2 deletions(-) diff --git a/dnscms/events/models.py b/dnscms/events/models.py index 2e0eedc..dceb693 100644 --- a/dnscms/events/models.py +++ b/dnscms/events/models.py @@ -164,7 +164,7 @@ class EventOrganizerLink(Orderable): ] -@register_snippet +# registered as a snippet via EventOrganizerSnippetViewSet in wagtail_hooks.py @register_query_field("eventOrganizer", "eventOrganizers") class EventOrganizer(index.Indexed, ClusterableModel): objects = WPAwareManager() diff --git a/dnscms/events/views.py b/dnscms/events/views.py index 64ab765..e92b470 100644 --- a/dnscms/events/views.py +++ b/dnscms/events/views.py @@ -1,11 +1,53 @@ +from django import forms +from django.db.models import Q from django.utils.translation import gettext_lazy as _ from wagtail.admin.forms import WagtailAdminModelForm +from wagtail.admin.forms.choosers import BaseFilterForm +from wagtail.admin.views.generic.chooser import ChooseResultsView, ChooseView from wagtail.admin.viewsets.chooser import ChooserViewSet +from wagtail.snippets.views.snippets import SnippetViewSet from dnscms.utils import slugify from events.models import EventOrganizer +class EventOrganizerSearchForm(BaseFilterForm): + """Substring search on name/slug""" + + q = forms.CharField( + label=_("Search term"), + widget=forms.TextInput(attrs={"placeholder": _("Search")}), + required=False, + ) + + def filter(self, objects): + objects = super().filter(objects) + search_query = self.cleaned_data.get("q") + if search_query: + objects = objects.filter( + Q(name__icontains=search_query) | Q(slug__icontains=search_query) + ) + self.is_searching = True + self.search_query = search_query + return objects + + +class EventOrganizerChooseView(ChooseView): + filter_form_class = EventOrganizerSearchForm + + +class EventOrganizerChooseResultsView(ChooseResultsView): + filter_form_class = EventOrganizerSearchForm + + +class EventOrganizerSnippetViewSet(SnippetViewSet): + model = EventOrganizer + icon = "group" + # unset the search backend so the listing search filters on icontains instead + search_backend_name = None + search_fields = ["name", "slug"] + + class EventOrganizerCreationForm(WagtailAdminModelForm): class Meta: model = EventOrganizer @@ -29,6 +71,8 @@ class EventOrganizerChooserViewSet(ChooserViewSet): choose_another_text = _("Choose another organizer") edit_item_text = _("Edit this organizer") creation_form_class = EventOrganizerCreationForm + choose_view_class = EventOrganizerChooseView + choose_results_view_class = EventOrganizerChooseResultsView event_organizer_chooser_viewset = EventOrganizerChooserViewSet("event_organizer_chooser") diff --git a/dnscms/events/wagtail_hooks.py b/dnscms/events/wagtail_hooks.py index 2039bba..fc62859 100644 --- a/dnscms/events/wagtail_hooks.py +++ b/dnscms/events/wagtail_hooks.py @@ -3,10 +3,13 @@ from django.utils.translation import gettext_lazy as _ from wagtail import hooks from wagtail.admin.auth import user_passes_test from wagtail.admin.menu import MenuItem +from wagtail.snippets.models import register_snippet from .admin import event_sidebar_viewset, event_explorer_viewset from .merge_organizers import MergeOrganizersConfirmView, MergeOrganizersSelectView -from .views import event_organizer_chooser_viewset +from .views import EventOrganizerSnippetViewSet, event_organizer_chooser_viewset + +register_snippet(EventOrganizerSnippetViewSet) superuser_only = user_passes_test(lambda user: user.is_superuser) diff --git a/dnscms/tests/test_events.py b/dnscms/tests/test_events.py index 42e904e..4d8617f 100644 --- a/dnscms/tests/test_events.py +++ b/dnscms/tests/test_events.py @@ -4,6 +4,7 @@ import pytest from django.core.exceptions import ValidationError from django.db import connection from django.test.utils import CaptureQueriesContext +from django.urls import reverse from django.utils import timezone from events.admin import EventDateColumn, OrganizersColumn @@ -572,3 +573,31 @@ def test_graphql_event_index_returns_all_fields_for_comprehensive_event( assert datetime.fromisoformat(venue_occ["end"]) == venue_occ_db.end assert datetime.fromisoformat(custom_occ["start"]) == custom_occ_db.start assert datetime.fromisoformat(custom_occ["end"]) == custom_occ_db.end + + +@pytest.fixture +def searchable_organizers(db): + return [ + EventOrganizer.objects.create(name="Arrangementsutvalget", slug="arrangementsutvalget"), + EventOrganizer.objects.create(name="Cinema Neuf", slug="kino"), + ] + + +@pytest.mark.parametrize( + "url_name", + ["event_organizer_chooser:choose", "wagtailsnippets_events_eventorganizer:list"], +) +def test_organizer_search_matches_substrings_of_name_and_slug( + admin_client, searchable_organizers, url_name +): + url = reverse(url_name) + + # mid-word match inside a compound name + content = admin_client.get(url, {"q": "utvalget"}).content.decode() + assert "Arrangementsutvalget" in content + assert "Cinema Neuf" not in content + + # slug-only match + content = admin_client.get(url, {"q": "kino"}).content.decode() + assert "Cinema Neuf" in content + assert "Arrangementsutvalget" not in content