From 509a50c321d0f932c259b67c321d393891161644 Mon Sep 17 00:00:00 2001 From: Jonas Braathen Date: Tue, 19 May 2026 06:09:54 +0200 Subject: [PATCH] dnscms: add studio page --- dnscms/dnscms/settings/base.py | 2 + dnscms/studio/__init__.py | 0 dnscms/studio/apps.py | 6 ++ dnscms/studio/migrations/0001_initial.py | 32 ++++++++++ dnscms/studio/migrations/__init__.py | 0 dnscms/studio/models.py | 63 +++++++++++++++++++ dnscms/tests/conftest.py | 8 +++ dnscms/tests/test_studio.py | 78 ++++++++++++++++++++++++ 8 files changed, 189 insertions(+) create mode 100644 dnscms/studio/__init__.py create mode 100644 dnscms/studio/apps.py create mode 100644 dnscms/studio/migrations/0001_initial.py create mode 100644 dnscms/studio/migrations/__init__.py create mode 100644 dnscms/studio/models.py create mode 100644 dnscms/tests/test_studio.py diff --git a/dnscms/dnscms/settings/base.py b/dnscms/dnscms/settings/base.py index 56d16f3..769bfa8 100644 --- a/dnscms/dnscms/settings/base.py +++ b/dnscms/dnscms/settings/base.py @@ -37,6 +37,7 @@ INSTALLED_APPS = [ "news", "openinghours", "sponsors", + "studio", # end cms apps "grapple", "graphene_django", @@ -206,6 +207,7 @@ GRAPPLE = { "news", "openinghours", "sponsors", + "studio", ], "EXPOSE_GRAPHIQL": True, "PAGE_SIZE": 100, diff --git a/dnscms/studio/__init__.py b/dnscms/studio/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/dnscms/studio/apps.py b/dnscms/studio/apps.py new file mode 100644 index 0000000..af34c85 --- /dev/null +++ b/dnscms/studio/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class StudioConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = "studio" diff --git a/dnscms/studio/migrations/0001_initial.py b/dnscms/studio/migrations/0001_initial.py new file mode 100644 index 0000000..cc21f06 --- /dev/null +++ b/dnscms/studio/migrations/0001_initial.py @@ -0,0 +1,32 @@ +# Generated by Django 6.0.5 on 2026-05-19 02:59 + +import django.db.models.deletion +import wagtail.fields +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('images', '0005_customimage_description'), + ('wagtailcore', '0097_baselogentry_uuid_action_timestamp_indexes'), + ] + + operations = [ + migrations.CreateModel( + name='StudioPage', + fields=[ + ('page_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='wagtailcore.page')), + ('lead', wagtail.fields.RichTextField(blank=True)), + ('body', wagtail.fields.StreamField([('paragraph', 0), ('image', 4), ('image_slider', 8), ('horizontal_rule', 10), ('featured', 18), ('page_section_navigation', 19), ('accordion', 23), ('fact_box', 26), ('embed', 27), ('raw_html', 28), ('page_section', 33)], block_lookup={0: ('wagtail.blocks.RichTextBlock', (), {'label': 'Rik tekst'}), 1: ('wagtail.images.blocks.ImageChooserBlock', (), {'label': 'Bilde'}), 2: ('wagtail.blocks.ChoiceBlock', [], {'choices': [('fullwidth', 'Fullbredde'), ('bleed', 'Utfallende'), ('original', 'Uendret størrelse')], 'icon': 'cup', 'label': 'Bildeformat'}), 3: ('wagtail.blocks.CharBlock', (), {'label': 'Bildetekst', 'max_length': 512, 'required': False}), 4: ('wagtail.blocks.StructBlock', [[('image', 1), ('image_format', 2), ('text', 3)]], {}), 5: ('wagtail.blocks.CharBlock', (), {'label': 'Tekst', 'max_length': 512, 'required': False}), 6: ('wagtail.blocks.StructBlock', [[('image', 1), ('text', 5)]], {}), 7: ('wagtail.blocks.ListBlock', (6,), {'label': 'Bilder', 'min_num': 1}), 8: ('wagtail.blocks.StructBlock', [[('images', 7)]], {}), 9: ('wagtail.blocks.ChoiceBlock', [], {'choices': [('deepBrick', 'Dyp tegl'), ('neufPink', 'Griserosa'), ('goldenOrange', 'Gyllen oransje'), ('goldenBeige', 'Gyllen beige'), ('chateauBlue', 'Slottsblå')], 'label': 'Farge', 'required': False}), 10: ('wagtail.blocks.StructBlock', [[('color', 9)]], {}), 11: ('wagtail.blocks.CharBlock', (), {'label': 'Tittel', 'max_length': 64, 'required': True}), 12: ('wagtail.blocks.RichTextBlock', (), {'features': ['bold', 'italic', 'link'], 'label': 'Tekst', 'required': True}), 13: ('wagtail.blocks.PageChooserBlock', (), {'header': 'Fremhevet side', 'required': True}), 14: ('wagtail.blocks.CharBlock', (), {'default': 'Les mer', 'help_text': 'Lenketeksten som tar deg videre til siden. Tips: Ikke start med "Trykk her"', 'label': 'Lenketekst', 'max_length': 64, 'required': True}), 15: ('wagtail.blocks.ChoiceBlock', [], {'choices': [('betongGray', 'Betonggrå'), ('deepBrick', 'Dyp tegl'), ('neufPink', 'Griserosa'), ('goldenOrange', 'Gyllen oransje'), ('goldenBeige', 'Gyllen beige'), ('chateauBlue', 'Slottsblå')], 'label': 'Bakgrunnsfarge'}), 16: ('wagtail.blocks.ChoiceBlock', [], {'choices': [('left', 'Venstre'), ('right', 'Høyre')], 'label': 'Bildeplassering'}), 17: ('wagtail.images.blocks.ImageChooserBlock', (), {'header': 'Overstyr bilde', 'help_text': 'Bildet som er tilknyttet undersiden du vil fremheve, vil automatisk brukes. Om det mangler eller du vil overstyre hvilket bilde som et brukes, kan du velge et her.', 'required': False}), 18: ('wagtail.blocks.StructBlock', [[('title', 11), ('text', 12), ('featured_page', 13), ('link_text', 14), ('background_color', 15), ('image_position', 16), ('featured_image_override', 17)]], {}), 19: ('dnscms.blocks.PageSectionNavigationBlock', (), {}), 20: ('wagtail.blocks.CharBlock', (), {'label': 'Overskrift', 'max_length': 64, 'required': True}), 21: ('wagtail.blocks.StructBlock', [[('image', 1), ('image_format', 2), ('text', 3)]], {'label': 'Bilde'}), 22: ('wagtail.blocks.StreamBlock', [[('paragraph', 0), ('image', 21)]], {}), 23: ('wagtail.blocks.StructBlock', [[('heading', 20), ('body', 22)]], {}), 24: ('wagtail.blocks.ChoiceBlock', [], {'choices': [('betongGray', 'Betonggrå'), ('deepBrick', 'Dyp tegl'), ('neufPink', 'Griserosa'), ('goldenOrange', 'Gyllen oransje'), ('goldenBeige', 'Gyllen beige'), ('chateauBlue', 'Slottsblå')], 'label': 'Bakgrunnsfarge', 'required': False}), 25: ('wagtail.blocks.RichTextBlock', (), {'features': ['bold', 'italic', 'link', 'ol', 'ul', 'h2', 'h3'], 'label': 'Innhold'}), 26: ('wagtail.blocks.StructBlock', [[('background_color', 24), ('body', 25)]], {}), 27: ('wagtail.embeds.blocks.EmbedBlock', (), {}), 28: ('wagtail.blocks.RawHTMLBlock', (), {}), 29: ('wagtail.blocks.ChoiceBlock', [], {'choices': [('pigHeadLogo', 'Grisehodelogo'), ('key', 'Nøkkel'), ('ticket', 'Billett'), ('shield', 'Skjold'), ('bottle', 'Flaske'), ('lostProperty', 'Hittegods'), ('pigsty', 'Grisebinge'), ('wheelchair', 'Rullestol'), ('clock', 'Klokke'), ('parking', 'Parkering'), ('coins', 'Mynter')], 'label': 'Ikon', 'required': False}), 30: ('dnscms.blocks.NeufAddressSectionBlock', (), {}), 31: ('dnscms.blocks.OpeningHoursSectionBlock', (), {}), 32: ('wagtail.blocks.StreamBlock', [[('paragraph', 0), ('image', 4), ('image_slider', 8), ('horizontal_rule', 10), ('featured', 18), ('accordion', 23), ('fact_box', 26), ('embed', 27), ('raw_html', 28), ('neuf_address', 30), ('opening_hours', 31)]], {}), 33: ('wagtail.blocks.StructBlock', [[('title', 11), ('background_color', 24), ('icon', 29), ('body', 32)]], {})})), + ('pig', models.CharField(blank=True, choices=[('', 'Ingen'), ('logo', 'Logogrisen'), ('music', 'Musikergrisen'), ('drink', 'Drikkegrisen'), ('dance', 'Dansegrisen'), ('point', 'Pekegrisen'), ('student', 'Studentgrisen'), ('listen', 'Lyttegrisen'), ('guard', 'Vaktgrisen'), ('key', 'Nøkkelgrisen'), ('chill', 'Liggegrisen'), ('peek', 'Tittegrisen')], default='', help_text='Grisen nedi hjørnet.', max_length=32)), + ('logo', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='images.customimage')), + ], + options={ + 'abstract': False, + }, + bases=('wagtailcore.page',), + ), + ] diff --git a/dnscms/studio/migrations/__init__.py b/dnscms/studio/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/dnscms/studio/models.py b/dnscms/studio/models.py new file mode 100644 index 0000000..a3035b4 --- /dev/null +++ b/dnscms/studio/models.py @@ -0,0 +1,63 @@ +from django.db import models +from grapple.helpers import register_singular_query_field +from grapple.models import ( + GraphQLImage, + GraphQLRichText, + GraphQLStreamfield, + GraphQLString, +) +from wagtail.admin.panels import FieldPanel +from wagtail.fields import RichTextField, StreamField +from wagtail.models import Page +from wagtail.search import index + +from dnscms.blocks import BASE_BLOCKS, PageSectionBlock +from dnscms.options import ALL_PIGS + + +@register_singular_query_field("studioPage") +class StudioPage(Page): + max_count = 1 + subpage_types = [] + show_in_menus = True + + logo = models.ForeignKey( + "images.CustomImage", + null=True, + blank=True, + on_delete=models.SET_NULL, + related_name="+", + ) + lead = RichTextField(features=["link"], blank=True) + body = StreamField(BASE_BLOCKS + [("page_section", PageSectionBlock())]) + + PIG_CHOICES = [ + ("", "Ingen"), + ] + ALL_PIGS + + pig = models.CharField( + max_length=32, + choices=PIG_CHOICES, + default="", + blank=True, + help_text="Grisen nedi hjørnet.", + ) + + content_panels = Page.content_panels + [ + FieldPanel("logo"), + FieldPanel("lead", heading="Ingress"), + FieldPanel("body", heading="Innhold"), + FieldPanel("pig", heading="Gris"), + ] + + graphql_fields = [ + GraphQLImage("logo"), + GraphQLRichText("lead"), + GraphQLStreamfield("body"), + GraphQLString("pig", required=True), + ] + + search_fields = Page.search_fields + [ + index.SearchField("lead"), + index.SearchField("body"), + ] diff --git a/dnscms/tests/conftest.py b/dnscms/tests/conftest.py index 88b5188..53991a1 100644 --- a/dnscms/tests/conftest.py +++ b/dnscms/tests/conftest.py @@ -10,6 +10,7 @@ from events.models import EventIndex, EventPage from generic.models import GenericPage from images.models import CustomImage from news.models import NewsIndex, NewsPage +from studio.models import StudioPage from venues.models import VenueIndex, VenuePage @@ -56,6 +57,13 @@ class GenericPageFactory(wagtail_factories.PageFactory): model = GenericPage +class StudioPageFactory(wagtail_factories.PageFactory): + title = factory.Sequence(lambda n: f"Studio {n}") + + class Meta: + model = StudioPage + + class NewsIndexFactory(wagtail_factories.PageFactory): title = factory.Sequence(lambda n: f"News {n}") diff --git a/dnscms/tests/test_studio.py b/dnscms/tests/test_studio.py new file mode 100644 index 0000000..fb60668 --- /dev/null +++ b/dnscms/tests/test_studio.py @@ -0,0 +1,78 @@ +from studio.models import StudioPage +from tests.conftest import CustomImageFactory, StudioPageFactory + + +def test_studio_page_persists_via_factory(home_page): + logo = CustomImageFactory() + page = StudioPageFactory( + parent=home_page, + title="STUDiO", + slug="studio", + lead="

Ingress.

", + body=[("paragraph", "

Body content.

")], + pig="drink", + logo=logo, + ) + + reloaded = StudioPage.objects.get(pk=page.pk) + assert reloaded.title == "STUDiO" + assert reloaded.slug == "studio" + assert "Ingress." in reloaded.lead + assert reloaded.pig == "drink" + assert reloaded.body[0].block_type == "paragraph" + assert reloaded.logo == logo + + +def test_studio_page_is_singleton(home_page): + StudioPageFactory(parent=home_page, slug="studio") + + assert StudioPage.can_create_at(home_page) is False + + +def test_graphql_studio_page_query(home_page, graphql_post): + logo = CustomImageFactory(alt="STUDiO-logo") + StudioPageFactory( + parent=home_page, + title="STUDiO", + slug="studio", + lead="

Ingress text.

", + body=[("paragraph", "

Body content.

")], + pig="drink", + logo=logo, + ) + + response, body = graphql_post( + """ + query { + page: studioPage { + ... on StudioPage { + title + slug + lead + pig + logo { + alt + } + body { + blockType + field + ... on RichTextBlock { + value + } + } + } + } + } + """ + ) + + assert response.status_code == 200 + assert "errors" not in body, body + data = body["data"]["page"] + assert data["title"] == "STUDiO" + assert data["slug"] == "studio" + assert "Ingress text." in data["lead"] + assert data["pig"] == "drink" + assert data["logo"]["alt"] == "STUDiO-logo" + assert data["body"][0]["blockType"] == "RichTextBlock" + assert "Body content." in data["body"][0]["value"]