dnscms: improve associations app

This commit is contained in:
2026-05-19 21:49:38 +02:00
parent 4a264c589d
commit 29c61ffc76
11 changed files with 295 additions and 95 deletions
+46
View File
@@ -0,0 +1,46 @@
from django.utils.translation import gettext_lazy as _
from wagtail.admin.ui.tables import Column, DateColumn
from wagtail.admin.ui.tables.pages import PageStatusColumn, PageTitleColumn
from wagtail.admin.viewsets.pages import PageListingViewSet
from associations.models import AssociationPage
from dnscms.admin import ListingRedirectChooseParentView
class AssociationTypeColumn(Column):
def get_value(self, instance):
return instance.get_association_type_display()
class AssociationChooseParentView(ListingRedirectChooseParentView):
listing_url_name = "associations:index"
class AssociationPageListingViewSet(PageListingViewSet):
model = AssociationPage
choose_parent_view_class = AssociationChooseParentView
icon = "group"
menu_label = _("Associations")
menu_order = 2
add_to_admin_menu = True
ordering = "title"
columns = [
PageTitleColumn("title", label=_("Title"), sort_key="title", classname="title"),
AssociationTypeColumn(
"association_type",
label=_("Type"),
sort_key="association_type",
width="15%",
),
DateColumn(
"latest_revision_created_at",
label=_("Updated"),
sort_key="latest_revision_created_at",
width="10%",
),
PageStatusColumn("status", label=_("Status"), sort_key="live", width="10%"),
]
association_page_listing_viewset = AssociationPageListingViewSet("associations")
@@ -0,0 +1,26 @@
# Generated by Django 6.0.5 on 2026-05-19 19:36
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('associations', '0025_associationpage_lead'),
]
operations = [
migrations.AlterModelOptions(
name='associationindex',
options={'verbose_name': 'association index', 'verbose_name_plural': 'association indexes'},
),
migrations.AlterModelOptions(
name='associationpage',
options={'verbose_name': 'association', 'verbose_name_plural': 'associations'},
),
migrations.AlterField(
model_name='associationpage',
name='association_type',
field=models.CharField(choices=[('forening', 'Association'), ('utvalg', 'Committee')], default='forening', max_length=64),
),
]
+19 -10
View File
@@ -1,4 +1,5 @@
from django.db import models
from django.utils.translation import gettext_lazy as _
from grapple.helpers import register_singular_query_field
from grapple.models import (
GraphQLImage,
@@ -26,8 +27,8 @@ class AssociationIndex(HeadlessMixin, Page):
body = CommonStreamField
content_panels = Page.content_panels + [
FieldPanel("lead", heading="Ingress"),
FieldPanel("body", heading="Innhold"),
FieldPanel("lead", heading=_("Lead")),
FieldPanel("body", heading=_("Content")),
]
graphql_fields = [
@@ -37,6 +38,10 @@ class AssociationIndex(HeadlessMixin, Page):
search_fields = Page.search_fields
class Meta:
verbose_name = _("association index")
verbose_name_plural = _("association indexes")
class AssociationPage(HeadlessMixin, WPImportedPageMixin, Page):
subpage_types = []
@@ -44,8 +49,8 @@ class AssociationPage(HeadlessMixin, WPImportedPageMixin, Page):
show_in_menus = False
class AssociationType(models.TextChoices):
FORENING = "forening", "Forening"
UTVALG = "utvalg", "Utvalg"
FORENING = "forening", _("Association")
UTVALG = "utvalg", _("Committee")
excerpt = models.TextField(max_length=512, blank=False)
lead = RichTextField(features=["italic", "link"], blank=True)
@@ -65,14 +70,14 @@ class AssociationPage(HeadlessMixin, WPImportedPageMixin, Page):
content_panels = Page.content_panels + [
FieldPanel(
"excerpt",
heading="Utdrag",
help_text="En veldig kort oppsummering av innholdet nedenfor. Brukes i listevisninger.",
heading=_("Excerpt"),
help_text=_("A very short summary of the content below. Used in listing views."),
),
FieldPanel("lead", heading="Ingress"),
FieldPanel("body", heading="Innhold"),
FieldPanel("lead", heading=_("Lead")),
FieldPanel("body", heading=_("Content")),
FieldPanel("logo"),
FieldPanel("association_type", heading="Type"),
FieldPanel("website_url", heading="Nettside"),
FieldPanel("association_type", heading=_("Type")),
FieldPanel("website_url", heading=_("Website")),
]
graphql_fields = [
@@ -89,6 +94,10 @@ class AssociationPage(HeadlessMixin, WPImportedPageMixin, Page):
index.SearchField("body"),
]
class Meta:
verbose_name = _("association")
verbose_name_plural = _("associations")
def import_wordpress_data(self, data):
import html
+4 -3
View File
@@ -1,12 +1,13 @@
from django.utils.translation import gettext_lazy as _
from wagtail.admin.viewsets.chooser import ChooserViewSet
class AssociationChooserViewSet(ChooserViewSet):
model = "associations.AssociationPage"
icon = "group"
choose_one_text = "Choose an association"
choose_another_text = "Choose another association"
edit_item_text = "Edit this association"
choose_one_text = _("Choose an association")
choose_another_text = _("Choose another association")
edit_item_text = _("Edit this association")
# form_fields = ["name"]
+6
View File
@@ -1,8 +1,14 @@
from wagtail import hooks
from .admin import association_page_listing_viewset
from .views import association_chooser_viewset
@hooks.register("register_admin_viewset")
def register_viewset():
return association_chooser_viewset
@hooks.register("register_admin_viewset")
def register_association_page_listing_viewset():
return association_page_listing_viewset
+27
View File
@@ -0,0 +1,27 @@
from urllib.parse import urlencode
from django.urls import reverse
from wagtail.admin.views.pages.choose_parent import ChooseParentView
class ListingRedirectChooseParentView(ChooseParentView):
"""ChooseParentView that redirects new pages back to a listing viewset.
Subclasses set ``listing_url_name`` (e.g. ``"events:index"``).
"""
listing_url_name: str
def _with_next(self, response):
if response.status_code != 302:
return response
url = response["Location"]
sep = "&" if "?" in url else "?"
response["Location"] = f"{url}{sep}{urlencode({'next': reverse(self.listing_url_name)})}"
return response
def get(self, request, *args, **kwargs):
return self._with_next(super().get(request, *args, **kwargs))
def form_valid(self, form):
return self._with_next(super().form_valid(form))
-10
View File
@@ -5,7 +5,6 @@ from django.utils.html import format_html
from wagtail import hooks
from wagtail.admin.menu import MenuItem
from associations.models import AssociationIndex
from news.models import NewsIndex
@@ -14,15 +13,6 @@ def enable_additional_rich_text_features(features):
features.default_features.extend(["h5", "h6", "blockquote"])
@hooks.register("register_admin_menu_item")
def register_associations_menu_item():
page = AssociationIndex.objects.first()
associations_url = "#"
if page:
associations_url = reverse("wagtailadmin_explore", args=(quote(page.pk),))
return MenuItem("Foreninger", associations_url, icon_name="group", order=2)
@hooks.register("register_admin_menu_item")
def register_news_menu_item():
page = NewsIndex.objects.first()
+3 -20
View File
@@ -1,15 +1,12 @@
from urllib.parse import urlencode
from django.urls import reverse
from django.utils import timezone
from django.utils.translation import gettext, gettext_lazy as _
from django.utils.translation import ngettext
from wagtail.admin.ui.tables import Column, DateColumn
from wagtail.admin.ui.tables.pages import PageStatusColumn, PageTitleColumn
from wagtail.admin.views.pages.choose_parent import ChooseParentView
from wagtail.admin.views.pages.listing import IndexView
from wagtail.admin.viewsets.pages import PageListingViewSet
from dnscms.admin import ListingRedirectChooseParentView
from events.models import EventPage
@@ -44,22 +41,8 @@ class EventPageIndexView(IndexView):
)
class EventChooseParentView(ChooseParentView):
"""Redirect newly-created EventPages back to the events listing."""
def _with_next(self, response):
if response.status_code != 302:
return response
url = response["Location"]
sep = "&" if "?" in url else "?"
response["Location"] = f"{url}{sep}{urlencode({'next': reverse('events:index')})}"
return response
def get(self, request, *args, **kwargs):
return self._with_next(super().get(request, *args, **kwargs))
def form_valid(self, form):
return self._with_next(super().form_valid(form))
class EventChooseParentView(ListingRedirectChooseParentView):
listing_url_name = "events:index"
class EventPageListingViewSet(PageListingViewSet):
Binary file not shown.
+111 -52
View File
@@ -7,13 +7,89 @@ msgid ""
msgstr ""
"Project-Id-Version: dnscms\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-05-19 21:17+0200\n"
"POT-Creation-Date: 2026-05-19 21:36+0200\n"
"Language: nb\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#: associations/admin.py:40
msgid "Associations"
msgstr "Foreninger"
#: associations/admin.py:46 events/admin.py:76
msgid "Title"
msgstr "Tittel"
#: associations/admin.py:49 associations/models.py:81
msgid "Type"
msgstr "Type"
#: associations/admin.py:55 events/admin.py:81
msgid "Updated"
msgstr "Oppdatert"
#: associations/admin.py:59 events/admin.py:85
msgid "Status"
msgstr "Status"
#: associations/models.py:30 associations/models.py:78 events/models.py:327
msgid "Lead"
msgstr "Ingress"
#: associations/models.py:31 associations/models.py:79
msgid "Content"
msgstr "Innhold"
#: associations/models.py:42
msgid "association index"
msgstr "foreningsoversikt"
#: associations/models.py:43
msgid "association indexes"
msgstr "foreningsoversikter"
#: associations/models.py:52
msgid "Association"
msgstr "Forening"
#: associations/models.py:53
msgid "Committee"
msgstr "Utvalg"
#: associations/models.py:73
msgid "Excerpt"
msgstr "Utdrag"
#: associations/models.py:75
msgid "A very short summary of the content below. Used in listing views."
msgstr "En veldig kort oppsummering av innholdet nedenfor. Brukes i listevisninger."
#: associations/models.py:82 events/models.py:189
msgid "Website"
msgstr "Nettsted"
#: associations/models.py:100
msgid "association"
msgstr "forening"
#: associations/models.py:101
msgid "associations"
msgstr "foreninger"
#: associations/views.py:8
msgid "Choose an association"
msgstr "Velg en forening"
#: associations/views.py:9
msgid "Choose another association"
msgstr "Velg en annen forening"
#: associations/views.py:10
msgid "Edit this association"
msgstr "Rediger denne foreningen"
#: events/admin.py:23
msgid "%Y-%m-%d at %H:%M"
msgstr "%Y-%m-%d kl %H:%M"
@@ -29,26 +105,14 @@ msgstr[1] "%(count)d forekomster"
msgid "Events"
msgstr "Arrangementer"
#: events/admin.py:76
msgid "Title"
msgstr "Tittel"
#: events/admin.py:77
msgid "Date"
msgstr "Dato"
#: events/admin.py:78 events/models.py:333
#: events/admin.py:78 events/models.py:331
msgid "Organizers"
msgstr "Arrangører"
#: events/admin.py:81
msgid "Updated"
msgstr "Oppdatert"
#: events/admin.py:85
msgid "Status"
msgstr "Status"
#: events/models.py:73 events/models.py:156
msgid "slug"
msgstr "permalenke"
@@ -69,7 +133,7 @@ msgstr "Ingen"
msgid "Default pig for events of this kind."
msgstr "Standardgris for arrangementer av denne typen."
#: events/models.py:98 events/models.py:343
#: events/models.py:98 events/models.py:341
msgid "Pig"
msgstr "Gris"
@@ -109,10 +173,6 @@ msgstr "Intern arrangør"
msgid "External organizer"
msgstr "Ekstern arrangør"
#: events/models.py:189
msgid "Website"
msgstr "Nettsted"
#: events/models.py:190
msgid "Leave this empty if the organizer exists in the list above."
msgstr "La denne stå tom om arrangøren finnes i lista over."
@@ -157,33 +217,34 @@ msgstr ""
#: events/models.py:284
msgid "Direct link to ticket purchase, e.g. TicketCo, Billetto or Ticketmaster"
msgstr "Lenke direkte til billettkjøp, f.eks. TicketCo, Billetto eller Ticketmaster"
msgstr ""
"Lenke direkte til billettkjøp, f.eks. TicketCo, Billetto eller Ticketmaster"
#: events/models.py:289
msgid "Direct link to the event on Facebook"
msgstr "Lenke direkte til arrangementet på Facebook"
#: events/models.py:299
#: events/models.py:298
msgid "Free"
msgstr "Gratis"
#: events/models.py:299
#: events/models.py:298
msgid "Is this event free for everyone?"
msgstr "Er dette arrangementet gratis for alle?"
#: events/models.py:305
#: events/models.py:303
msgid "Regular price"
msgstr "Ordinær pris"
#: events/models.py:306
#: events/models.py:304
msgid "Price for students"
msgstr "Pris for studenter"
#: events/models.py:307
#: events/models.py:305
msgid "Price for DNS members"
msgstr "Pris for medlemmer av DNS"
#: events/models.py:314
#: events/models.py:312
msgid ""
"Write <strong>0</strong> for free. An empty field hides the price category. "
"If possible, write digits only."
@@ -191,59 +252,57 @@ msgstr ""
"Skriv <strong>0</strong> om gratis. Tomt felt skjuler priskategorien. Om "
"mulig, skriv kun tall."
#: events/models.py:323
#: events/models.py:321
msgid "Ticket purchase link"
msgstr "Billettkjøpslenke"
#: events/models.py:327
#: events/models.py:325
msgid "Subtitle"
msgstr "Undertittel"
#: events/models.py:329
msgid "Lead"
msgstr "Ingress"
#: events/models.py:336
#: events/models.py:334
msgid "Who is behind the event?"
msgstr "Hvem står bak arrangementet?"
#: events/models.py:339
#: events/models.py:337
msgid "Organizer"
msgstr "Arrangør"
#: events/models.py:346
#: events/models.py:344
msgid "Facebook link"
msgstr "Facebook-lenke"
#: events/models.py:347
#: events/models.py:345
msgid "Direct link to the event on Facebook."
msgstr "Lenke direkte til arrangementet på Facebook."
#: events/models.py:349
#: events/models.py:347
msgid "Pricing and tickets"
msgstr "Priser og billettkjøp"
#: events/models.py:351
#: events/models.py:349
msgid "Date, time and venue"
msgstr "Dato, tid og lokale"
#: events/models.py:355
#: events/models.py:353
msgid "If the event spans several days, add each day as a separate occurrence."
msgstr "Om arrangementet går over flere dager, legg inn hver dag som en egen forekomst."
msgstr ""
"Om arrangementet går over flere dager, legg inn hver dag som en egen "
"forekomst."
#: events/models.py:359
#: events/models.py:356
msgid "Occurrence"
msgstr "Forekomst"
#: events/models.py:402
#: events/models.py:399
msgid "event"
msgstr "arrangement"
#: events/models.py:403
#: events/models.py:400
msgid "events"
msgstr "arrangementer"
#: events/models.py:563
#: events/models.py:560
msgid ""
"Use this <em>if none of the venues that can be selected on the left</em> "
"fit. E.g. <em>Frederikkeplassen</em> or <em>Sirkusteltet</em>."
@@ -251,35 +310,35 @@ msgstr ""
"Bruk denne <em>om ingen av lokalene som kan velges til venstre</em> passer. "
"F.eks. <em>Frederikkeplassen</em> eller <em>Sirkusteltet</em>."
#: events/models.py:572
#: events/models.py:569
msgid "Start"
msgstr "Start"
#: events/models.py:573
#: events/models.py:570
msgid "End"
msgstr "Slutt"
#: events/models.py:578
#: events/models.py:575
msgid "Venue"
msgstr "Lokale"
#: events/models.py:579
#: events/models.py:576
msgid "Venue as free text"
msgstr "Lokale som fritekst"
#: events/models.py:596
#: events/models.py:593
msgid "You can't both pick a venue and write something in this field."
msgstr "Du kan ikke både velge et lokale og skrive noe i dette feltet."
#: events/models.py:601
#: events/models.py:598
msgid "Venue is required."
msgstr "Lokale er påkrevd."
#: events/models.py:607
#: events/models.py:604
msgid "occurrence"
msgstr "forekomst"
#: events/models.py:608
#: events/models.py:605
msgid "occurrences"
msgstr "forekomster"
+53
View File
@@ -0,0 +1,53 @@
from associations.admin import AssociationTypeColumn
from associations.models import AssociationPage
from tests.conftest import AssociationPageFactory
def test_associationpage_persists_via_factory(association_index):
page = AssociationPageFactory(
parent=association_index,
title="EDB-gjengen",
excerpt="WOW FLINKE",
association_type=AssociationPage.AssociationType.UTVALG,
)
reloaded = AssociationPage.objects.get(pk=page.pk)
assert reloaded.title == "EDB-gjengen"
assert reloaded.excerpt == "WOW FLINKE"
assert reloaded.association_type == "utvalg"
def test_association_type_column_renders_forening_display(association_index):
page = AssociationPageFactory(
parent=association_index,
association_type=AssociationPage.AssociationType.FORENING,
)
column = AssociationTypeColumn("association_type")
assert column.get_value(page) == "Forening"
def test_association_type_column_renders_utvalg_display(association_index):
page = AssociationPageFactory(
parent=association_index,
association_type=AssociationPage.AssociationType.UTVALG,
)
column = AssociationTypeColumn("association_type")
assert column.get_value(page) == "Utvalg"
def test_graphql_association_index_query(association_index, graphql_post):
response, body = graphql_post(
"""
query {
associationIndex {
title
}
}
"""
)
assert response.status_code == 200
assert "errors" not in body, body
assert body["data"]["associationIndex"]["title"] == association_index.title