Compare commits
10 Commits
09d1078dce
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
5d5f0879c2
|
|||
|
046099b7f1
|
|||
|
d245f2f00a
|
|||
|
8ad7df30d7
|
|||
|
dcb1a59777
|
|||
|
e3a58556f7
|
|||
|
7b84b2d480
|
|||
|
ec94d82863
|
|||
|
089970a5cd
|
|||
|
38229c97f0
|
@@ -1,7 +1,7 @@
|
||||
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 wagtail.admin.viewsets.pages import PageListingViewSet, PageViewSet
|
||||
|
||||
from associations.models import AssociationPage
|
||||
from dnscms.admin import ListingRedirectChooseParentView
|
||||
@@ -16,15 +16,11 @@ 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"
|
||||
class AssociationListingMixin:
|
||||
"""Shared model + columns for the standalone listing and the page explorer."""
|
||||
|
||||
model = AssociationPage
|
||||
icon = "group"
|
||||
columns = [
|
||||
PageTitleColumn("title", label=_("Title"), sort_key="title", classname="title"),
|
||||
AssociationTypeColumn(
|
||||
@@ -43,4 +39,19 @@ class AssociationPageListingViewSet(PageListingViewSet):
|
||||
]
|
||||
|
||||
|
||||
association_page_listing_viewset = AssociationPageListingViewSet("associations")
|
||||
class AssociationSidebarViewSet(AssociationListingMixin, PageListingViewSet):
|
||||
"""Standalone 'Associations' sidebar entry, reached independently of the page tree."""
|
||||
|
||||
choose_parent_view_class = AssociationChooseParentView
|
||||
menu_label = _("Associations")
|
||||
menu_order = 2
|
||||
add_to_admin_menu = True
|
||||
ordering = "title"
|
||||
|
||||
|
||||
class AssociationExplorerViewSet(AssociationListingMixin, PageViewSet):
|
||||
"""Applies the same columns when navigating into AssociationIndex via the page explorer."""
|
||||
|
||||
|
||||
association_sidebar_viewset = AssociationSidebarViewSet("associations")
|
||||
association_explorer_viewset = AssociationExplorerViewSet()
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from wagtail import hooks
|
||||
|
||||
from .admin import association_page_listing_viewset
|
||||
from .admin import association_sidebar_viewset, association_explorer_viewset
|
||||
from .views import association_chooser_viewset
|
||||
|
||||
|
||||
@@ -10,5 +10,10 @@ def register_viewset():
|
||||
|
||||
|
||||
@hooks.register("register_admin_viewset")
|
||||
def register_association_page_listing_viewset():
|
||||
return association_page_listing_viewset
|
||||
def register_association_sidebar_viewset():
|
||||
return association_sidebar_viewset
|
||||
|
||||
|
||||
@hooks.register("register_admin_viewset")
|
||||
def register_association_explorer_viewset():
|
||||
return association_explorer_viewset
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class DnsCmsConfig(AppConfig):
|
||||
name = "dnscms"
|
||||
|
||||
def ready(self):
|
||||
from dnscms import signals # noqa: F401
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
@@ -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))
|
||||
@@ -1,10 +1,7 @@
|
||||
from django.apps import apps as django_apps
|
||||
from django.templatetags.static import static
|
||||
from django.utils.html import format_html
|
||||
from grapple.registry import registry as grapple_registry
|
||||
from wagtail import hooks
|
||||
from wagtail.documents import get_document_model
|
||||
from wagtail.images import get_image_model
|
||||
from wagtail.models import Page
|
||||
from wagtail.search.backends import get_search_backend
|
||||
|
||||
@@ -15,33 +12,37 @@ def enable_additional_rich_text_features(features):
|
||||
|
||||
|
||||
@hooks.register("register_schema_query")
|
||||
def filter_search_to_live_pages(query_mixins):
|
||||
def override_search_resolver(query_mixins):
|
||||
"""
|
||||
Grapple's default `search` resolver hits every page regardless of publish
|
||||
state, exposing drafts on the public API. Prepend a mixin so MRO picks our
|
||||
`resolve_search`, which restricts Page subclasses to live + public.
|
||||
Override Grapple's `search` resolver. Two fixes vs. the upstream version:
|
||||
1. Restrict pages to live + public so drafts and access-restricted pages
|
||||
don't leak via the public API.
|
||||
2. Run a single search across all `Page` subclasses (instead of iterating
|
||||
per-model) so results are ranked by relevance across types rather than
|
||||
grouped by content type. Specific instances are fetched in a second
|
||||
bulk query and reordered to match the search ranking.
|
||||
|
||||
Documents and images are intentionally not searched. The upstream resolver
|
||||
includes them, but the frontend search page only renders Page types and
|
||||
discards everything else, so iterating those indexes is wasted work.
|
||||
"""
|
||||
if not grapple_registry.class_models:
|
||||
return
|
||||
|
||||
class SearchLivePublicMixin:
|
||||
class SearchOverrideMixin:
|
||||
def resolve_search(self, info, **kwargs):
|
||||
query = kwargs.get("query")
|
||||
if not query:
|
||||
return None
|
||||
s = get_search_backend()
|
||||
results = []
|
||||
models = [get_document_model(), get_image_model()]
|
||||
for app in grapple_registry.apps:
|
||||
models += django_apps.all_models[app].values()
|
||||
for model in models:
|
||||
if issubclass(model, Page):
|
||||
results += s.search(query, model.objects.live().public())
|
||||
else:
|
||||
results += s.search(query, model)
|
||||
return results
|
||||
ranked = list(s.search(query, Page.objects.live().public()))
|
||||
if not ranked:
|
||||
return []
|
||||
ids = [p.id for p in ranked]
|
||||
specific_map = {p.id: p for p in Page.objects.filter(id__in=ids).specific()}
|
||||
return [specific_map[i] for i in ids if i in specific_map]
|
||||
|
||||
query_mixins.insert(0, SearchLivePublicMixin)
|
||||
query_mixins.insert(0, SearchOverrideMixin)
|
||||
|
||||
|
||||
@hooks.register("construct_page_action_menu")
|
||||
|
||||
+36
-13
@@ -3,8 +3,8 @@ 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.listing import IndexView
|
||||
from wagtail.admin.viewsets.pages import PageListingViewSet
|
||||
from wagtail.admin.views.pages.listing import ExplorableIndexView, IndexView
|
||||
from wagtail.admin.viewsets.pages import PageListingViewSet, PageViewSet
|
||||
|
||||
from dnscms.admin import ListingRedirectChooseParentView
|
||||
from events.models import EventPage
|
||||
@@ -32,7 +32,9 @@ class OrganizersColumn(Column):
|
||||
return f"{names[0]} (+{len(names) - 1})"
|
||||
|
||||
|
||||
class EventPageIndexView(IndexView):
|
||||
class EventPagePrefetchMixin:
|
||||
"""Prefetch the relations the event columns read, so the listing avoids N+1."""
|
||||
|
||||
def annotate_queryset(self, pages):
|
||||
pages = super().annotate_queryset(pages)
|
||||
return pages.prefetch_related(
|
||||
@@ -41,20 +43,23 @@ class EventPageIndexView(IndexView):
|
||||
)
|
||||
|
||||
|
||||
class EventPageIndexView(EventPagePrefetchMixin, IndexView):
|
||||
pass
|
||||
|
||||
|
||||
class EventPageExplorableIndexView(EventPagePrefetchMixin, ExplorableIndexView):
|
||||
pass
|
||||
|
||||
|
||||
class EventChooseParentView(ListingRedirectChooseParentView):
|
||||
listing_url_name = "events:index"
|
||||
|
||||
|
||||
class EventPageListingViewSet(PageListingViewSet):
|
||||
model = EventPage
|
||||
index_view_class = EventPageIndexView
|
||||
choose_parent_view_class = EventChooseParentView
|
||||
icon = "date"
|
||||
menu_label = _("Events")
|
||||
menu_order = 1
|
||||
add_to_admin_menu = True
|
||||
ordering = "-latest_revision_created_at"
|
||||
class EventListingMixin:
|
||||
"""Shared model + columns for the standalone listing and the page explorer."""
|
||||
|
||||
model = EventPage
|
||||
icon = "date"
|
||||
columns = [
|
||||
PageTitleColumn("title", label=_("Title"), sort_key="title", classname="title"),
|
||||
EventDateColumn("event_date", label=_("Date"), width="13%"),
|
||||
@@ -69,4 +74,22 @@ class EventPageListingViewSet(PageListingViewSet):
|
||||
]
|
||||
|
||||
|
||||
event_page_listing_viewset = EventPageListingViewSet("events")
|
||||
class EventSidebarViewSet(EventListingMixin, PageListingViewSet):
|
||||
"""Standalone 'Events' sidebar entry, reached independently of the page tree."""
|
||||
|
||||
index_view_class = EventPageIndexView
|
||||
choose_parent_view_class = EventChooseParentView
|
||||
menu_label = _("Events")
|
||||
menu_order = 1
|
||||
add_to_admin_menu = True
|
||||
ordering = "-latest_revision_created_at"
|
||||
|
||||
|
||||
class EventExplorerViewSet(EventListingMixin, PageViewSet):
|
||||
"""Applies the same columns when navigating into EventIndex via the page explorer."""
|
||||
|
||||
index_view_class = EventPageExplorableIndexView
|
||||
|
||||
|
||||
event_sidebar_viewset = EventSidebarViewSet("events")
|
||||
event_explorer_viewset = EventExplorerViewSet()
|
||||
|
||||
+16
-2
@@ -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")
|
||||
@@ -478,7 +483,7 @@ class EventOccurrence(Orderable):
|
||||
),
|
||||
FieldRowPanel(
|
||||
children=[
|
||||
FieldPanel("venue", heading=_("Venue")),
|
||||
FieldPanel("venue", heading=_("Venue"), widget=forms.Select),
|
||||
FieldPanel("venue_custom", heading=_("Venue as free text")),
|
||||
],
|
||||
),
|
||||
@@ -492,6 +497,15 @@ class EventOccurrence(Orderable):
|
||||
]
|
||||
|
||||
def clean(self):
|
||||
if self.venue_custom:
|
||||
trimmed = self.venue_custom.strip()
|
||||
self.venue_custom = trimmed
|
||||
if trimmed:
|
||||
match = VenuePage.objects.filter(title=trimmed).first()
|
||||
if match:
|
||||
self.venue = match
|
||||
self.venue_custom = ""
|
||||
|
||||
if self.venue and self.venue_custom:
|
||||
raise ValidationError(
|
||||
{
|
||||
|
||||
+19
-1
@@ -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")
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from wagtail import hooks
|
||||
|
||||
from .admin import event_page_listing_viewset
|
||||
from .admin import event_sidebar_viewset, event_explorer_viewset
|
||||
from .views import event_organizer_chooser_viewset
|
||||
|
||||
|
||||
@@ -10,5 +10,10 @@ def register_viewset():
|
||||
|
||||
|
||||
@hooks.register("register_admin_viewset")
|
||||
def register_event_page_listing_viewset():
|
||||
return event_page_listing_viewset
|
||||
def register_event_sidebar_viewset():
|
||||
return event_sidebar_viewset
|
||||
|
||||
|
||||
@hooks.register("register_admin_viewset")
|
||||
def register_event_explorer_viewset():
|
||||
return event_explorer_viewset
|
||||
|
||||
Binary file not shown.
@@ -7,187 +7,143 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: dnscms\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2026-05-19 21:55+0200\n"
|
||||
"POT-Creation-Date: 2026-05-26 01:39+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:23
|
||||
msgid "Associations"
|
||||
msgstr "Foreninger"
|
||||
|
||||
#: associations/admin.py:29 events/admin.py:59 news/admin.py:24
|
||||
msgid "Title"
|
||||
msgstr "Tittel"
|
||||
|
||||
#: associations/admin.py:32 associations/models.py:79
|
||||
msgid "Type"
|
||||
msgstr "Type"
|
||||
|
||||
#: associations/admin.py:38 events/admin.py:64 news/admin.py:27
|
||||
msgid "Updated"
|
||||
msgstr "Oppdatert"
|
||||
|
||||
#: associations/admin.py:42 events/admin.py:68 news/admin.py:31
|
||||
msgid "Status"
|
||||
msgstr "Status"
|
||||
|
||||
#: associations/models.py:30 associations/models.py:76 events/models.py:327
|
||||
#: news/models.py:23 news/models.py:69
|
||||
msgid "Associations"
|
||||
msgstr "Foreninger"
|
||||
|
||||
msgid "Lead"
|
||||
msgstr "Ingress"
|
||||
|
||||
#: associations/models.py:31 associations/models.py:77
|
||||
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 news/models.py:60
|
||||
msgid "Excerpt"
|
||||
msgstr "Utdrag"
|
||||
|
||||
#: associations/models.py:74
|
||||
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:80 events/models.py:189
|
||||
msgid "Website"
|
||||
msgstr "Nettsted"
|
||||
|
||||
#: associations/models.py:98
|
||||
msgid "association"
|
||||
msgstr "forening"
|
||||
|
||||
#: associations/models.py:99
|
||||
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:20
|
||||
msgid "%Y-%m-%d at %H:%M"
|
||||
msgstr "%Y-%m-%d kl %H:%M"
|
||||
|
||||
#: events/admin.py:22
|
||||
#, python-format
|
||||
msgid "%(count)d occurrence"
|
||||
msgid_plural "%(count)d occurrences"
|
||||
msgstr[0] "%(count)d forekomst"
|
||||
msgstr[1] "%(count)d forekomster"
|
||||
|
||||
#: events/admin.py:53
|
||||
msgid "Events"
|
||||
msgstr "Arrangementer"
|
||||
|
||||
#: events/admin.py:60
|
||||
msgid "Date"
|
||||
msgstr "Dato"
|
||||
|
||||
#: events/admin.py:61 events/models.py:331
|
||||
msgid "Organizers"
|
||||
msgstr "Arrangører"
|
||||
|
||||
#: events/models.py:73 events/models.py:156
|
||||
msgid "Events"
|
||||
msgstr "Arrangementer"
|
||||
|
||||
msgid "slug"
|
||||
msgstr "permalenke"
|
||||
|
||||
#: events/models.py:75
|
||||
msgid "The name of the category as it will appear in URLs."
|
||||
msgstr "Navnet på kategorien slik det vil vises i URL-er."
|
||||
|
||||
#: events/models.py:79
|
||||
msgid "Should this category be available as a filter in the event programme?"
|
||||
msgstr "Skal denne kategorien være mulig å filtrere på i programmet?"
|
||||
|
||||
#: events/models.py:83 events/models.py:266
|
||||
msgid "None"
|
||||
msgstr "Ingen"
|
||||
|
||||
#: events/models.py:91
|
||||
msgid "Default pig for events of this kind."
|
||||
msgstr "Standardgris for arrangementer av denne typen."
|
||||
|
||||
#: events/models.py:98 events/models.py:341
|
||||
msgid "Pig"
|
||||
msgstr "Gris"
|
||||
|
||||
#: events/models.py:109
|
||||
msgid "event category"
|
||||
msgstr "arrangementskategori"
|
||||
|
||||
#: events/models.py:110
|
||||
msgid "event categories"
|
||||
msgstr "arrangementskategorier"
|
||||
|
||||
#: events/models.py:138
|
||||
msgid "organizer"
|
||||
msgstr "arrangør"
|
||||
|
||||
#: events/models.py:139
|
||||
msgid "organizers"
|
||||
msgstr "arrangører"
|
||||
|
||||
#: events/models.py:158
|
||||
msgid "The name of the organizer as it will appear in URLs."
|
||||
msgstr "Navnet på arrangøren slik det vil vises i URL-er."
|
||||
|
||||
#: events/models.py:167
|
||||
msgid "If a DNS association or committee is behind it, choose it here."
|
||||
msgstr "Om en samfundsforening eller -utvalg står bak, velg det her."
|
||||
|
||||
#: events/models.py:173
|
||||
msgid "Link to the external organizer's website"
|
||||
msgstr "Lenke til nettstedet til ekstern arrangør"
|
||||
|
||||
#: events/models.py:182
|
||||
msgid "Internal organizer"
|
||||
msgstr "Intern arrangør"
|
||||
|
||||
#: events/models.py:185
|
||||
msgid "External organizer"
|
||||
msgstr "Ekstern arrangør"
|
||||
|
||||
#: 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."
|
||||
|
||||
#: events/models.py:204
|
||||
msgid "event organizer"
|
||||
msgstr "arrangør"
|
||||
|
||||
#: events/models.py:205
|
||||
msgid "event organizers"
|
||||
msgstr "arrangører"
|
||||
|
||||
#: events/models.py:239
|
||||
msgid ""
|
||||
"Choose an image for use in the programme and other surfaces. Should be a "
|
||||
"photo or an illustration without too much text – don't reuse a Facebook "
|
||||
@@ -197,7 +153,6 @@ msgstr ""
|
||||
"bilde eller en illustrasjon uten for mye tekst – ikke gjenbruk et Facebook-"
|
||||
"cover ukritisk!"
|
||||
|
||||
#: events/models.py:249
|
||||
msgid ""
|
||||
"A short text that appears right below the title. Feel free to leave it empty "
|
||||
"if you fit most of it in the main title."
|
||||
@@ -205,11 +160,9 @@ msgstr ""
|
||||
"En kort tekst som kommer rett under tittelen. La denne gjerne stå tom om du "
|
||||
"fikk plass til det meste i hovedtittelen."
|
||||
|
||||
#: events/models.py:267
|
||||
msgid "Automatic"
|
||||
msgstr "Automatisk"
|
||||
|
||||
#: events/models.py:276
|
||||
msgid ""
|
||||
"The pig that hangs out on the event page. Automatic causes one to be chosen "
|
||||
"based on the event's category."
|
||||
@@ -217,36 +170,28 @@ msgstr ""
|
||||
"Grisen som henger på arrangementssiden. Automatisk fører til at en velges "
|
||||
"basert på arrangementets kategori."
|
||||
|
||||
#: 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"
|
||||
|
||||
#: events/models.py:289
|
||||
msgid "Direct link to the event on Facebook"
|
||||
msgstr "Lenke direkte til arrangementet på Facebook"
|
||||
|
||||
#: events/models.py:298
|
||||
msgid "Free"
|
||||
msgstr "Gratis"
|
||||
|
||||
#: events/models.py:298
|
||||
msgid "Is this event free for everyone?"
|
||||
msgstr "Er dette arrangementet gratis for alle?"
|
||||
|
||||
#: events/models.py:303
|
||||
msgid "Regular price"
|
||||
msgstr "Ordinær pris"
|
||||
|
||||
#: events/models.py:304
|
||||
msgid "Price for students"
|
||||
msgstr "Pris for studenter"
|
||||
|
||||
#: events/models.py:305
|
||||
msgid "Price for DNS members"
|
||||
msgstr "Pris for medlemmer av DNS"
|
||||
|
||||
#: events/models.py:312
|
||||
msgid ""
|
||||
"Write <strong>0</strong> for free. An empty field hides the price category. "
|
||||
"If possible, write digits only."
|
||||
@@ -254,57 +199,44 @@ msgstr ""
|
||||
"Skriv <strong>0</strong> om gratis. Tomt felt skjuler priskategorien. Om "
|
||||
"mulig, skriv kun tall."
|
||||
|
||||
#: events/models.py:321
|
||||
msgid "Ticket purchase link"
|
||||
msgstr "Billettkjøpslenke"
|
||||
|
||||
#: events/models.py:325
|
||||
msgid "Subtitle"
|
||||
msgstr "Undertittel"
|
||||
|
||||
#: events/models.py:334
|
||||
msgid "Who is behind the event?"
|
||||
msgstr "Hvem står bak arrangementet?"
|
||||
|
||||
#: events/models.py:337
|
||||
msgid "Organizer"
|
||||
msgstr "Arrangør"
|
||||
|
||||
#: events/models.py:344
|
||||
msgid "Facebook link"
|
||||
msgstr "Facebook-lenke"
|
||||
|
||||
#: events/models.py:345
|
||||
msgid "Direct link to the event on Facebook."
|
||||
msgstr "Lenke direkte til arrangementet på Facebook."
|
||||
|
||||
#: events/models.py:347
|
||||
msgid "Pricing and tickets"
|
||||
msgstr "Priser og billettkjøp"
|
||||
|
||||
#: events/models.py:349
|
||||
msgid "Date, time and venue"
|
||||
msgstr "Dato, tid og lokale"
|
||||
|
||||
#: 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."
|
||||
|
||||
#: events/models.py:356
|
||||
msgid "Occurrence"
|
||||
msgstr "Forekomst"
|
||||
|
||||
#: events/models.py:399
|
||||
msgid "event"
|
||||
msgstr "arrangement"
|
||||
|
||||
#: events/models.py:400
|
||||
msgid "events"
|
||||
msgstr "arrangementer"
|
||||
|
||||
#: 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>."
|
||||
@@ -312,75 +244,60 @@ 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:569
|
||||
msgid "Start"
|
||||
msgstr "Start"
|
||||
|
||||
#: events/models.py:570
|
||||
msgid "End"
|
||||
msgstr "Slutt"
|
||||
|
||||
#: events/models.py:575
|
||||
msgid "Venue"
|
||||
msgstr "Lokale"
|
||||
|
||||
#: events/models.py:576
|
||||
msgid "Venue as free text"
|
||||
msgstr "Lokale som fritekst"
|
||||
|
||||
#: 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:598
|
||||
msgid "Venue is required."
|
||||
msgstr "Lokale er påkrevd."
|
||||
|
||||
#: events/models.py:604
|
||||
msgid "occurrence"
|
||||
msgstr "forekomst"
|
||||
|
||||
#: events/models.py:605
|
||||
msgid "occurrences"
|
||||
msgstr "forekomster"
|
||||
|
||||
#: events/views.py:9
|
||||
msgid "Choose organizers"
|
||||
msgstr "Velg arrangører"
|
||||
|
||||
#: events/views.py:10
|
||||
msgid "Choose an organizer"
|
||||
msgstr "Velg en arrangør"
|
||||
|
||||
#: events/views.py:11
|
||||
msgid "Choose another organizer"
|
||||
msgstr "Velg en annen arrangør"
|
||||
|
||||
#: events/views.py:12
|
||||
msgid "Edit this organizer"
|
||||
msgstr "Rediger denne arrangøren"
|
||||
|
||||
#: images/models.py:40
|
||||
msgid "image"
|
||||
msgstr "bilde"
|
||||
|
||||
#: images/models.py:41
|
||||
msgid "images"
|
||||
msgstr "bilder"
|
||||
|
||||
#: news/admin.py:18
|
||||
msgid "First published"
|
||||
msgstr "Først publisert"
|
||||
|
||||
msgid "News"
|
||||
msgstr "Nyheter"
|
||||
|
||||
#: news/models.py:33
|
||||
msgid "news index"
|
||||
msgstr "nyhetsoversikt"
|
||||
|
||||
#: news/models.py:34
|
||||
msgid "news indexes"
|
||||
msgstr "nyhetsoversikter"
|
||||
|
||||
#: news/models.py:52
|
||||
msgid ""
|
||||
"Choose an image for use on the front page and other surfaces. Should be a "
|
||||
"photo or an illustration without too much text."
|
||||
@@ -388,15 +305,13 @@ msgstr ""
|
||||
"Velg et bilde til bruk på forsiden og andre visningsflater. Bør være et "
|
||||
"bilde eller en illustrasjon uten for mye tekst."
|
||||
|
||||
#: news/models.py:62
|
||||
msgid ""
|
||||
"A very short summary of the article's content. Used on the front page and in "
|
||||
"the article listing."
|
||||
msgstr ""
|
||||
"En veldig kort oppsummering av innholdet i artikkelen. Brukes på forsiden "
|
||||
"og i artikkeloversikten."
|
||||
"En veldig kort oppsummering av innholdet i artikkelen. Brukes på forsiden og "
|
||||
"i artikkeloversikten."
|
||||
|
||||
#: news/models.py:71
|
||||
msgid ""
|
||||
"A brief, introductory paragraph that summarizes the main content of the "
|
||||
"article."
|
||||
@@ -404,10 +319,41 @@ msgstr ""
|
||||
"Et kortfattet, innledende avsnitt som oppsummerer hovedinnholdet i "
|
||||
"artikkelen."
|
||||
|
||||
#: news/models.py:92
|
||||
msgid "news article"
|
||||
msgstr "nyhetsartikkel"
|
||||
|
||||
#: news/models.py:93
|
||||
msgid "news articles"
|
||||
msgstr "nyhetsartikler"
|
||||
|
||||
msgid "Rentals page"
|
||||
msgstr "Utleieside"
|
||||
|
||||
msgid "Venue overview"
|
||||
msgstr "Lokaleoversikt"
|
||||
|
||||
msgid "Venues"
|
||||
msgstr "Lokaler"
|
||||
|
||||
msgid "venue index"
|
||||
msgstr "lokaleoversikt"
|
||||
|
||||
msgid "venue indexes"
|
||||
msgstr "lokaleoversikter"
|
||||
|
||||
msgid "rentals page"
|
||||
msgstr "utleieside"
|
||||
|
||||
msgid "rentals pages"
|
||||
msgstr "utleiesider"
|
||||
|
||||
msgid "venue"
|
||||
msgstr "lokale"
|
||||
|
||||
msgid "venues"
|
||||
msgstr "lokaler"
|
||||
|
||||
#~ msgid "Bookable"
|
||||
#~ msgstr "Til utleie"
|
||||
|
||||
#~ msgid "Listed"
|
||||
#~ msgstr "I oversikt"
|
||||
|
||||
+27
-10
@@ -1,7 +1,7 @@
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from wagtail.admin.ui.tables import DateColumn
|
||||
from wagtail.admin.ui.tables.pages import PageStatusColumn, PageTitleColumn
|
||||
from wagtail.admin.viewsets.pages import PageListingViewSet
|
||||
from wagtail.admin.viewsets.pages import PageListingViewSet, PageViewSet
|
||||
|
||||
from dnscms.admin import ListingRedirectChooseParentView
|
||||
from news.models import NewsPage
|
||||
@@ -11,17 +11,19 @@ class NewsChooseParentView(ListingRedirectChooseParentView):
|
||||
listing_url_name = "news:index"
|
||||
|
||||
|
||||
class NewsPageListingViewSet(PageListingViewSet):
|
||||
model = NewsPage
|
||||
choose_parent_view_class = NewsChooseParentView
|
||||
icon = "info-circle"
|
||||
menu_label = _("News")
|
||||
menu_order = 3
|
||||
add_to_admin_menu = True
|
||||
ordering = "-latest_revision_created_at"
|
||||
class NewsListingMixin:
|
||||
"""Shared model + columns for the standalone listing and the page explorer."""
|
||||
|
||||
model = NewsPage
|
||||
icon = "info-circle"
|
||||
columns = [
|
||||
PageTitleColumn("title", label=_("Title"), sort_key="title", classname="title"),
|
||||
DateColumn(
|
||||
"first_published_at",
|
||||
label=_("First published"),
|
||||
sort_key="first_published_at",
|
||||
width="10%",
|
||||
),
|
||||
DateColumn(
|
||||
"latest_revision_created_at",
|
||||
label=_("Updated"),
|
||||
@@ -32,4 +34,19 @@ class NewsPageListingViewSet(PageListingViewSet):
|
||||
]
|
||||
|
||||
|
||||
news_page_listing_viewset = NewsPageListingViewSet("news")
|
||||
class NewsSidebarViewSet(NewsListingMixin, PageListingViewSet):
|
||||
"""Standalone 'News' sidebar entry, reached independently of the page tree."""
|
||||
|
||||
choose_parent_view_class = NewsChooseParentView
|
||||
menu_label = _("News")
|
||||
menu_order = 3
|
||||
add_to_admin_menu = True
|
||||
ordering = "-latest_revision_created_at"
|
||||
|
||||
|
||||
class NewsExplorerViewSet(NewsListingMixin, PageViewSet):
|
||||
"""Applies the same columns when navigating into NewsIndex via the page explorer."""
|
||||
|
||||
|
||||
news_sidebar_viewset = NewsSidebarViewSet("news")
|
||||
news_explorer_viewset = NewsExplorerViewSet()
|
||||
|
||||
@@ -1,8 +1,13 @@
|
||||
from wagtail import hooks
|
||||
|
||||
from .admin import news_page_listing_viewset
|
||||
from .admin import news_sidebar_viewset, news_explorer_viewset
|
||||
|
||||
|
||||
@hooks.register("register_admin_viewset")
|
||||
def register_news_page_listing_viewset():
|
||||
return news_page_listing_viewset
|
||||
def register_news_sidebar_viewset():
|
||||
return news_sidebar_viewset
|
||||
|
||||
|
||||
@hooks.register("register_admin_viewset")
|
||||
def register_news_explorer_viewset():
|
||||
return news_explorer_viewset
|
||||
|
||||
@@ -14,6 +14,7 @@ from events.models import (
|
||||
EventOrganizerLink,
|
||||
EventPage,
|
||||
)
|
||||
from events.views import EventOrganizerCreationForm
|
||||
from tests.conftest import (
|
||||
AssociationPageFactory,
|
||||
CustomImageFactory,
|
||||
@@ -104,6 +105,57 @@ def test_eventoccurrence_clean_rejects_both_venue_and_venue_custom(event_index,
|
||||
assert "venue_custom" in exc.value.message_dict
|
||||
|
||||
|
||||
def test_eventoccurrence_clean_promotes_matching_custom_text_to_venue(event_index, venue):
|
||||
event = EventPageFactory(parent=event_index)
|
||||
occurrence = EventOccurrence(
|
||||
event=event,
|
||||
start=timezone.now(),
|
||||
venue_custom=f" {venue.title} ",
|
||||
)
|
||||
|
||||
occurrence.clean()
|
||||
|
||||
assert occurrence.venue_id == venue.pk
|
||||
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(
|
||||
event=event,
|
||||
start=timezone.now(),
|
||||
venue_custom=" Frederikkeplassen ",
|
||||
)
|
||||
|
||||
occurrence.clean()
|
||||
|
||||
assert occurrence.venue is None
|
||||
assert occurrence.venue_custom == "Frederikkeplassen"
|
||||
|
||||
|
||||
def test_eventoccurrence_clean_requires_venue_or_venue_custom(event_index):
|
||||
event = EventPageFactory(parent=event_index)
|
||||
occurrence = EventOccurrence(event=event, start=timezone.now())
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from news.admin import NewsPageListingViewSet
|
||||
from news.admin import NewsSidebarViewSet
|
||||
from news.models import NewsPage
|
||||
from tests.conftest import NewsPageFactory
|
||||
|
||||
@@ -11,9 +11,9 @@ def test_news_page_persists_via_factory(news_index):
|
||||
assert reloaded.excerpt == "Short summary"
|
||||
|
||||
|
||||
def test_news_listing_viewset_wired_to_newspage():
|
||||
assert NewsPageListingViewSet.model is NewsPage
|
||||
assert NewsPageListingViewSet.add_to_admin_menu is True
|
||||
def test_news_sidebar_viewset_wired_to_newspage():
|
||||
assert NewsSidebarViewSet.model is NewsPage
|
||||
assert NewsSidebarViewSet.add_to_admin_menu is True
|
||||
|
||||
|
||||
def test_graphql_news_index_query(news_index, graphql_post):
|
||||
|
||||
@@ -86,3 +86,46 @@ def test_search_excludes_draft_event_page(home_page, event_index, graphql_post):
|
||||
assert response.status_code == 200
|
||||
assert "errors" not in body, body
|
||||
assert "DraftEventSearchToken" not in _titles_for(body, "EventPage")
|
||||
|
||||
|
||||
def test_search_results_not_grouped_by_type(home_page, event_index, graphql_post):
|
||||
# Two pages of different types matching the query equally, plus a third
|
||||
# page of one of those types that should rank highest. Under the
|
||||
# per-model-iteration resolver, all Generic results come before all Event
|
||||
# results (or vice versa) — type-grouped — so the highest-relevance Event
|
||||
# ends up after a less-relevant Generic. Cross-type relevance ordering
|
||||
# should put the strongest match first regardless of type.
|
||||
weak_generic = GenericPageFactory(
|
||||
parent=home_page,
|
||||
title="Klatremus klatremus klatremus",
|
||||
slug="weak-generic",
|
||||
)
|
||||
weak_event = EventPageFactory(
|
||||
parent=event_index,
|
||||
title="Klatremus klatremus klatremus",
|
||||
slug="weak-event",
|
||||
)
|
||||
strong_event = EventPageFactory(
|
||||
parent=event_index,
|
||||
title="Klatremus klatremus klatremus klatremus klatremus klatremus",
|
||||
slug="strong-event",
|
||||
)
|
||||
_index(weak_generic)
|
||||
_index(weak_event)
|
||||
_index(strong_event)
|
||||
|
||||
response, body = graphql_post(SEARCH_QUERY, {"query": "klatremus"})
|
||||
|
||||
assert response.status_code == 200
|
||||
assert "errors" not in body, body
|
||||
order = [
|
||||
(r["__typename"], r["title"])
|
||||
for r in body["data"]["results"]
|
||||
if r["__typename"] in ("GenericPage", "EventPage")
|
||||
]
|
||||
assert len(order) == 3, order
|
||||
# Per-type grouping would put all results of one type consecutively
|
||||
# before the other type. Cross-type relevance ordering should interleave.
|
||||
types_seen = [t for t, _ in order]
|
||||
assert types_seen != ["GenericPage", "EventPage", "EventPage"], order
|
||||
assert types_seen != ["EventPage", "EventPage", "GenericPage"], order
|
||||
|
||||
@@ -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"
|
||||
@@ -0,0 +1,58 @@
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from wagtail.admin.ui.tables import BooleanColumn, DateColumn
|
||||
from wagtail.admin.ui.tables.pages import PageStatusColumn, PageTitleColumn
|
||||
from wagtail.admin.viewsets.pages import PageListingViewSet, PageViewSet
|
||||
|
||||
from dnscms.admin import ListingRedirectChooseParentView
|
||||
from venues.models import VenuePage
|
||||
|
||||
|
||||
class VenueChooseParentView(ListingRedirectChooseParentView):
|
||||
listing_url_name = "venues:index"
|
||||
|
||||
|
||||
class VenueListingMixin:
|
||||
"""Shared model + columns for the standalone listing and the page explorer."""
|
||||
|
||||
model = VenuePage
|
||||
icon = "home"
|
||||
columns = [
|
||||
PageTitleColumn("title", label=_("Title"), sort_key="title", classname="title"),
|
||||
BooleanColumn(
|
||||
"show_as_bookable",
|
||||
label=_("Rentals page"),
|
||||
sort_key="show_as_bookable",
|
||||
width="10%",
|
||||
),
|
||||
BooleanColumn(
|
||||
"show_in_overview",
|
||||
label=_("Venue overview"),
|
||||
sort_key="show_in_overview",
|
||||
width="10%",
|
||||
),
|
||||
DateColumn(
|
||||
"latest_revision_created_at",
|
||||
label=_("Updated"),
|
||||
sort_key="latest_revision_created_at",
|
||||
width="10%",
|
||||
),
|
||||
PageStatusColumn("status", label=_("Status"), sort_key="live", width="10%"),
|
||||
]
|
||||
|
||||
|
||||
class VenueSidebarViewSet(VenueListingMixin, PageListingViewSet):
|
||||
"""Standalone 'Venues' sidebar entry, reached independently of the page tree."""
|
||||
|
||||
choose_parent_view_class = VenueChooseParentView
|
||||
menu_label = _("Venues")
|
||||
menu_order = 4
|
||||
add_to_admin_menu = True
|
||||
ordering = "title"
|
||||
|
||||
|
||||
class VenueExplorerViewSet(VenueListingMixin, PageViewSet):
|
||||
"""Applies the same columns when navigating into VenueIndex via the page explorer."""
|
||||
|
||||
|
||||
venue_sidebar_viewset = VenueSidebarViewSet("venues")
|
||||
venue_explorer_viewset = VenueExplorerViewSet()
|
||||
@@ -0,0 +1,25 @@
|
||||
# Generated by Django 6.0.5 on 2026-05-25 23:40
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('venues', '0024_venuepage_show_in_overview_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name='venueindex',
|
||||
options={'verbose_name': 'venue index', 'verbose_name_plural': 'venue indexes'},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='venuepage',
|
||||
options={'verbose_name': 'venue', 'verbose_name_plural': 'venues'},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='venuerentalindex',
|
||||
options={'verbose_name': 'rentals page', 'verbose_name_plural': 'rentals pages'},
|
||||
),
|
||||
]
|
||||
@@ -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 (
|
||||
GraphQLBoolean,
|
||||
@@ -38,6 +39,10 @@ class VenueIndex(HeadlessMixin, Page):
|
||||
|
||||
graphql_fields = [GraphQLRichText("lead"), GraphQLStreamfield("body")]
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("venue index")
|
||||
verbose_name_plural = _("venue indexes")
|
||||
|
||||
|
||||
@register_singular_query_field("venueRentalIndex")
|
||||
class VenueRentalIndex(HeadlessMixin, Page):
|
||||
@@ -55,6 +60,10 @@ class VenueRentalIndex(HeadlessMixin, Page):
|
||||
|
||||
graphql_fields = [GraphQLRichText("lead"), GraphQLStreamfield("body")]
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("rentals page")
|
||||
verbose_name_plural = _("rentals pages")
|
||||
|
||||
|
||||
class VenuePage(HeadlessMixin, WPImportedPageMixin, Page):
|
||||
# no children
|
||||
@@ -184,3 +193,7 @@ class VenuePage(HeadlessMixin, WPImportedPageMixin, Page):
|
||||
search_fields = Page.search_fields + [
|
||||
index.SearchField("body"),
|
||||
]
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("venue")
|
||||
verbose_name_plural = _("venues")
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
from wagtail import hooks
|
||||
|
||||
from .admin import venue_explorer_viewset, venue_sidebar_viewset
|
||||
|
||||
|
||||
@hooks.register("register_admin_viewset")
|
||||
def register_venue_sidebar_viewset():
|
||||
return venue_sidebar_viewset
|
||||
|
||||
|
||||
@hooks.register("register_admin_viewset")
|
||||
def register_venue_explorer_viewset():
|
||||
return venue_explorer_viewset
|
||||
+5
-1
@@ -48,5 +48,9 @@
|
||||
"overrides": {
|
||||
"@types/react": "19.2.14",
|
||||
"@types/react-dom": "19.2.3"
|
||||
}
|
||||
},
|
||||
"browserslist": [
|
||||
"> 0.5% in NO",
|
||||
"not dead"
|
||||
]
|
||||
}
|
||||
|
||||
+16
-1
@@ -35,7 +35,22 @@ export default function RootLayout({
|
||||
<html lang="no">
|
||||
<head>
|
||||
<link rel="preconnect" href="https://use.typekit.net" crossOrigin="anonymous" />
|
||||
<link rel="stylesheet" href="https://use.typekit.net/spa5smt.css" />
|
||||
<link rel="preconnect" href="https://p.typekit.net" crossOrigin="anonymous" />
|
||||
{/*
|
||||
Load Adobe Fonts without blocking render: the stylesheet is requested
|
||||
with media="print" (fetched but not applied), then the inline script
|
||||
below swaps it to media="all" once it has loaded.
|
||||
*/}
|
||||
<link
|
||||
id="typekit-css"
|
||||
rel="stylesheet"
|
||||
href="https://use.typekit.net/spa5smt.css"
|
||||
media="print"
|
||||
suppressHydrationWarning
|
||||
/>
|
||||
<script>
|
||||
{`(function(){var l=document.getElementById('typekit-css');if(!l)return;function s(){l.media='all'}if(l.sheet){s()}else{l.addEventListener('load',s)}})();`}
|
||||
</script>
|
||||
{process.env.UMAMI_SCRIPT_URL && process.env.UMAMI_WEBSITE_ID && (
|
||||
<script
|
||||
defer
|
||||
|
||||
@@ -10,6 +10,8 @@ import { getSeoMetadata } from "@/lib/seo";
|
||||
|
||||
type Params = Promise<{ slug: string }>;
|
||||
|
||||
const EXCLUDED_SLUGS = ["hele-huset"];
|
||||
|
||||
export async function generateMetadata(
|
||||
{ params }: { params: Params },
|
||||
parent: ResolvingMetadata
|
||||
@@ -40,13 +42,18 @@ export async function generateStaticParams() {
|
||||
);
|
||||
}
|
||||
|
||||
return data.pages.map((page: any) => ({
|
||||
slug: page.slug,
|
||||
}));
|
||||
return data.pages
|
||||
.filter((page) => !EXCLUDED_SLUGS.includes(page.slug))
|
||||
.map((page) => ({
|
||||
slug: page.slug,
|
||||
}));
|
||||
}
|
||||
|
||||
export default async function Page({ params }: { params: Params }) {
|
||||
const { slug } = await params;
|
||||
if (EXCLUDED_SLUGS.includes(slug)) {
|
||||
return notFound();
|
||||
}
|
||||
const props = await loadVenuePageProps({ slug });
|
||||
if (!props) return notFound();
|
||||
return <VenuePageView {...props} />;
|
||||
|
||||
@@ -58,7 +58,7 @@ export const EventItem = ({
|
||||
alt={featuredImage.alt}
|
||||
width={0}
|
||||
height={0}
|
||||
sizes="(max-width: 900px) 100vw, 25vw"
|
||||
sizes="(max-width: 900px) calc(100vw - 2rem), 30vw"
|
||||
loading={imageLoading}
|
||||
fetchPriority={imageFetchPriority}
|
||||
/>
|
||||
|
||||
@@ -9,13 +9,13 @@ export const FeaturedEvents = ({ events }: { events: EventListItemFragment[] })
|
||||
<section className={styles.featuredEvents}>
|
||||
<SectionHeader heading="Arrangementer" link="/arrangementer" linkText="Se alle arrangementer" />
|
||||
<ul className={styles.eventList}>
|
||||
{events.slice(0, 3).map((event, index) => (
|
||||
{events.slice(0, 3).map((event) => (
|
||||
<EventItem
|
||||
key={event.id}
|
||||
event={event}
|
||||
mode="list"
|
||||
imageLoading="eager"
|
||||
imageFetchPriority={index < 2 ? "high" : undefined}
|
||||
imageFetchPriority="high"
|
||||
/>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
@@ -25,8 +25,12 @@ export const Header = () => {
|
||||
const { replace } = useRouter();
|
||||
const pathname = usePathname();
|
||||
const [showMenu, setShowMenu] = useState(false);
|
||||
// Only enable the menu's slide/fade animations once the user has interacted,
|
||||
// so they don't play on first page load
|
||||
const [hasInteracted, setHasInteracted] = useState(false);
|
||||
|
||||
function toggleMenu() {
|
||||
setHasInteracted(true);
|
||||
setShowMenu(!showMenu);
|
||||
}
|
||||
|
||||
@@ -61,6 +65,7 @@ export const Header = () => {
|
||||
<header
|
||||
className={styles.header}
|
||||
data-show={showMenu}
|
||||
data-animate={hasInteracted}
|
||||
data-small={!isInView}
|
||||
>
|
||||
<Link href="/" aria-label="Hjem">
|
||||
|
||||
@@ -54,6 +54,12 @@
|
||||
transition: transform .6s ease;
|
||||
}
|
||||
}
|
||||
|
||||
&[data-animate=false] {
|
||||
.mainMenu {
|
||||
animation: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.overlay {
|
||||
@@ -282,4 +288,4 @@
|
||||
.logo {
|
||||
font-size: 1rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user