diff --git a/dnscms/associations/admin.py b/dnscms/associations/admin.py
new file mode 100644
index 0000000..82add05
--- /dev/null
+++ b/dnscms/associations/admin.py
@@ -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")
diff --git a/dnscms/associations/migrations/0026_alter_association_options.py b/dnscms/associations/migrations/0026_alter_association_options.py
new file mode 100644
index 0000000..e28b607
--- /dev/null
+++ b/dnscms/associations/migrations/0026_alter_association_options.py
@@ -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),
+ ),
+ ]
diff --git a/dnscms/associations/models.py b/dnscms/associations/models.py
index 52b08db..5d526dc 100644
--- a/dnscms/associations/models.py
+++ b/dnscms/associations/models.py
@@ -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
diff --git a/dnscms/associations/views.py b/dnscms/associations/views.py
index 85e17bf..c5e02e9 100644
--- a/dnscms/associations/views.py
+++ b/dnscms/associations/views.py
@@ -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"]
diff --git a/dnscms/associations/wagtail_hooks.py b/dnscms/associations/wagtail_hooks.py
index 1f43470..ce9cfab 100644
--- a/dnscms/associations/wagtail_hooks.py
+++ b/dnscms/associations/wagtail_hooks.py
@@ -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
diff --git a/dnscms/dnscms/admin.py b/dnscms/dnscms/admin.py
new file mode 100644
index 0000000..99f6048
--- /dev/null
+++ b/dnscms/dnscms/admin.py
@@ -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))
diff --git a/dnscms/dnscms/wagtail_hooks.py b/dnscms/dnscms/wagtail_hooks.py
index d48957e..7ab829d 100644
--- a/dnscms/dnscms/wagtail_hooks.py
+++ b/dnscms/dnscms/wagtail_hooks.py
@@ -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()
diff --git a/dnscms/events/admin.py b/dnscms/events/admin.py
index 18e72df..2bb1e15 100644
--- a/dnscms/events/admin.py
+++ b/dnscms/events/admin.py
@@ -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):
diff --git a/dnscms/locale/nb/LC_MESSAGES/django.mo b/dnscms/locale/nb/LC_MESSAGES/django.mo
index 4aad5b3..b2952ed 100644
Binary files a/dnscms/locale/nb/LC_MESSAGES/django.mo and b/dnscms/locale/nb/LC_MESSAGES/django.mo differ
diff --git a/dnscms/locale/nb/LC_MESSAGES/django.po b/dnscms/locale/nb/LC_MESSAGES/django.po
index 1052573..ad3c85e 100644
--- a/dnscms/locale/nb/LC_MESSAGES/django.po
+++ b/dnscms/locale/nb/LC_MESSAGES/django.po
@@ -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 0 for free. An empty field hides the price category. "
"If possible, write digits only."
@@ -191,59 +252,57 @@ msgstr ""
"Skriv 0 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 if none of the venues that can be selected on the left "
"fit. E.g. Frederikkeplassen or Sirkusteltet."
@@ -251,35 +310,35 @@ msgstr ""
"Bruk denne om ingen av lokalene som kan velges til venstre passer. "
"F.eks. Frederikkeplassen eller Sirkusteltet."
-#: 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"
diff --git a/dnscms/tests/test_associations.py b/dnscms/tests/test_associations.py
new file mode 100644
index 0000000..e48e633
--- /dev/null
+++ b/dnscms/tests/test_associations.py
@@ -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