From 7b84b2d480ce3745e34791d5ab8a4159b291d0ed Mon Sep 17 00:00:00 2001 From: Jonas Braathen Date: Tue, 26 May 2026 02:24:42 +0200 Subject: [PATCH] dnscms: better organizer chooser, fixes slugs for organizers, better slugs --- dnscms/dnscms/apps.py | 8 +++++++ dnscms/dnscms/settings/base.py | 2 +- dnscms/dnscms/signals.py | 15 +++++++++++++ dnscms/dnscms/utils.py | 7 ++++++ dnscms/events/models.py | 7 +++++- dnscms/events/views.py | 20 ++++++++++++++++- dnscms/tests/test_events.py | 24 ++++++++++++++++++++ dnscms/tests/test_slugs.py | 41 ++++++++++++++++++++++++++++++++++ 8 files changed, 121 insertions(+), 3 deletions(-) create mode 100644 dnscms/dnscms/apps.py create mode 100644 dnscms/dnscms/signals.py create mode 100644 dnscms/dnscms/utils.py create mode 100644 dnscms/tests/test_slugs.py diff --git a/dnscms/dnscms/apps.py b/dnscms/dnscms/apps.py new file mode 100644 index 0000000..f665e7b --- /dev/null +++ b/dnscms/dnscms/apps.py @@ -0,0 +1,8 @@ +from django.apps import AppConfig + + +class DnsCmsConfig(AppConfig): + name = "dnscms" + + def ready(self): + from dnscms import signals # noqa: F401 diff --git a/dnscms/dnscms/settings/base.py b/dnscms/dnscms/settings/base.py index e184a09..f5ce506 100644 --- a/dnscms/dnscms/settings/base.py +++ b/dnscms/dnscms/settings/base.py @@ -173,7 +173,7 @@ MEDIA_URL = "/media/" # Wagtail settings WAGTAIL_SITE_NAME = "dnscms" -WAGTAIL_ALLOW_UNICODE_SLUGS = False +WAGTAIL_ALLOW_UNICODE_SLUGS = True # Headless: the Next.js frontend uses trailing-slash-free URLs, so strip # trailing slashes from links generated by Wagtail (e.g. the GraphQL `url` field). WAGTAIL_APPEND_SLASH = False diff --git a/dnscms/dnscms/signals.py b/dnscms/dnscms/signals.py new file mode 100644 index 0000000..0911126 --- /dev/null +++ b/dnscms/dnscms/signals.py @@ -0,0 +1,15 @@ +from django.db.models.signals import pre_save +from django.dispatch import receiver +from wagtail.models import Page + +from dnscms.utils import slugify + +SLUGGED_SNIPPETS = {"events.EventOrganizer", "events.EventCategory"} + + +@receiver(pre_save) +def normalize_slug(sender, instance, **kwargs): + label = f"{sender._meta.app_label}.{sender.__name__}" + if isinstance(instance, Page) or label in SLUGGED_SNIPPETS: + if getattr(instance, "slug", None): + instance.slug = slugify(instance.slug) diff --git a/dnscms/dnscms/utils.py b/dnscms/dnscms/utils.py new file mode 100644 index 0000000..4c95cfb --- /dev/null +++ b/dnscms/dnscms/utils.py @@ -0,0 +1,7 @@ +from django.utils.text import slugify as django_slugify + +NORWEGIAN_TRANSLITERATIONS = str.maketrans({"æ": "ae", "ø": "o", "å": "a"}) + + +def slugify(value: str) -> str: + return django_slugify(value.lower().translate(NORWEGIAN_TRANSLITERATIONS)) diff --git a/dnscms/events/models.py b/dnscms/events/models.py index ce22414..2e0eedc 100644 --- a/dnscms/events/models.py +++ b/dnscms/events/models.py @@ -166,7 +166,7 @@ class EventOrganizerLink(Orderable): @register_snippet @register_query_field("eventOrganizer", "eventOrganizers") -class EventOrganizer(ClusterableModel): +class EventOrganizer(index.Indexed, ClusterableModel): objects = WPAwareManager() name = models.CharField( @@ -222,6 +222,11 @@ class EventOrganizer(ClusterableModel): GraphQLString("external_url"), ] + search_fields = [ + index.SearchField("name"), + index.AutocompleteField("name"), + ] + class Meta: verbose_name = _("event organizer") verbose_name_plural = _("event organizers") diff --git a/dnscms/events/views.py b/dnscms/events/views.py index 889cb5d..64ab765 100644 --- a/dnscms/events/views.py +++ b/dnscms/events/views.py @@ -1,6 +1,24 @@ from django.utils.translation import gettext_lazy as _ +from wagtail.admin.forms import WagtailAdminModelForm from wagtail.admin.viewsets.chooser import ChooserViewSet +from dnscms.utils import slugify +from events.models import EventOrganizer + + +class EventOrganizerCreationForm(WagtailAdminModelForm): + class Meta: + model = EventOrganizer + fields = ["name", "association", "external_url"] + + def save(self, commit=True): + instance = super().save(commit=False) + if not instance.slug: + instance.slug = slugify(instance.name) + if commit: + instance.save() + return instance + class EventOrganizerChooserViewSet(ChooserViewSet): model = "events.EventOrganizer" @@ -10,7 +28,7 @@ class EventOrganizerChooserViewSet(ChooserViewSet): choose_one_text = _("Choose an organizer") choose_another_text = _("Choose another organizer") edit_item_text = _("Edit this organizer") - form_fields = ["name", "association", "external_url"] + creation_form_class = EventOrganizerCreationForm event_organizer_chooser_viewset = EventOrganizerChooserViewSet("event_organizer_chooser") diff --git a/dnscms/tests/test_events.py b/dnscms/tests/test_events.py index c3e29c1..42e904e 100644 --- a/dnscms/tests/test_events.py +++ b/dnscms/tests/test_events.py @@ -14,6 +14,7 @@ from events.models import ( EventOrganizerLink, EventPage, ) +from events.views import EventOrganizerCreationForm from tests.conftest import ( AssociationPageFactory, CustomImageFactory, @@ -118,6 +119,29 @@ def test_eventoccurrence_clean_promotes_matching_custom_text_to_venue(event_inde assert occurrence.venue_custom == "" +def test_event_organizer_creation_form_auto_slugifies_name(db): + form = EventOrganizerCreationForm(data={"name": "Forening for ÆØÅ", "external_url": ""}) + + assert form.is_valid(), form.errors + organizer = form.save() + + assert organizer.pk is not None + assert organizer.name == "Forening for ÆØÅ" + assert organizer.slug == "forening-for-aeoa" + + +def test_event_organizer_creation_form_keeps_explicit_slug(db): + organizer = EventOrganizer(name="Forening", slug="custom-slug") + form = EventOrganizerCreationForm( + data={"name": "Forening", "external_url": ""}, instance=organizer + ) + + assert form.is_valid(), form.errors + organizer = form.save() + + assert organizer.slug == "custom-slug" + + def test_eventoccurrence_clean_keeps_custom_text_when_no_venue_matches(event_index): event = EventPageFactory(parent=event_index) occurrence = EventOccurrence( diff --git a/dnscms/tests/test_slugs.py b/dnscms/tests/test_slugs.py new file mode 100644 index 0000000..b245bc8 --- /dev/null +++ b/dnscms/tests/test_slugs.py @@ -0,0 +1,41 @@ +import pytest + +from dnscms.utils import slugify +from events.models import EventCategory, EventOrganizer +from tests.conftest import GenericPageFactory + + +def test_slugify_transliterates_norwegian_letters(): + assert slugify("Bjørn") == "bjorn" + assert slugify("Møterom") == "moterom" + assert slugify("Forening for ÆØÅ") == "forening-for-aeoa" + + +def test_slugify_is_idempotent_on_ascii(): + assert slugify("already-clean-slug") == "already-clean-slug" + + +def test_page_save_transliterates_unicode_in_slug(home_page): + page = GenericPageFactory(parent=home_page, title="Møterom", slug="møterom") + + assert page.slug == "moterom" + + +def test_page_save_leaves_clean_slug_untouched(home_page): + page = GenericPageFactory(parent=home_page, title="Om oss", slug="om-oss") + + assert page.slug == "om-oss" + + +@pytest.mark.django_db +def test_event_organizer_save_transliterates_unicode_in_slug(): + organizer = EventOrganizer.objects.create(name="Bjørn", slug="bjørn") + + assert organizer.slug == "bjorn" + + +@pytest.mark.django_db +def test_event_category_save_transliterates_unicode_in_slug(): + category = EventCategory.objects.create(name="Mørkerom", slug="mørkerom") + + assert category.slug == "morkerom"