Compare commits
91
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b231f40cdc
|
||
|
|
3892624382
|
||
|
|
ca397070ea
|
||
|
|
09207e57e2
|
||
|
|
9a3cd0032f
|
||
|
|
81c32719c8 | ||
|
|
7a784953a8 | ||
|
|
6092fc32fd | ||
|
|
6ee3695074 | ||
|
|
75282104f6 | ||
|
|
01ce1c76f3 | ||
|
|
38d332b574 | ||
|
|
e600d0a663
|
||
|
|
d63b14b2e5
|
||
|
|
36c01902a1
|
||
|
|
e57eb48c88
|
||
|
|
7ac332e352
|
||
|
|
acf70b99d3
|
||
|
|
93956435cf
|
||
|
|
39364b5023
|
||
|
|
b46320ea94
|
||
|
|
9e503d8165
|
||
|
|
7b537c9ffa
|
||
|
|
8ebd17c99c
|
||
|
|
0e239d06d8
|
||
|
|
6f1d531d5d
|
||
|
|
b0698247b6
|
||
|
|
31453af238
|
||
|
|
348a51f2ea | ||
|
|
90bc6917e8 | ||
|
|
73be8c23f2
|
||
|
|
d851effff4
|
||
|
|
f9aeebfca6
|
||
|
|
5d5f0879c2
|
||
|
|
046099b7f1
|
||
|
|
d245f2f00a
|
||
|
|
8ad7df30d7
|
||
|
|
dcb1a59777
|
||
|
|
e3a58556f7
|
||
|
|
7b84b2d480
|
||
|
|
ec94d82863
|
||
|
|
089970a5cd
|
||
|
|
38229c97f0
|
||
|
|
09d1078dce
|
||
|
|
1b5483602f
|
||
|
|
2c8f8a218c
|
||
|
|
b5c9188488
|
||
|
|
433c88c921
|
||
|
|
af8c3fe768
|
||
|
|
a58e2b224e
|
||
|
|
c9a2720d64
|
||
|
|
0b0fba174e | ||
|
|
10763f0b5d | ||
|
|
f536cfc591 | ||
|
|
80f7641e74 | ||
|
|
8b5caa2bea | ||
|
|
5ac3c71ff0 | ||
|
|
a3d71b18da | ||
|
|
7f95d8e252 | ||
|
|
e4c0558527 | ||
|
|
80b9cdbc33 | ||
|
|
154338057d | ||
|
|
337407c771 | ||
|
|
cb9b108526 | ||
|
|
6d712d31be | ||
|
|
447e1bd3ff | ||
|
|
29c61ffc76 | ||
|
|
4a264c589d | ||
|
|
9ca9f5db11 | ||
|
|
696e6b8f11 | ||
|
|
5f354972d9 | ||
|
|
6a9fff8917 | ||
|
|
1073adacbb | ||
|
|
7e114adcc3 | ||
|
|
0e074b5f1f | ||
|
|
e960da6f1c | ||
|
|
a5ebb897f1 | ||
|
|
f91c67f526 | ||
|
|
0c5a9876d6 | ||
|
|
cf945d8647 | ||
|
|
0e5f9f7769 | ||
|
|
10c8ce194c | ||
|
|
509a50c321 | ||
|
|
843062bb13 | ||
|
|
f7e0200a0a | ||
|
|
b09ce9808d | ||
|
|
bc8642b1fc | ||
|
|
2155a149e8 | ||
|
|
676e58c361 | ||
|
|
a9e59b947a | ||
|
|
202cfe47f3 |
@@ -1,3 +1,4 @@
|
||||
.vscode
|
||||
.DS_Store
|
||||
*.swp
|
||||
scratch/
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
# neuf-www
|
||||
|
||||
The neuf.no website. Wagtail CMS backend (`dnscms/`) feeding a Next.js frontend (`web/`) over GraphQL.
|
||||
|
||||
Tools are managed by [mise](https://mise.jdx.dev/). Run `mise install` to get python, uv, node, and prek.
|
||||
|
||||
## Backend (`dnscms/`)
|
||||
|
||||
```bash
|
||||
cd dnscms
|
||||
uv sync
|
||||
uv run ./manage.py migrate
|
||||
uv run ./manage.py runserver
|
||||
uv run pytest
|
||||
```
|
||||
|
||||
GraphQL endpoint: <http://127.0.0.1:8000/api/graphql/>.
|
||||
|
||||
## Frontend (`web/`)
|
||||
|
||||
```bash
|
||||
cd web
|
||||
npm install
|
||||
npm run dev # http://localhost:3000
|
||||
npm run codegen # regenerate GraphQL types (needs the backend running)
|
||||
npm run build
|
||||
```
|
||||
|
||||
## Pre-commit hooks
|
||||
|
||||
[prek](https://github.com/j178/prek) runs ruff lint + format on `dnscms/**/*.py` plus a few sanity hooks. Hooks are configured in [prek.toml](prek.toml).
|
||||
|
||||
```bash
|
||||
prek install # registers the git hook
|
||||
prek run --all-files # run on everything
|
||||
```
|
||||
|
||||
## Caching and revalidation
|
||||
|
||||
The frontend caches all pages and GraphQL responses (ISR) under a single `cms` cache tag.
|
||||
Wagtail purges it via a webhook on any publish/unpublish/move/delete (see `dnscms/dnscms/revalidation.py`).
|
||||
Date-sensitive queries (`futureEvents`, opening hours) additionally expire at Oslo midnight.
|
||||
`REVALIDATE_WEBHOOK_SECRET` must be set to the same value on both sides
|
||||
(`web/.env.local` and `dnscms/dnscms/settings/local.py` or env); unset disables the webhook.
|
||||
|
||||
```bash
|
||||
# manual purge
|
||||
curl -X POST -H "X-Revalidate-Secret: $SECRET" https://<frontend>/api/revalidate
|
||||
```
|
||||
|
||||
Note: ISR is off under `npm run dev`; use `npm run build && npm run start` to test caching.
|
||||
@@ -2,6 +2,7 @@
|
||||
.DS_Store
|
||||
*.swp
|
||||
.vscode/
|
||||
.coverage
|
||||
/venv/
|
||||
/.venv/
|
||||
/static/
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
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, PageViewSet
|
||||
|
||||
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 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(
|
||||
"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%"),
|
||||
]
|
||||
|
||||
|
||||
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()
|
||||
@@ -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),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,24 @@
|
||||
# Generated by Django 6.0.5 on 2026-07-02 14:53
|
||||
|
||||
import wagtail.fields
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('associations', '0026_alter_association_options'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='associationindex',
|
||||
name='body',
|
||||
field=wagtail.fields.StreamField([('paragraph', 0), ('image', 4), ('image_slider', 8), ('horizontal_rule', 10), ('featured', 18), ('page_section_navigation', 19), ('accordion', 23), ('fact_box', 26), ('schedule', 33), ('embed', 34), ('raw_html', 35)], 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.blocks.CharBlock', (), {'help_text': 'Overskrift for timeplanen (valgfritt)', 'label': 'Tittel', 'max_length': 64, 'required': False}), 28: ('wagtail.blocks.CharBlock', (), {'help_text': 'Fritekst, f.eks. "13:00", "20:00 – 23:00" eller "23.06"', 'label': 'Tidspunkt/dato', 'max_length': 64, 'required': True}), 29: ('wagtail.blocks.RichTextBlock', (), {'features': ['bold', 'italic', 'link'], 'help_text': 'Kort tittel på én linje. Du kan legge inn lenker.', 'label': 'Tittel', 'required': True}), 30: ('wagtail.blocks.RichTextBlock', (), {'features': ['bold', 'italic', 'link'], 'help_text': 'Valgfri tilleggsinformasjon.', 'label': 'Beskrivelse', 'required': False}), 31: ('wagtail.blocks.StructBlock', [[('time', 28), ('title', 29), ('description', 30)]], {}), 32: ('wagtail.blocks.ListBlock', (31,), {'label': 'Punkter', 'min_num': 1}), 33: ('wagtail.blocks.StructBlock', [[('title', 27), ('items', 32)]], {}), 34: ('wagtail.embeds.blocks.EmbedBlock', (), {}), 35: ('wagtail.blocks.RawHTMLBlock', (), {})}, default=[('paragraph', '')]),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='associationpage',
|
||||
name='body',
|
||||
field=wagtail.fields.StreamField([('paragraph', 0), ('image', 4), ('image_slider', 8), ('horizontal_rule', 10), ('featured', 18), ('page_section_navigation', 19), ('accordion', 23), ('fact_box', 26), ('schedule', 33), ('embed', 34), ('raw_html', 35)], 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.blocks.CharBlock', (), {'help_text': 'Overskrift for timeplanen (valgfritt)', 'label': 'Tittel', 'max_length': 64, 'required': False}), 28: ('wagtail.blocks.CharBlock', (), {'help_text': 'Fritekst, f.eks. "13:00", "20:00 – 23:00" eller "23.06"', 'label': 'Tidspunkt/dato', 'max_length': 64, 'required': True}), 29: ('wagtail.blocks.RichTextBlock', (), {'features': ['bold', 'italic', 'link'], 'help_text': 'Kort tittel på én linje. Du kan legge inn lenker.', 'label': 'Tittel', 'required': True}), 30: ('wagtail.blocks.RichTextBlock', (), {'features': ['bold', 'italic', 'link'], 'help_text': 'Valgfri tilleggsinformasjon.', 'label': 'Beskrivelse', 'required': False}), 31: ('wagtail.blocks.StructBlock', [[('time', 28), ('title', 29), ('description', 30)]], {}), 32: ('wagtail.blocks.ListBlock', (31,), {'label': 'Punkter', 'min_num': 1}), 33: ('wagtail.blocks.StructBlock', [[('title', 27), ('items', 32)]], {}), 34: ('wagtail.embeds.blocks.EmbedBlock', (), {}), 35: ('wagtail.blocks.RawHTMLBlock', (), {})}, default=[('paragraph', '')]),
|
||||
),
|
||||
]
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,24 @@
|
||||
# Generated by Django 6.0.5 on 2026-07-04 22:29
|
||||
|
||||
import wagtail.fields
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('associations', '0028_alter_associationindex_body_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='associationindex',
|
||||
name='body',
|
||||
field=wagtail.fields.StreamField([('paragraph', 0), ('image', 4), ('image_slider', 8), ('horizontal_rule', 10), ('featured', 18), ('page_section_navigation', 19), ('accordion', 23), ('fact_box', 26), ('schedule', 33), ('embed', 34), ('raw_html', 35)], 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.blocks.CharBlock', (), {'help_text': 'Overskrift for timeplanen (valgfritt)', 'label': 'Tittel', 'max_length': 64, 'required': False}), 28: ('wagtail.blocks.CharBlock', (), {'help_text': 'Fritekst, f.eks. "13:00", "20:00 – 23:00" eller "23.06"', 'label': 'Tidspunkt/dato', 'max_length': 64, 'required': True}), 29: ('wagtail.blocks.RichTextBlock', (), {'features': ['bold', 'italic', 'link'], 'help_text': 'Kort tittel på én linje. Du kan legge inn lenker.', 'label': 'Tittel', 'required': True}), 30: ('wagtail.blocks.RichTextBlock', (), {'features': ['bold', 'italic', 'link'], 'help_text': 'Valgfri tilleggsinformasjon.', 'label': 'Beskrivelse', 'required': False}), 31: ('wagtail.blocks.StructBlock', [[('time', 28), ('title', 29), ('description', 30)]], {}), 32: ('wagtail.blocks.ListBlock', (31,), {'label': 'Punkter', 'min_num': 1}), 33: ('wagtail.blocks.StructBlock', [[('title', 27), ('items', 32)]], {}), 34: ('wagtail.embeds.blocks.EmbedBlock', (), {}), 35: ('wagtail.blocks.RawHTMLBlock', (), {})}, default=[('paragraph', '')]),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='associationpage',
|
||||
name='body',
|
||||
field=wagtail.fields.StreamField([('paragraph', 0), ('image', 4), ('image_slider', 8), ('horizontal_rule', 10), ('featured', 18), ('page_section_navigation', 19), ('accordion', 23), ('fact_box', 26), ('schedule', 33), ('embed', 34), ('raw_html', 35)], 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.blocks.CharBlock', (), {'help_text': 'Overskrift for timeplanen (valgfritt)', 'label': 'Tittel', 'max_length': 64, 'required': False}), 28: ('wagtail.blocks.CharBlock', (), {'help_text': 'Fritekst, f.eks. "13:00", "20:00 – 23:00" eller "23.06"', 'label': 'Tidspunkt/dato', 'max_length': 64, 'required': True}), 29: ('wagtail.blocks.RichTextBlock', (), {'features': ['bold', 'italic', 'link'], 'help_text': 'Kort tittel på én linje. Du kan legge inn lenker.', 'label': 'Tittel', 'required': True}), 30: ('wagtail.blocks.RichTextBlock', (), {'features': ['bold', 'italic', 'link'], 'help_text': 'Valgfri tilleggsinformasjon.', 'label': 'Beskrivelse', 'required': False}), 31: ('wagtail.blocks.StructBlock', [[('time', 28), ('title', 29), ('description', 30)]], {}), 32: ('wagtail.blocks.ListBlock', (31,), {'label': 'Punkter', 'min_num': 1}), 33: ('wagtail.blocks.StructBlock', [[('title', 27), ('items', 32)]], {}), 34: ('wagtail.embeds.blocks.EmbedBlock', (), {}), 35: ('wagtail.blocks.RawHTMLBlock', (), {})}, default=[('paragraph', '')]),
|
||||
),
|
||||
]
|
||||
@@ -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,
|
||||
@@ -8,15 +9,20 @@ from grapple.models import (
|
||||
)
|
||||
from wagtail.admin.panels import FieldPanel
|
||||
from wagtail.fields import RichTextField
|
||||
from wagtail.models import Page
|
||||
from wagtail.models import Page, PageManager
|
||||
from wagtail.search import index
|
||||
from wagtail_headless_preview.models import HeadlessMixin
|
||||
|
||||
from dnscms.fields import CommonStreamField
|
||||
from dnscms.wordpress.models import WPImportedPageMixin
|
||||
from dnscms.wordpress.models import DeferWPFieldsManagerMixin, WPImportedPageMixin
|
||||
|
||||
|
||||
class AssociationPageManager(DeferWPFieldsManagerMixin, PageManager):
|
||||
pass
|
||||
|
||||
|
||||
@register_singular_query_field("associationIndex")
|
||||
class AssociationIndex(Page):
|
||||
class AssociationIndex(HeadlessMixin, Page):
|
||||
max_count = 1
|
||||
subpage_types = ["associations.AssociationPage"]
|
||||
|
||||
@@ -25,8 +31,8 @@ class AssociationIndex(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 = [
|
||||
@@ -36,15 +42,21 @@ class AssociationIndex(Page):
|
||||
|
||||
search_fields = Page.search_fields
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("association index")
|
||||
verbose_name_plural = _("association indexes")
|
||||
|
||||
class AssociationPage(WPImportedPageMixin, Page):
|
||||
|
||||
class AssociationPage(HeadlessMixin, WPImportedPageMixin, Page):
|
||||
subpage_types = []
|
||||
parent_page_types = ["associations.AssociationIndex"]
|
||||
show_in_menus = False
|
||||
|
||||
objects = AssociationPageManager()
|
||||
|
||||
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)
|
||||
@@ -64,14 +76,14 @@ class AssociationPage(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 = [
|
||||
@@ -88,32 +100,6 @@ class AssociationPage(WPImportedPageMixin, Page):
|
||||
index.SearchField("body"),
|
||||
]
|
||||
|
||||
def import_wordpress_data(self, data):
|
||||
import html
|
||||
|
||||
# Wagtail page model fields
|
||||
self.title = html.unescape(data["title"])
|
||||
self.slug = data["slug"]
|
||||
self.first_published_at = data["first_published_at"]
|
||||
self.last_published_at = data["last_published_at"]
|
||||
self.latest_revision_created_at = data["latest_revision_created_at"]
|
||||
self.search_description = data["search_description"]
|
||||
|
||||
# debug fields
|
||||
self.wp_post_id = data["wp_post_id"]
|
||||
self.wp_post_type = data["wp_post_type"]
|
||||
self.wp_link = data["wp_link"]
|
||||
self.wp_raw_content = data["wp_raw_content"]
|
||||
self.wp_block_json = data["wp_block_json"]
|
||||
self.wp_processed_content = data["wp_processed_content"]
|
||||
self.wp_normalized_styles = data["wp_normalized_styles"]
|
||||
self.wp_post_meta = data["wp_post_meta"]
|
||||
|
||||
# own model fields
|
||||
self.body = data["body"] or ""
|
||||
|
||||
meta = data["wp_post_meta"]
|
||||
self.association_type = meta.get("neuf_associations_type").lower()
|
||||
self.website_url = meta.get("neuf_associations_homepage") or ""
|
||||
|
||||
self.excerpt = meta.get("excerpt_encoded") or "TODO"
|
||||
class Meta:
|
||||
verbose_name = _("association")
|
||||
verbose_name_plural = _("associations")
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
|
||||
# Create your tests here.
|
||||
@@ -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"]
|
||||
|
||||
|
||||
|
||||
@@ -1,8 +1,19 @@
|
||||
from wagtail import hooks
|
||||
|
||||
from .admin import association_sidebar_viewset, association_explorer_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_sidebar_viewset():
|
||||
return association_sidebar_viewset
|
||||
|
||||
|
||||
@hooks.register("register_admin_viewset")
|
||||
def register_association_explorer_viewset():
|
||||
return association_explorer_viewset
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -11,6 +11,7 @@ from wagtail.fields import RichTextField, StreamField
|
||||
from wagtail.models import Page
|
||||
from wagtail.search import index
|
||||
from wagtail.snippets.models import register_snippet
|
||||
from wagtail_headless_preview.models import HeadlessMixin
|
||||
|
||||
from contacts.blocks import ContactSectionBlock
|
||||
from dnscms.blocks import BASE_BLOCKS
|
||||
@@ -22,7 +23,7 @@ PHONE_REGEX_VALIDATOR = RegexValidator(
|
||||
|
||||
|
||||
@register_singular_query_field("contactIndex")
|
||||
class ContactIndex(Page):
|
||||
class ContactIndex(HeadlessMixin, Page):
|
||||
max_count = 1
|
||||
subpage_types = []
|
||||
|
||||
|
||||
@@ -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))
|
||||
@@ -0,0 +1,11 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class DnsCmsConfig(AppConfig):
|
||||
name = "dnscms"
|
||||
|
||||
def ready(self):
|
||||
from dnscms import signals # noqa: F401
|
||||
from dnscms.revalidation import register_signal_handlers
|
||||
|
||||
register_signal_handlers()
|
||||
@@ -225,6 +225,62 @@ class FactBoxBlock(blocks.StructBlock):
|
||||
label = "Faktaboks"
|
||||
|
||||
|
||||
@register_streamfield_block
|
||||
class ScheduleItemBlock(blocks.StructBlock):
|
||||
time = blocks.CharBlock(
|
||||
label="Tidspunkt/dato",
|
||||
required=True,
|
||||
max_length=64,
|
||||
help_text='Fritekst, f.eks. "13:00", "20:00 – 23:00" eller "23.06"',
|
||||
)
|
||||
title = blocks.RichTextBlock(
|
||||
features=["bold", "italic", "link"],
|
||||
label="Tittel",
|
||||
required=True,
|
||||
help_text="Kort tittel på én linje. Du kan legge inn lenker.",
|
||||
)
|
||||
description = blocks.RichTextBlock(
|
||||
features=["bold", "italic", "link"],
|
||||
label="Beskrivelse",
|
||||
required=False,
|
||||
help_text="Valgfri tilleggsinformasjon.",
|
||||
)
|
||||
|
||||
graphql_fields = [
|
||||
GraphQLString("time", required=True),
|
||||
GraphQLRichText("title", required=True),
|
||||
GraphQLRichText("description"),
|
||||
]
|
||||
|
||||
class Meta:
|
||||
icon = "time"
|
||||
label = "Punkt"
|
||||
|
||||
|
||||
@register_streamfield_block
|
||||
class ScheduleBlock(blocks.StructBlock):
|
||||
title = blocks.CharBlock(
|
||||
max_length=64,
|
||||
required=False,
|
||||
label="Tittel",
|
||||
help_text="Overskrift for timeplanen (valgfritt)",
|
||||
)
|
||||
items = blocks.ListBlock(
|
||||
ScheduleItemBlock(),
|
||||
min_num=1,
|
||||
label="Punkter",
|
||||
)
|
||||
|
||||
graphql_fields = [
|
||||
GraphQLString("title"),
|
||||
GraphQLStreamfield("items", required=True),
|
||||
]
|
||||
|
||||
class Meta:
|
||||
icon = "time"
|
||||
label = "Timeplan"
|
||||
|
||||
|
||||
BASE_BLOCKS = [
|
||||
("paragraph", blocks.RichTextBlock(label="Rik tekst")),
|
||||
("image", ImageWithTextBlock()),
|
||||
@@ -234,6 +290,7 @@ BASE_BLOCKS = [
|
||||
("page_section_navigation", PageSectionNavigationBlock()),
|
||||
("accordion", AccordionBlock()),
|
||||
("fact_box", FactBoxBlock()),
|
||||
("schedule", ScheduleBlock()),
|
||||
("embed", EmbedBlock()),
|
||||
("raw_html", blocks.RawHTMLBlock()),
|
||||
]
|
||||
@@ -253,6 +310,11 @@ class PageSectionBlock(blocks.StructBlock):
|
||||
("clock", "Klokke"),
|
||||
("parking", "Parkering"),
|
||||
("coins", "Mynter"),
|
||||
("studioNote", "Note (STUDiO)"),
|
||||
("studioCalendar", "Kalender (STUDiO)"),
|
||||
("studioGuitar", "Gitar (STUDiO)"),
|
||||
("studioDisco", "Discokule (STUDiO)"),
|
||||
("studioMole", "Muldvarpen (STUDiO)"),
|
||||
)
|
||||
|
||||
title = blocks.CharBlock(max_length=64, required=True, label="Tittel")
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
"""Purge the frontend cache when content changes.
|
||||
|
||||
The frontend caches all GraphQL responses under a single "cms" cache tag;
|
||||
any content change POSTs to its /api/revalidate endpoint for a global purge.
|
||||
"""
|
||||
|
||||
import logging
|
||||
import threading
|
||||
import urllib.request
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import transaction
|
||||
from django.db.models.signals import post_delete, post_save
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
WEBHOOK_TIMEOUT_SECONDS = 5
|
||||
|
||||
# Non-page models exposed over GraphQL; pages are covered by wagtail signals
|
||||
REVALIDATING_MODELS = [
|
||||
"contacts.ContactEntity",
|
||||
"events.EventCategory",
|
||||
"events.EventOrganizer",
|
||||
"images.CustomImage",
|
||||
"openinghours.OpeningHoursSet",
|
||||
]
|
||||
|
||||
_warned_unconfigured = False
|
||||
|
||||
|
||||
def _send_webhook(url, secret, reason):
|
||||
request = urllib.request.Request(
|
||||
url, data=b"", method="POST", headers={"X-Revalidate-Secret": secret}
|
||||
)
|
||||
try:
|
||||
with urllib.request.urlopen(request, timeout=WEBHOOK_TIMEOUT_SECONDS) as response:
|
||||
logger.info("frontend revalidation ok (%s): HTTP %s", reason, response.status)
|
||||
except Exception:
|
||||
logger.exception("frontend revalidation failed (%s)", reason)
|
||||
|
||||
|
||||
def trigger_frontend_revalidation(reason):
|
||||
global _warned_unconfigured
|
||||
secret = settings.REVALIDATE_WEBHOOK_SECRET
|
||||
if not secret:
|
||||
if not _warned_unconfigured:
|
||||
logger.warning("REVALIDATE_WEBHOOK_SECRET is not set, skipping frontend revalidation")
|
||||
_warned_unconfigured = True
|
||||
return
|
||||
url = f"{settings.FRONTEND_BASE_URL}/api/revalidate"
|
||||
# After commit so the frontend can't re-render from pre-commit data,
|
||||
# in a thread so publishing never blocks on the frontend
|
||||
transaction.on_commit(
|
||||
lambda: threading.Thread(
|
||||
target=_send_webhook, args=(url, secret, reason), daemon=True
|
||||
).start()
|
||||
)
|
||||
|
||||
|
||||
def _on_page_published(sender, instance, **kwargs):
|
||||
trigger_frontend_revalidation(f"page_published: {instance.slug}")
|
||||
|
||||
|
||||
def _on_page_unpublished(sender, instance, **kwargs):
|
||||
trigger_frontend_revalidation(f"page_unpublished: {instance.slug}")
|
||||
|
||||
|
||||
def _on_page_moved(sender, instance, **kwargs):
|
||||
trigger_frontend_revalidation(f"post_page_move: {instance.slug}")
|
||||
|
||||
|
||||
def _on_page_deleted(sender, instance, **kwargs):
|
||||
trigger_frontend_revalidation(f"page_deleted: {instance.slug}")
|
||||
|
||||
|
||||
def _on_model_changed(sender, instance, **kwargs):
|
||||
trigger_frontend_revalidation(f"{sender._meta.label}: {instance}")
|
||||
|
||||
|
||||
def register_signal_handlers():
|
||||
from wagtail.models import Page
|
||||
from wagtail.signals import page_published, page_unpublished, post_page_move
|
||||
|
||||
page_published.connect(_on_page_published, dispatch_uid="revalidation.page_published")
|
||||
page_unpublished.connect(_on_page_unpublished, dispatch_uid="revalidation.page_unpublished")
|
||||
post_page_move.connect(_on_page_moved, dispatch_uid="revalidation.post_page_move")
|
||||
# Wagtail sends no signal on page deletion; MTI deletes of any page type
|
||||
# also emit post_delete for the base Page row
|
||||
post_delete.connect(_on_page_deleted, sender=Page, dispatch_uid="revalidation.page_deleted")
|
||||
|
||||
for label in REVALIDATING_MODELS:
|
||||
post_save.connect(
|
||||
_on_model_changed, sender=label, dispatch_uid=f"revalidation.save.{label}"
|
||||
)
|
||||
post_delete.connect(
|
||||
_on_model_changed, sender=label, dispatch_uid=f"revalidation.delete.{label}"
|
||||
)
|
||||
@@ -0,0 +1,92 @@
|
||||
"""Inline "button" link for Draftail rich text fields.
|
||||
|
||||
Stored as `<a linktype="button" data-href="...">label</a>`. The linktype (not a
|
||||
plain href) avoids colliding with the built-in link feature, and lets
|
||||
expand_db_html rewrite it to `<a class="button" href="...">` for the front end.
|
||||
Wired up in wagtail_hooks.py.
|
||||
"""
|
||||
|
||||
from django.urls import reverse_lazy
|
||||
from django.utils.html import escape
|
||||
from draftjs_exporter.dom import DOM
|
||||
from wagtail.admin.rich_text.converters.html_to_contentstate import (
|
||||
InlineEntityElementHandler,
|
||||
)
|
||||
from wagtail.admin.rich_text.editors.draftail import features as draftail_features
|
||||
from wagtail.rich_text import LinkHandler
|
||||
|
||||
FEATURE_NAME = "button-link"
|
||||
ENTITY_TYPE = "BUTTON"
|
||||
|
||||
|
||||
def button_link_entity_decorator(props):
|
||||
"""ContentState to DB HTML (keeps just the chosen URL)."""
|
||||
return DOM.create_element(
|
||||
"a",
|
||||
{"linktype": "button", "data-href": props.get("url", "")},
|
||||
props["children"],
|
||||
)
|
||||
|
||||
|
||||
class ButtonLinkEntityElementHandler(InlineEntityElementHandler):
|
||||
"""DB HTML to ContentState."""
|
||||
|
||||
mutability = "MUTABLE"
|
||||
|
||||
def get_attribute_data(self, attrs):
|
||||
return {"url": attrs.get("data-href", "")}
|
||||
|
||||
|
||||
class ButtonLinkRichTextHandler(LinkHandler):
|
||||
"""DB HTML to front-end HTML (expand_db_html)."""
|
||||
|
||||
identifier = "button"
|
||||
|
||||
@classmethod
|
||||
def expand_db_attributes(cls, attrs):
|
||||
href = escape(attrs.get("data-href", ""))
|
||||
return f'<a class="button" href="{href}">'
|
||||
|
||||
|
||||
def register_button_link_feature(features):
|
||||
features.register_editor_plugin(
|
||||
"draftail",
|
||||
FEATURE_NAME,
|
||||
draftail_features.EntityFeature(
|
||||
{
|
||||
"type": ENTITY_TYPE,
|
||||
"icon": "arrow-right-full",
|
||||
"description": "Knapp",
|
||||
# Reuse the link chooser modal + payload, so buttons work like links.
|
||||
"attributes": ["url", "id", "parentId"],
|
||||
"allowlist": {"href": "^(http:|https:|mailto:|tel:|#|/|undefined$)"},
|
||||
"chooserUrls": {
|
||||
"pageChooser": reverse_lazy("wagtailadmin_choose_page"),
|
||||
"externalLinkChooser": reverse_lazy("wagtailadmin_choose_page_external_link"),
|
||||
"emailLinkChooser": reverse_lazy("wagtailadmin_choose_page_email_link"),
|
||||
"phoneLinkChooser": reverse_lazy("wagtailadmin_choose_page_phone_link"),
|
||||
"anchorLinkChooser": reverse_lazy("wagtailadmin_choose_page_anchor_link"),
|
||||
},
|
||||
},
|
||||
js=[
|
||||
# Gives LinkModalWorkflowSource its PAGE_CHOOSER_MODAL_ONLOAD_HANDLERS.
|
||||
"wagtailadmin/js/page-chooser-modal.js",
|
||||
"js/draftail-button-link.js",
|
||||
],
|
||||
css={"all": ["css/draftail-button-link.css"]},
|
||||
),
|
||||
)
|
||||
features.register_converter_rule(
|
||||
"contentstate",
|
||||
FEATURE_NAME,
|
||||
{
|
||||
"from_database_format": {
|
||||
'a[linktype="button"]': ButtonLinkEntityElementHandler(ENTITY_TYPE),
|
||||
},
|
||||
"to_database_format": {
|
||||
"entity_decorators": {ENTITY_TYPE: button_link_entity_decorator},
|
||||
},
|
||||
},
|
||||
)
|
||||
features.register_link_type(ButtonLinkRichTextHandler)
|
||||
features.default_features.append(FEATURE_NAME)
|
||||
@@ -12,7 +12,6 @@ https://docs.djangoproject.com/en/4.2/ref/settings/
|
||||
|
||||
# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
|
||||
import os
|
||||
import re
|
||||
|
||||
PROJECT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
BASE_DIR = os.path.dirname(PROJECT_DIR)
|
||||
@@ -37,9 +36,11 @@ INSTALLED_APPS = [
|
||||
"news",
|
||||
"openinghours",
|
||||
"sponsors",
|
||||
"studio",
|
||||
# end cms apps
|
||||
"grapple",
|
||||
"graphene_django",
|
||||
"wagtail_headless_preview",
|
||||
"wagtail.contrib.forms",
|
||||
"wagtail.contrib.redirects",
|
||||
"wagtail.contrib.settings",
|
||||
@@ -142,6 +143,8 @@ USE_L10N = True
|
||||
|
||||
USE_TZ = True
|
||||
|
||||
LOCALE_PATHS = [os.path.join(BASE_DIR, "locale")]
|
||||
|
||||
|
||||
# Static files (CSS, JavaScript, Images)
|
||||
# https://docs.djangoproject.com/en/4.2/howto/static-files/
|
||||
@@ -170,9 +173,13 @@ 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
|
||||
|
||||
WAGTAILIMAGES_IMAGE_MODEL = "images.CustomImage"
|
||||
WAGTAILIMAGES_EXTENSIONS = ["avif", "gif", "jpg", "jpeg", "png", "webp", "svg"]
|
||||
|
||||
# Search
|
||||
# https://docs.wagtail.org/en/stable/topics/search/backends.html
|
||||
@@ -183,11 +190,26 @@ WAGTAILSEARCH_BACKENDS = {
|
||||
}
|
||||
|
||||
# Base URL to use when referring to full URLs within the Wagtail admin backend -
|
||||
# e.g. in notification emails. Don't include '/admin' or a trailing slash
|
||||
WAGTAILADMIN_BASE_URL = "http://example.com"
|
||||
# e.g. in notification emails. Don't include '/admin' or a trailing slash.
|
||||
# Also used by wagtail-grapple to make image URLs absolute.
|
||||
WAGTAIL_BASE_URL = os.environ.get("WAGTAIL_BASE_URL", "http://127.0.0.1:8000").rstrip("/")
|
||||
WAGTAILADMIN_BASE_URL = WAGTAIL_BASE_URL
|
||||
BASE_URL = WAGTAIL_BASE_URL
|
||||
|
||||
# Required by wagtail-grapple to make image URLs absolute
|
||||
BASE_URL = "http://example.com"
|
||||
# Public URL of the Next.js frontend. Used to direct preview iframes and to
|
||||
# redirect "View Live" clicks on the CMS host over to the headless frontend.
|
||||
FRONTEND_BASE_URL = os.environ.get("FRONTEND_BASE_URL", "http://localhost:3000").rstrip("/")
|
||||
|
||||
# Shared secret for the frontend cache purge webhook (see dnscms/revalidation.py).
|
||||
# Unset means no webhooks are sent.
|
||||
REVALIDATE_WEBHOOK_SECRET = os.environ.get("REVALIDATE_WEBHOOK_SECRET", "")
|
||||
|
||||
WAGTAIL_HEADLESS_PREVIEW = {
|
||||
"CLIENT_URLS": {"default": f"{FRONTEND_BASE_URL}/api/preview"},
|
||||
"SERVE_BASE_URL": FRONTEND_BASE_URL,
|
||||
"ENFORCE_TRAILING_SLASH": False,
|
||||
"REDIRECT_ON_PREVIEW": False,
|
||||
}
|
||||
|
||||
# https://docs.wagtail.org/en/latest/releases/6.4.html#data-upload-max-number-fields-update
|
||||
DATA_UPLOAD_MAX_NUMBER_FIELDS = 10_000
|
||||
@@ -206,79 +228,9 @@ GRAPPLE = {
|
||||
"news",
|
||||
"openinghours",
|
||||
"sponsors",
|
||||
"studio",
|
||||
],
|
||||
"EXPOSE_GRAPHIQL": True,
|
||||
"PAGE_SIZE": 100,
|
||||
"MAX_PAGE_SIZE": 5000,
|
||||
}
|
||||
|
||||
# Wgtail WordPress import
|
||||
WAGTAIL_WORDPRESS_IMPORTER_SOURCE_DOMAIN = "https://studentersamfundet.no/"
|
||||
WAGTAIL_WORDPRESS_IMPORTER_CONVERT_HTML_TAGS_TO_BLOCKS = {
|
||||
# "h1": "wagtail_wordpress_import.block_builder_defaults.build_heading_block",
|
||||
"table": "wagtail_wordpress_import.block_builder_defaults.build_table_block",
|
||||
# "iframe": "wagtail_wordpress_import.block_builder_defaults.build_iframe_block",
|
||||
# "form": "wagtail_wordpress_import.block_builder_defaults.build_form_block",
|
||||
# "img": "wagtail_wordpress_import.block_builder_defaults.build_image_block",
|
||||
# "blockquote": "wagtail_wordpress_import.block_builder_defaults.build_block_quote_block",
|
||||
}
|
||||
WAGTAIL_WORDPRESS_IMPORTER_FALLBACK_BLOCK = (
|
||||
"dnscms.wordpress.block_builder.build_richtext_block_content"
|
||||
)
|
||||
WORDPRESS_IMPORT_HOOKS_ITEMS_TO_CACHE = {
|
||||
"attachment": {
|
||||
"DATA_TAG": "thumbnail_id",
|
||||
"FUNCTION": "dnscms.wordpress.import_hooks.header_image_processor",
|
||||
}
|
||||
}
|
||||
# WORDPRESS_IMPORT_HOOKS_TAGS_TO_CACHE = {
|
||||
# "wp:term": {
|
||||
# "DATA_TAG": "category",
|
||||
# "FUNCTION": "dnscms.wordpress.import_hooks.categories_processor",
|
||||
# }
|
||||
# }
|
||||
WAGTAIL_WORDPRESS_IMPORTER_PROMOTE_CHILD_TAGS = {
|
||||
"TAGS_TO_PROMOTE": [],
|
||||
"PARENTS_TO_REMOVE": ["p", "div", "span"],
|
||||
}
|
||||
WAGTAIL_WORDPRESS_IMPORT_PREFILTERS = [
|
||||
{
|
||||
"FUNCTION": "wagtail_wordpress_import.prefilters.linebreaks_wp",
|
||||
},
|
||||
{
|
||||
"FUNCTION": "wagtail_wordpress_import.prefilters.transform_shortcodes",
|
||||
},
|
||||
{
|
||||
"FUNCTION": "wagtail_wordpress_import.prefilters.transform_inline_styles",
|
||||
"OPTIONS": {
|
||||
"TRANSFORM_STYLES_MAPPING": [
|
||||
(
|
||||
re.compile(r"font-style:italic;font-weight:bold;", re.IGNORECASE),
|
||||
"wagtail_wordpress_import.prefilters.transform_styles_defaults.transform_style_bold_italic",
|
||||
),
|
||||
(
|
||||
re.compile(r"font-weight:bold;", re.IGNORECASE),
|
||||
"wagtail_wordpress_import.prefilters.transform_styles_defaults.transform_style_bold",
|
||||
),
|
||||
(
|
||||
re.compile(r"font-style:italic;", re.IGNORECASE),
|
||||
"wagtail_wordpress_import.prefilters.transform_styles_defaults.transform_style_italic",
|
||||
),
|
||||
# (
|
||||
# re.compile(
|
||||
# r"text-align:center;",
|
||||
# re.IGNORECASE,
|
||||
# ),
|
||||
# transform_style_center,
|
||||
# ),
|
||||
# (re.compile(r"text-align:left;", re.IGNORECASE), transform_style_left),
|
||||
# (re.compile(r"text-align:right;", re.IGNORECASE), transform_style_right),
|
||||
# (re.compile(r"float:left;", re.IGNORECASE), transform_float_left),
|
||||
# (re.compile(r"float:right;", re.IGNORECASE), transform_float_right),
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
"FUNCTION": "wagtail_wordpress_import.prefilters.bleach_clean",
|
||||
},
|
||||
]
|
||||
|
||||
@@ -11,13 +11,6 @@ ALLOWED_HOSTS = ["*"]
|
||||
|
||||
EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend"
|
||||
|
||||
# Base URL to use when referring to full URLs within the Wagtail admin backend -
|
||||
# e.g. in notification emails. Don't include '/admin' or a trailing slash
|
||||
WAGTAILADMIN_BASE_URL = "http://127.0.0.1:8000"
|
||||
|
||||
# Required by wagtail-grapple to make image URLs absolute
|
||||
BASE_URL = "http://127.0.0.1:8000"
|
||||
|
||||
try:
|
||||
from .local import *
|
||||
except ImportError:
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
from .base import * # noqa: F401, F403
|
||||
|
||||
SECRET_KEY = "test-secret-key"
|
||||
DEBUG = False
|
||||
ALLOWED_HOSTS = ["*"]
|
||||
|
||||
DATABASES = {
|
||||
"default": {
|
||||
"ENGINE": "django.db.backends.sqlite3",
|
||||
"NAME": ":memory:",
|
||||
}
|
||||
}
|
||||
|
||||
STORAGES = {
|
||||
"default": {
|
||||
"BACKEND": "django.core.files.storage.FileSystemStorage",
|
||||
},
|
||||
"staticfiles": {
|
||||
"BACKEND": "django.contrib.staticfiles.storage.StaticFilesStorage",
|
||||
},
|
||||
}
|
||||
|
||||
PASSWORD_HASHERS = ["django.contrib.auth.hashers.MD5PasswordHasher"]
|
||||
@@ -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,17 @@
|
||||
/* Styles the button-link entity as a button in the editor. Front end uses the global .button class. */
|
||||
.button-link-entity .TooltipEntity {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.25em;
|
||||
padding: 0.1em 0.7em;
|
||||
border: 1.5px solid currentColor;
|
||||
border-radius: 999px;
|
||||
font-weight: 600;
|
||||
text-decoration: none;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.button-link-entity .TooltipEntity__icon {
|
||||
width: 0.85em;
|
||||
height: 0.85em;
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
/*
|
||||
* Draftail plugin for the "button-link" feature (see dnscms/rich_text.py).
|
||||
* Reuses Wagtail's link chooser (source) and edit/remove tooltip (decorator),
|
||||
* so buttons insert and edit like normal links. No build step: uses Wagtail's
|
||||
* globals and React.createElement.
|
||||
*/
|
||||
(function () {
|
||||
"use strict";
|
||||
|
||||
var draftail = window.draftail;
|
||||
var React = window.React;
|
||||
var Icon = window.wagtail.components.Icon;
|
||||
var TooltipEntity = draftail.TooltipEntity;
|
||||
|
||||
// Wrap TooltipEntity so CSS can style it as a button
|
||||
function ButtonLink(props) {
|
||||
var url = props.contentState.getEntity(props.entityKey).getData().url || null;
|
||||
return React.createElement(
|
||||
"span",
|
||||
{ className: "button-link-entity" },
|
||||
React.createElement(
|
||||
TooltipEntity,
|
||||
Object.assign({}, props, {
|
||||
icon: React.createElement(Icon, { name: "arrow-right-full" }),
|
||||
label: url || "",
|
||||
url: url,
|
||||
})
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
draftail.registerPlugin(
|
||||
{
|
||||
type: "BUTTON",
|
||||
source: draftail.LinkModalWorkflowSource,
|
||||
decorator: ButtonLink,
|
||||
},
|
||||
"entityTypes"
|
||||
);
|
||||
})();
|
||||
@@ -16,6 +16,76 @@ function registerPricingPanelToggler() {
|
||||
}
|
||||
}
|
||||
|
||||
// Adds a "Duplicate" button to each event occurrence ("Forekomst") row, copying
|
||||
// its date, time and venue. Wagtail's InlinePanel has no native duplicate.
|
||||
function registerOccurrenceCloning() {
|
||||
const FORMSET_PREFIX = "occurrences";
|
||||
const container = document.getElementById(`id_${FORMSET_PREFIX}-FORMS`);
|
||||
if (!container) return; // not an event page
|
||||
|
||||
const FIELDS = ["start", "end", "venue", "venue_custom"];
|
||||
const duplicateIcon =
|
||||
'<svg class="icon icon-copy" aria-hidden="true"><use href="#icon-copy"></use></svg>';
|
||||
|
||||
function duplicateRow(sourceChild) {
|
||||
const addButton = document.getElementById(`id_${FORMSET_PREFIX}-ADD`);
|
||||
const totalForms = document.getElementById(
|
||||
`id_${FORMSET_PREFIX}-TOTAL_FORMS`
|
||||
);
|
||||
if (!addButton || !totalForms) return;
|
||||
|
||||
// `inline_child_occurrences-0` -> form prefix `occurrences-0`, used by field ids.
|
||||
const sourcePrefix = sourceChild.id.replace(/^inline_child_/, "");
|
||||
const values = {};
|
||||
FIELDS.forEach((name) => {
|
||||
const field = document.getElementById(`id_${sourcePrefix}-${name}`);
|
||||
values[name] = field ? field.value : "";
|
||||
});
|
||||
|
||||
// The new form takes the current TOTAL_FORMS index. Click Wagtail's real
|
||||
// "Add" so it appends and initialises the row (e.g. date/time pickers).
|
||||
const newIndex = parseInt(totalForms.value, 10);
|
||||
addButton.click();
|
||||
|
||||
const newPrefix = `${FORMSET_PREFIX}-${newIndex}`;
|
||||
FIELDS.forEach((name) => {
|
||||
const field = document.getElementById(`id_${newPrefix}-${name}`);
|
||||
if (!field) return;
|
||||
field.value = values[name];
|
||||
field.dispatchEvent(new Event("input", { bubbles: true }));
|
||||
field.dispatchEvent(new Event("change", { bubbles: true }));
|
||||
});
|
||||
}
|
||||
|
||||
function addDuplicateButton(child) {
|
||||
const controls = child.querySelector("[data-panel-controls]");
|
||||
if (!controls || controls.querySelector("[data-duplicate-occurrence]")) return;
|
||||
|
||||
const button = document.createElement("button");
|
||||
button.type = "button";
|
||||
button.className = "button button--icon text-replace white";
|
||||
button.title = "Duplicate";
|
||||
button.setAttribute("data-duplicate-occurrence", "");
|
||||
button.innerHTML = duplicateIcon;
|
||||
button.addEventListener("click", () => duplicateRow(child));
|
||||
|
||||
// Sit just before the delete button, matching the native control order.
|
||||
const deleteButton = controls.querySelector('[id$="-DELETE-button"]');
|
||||
controls.insertBefore(button, deleteButton);
|
||||
}
|
||||
|
||||
function refresh() {
|
||||
container
|
||||
.querySelectorAll("[data-inline-panel-child]")
|
||||
.forEach((child) => addDuplicateButton(child));
|
||||
}
|
||||
|
||||
// Cover existing rows plus any added later (via "Add" or "Duplicate").
|
||||
refresh();
|
||||
new MutationObserver(() => refresh()).observe(container, { childList: true });
|
||||
}
|
||||
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
registerPricingPanelToggler();
|
||||
registerOccurrenceCloning();
|
||||
});
|
||||
|
||||
@@ -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,13 +1,11 @@
|
||||
from django.contrib.admin.utils import quote
|
||||
from django.templatetags.static import static
|
||||
from django.urls import reverse
|
||||
from django.utils.html import format_html
|
||||
from grapple.registry import registry as grapple_registry
|
||||
from wagtail import hooks
|
||||
from wagtail.admin.menu import MenuItem
|
||||
from wagtail.models import Page
|
||||
from wagtail.search.backends import get_search_backend
|
||||
|
||||
from associations.models import AssociationIndex
|
||||
from events.models import EventIndex
|
||||
from news.models import NewsIndex
|
||||
from dnscms.rich_text import register_button_link_feature
|
||||
|
||||
|
||||
@hooks.register("register_rich_text_features")
|
||||
@@ -15,31 +13,43 @@ def enable_additional_rich_text_features(features):
|
||||
features.default_features.extend(["h5", "h6", "blockquote"])
|
||||
|
||||
|
||||
@hooks.register("register_admin_menu_item")
|
||||
def register_events_menu_item():
|
||||
page = EventIndex.objects.first()
|
||||
events_url = "#"
|
||||
if page:
|
||||
events_url = reverse("wagtailadmin_explore", args=(quote(page.pk),))
|
||||
return MenuItem("Arrangementer", events_url, icon_name="date", order=1)
|
||||
@hooks.register("register_rich_text_features")
|
||||
def register_button_link(features):
|
||||
register_button_link_feature(features)
|
||||
|
||||
|
||||
@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_schema_query")
|
||||
def override_search_resolver(query_mixins):
|
||||
"""
|
||||
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
|
||||
|
||||
@hooks.register("register_admin_menu_item")
|
||||
def register_associations_menu_item():
|
||||
page = NewsIndex.objects.first()
|
||||
news_url = "#"
|
||||
if page:
|
||||
news_url = reverse("wagtailadmin_explore", args=(quote(page.pk),))
|
||||
return MenuItem("Nyheter", news_url, icon_name="info-circle", order=3)
|
||||
class SearchOverrideMixin:
|
||||
def resolve_search(self, info, **kwargs):
|
||||
query = kwargs.get("query")
|
||||
if not query:
|
||||
return None
|
||||
s = get_search_backend()
|
||||
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, SearchOverrideMixin)
|
||||
|
||||
|
||||
@hooks.register("construct_page_action_menu")
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
from django.conf import settings
|
||||
from wagtail_wordpress_import.block_builder_defaults import (
|
||||
document_linker,
|
||||
image_linker,
|
||||
import_string,
|
||||
)
|
||||
|
||||
|
||||
def build_richtext_block_content(html, blocks):
|
||||
"""
|
||||
image_linker is called to link up and retrive the remote image
|
||||
document_linker is called to link up and retrive the remote documents
|
||||
filters are called to replace inline shortcodes
|
||||
"""
|
||||
html = image_linker(html)
|
||||
html = document_linker(html)
|
||||
for inline_shortcode_handler in getattr(
|
||||
settings, "WAGTAIL_WORDPRESS_IMPORTER_INLINE_SHORTCODE_HANDLERS", []
|
||||
):
|
||||
function = import_string(inline_shortcode_handler).construct_html_tag
|
||||
html = function(html)
|
||||
blocks.append({"type": "paragraph", "value": html})
|
||||
html = ""
|
||||
return html
|
||||
@@ -1,51 +0,0 @@
|
||||
from wagtail_wordpress_import.block_builder_defaults import get_or_save_image
|
||||
|
||||
|
||||
def header_image_processor(imported_pages, data_tag, items_cache):
|
||||
"""
|
||||
imported_pages:
|
||||
Is a specific() page model queryset of all imported pages.
|
||||
data_tag:
|
||||
Is the value of the `DATA_TAG` key from the configuration above.
|
||||
items_cache:
|
||||
Is a list of dictionaries, one for each item in the XML file.
|
||||
"""
|
||||
|
||||
# See note above about leading _ and : characters in the XML value
|
||||
lookup = f"wp_post_meta__{data_tag}"
|
||||
|
||||
for attachment in items_cache:
|
||||
# The id of the cached item used in the filter
|
||||
thumbnail_id = attachment.get("wp:post_id")
|
||||
|
||||
# Filter the imported_pages for only pages that include the
|
||||
# matching thumbnail_id in the wp_post_meta field
|
||||
pages = imported_pages.filter(**{lookup: thumbnail_id})
|
||||
|
||||
if pages.exists():
|
||||
# guid is the url of the image to fetch, the get_or_save_image()
|
||||
# function will fetch the image if it doesn't exist
|
||||
image_url = attachment.get("guid")
|
||||
# fix cases where the /wp prefix is missing from the image url
|
||||
if image_url.startswith("https://studentersamfundet.no/wp-content/uploads/"):
|
||||
image_url = image_url.replace(
|
||||
"https://studentersamfundet.no/wp-content/uploads/",
|
||||
"https://studentersamfundet.no/wp/wp-content/uploads/",
|
||||
)
|
||||
try:
|
||||
image = get_or_save_image(image_url)
|
||||
except Exception as e:
|
||||
print("Error with image", image_url, "associated with pages:", pages)
|
||||
print(e)
|
||||
continue
|
||||
|
||||
print("Attaching header images to pages:", pages)
|
||||
try:
|
||||
pages.update(featured_image=image)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
try:
|
||||
pages.update(logo=image)
|
||||
except Exception:
|
||||
pass
|
||||
@@ -1,17 +1,84 @@
|
||||
from django.core.exceptions import FieldDoesNotExist
|
||||
from django.db import models
|
||||
from wagtail.models import Page
|
||||
|
||||
# Field names declared by WPImportedPageMixin. Concrete models that mix it in
|
||||
# get a manager that .defer()s these so they're never loaded by default — the
|
||||
# columns stay in the database (no migration), and any code path that
|
||||
# explicitly reads them still works via Django's lazy-load.
|
||||
WP_IMPORT_FIELDS = (
|
||||
"wp_post_id",
|
||||
"wp_post_type",
|
||||
"wp_link",
|
||||
"wp_raw_content",
|
||||
"wp_processed_content",
|
||||
"wp_block_json",
|
||||
"wp_normalized_styles",
|
||||
"wp_post_meta",
|
||||
)
|
||||
|
||||
|
||||
# https://github.com/wagtail/wagtail-wordpress-import/blob/main/wagtail_wordpress_import/models.py
|
||||
# DJ001 (null=True on string fields) is suppressed: the schema mirrors the
|
||||
# upstream mixin and changing nullability would force a migration we avoid.
|
||||
class WPImportedPageMixin(Page):
|
||||
wp_post_id = models.IntegerField(blank=True, null=True)
|
||||
wp_post_type = models.CharField(max_length=255, blank=True, null=True)
|
||||
wp_link = models.TextField(blank=True, null=True)
|
||||
wp_raw_content = models.TextField(blank=True, null=True)
|
||||
wp_processed_content = models.TextField(blank=True, null=True)
|
||||
wp_block_json = models.TextField(blank=True, null=True)
|
||||
wp_normalized_styles = models.TextField(blank=True, null=True)
|
||||
wp_post_type = models.CharField(max_length=255, blank=True, null=True) # noqa: DJ001
|
||||
wp_link = models.TextField(blank=True, null=True) # noqa: DJ001
|
||||
wp_raw_content = models.TextField(blank=True, null=True) # noqa: DJ001
|
||||
wp_processed_content = models.TextField(blank=True, null=True) # noqa: DJ001
|
||||
wp_block_json = models.TextField(blank=True, null=True) # noqa: DJ001
|
||||
wp_normalized_styles = models.TextField(blank=True, null=True) # noqa: DJ001
|
||||
wp_post_meta = models.JSONField(blank=True, null=True)
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
|
||||
|
||||
class DeferWPFieldsManagerMixin:
|
||||
"""
|
||||
Manager mixin that always .defer()s the wp_* import columns, so they are
|
||||
never SELECTed by default queries. The columns remain in the database;
|
||||
accessing one on an instance still works (Django lazy-loads it).
|
||||
"""
|
||||
|
||||
def get_queryset(self):
|
||||
return super().get_queryset().defer(*WP_IMPORT_FIELDS)
|
||||
|
||||
|
||||
def _resolve_related_model(model, lookup_path):
|
||||
"""Walk a ``foo__bar`` lookup path and return the final related model, or None."""
|
||||
current = model
|
||||
for part in lookup_path.split("__"):
|
||||
try:
|
||||
field = current._meta.get_field(part)
|
||||
except FieldDoesNotExist:
|
||||
return None
|
||||
current = getattr(field, "related_model", None)
|
||||
if current is None:
|
||||
return None
|
||||
return current
|
||||
|
||||
|
||||
class WPAwareQuerySet(models.QuerySet):
|
||||
"""
|
||||
QuerySet whose ``select_related()`` auto-defers wp_* columns when a join
|
||||
targets a WPImportedPageMixin model. Without this, ``select_related``
|
||||
builds a JOIN that ignores the related model's manager and SELECTs every
|
||||
column including the wp_* blobs. Apply via ``objects = WPAwareManager()``
|
||||
on any model that has a ForeignKey into a WPImported page.
|
||||
"""
|
||||
|
||||
def select_related(self, *fields):
|
||||
qs = super().select_related(*fields)
|
||||
defers = [
|
||||
f"{path}__{name}"
|
||||
for path in fields
|
||||
if (related := _resolve_related_model(qs.model, path)) is not None
|
||||
and issubclass(related, WPImportedPageMixin)
|
||||
for name in WP_IMPORT_FIELDS
|
||||
]
|
||||
return qs.defer(*defers) if defers else qs
|
||||
|
||||
|
||||
WPAwareManager = models.Manager.from_queryset(WPAwareQuerySet)
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
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.listing import ExplorableIndexView, IndexView
|
||||
from wagtail.admin.viewsets.pages import PageListingViewSet, PageViewSet
|
||||
|
||||
from dnscms.admin import ListingRedirectChooseParentView
|
||||
from events.models import EventPage
|
||||
|
||||
|
||||
class EventDateColumn(Column):
|
||||
def get_value(self, instance):
|
||||
occurrences = list(instance.occurrences.order_by("start"))
|
||||
if not occurrences:
|
||||
return "—"
|
||||
if len(occurrences) == 1:
|
||||
local = timezone.localtime(occurrences[0].start)
|
||||
return local.strftime(gettext("%Y-%m-%d at %H:%M"))
|
||||
count = len(occurrences)
|
||||
return ngettext("%(count)d occurrence", "%(count)d occurrences", count) % {"count": count}
|
||||
|
||||
|
||||
class OrganizersColumn(Column):
|
||||
def get_value(self, instance):
|
||||
names = list(instance.organizers.values_list("name", flat=True))
|
||||
if not names:
|
||||
return "—"
|
||||
if len(names) == 1:
|
||||
return names[0]
|
||||
return f"{names[0]} (+{len(names) - 1})"
|
||||
|
||||
|
||||
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(
|
||||
"occurrences",
|
||||
"organizer_links__organizer",
|
||||
)
|
||||
|
||||
|
||||
class EventPageIndexView(EventPagePrefetchMixin, IndexView):
|
||||
pass
|
||||
|
||||
|
||||
class EventPageExplorableIndexView(EventPagePrefetchMixin, ExplorableIndexView):
|
||||
pass
|
||||
|
||||
|
||||
class EventChooseParentView(ListingRedirectChooseParentView):
|
||||
listing_url_name = "events:index"
|
||||
|
||||
|
||||
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%"),
|
||||
OrganizersColumn("organizers", label=_("Organizers"), width="12%"),
|
||||
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 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()
|
||||
@@ -0,0 +1,221 @@
|
||||
from django import forms
|
||||
from django.contrib import messages
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db import transaction
|
||||
from django.shortcuts import redirect
|
||||
from django.urls import reverse
|
||||
from django.utils.http import urlencode
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.views.generic import FormView
|
||||
from wagtail.admin.views.generic.base import WagtailAdminTemplateMixin
|
||||
from wagtail.models import ReferenceIndex, Revision
|
||||
|
||||
from dnscms.utils import slugify
|
||||
from events.models import EventOrganizer, EventOrganizerLink, EventPage
|
||||
from events.views import event_organizer_chooser_viewset
|
||||
|
||||
|
||||
def _check_relations_are_handled():
|
||||
"""Refuse to merge if EventOrganizer has gained a relation this module doesn't handle."""
|
||||
for relation in EventOrganizer._meta.related_objects:
|
||||
if relation.many_to_many and relation.through is EventOrganizerLink:
|
||||
continue
|
||||
if relation.related_model is EventOrganizerLink and relation.field.name == "organizer":
|
||||
continue
|
||||
raise NotImplementedError(
|
||||
f"merge_event_organizers does not handle the {relation.name!r} relation"
|
||||
)
|
||||
|
||||
|
||||
def _repoint_links(survivor, loser):
|
||||
loser_links = EventOrganizerLink.objects.filter(organizer=loser)
|
||||
affected_event_ids = set(loser_links.values_list("event_id", flat=True))
|
||||
# drop the loser's link where the event links both, to respect the unique constraint
|
||||
doubly_linked = EventOrganizerLink.objects.filter(
|
||||
organizer=survivor, event_id__in=affected_event_ids
|
||||
).values_list("event_id", flat=True)
|
||||
loser_links.filter(event_id__in=doubly_linked).delete()
|
||||
loser_links.update(organizer=survivor)
|
||||
return affected_event_ids
|
||||
|
||||
|
||||
def _rewrite_revisions(survivor, loser):
|
||||
"""Rewrite organizer ids in revision JSON, which would otherwise dangle after the delete."""
|
||||
affected_event_ids = set()
|
||||
event_content_type = ContentType.objects.get_for_model(EventPage)
|
||||
for revision in Revision.objects.filter(content_type=event_content_type).iterator():
|
||||
links = revision.content.get("organizer_links") or []
|
||||
if not any(link.get("organizer") == loser.pk for link in links):
|
||||
continue
|
||||
seen_organizer_ids = set()
|
||||
rewritten = []
|
||||
for link in links:
|
||||
if link.get("organizer") == loser.pk:
|
||||
link = {**link, "organizer": survivor.pk}
|
||||
if link.get("organizer") in seen_organizer_ids:
|
||||
continue
|
||||
seen_organizer_ids.add(link.get("organizer"))
|
||||
rewritten.append(link)
|
||||
revision.content["organizer_links"] = rewritten
|
||||
revision.save(update_fields=["content"])
|
||||
affected_event_ids.add(int(revision.object_id))
|
||||
return affected_event_ids
|
||||
|
||||
|
||||
@transaction.atomic
|
||||
def merge_event_organizers(organizer_a, organizer_b, *, name=None, slug=None):
|
||||
"""
|
||||
Merge two organizers, keeping the one with the lowest primary key. Event
|
||||
links and event page revisions are repointed before the other organizer is
|
||||
deleted. ``name``/``slug`` override the survivor's fields; a blank
|
||||
``association``/``external_url`` is filled in from the deleted organizer.
|
||||
"""
|
||||
if organizer_a.pk == organizer_b.pk:
|
||||
raise ValueError("Cannot merge an organizer with itself.")
|
||||
_check_relations_are_handled()
|
||||
|
||||
survivor, loser = sorted([organizer_a, organizer_b], key=lambda organizer: organizer.pk)
|
||||
|
||||
affected_event_ids = _repoint_links(survivor, loser)
|
||||
affected_event_ids |= _rewrite_revisions(survivor, loser)
|
||||
|
||||
if name:
|
||||
survivor.name = name
|
||||
if slug:
|
||||
survivor.slug = slug
|
||||
if not survivor.association_id:
|
||||
survivor.association_id = loser.association_id
|
||||
if not survivor.external_url:
|
||||
survivor.external_url = loser.external_url
|
||||
survivor.save()
|
||||
|
||||
loser.delete()
|
||||
|
||||
# the bulk link update bypasses the save-time reference index maintenance
|
||||
for event in EventPage.objects.filter(pk__in=affected_event_ids):
|
||||
ReferenceIndex.create_or_update_for_object(event)
|
||||
|
||||
return survivor
|
||||
|
||||
|
||||
CUSTOM_VALUE = "__custom__"
|
||||
|
||||
|
||||
class MergeOrganizersSelectForm(forms.Form):
|
||||
organizer_a = forms.ModelChoiceField(
|
||||
label=_("First organizer"),
|
||||
queryset=EventOrganizer.objects.all(),
|
||||
widget=event_organizer_chooser_viewset.widget_class(),
|
||||
)
|
||||
organizer_b = forms.ModelChoiceField(
|
||||
label=_("Second organizer"),
|
||||
queryset=EventOrganizer.objects.all(),
|
||||
widget=event_organizer_chooser_viewset.widget_class(),
|
||||
)
|
||||
|
||||
def clean(self):
|
||||
cleaned = super().clean()
|
||||
organizer_a = cleaned.get("organizer_a")
|
||||
organizer_b = cleaned.get("organizer_b")
|
||||
if organizer_a and organizer_b and organizer_a.pk == organizer_b.pk:
|
||||
raise ValidationError(_("Pick two different organizers."))
|
||||
return cleaned
|
||||
|
||||
|
||||
class MergeOrganizersConfirmForm(forms.Form):
|
||||
name = forms.ChoiceField(label=_("Name to keep"), widget=forms.RadioSelect)
|
||||
name_custom = forms.CharField(label=_("Custom name"), required=False, max_length=100)
|
||||
slug = forms.ChoiceField(label=_("Slug to keep"), widget=forms.RadioSelect)
|
||||
slug_custom = forms.CharField(label=_("Custom slug"), required=False, max_length=255)
|
||||
|
||||
def __init__(self, *args, survivor, loser, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.fields["name"].choices = self._choices(survivor.name, loser.name)
|
||||
self.fields["name"].initial = survivor.name
|
||||
self.fields["slug"].choices = self._choices(survivor.slug, loser.slug)
|
||||
self.fields["slug"].initial = survivor.slug
|
||||
|
||||
@staticmethod
|
||||
def _choices(*values):
|
||||
unique_values = dict.fromkeys(values)
|
||||
return [(value, value) for value in unique_values] + [
|
||||
(CUSTOM_VALUE, _("Something else (enter below)"))
|
||||
]
|
||||
|
||||
def _resolve(self, cleaned, field):
|
||||
choice = cleaned.get(field)
|
||||
custom = (cleaned.get(f"{field}_custom") or "").strip()
|
||||
if choice == CUSTOM_VALUE:
|
||||
if not custom:
|
||||
self.add_error(
|
||||
f"{field}_custom", _("Enter a value, or pick an existing one above.")
|
||||
)
|
||||
return custom
|
||||
return choice
|
||||
|
||||
def clean(self):
|
||||
cleaned = super().clean()
|
||||
cleaned["final_name"] = self._resolve(cleaned, "name")
|
||||
cleaned["final_slug"] = slugify(self._resolve(cleaned, "slug") or "")
|
||||
return cleaned
|
||||
|
||||
|
||||
class MergeOrganizersSelectView(WagtailAdminTemplateMixin, FormView):
|
||||
template_name = "events/merge_organizers_select.html"
|
||||
form_class = MergeOrganizersSelectForm
|
||||
page_title = _("Merge organizers")
|
||||
header_icon = "group"
|
||||
|
||||
def form_valid(self, form):
|
||||
params = urlencode(
|
||||
{
|
||||
"organizer_a": form.cleaned_data["organizer_a"].pk,
|
||||
"organizer_b": form.cleaned_data["organizer_b"].pk,
|
||||
}
|
||||
)
|
||||
return redirect(f"{reverse('events_merge_organizers_confirm')}?{params}")
|
||||
|
||||
|
||||
class MergeOrganizersConfirmView(WagtailAdminTemplateMixin, FormView):
|
||||
template_name = "events/merge_organizers_confirm.html"
|
||||
form_class = MergeOrganizersConfirmForm
|
||||
page_title = _("Merge organizers")
|
||||
header_icon = "group"
|
||||
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
try:
|
||||
pks = {int(request.GET.get(param, "")) for param in ("organizer_a", "organizer_b")}
|
||||
except ValueError:
|
||||
pks = set()
|
||||
organizers = list(EventOrganizer.objects.filter(pk__in=pks).order_by("pk"))
|
||||
if len(organizers) != 2:
|
||||
messages.error(request, _("Pick two different organizers to merge."))
|
||||
return redirect("events_merge_organizers")
|
||||
self.survivor, self.loser = organizers
|
||||
return super().dispatch(request, *args, **kwargs)
|
||||
|
||||
def get_form_kwargs(self):
|
||||
return {**super().get_form_kwargs(), "survivor": self.survivor, "loser": self.loser}
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
context["survivor"] = self.survivor
|
||||
context["loser"] = self.loser
|
||||
context["survivor_event_count"] = self.survivor.organized_events.count()
|
||||
context["loser_event_count"] = self.loser.organized_events.count()
|
||||
return context
|
||||
|
||||
def form_valid(self, form):
|
||||
survivor = merge_event_organizers(
|
||||
self.survivor,
|
||||
self.loser,
|
||||
name=form.cleaned_data["final_name"],
|
||||
slug=form.cleaned_data["final_slug"],
|
||||
)
|
||||
messages.success(
|
||||
self.request,
|
||||
_('Merged "%(loser)s" into "%(survivor)s".')
|
||||
% {"loser": self.loser.name, "survivor": survivor.name},
|
||||
)
|
||||
return redirect(reverse("wagtailsnippets_events_eventorganizer:list"))
|
||||
@@ -0,0 +1,17 @@
|
||||
# Generated by Django 6.0.5 on 2026-05-19 18:40
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('events', '0053_eventpage_lead'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name='eventpage',
|
||||
options={'verbose_name': 'event', 'verbose_name_plural': 'events'},
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,82 @@
|
||||
# Generated by Django 6.0.5 on 2026-05-22 23:27
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('associations', '0026_alter_association_options'),
|
||||
('events', '0054_alter_eventpage_options'),
|
||||
('images', '0005_customimage_description'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name='eventcategory',
|
||||
options={'ordering': ['name'], 'verbose_name': 'event category', 'verbose_name_plural': 'event categories'},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='eventoccurrence',
|
||||
options={'verbose_name': 'occurrence', 'verbose_name_plural': 'occurrences'},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='eventorganizer',
|
||||
options={'ordering': ['name'], 'verbose_name': 'event organizer', 'verbose_name_plural': 'event organizers'},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='eventorganizerlink',
|
||||
options={'verbose_name': 'organizer', 'verbose_name_plural': 'organizers'},
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='eventcategory',
|
||||
name='pig',
|
||||
field=models.CharField(blank=True, choices=[('', 'None'), ('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='Default pig for events of this kind.', max_length=32),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='eventcategory',
|
||||
name='show_in_filters',
|
||||
field=models.BooleanField(default=False, help_text='Should this category be available as a filter in the event programme?'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='eventoccurrence',
|
||||
name='venue_custom',
|
||||
field=models.CharField(blank=True, help_text='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>.', max_length=128),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='eventorganizer',
|
||||
name='association',
|
||||
field=models.ForeignKey(blank=True, help_text='If a DNS association or committee is behind it, choose it here.', null=True, on_delete=django.db.models.deletion.PROTECT, related_name='organizers', to='associations.associationpage'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='eventorganizer',
|
||||
name='external_url',
|
||||
field=models.URLField(blank=True, help_text="Link to the external organizer's website", max_length=512),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='eventpage',
|
||||
name='facebook_url',
|
||||
field=models.URLField(blank=True, help_text='Direct link to the event on Facebook', max_length=1024),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='eventpage',
|
||||
name='featured_image',
|
||||
field=models.ForeignKey(blank=True, help_text="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 cover uncritically!", null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='images.customimage'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='eventpage',
|
||||
name='pig',
|
||||
field=models.CharField(blank=True, choices=[('', 'None'), ('automatic', 'Automatic'), ('logo', 'Logogrisen'), ('music', 'Musikergrisen'), ('drink', 'Drikkegrisen'), ('dance', 'Dansegrisen'), ('point', 'Pekegrisen'), ('student', 'Studentgrisen'), ('listen', 'Lyttegrisen'), ('guard', 'Vaktgrisen'), ('key', 'Nøkkelgrisen'), ('chill', 'Liggegrisen'), ('peek', 'Tittegrisen')], default='automatic', help_text="The pig that hangs out on the event page. Automatic causes one to be chosen based on the event's category.", max_length=32),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='eventpage',
|
||||
name='subtitle',
|
||||
field=models.CharField(blank=True, help_text='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.', max_length=128),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='eventpage',
|
||||
name='ticket_url',
|
||||
field=models.URLField(blank=True, help_text='Direct link to ticket purchase, e.g. TicketCo, Billetto or Ticketmaster', max_length=1024),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,19 @@
|
||||
# Generated by Django 6.0.5 on 2026-07-02 14:53
|
||||
|
||||
import wagtail.fields
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('events', '0055_alter_eventcategory_options_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='eventpage',
|
||||
name='body',
|
||||
field=wagtail.fields.StreamField([('paragraph', 0), ('image', 4), ('image_slider', 8), ('horizontal_rule', 10), ('featured', 18), ('page_section_navigation', 19), ('accordion', 23), ('fact_box', 26), ('schedule', 33), ('embed', 34), ('raw_html', 35)], 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.blocks.CharBlock', (), {'help_text': 'Overskrift for timeplanen (valgfritt)', 'label': 'Tittel', 'max_length': 64, 'required': False}), 28: ('wagtail.blocks.CharBlock', (), {'help_text': 'Fritekst, f.eks. "13:00", "20:00 – 23:00" eller "23.06"', 'label': 'Tidspunkt/dato', 'max_length': 64, 'required': True}), 29: ('wagtail.blocks.RichTextBlock', (), {'features': ['bold', 'italic', 'link'], 'help_text': 'Kort tittel på én linje. Du kan legge inn lenker.', 'label': 'Tittel', 'required': True}), 30: ('wagtail.blocks.RichTextBlock', (), {'features': ['bold', 'italic', 'link'], 'help_text': 'Valgfri tilleggsinformasjon.', 'label': 'Beskrivelse', 'required': False}), 31: ('wagtail.blocks.StructBlock', [[('time', 28), ('title', 29), ('description', 30)]], {}), 32: ('wagtail.blocks.ListBlock', (31,), {'label': 'Punkter', 'min_num': 1}), 33: ('wagtail.blocks.StructBlock', [[('title', 27), ('items', 32)]], {}), 34: ('wagtail.embeds.blocks.EmbedBlock', (), {}), 35: ('wagtail.blocks.RawHTMLBlock', (), {})}, default=[('paragraph', '')]),
|
||||
),
|
||||
]
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,19 @@
|
||||
# Generated by Django 6.0.5 on 2026-07-04 22:29
|
||||
|
||||
import wagtail.fields
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('events', '0057_alter_eventpage_body'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='eventpage',
|
||||
name='body',
|
||||
field=wagtail.fields.StreamField([('paragraph', 0), ('image', 4), ('image_slider', 8), ('horizontal_rule', 10), ('featured', 18), ('page_section_navigation', 19), ('accordion', 23), ('fact_box', 26), ('schedule', 33), ('embed', 34), ('raw_html', 35)], 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.blocks.CharBlock', (), {'help_text': 'Overskrift for timeplanen (valgfritt)', 'label': 'Tittel', 'max_length': 64, 'required': False}), 28: ('wagtail.blocks.CharBlock', (), {'help_text': 'Fritekst, f.eks. "13:00", "20:00 – 23:00" eller "23.06"', 'label': 'Tidspunkt/dato', 'max_length': 64, 'required': True}), 29: ('wagtail.blocks.RichTextBlock', (), {'features': ['bold', 'italic', 'link'], 'help_text': 'Kort tittel på én linje. Du kan legge inn lenker.', 'label': 'Tittel', 'required': True}), 30: ('wagtail.blocks.RichTextBlock', (), {'features': ['bold', 'italic', 'link'], 'help_text': 'Valgfri tilleggsinformasjon.', 'label': 'Beskrivelse', 'required': False}), 31: ('wagtail.blocks.StructBlock', [[('time', 28), ('title', 29), ('description', 30)]], {}), 32: ('wagtail.blocks.ListBlock', (31,), {'label': 'Punkter', 'min_num': 1}), 33: ('wagtail.blocks.StructBlock', [[('title', 27), ('items', 32)]], {}), 34: ('wagtail.embeds.blocks.EmbedBlock', (), {}), 35: ('wagtail.blocks.RawHTMLBlock', (), {})}, default=[('paragraph', '')]),
|
||||
),
|
||||
]
|
||||
+132
-318
@@ -1,7 +1,7 @@
|
||||
from django import forms
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db import models
|
||||
from django.db.models import Min, Q, UniqueConstraint
|
||||
from django.db.models import Min, Prefetch, Q, UniqueConstraint
|
||||
from django.utils import timezone
|
||||
from django.utils.html import mark_safe
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
@@ -30,21 +30,42 @@ from wagtail.fields import RichTextField
|
||||
from wagtail.models import Orderable, Page, PageManager, PageQuerySet
|
||||
from wagtail.search import index
|
||||
from wagtail.snippets.models import register_snippet
|
||||
from wagtail_headless_preview.models import HeadlessMixin
|
||||
|
||||
from associations.widgets import AssociationChooserWidget
|
||||
from dnscms.fields import CommonStreamField
|
||||
from dnscms.options import ALL_PIGS
|
||||
from dnscms.wordpress.models import WPImportedPageMixin
|
||||
from dnscms.wordpress.models import (
|
||||
DeferWPFieldsManagerMixin,
|
||||
WPAwareManager,
|
||||
WPImportedPageMixin,
|
||||
)
|
||||
from venues.models import VenuePage
|
||||
|
||||
|
||||
@register_singular_query_field("eventIndex")
|
||||
class EventIndex(Page):
|
||||
class EventIndex(HeadlessMixin, Page):
|
||||
max_count = 1
|
||||
subpage_types = ["events.EventPage"]
|
||||
|
||||
def future_events(self, info, **kwargs):
|
||||
return EventPage.objects.live().future().order_by("next_occurrence")
|
||||
return (
|
||||
EventPage.objects.live()
|
||||
.future()
|
||||
.order_by("next_occurrence")
|
||||
.select_related("featured_image")
|
||||
.prefetch_related(
|
||||
Prefetch(
|
||||
"occurrences",
|
||||
queryset=EventOccurrence.objects.select_related("venue"),
|
||||
),
|
||||
"categories",
|
||||
Prefetch(
|
||||
"organizers",
|
||||
queryset=EventOrganizer.objects.select_related("association"),
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
graphql_fields = [
|
||||
GraphQLCollection(
|
||||
@@ -74,11 +95,12 @@ class EventCategory(models.Model):
|
||||
help_text=_("The name of the category as it will appear in URLs."),
|
||||
)
|
||||
show_in_filters = models.BooleanField(
|
||||
default=False, help_text="Skal denne kategorien være mulig å filtrere på i programmet?"
|
||||
default=False,
|
||||
help_text=_("Should this category be available as a filter in the event programme?"),
|
||||
)
|
||||
|
||||
PIG_CHOICES = [
|
||||
("", "Ingen"),
|
||||
("", _("None")),
|
||||
] + ALL_PIGS
|
||||
|
||||
pig = models.CharField(
|
||||
@@ -86,14 +108,14 @@ class EventCategory(models.Model):
|
||||
choices=PIG_CHOICES,
|
||||
default="",
|
||||
blank=True,
|
||||
help_text="Standardgris for arrangementer av denne typen.",
|
||||
help_text=_("Default pig for events of this kind."),
|
||||
)
|
||||
|
||||
panels = [
|
||||
TitleFieldPanel("name"),
|
||||
FieldPanel("slug"),
|
||||
FieldPanel("show_in_filters"),
|
||||
FieldPanel("pig", heading="Gris"),
|
||||
FieldPanel("pig", heading=_("Pig")),
|
||||
]
|
||||
|
||||
graphql_fields = [
|
||||
@@ -104,8 +126,8 @@ class EventCategory(models.Model):
|
||||
]
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Event category"
|
||||
verbose_name_plural = "Event categories"
|
||||
verbose_name = _("event category")
|
||||
verbose_name_plural = _("event categories")
|
||||
ordering = ["name"]
|
||||
|
||||
def __str__(self):
|
||||
@@ -133,8 +155,8 @@ class EventOrganizerLink(Orderable):
|
||||
return f"{self.organizer.name}"
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Arrangør"
|
||||
verbose_name_plural = "Arrangører"
|
||||
verbose_name = _("organizer")
|
||||
verbose_name_plural = _("organizers")
|
||||
constraints = [
|
||||
UniqueConstraint(
|
||||
"event", "organizer", name="event_organizer_link_event_organizer_unique"
|
||||
@@ -142,9 +164,11 @@ class EventOrganizerLink(Orderable):
|
||||
]
|
||||
|
||||
|
||||
@register_snippet
|
||||
# registered as a snippet via EventOrganizerSnippetViewSet in wagtail_hooks.py
|
||||
@register_query_field("eventOrganizer", "eventOrganizers")
|
||||
class EventOrganizer(ClusterableModel):
|
||||
class EventOrganizer(index.Indexed, ClusterableModel):
|
||||
objects = WPAwareManager()
|
||||
|
||||
name = models.CharField(
|
||||
max_length=100,
|
||||
null=False,
|
||||
@@ -162,13 +186,13 @@ class EventOrganizer(ClusterableModel):
|
||||
blank=True,
|
||||
on_delete=models.PROTECT,
|
||||
related_name="organizers",
|
||||
help_text="Om en samfundsforening eller et utvalg står bak, velg det her.",
|
||||
help_text=_("If a DNS association or committee is behind it, choose it here."),
|
||||
)
|
||||
|
||||
external_url = models.URLField(
|
||||
blank=True,
|
||||
max_length=512,
|
||||
help_text="Lenke til nettstedet til ekstern arrangør",
|
||||
help_text=_("Link to the external organizer's website"),
|
||||
)
|
||||
|
||||
panels = [
|
||||
@@ -177,15 +201,15 @@ class EventOrganizer(ClusterableModel):
|
||||
FieldPanel(
|
||||
"association",
|
||||
widget=AssociationChooserWidget(linked_fields={"association": "#id_association"}),
|
||||
heading="Intern arrangør",
|
||||
heading=_("Internal organizer"),
|
||||
),
|
||||
MultiFieldPanel(
|
||||
heading="Ekstern arrangør",
|
||||
heading=_("External organizer"),
|
||||
children=[
|
||||
FieldPanel(
|
||||
"external_url",
|
||||
heading="Nettsted",
|
||||
help_text="La denne stå tom om arrangøren finnes i lista over.",
|
||||
heading=_("Website"),
|
||||
help_text=_("Leave this empty if the organizer exists in the list above."),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -198,9 +222,14 @@ 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"
|
||||
verbose_name = _("event organizer")
|
||||
verbose_name_plural = _("event organizers")
|
||||
ordering = ["name"]
|
||||
|
||||
def __str__(self):
|
||||
@@ -209,15 +238,22 @@ class EventOrganizer(ClusterableModel):
|
||||
|
||||
class EventPageQuerySet(PageQuerySet):
|
||||
def future(self):
|
||||
today = timezone.localtime(timezone.now()).date()
|
||||
next_occurrence = Min("occurrences__start", filter=Q(occurrences__start__gte=today))
|
||||
return self.filter(occurrences__start__gte=today).annotate(next_occurrence=next_occurrence)
|
||||
now = timezone.now()
|
||||
today_start = timezone.localtime(now).replace(hour=0, minute=0, second=0, microsecond=0)
|
||||
next_occurrence = Min("occurrences__start", filter=Q(occurrences__start__gte=today_start))
|
||||
return self.filter(occurrences__start__gte=today_start).annotate(
|
||||
next_occurrence=next_occurrence
|
||||
)
|
||||
|
||||
|
||||
EventPageManager = PageManager.from_queryset(EventPageQuerySet)
|
||||
class EventPageManager(
|
||||
DeferWPFieldsManagerMixin,
|
||||
PageManager.from_queryset(EventPageQuerySet),
|
||||
):
|
||||
pass
|
||||
|
||||
|
||||
class EventPage(WPImportedPageMixin, Page):
|
||||
class EventPage(HeadlessMixin, WPImportedPageMixin, Page):
|
||||
subpage_types = []
|
||||
parent_page_types = ["events.EventIndex"]
|
||||
show_in_menus = False
|
||||
@@ -230,19 +266,19 @@ class EventPage(WPImportedPageMixin, Page):
|
||||
blank=True,
|
||||
on_delete=models.SET_NULL,
|
||||
related_name="+",
|
||||
help_text=(
|
||||
"Velg et bilde til bruk i programmet og andre visningsflater. "
|
||||
"Bør være et bilde eller en illustrasjon uten tekst "
|
||||
"– ikke gjenbruk et Facebook-cover ukritisk!"
|
||||
help_text=_(
|
||||
"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 cover uncritically!"
|
||||
),
|
||||
)
|
||||
|
||||
subtitle = models.CharField(
|
||||
blank=True,
|
||||
max_length=128,
|
||||
help_text=(
|
||||
"En kort tekst som kommer rett under under tittelen. "
|
||||
"La denne gjerne stå tom om du fikk plass til det meste i tittelen."
|
||||
help_text=_(
|
||||
"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."
|
||||
),
|
||||
)
|
||||
lead = RichTextField(features=["italic", "link"], blank=True)
|
||||
@@ -258,8 +294,8 @@ class EventPage(WPImportedPageMixin, Page):
|
||||
)
|
||||
|
||||
PIG_CHOICES = [
|
||||
("", "Ingen"),
|
||||
("automatic", "Automatisk"),
|
||||
("", _("None")),
|
||||
("automatic", _("Automatic")),
|
||||
] + ALL_PIGS
|
||||
|
||||
pig = models.CharField(
|
||||
@@ -267,21 +303,21 @@ class EventPage(WPImportedPageMixin, Page):
|
||||
choices=PIG_CHOICES,
|
||||
default="automatic",
|
||||
blank=True,
|
||||
help_text=(
|
||||
"Grisen som henger på arrangementssiden. "
|
||||
"Automatisk fører til at en velges basert på arrangementets kategori."
|
||||
help_text=_(
|
||||
"The pig that hangs out on the event page. "
|
||||
"Automatic causes one to be chosen based on the event's category."
|
||||
),
|
||||
)
|
||||
|
||||
ticket_url = models.URLField(
|
||||
blank=True,
|
||||
max_length=1024,
|
||||
help_text="Lenke direkte til billettkjøp, f.eks. TicketCo eller Ticketmaster",
|
||||
help_text=_("Direct link to ticket purchase, e.g. TicketCo, Billetto or Ticketmaster"),
|
||||
)
|
||||
facebook_url = models.URLField(
|
||||
blank=True,
|
||||
max_length=1024,
|
||||
help_text="Lenke direkte til arrangementet på Facebook",
|
||||
help_text=_("Direct link to the event on Facebook"),
|
||||
)
|
||||
|
||||
free = models.BooleanField(null=False, default=False)
|
||||
@@ -290,62 +326,65 @@ class EventPage(WPImportedPageMixin, Page):
|
||||
price_member = models.CharField(max_length=32, blank=True)
|
||||
|
||||
ticket_panels = [
|
||||
FieldPanel("free", heading="Gratis", help_text="Er dette arrangementet gratis for alle?"),
|
||||
FieldPanel("free", heading=_("Free"), help_text=_("Is this event free for everyone?")),
|
||||
MultiFieldPanel(
|
||||
children=[
|
||||
FieldRowPanel(
|
||||
children=[
|
||||
FieldPanel("price_regular", heading="Ordinær pris"),
|
||||
FieldPanel("price_student", heading="Pris for studenter"),
|
||||
FieldPanel("price_member", heading="Pris for medlemmer av DNS"),
|
||||
FieldPanel("price_regular", heading=_("Regular price")),
|
||||
FieldPanel("price_student", heading=_("Price for students")),
|
||||
FieldPanel("price_member", heading=_("Price for DNS members")),
|
||||
],
|
||||
help_text="",
|
||||
),
|
||||
HelpPanel(
|
||||
content=mark_safe(
|
||||
"Skriv <strong>0</strong> om gratis. Tomt felt skjuler priskategorien. Om mulig, skriv kun tall."
|
||||
_(
|
||||
"Write <strong>0</strong> for free. "
|
||||
"An empty field hides the price category. "
|
||||
"If possible, write digits only."
|
||||
)
|
||||
)
|
||||
),
|
||||
],
|
||||
attrs={"id": "specific_pricing_panel"},
|
||||
),
|
||||
FieldPanel("ticket_url", heading="Billettkjøpslenke"),
|
||||
FieldPanel("ticket_url", heading=_("Ticket purchase link")),
|
||||
]
|
||||
|
||||
content_panels = Page.content_panels + [
|
||||
FieldPanel("subtitle", heading="Undertittel"),
|
||||
FieldPanel("subtitle", heading=_("Subtitle")),
|
||||
FieldPanel("featured_image"),
|
||||
FieldPanel("lead", heading="Ingress"),
|
||||
FieldPanel("lead", heading=_("Lead")),
|
||||
FieldPanel("body"),
|
||||
FieldPanel("categories", widget=forms.CheckboxSelectMultiple),
|
||||
MultiFieldPanel(
|
||||
heading="Arrangører",
|
||||
heading=_("Organizers"),
|
||||
children=[
|
||||
HelpPanel(
|
||||
content=("Hvem står bak arrangementet?"),
|
||||
content=_("Who is behind the event?"),
|
||||
),
|
||||
MultipleChooserPanel(
|
||||
"organizer_links", chooser_field_name="organizer", label="Arrangør"
|
||||
"organizer_links", chooser_field_name="organizer", label=_("Organizer")
|
||||
),
|
||||
],
|
||||
),
|
||||
FieldPanel("pig", heading="Gris"),
|
||||
FieldPanel("pig", heading=_("Pig")),
|
||||
FieldPanel(
|
||||
"facebook_url",
|
||||
heading="Facebook-lenke",
|
||||
help_text="Lenke direkte til arrangementet på Facebook.",
|
||||
heading=_("Facebook link"),
|
||||
help_text=_("Direct link to the event on Facebook."),
|
||||
),
|
||||
MultiFieldPanel(heading="Priser og billettkjøp", children=ticket_panels),
|
||||
MultiFieldPanel(heading=_("Pricing and tickets"), children=ticket_panels),
|
||||
MultiFieldPanel(
|
||||
heading="Dato, tid og lokale",
|
||||
heading=_("Date, time and venue"),
|
||||
children=[
|
||||
HelpPanel(
|
||||
content=(
|
||||
"Om arrangementet går over flere dager, "
|
||||
"legg inn hver dag som en egen forekomst."
|
||||
content=_(
|
||||
"If the event spans several days, add each day as a separate occurrence."
|
||||
),
|
||||
),
|
||||
InlinePanel("occurrences", min_num=1, label="Forekomst"),
|
||||
InlinePanel("occurrences", min_num=1, label=_("Occurrence")),
|
||||
],
|
||||
),
|
||||
]
|
||||
@@ -355,7 +394,7 @@ class EventPage(WPImportedPageMixin, Page):
|
||||
GraphQLImage("featured_image"),
|
||||
GraphQLRichText("lead"),
|
||||
GraphQLStreamfield("body"),
|
||||
GraphQLString("pig"),
|
||||
GraphQLString("pig", required=True),
|
||||
GraphQLString("ticket_url"),
|
||||
GraphQLString("facebook_url"),
|
||||
GraphQLBoolean("free"),
|
||||
@@ -387,6 +426,10 @@ class EventPage(WPImportedPageMixin, Page):
|
||||
|
||||
search_fields = Page.search_fields + [index.SearchField("body")]
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("event")
|
||||
verbose_name_plural = _("events")
|
||||
|
||||
def clean(self):
|
||||
super().clean()
|
||||
|
||||
@@ -406,130 +449,10 @@ class EventPage(WPImportedPageMixin, Page):
|
||||
self.price_student = ""
|
||||
self.price_member = ""
|
||||
|
||||
def import_wordpress_data(self, data):
|
||||
import datetime
|
||||
import html
|
||||
from zoneinfo import ZoneInfo
|
||||
|
||||
from django.core.validators import URLValidator
|
||||
|
||||
validate_url = URLValidator(schemes=["http", "https"])
|
||||
|
||||
def fix_url(url):
|
||||
if not url:
|
||||
return None
|
||||
url = url.strip()
|
||||
try:
|
||||
validate_url(url)
|
||||
except Exception:
|
||||
print(f"Bogus URL for {self.wp_post_id}: {url}")
|
||||
return None
|
||||
return url
|
||||
|
||||
# Wagtail page model fields
|
||||
self.title = html.unescape(data["title"])
|
||||
self.slug = data["slug"]
|
||||
self.first_published_at = data["first_published_at"]
|
||||
self.last_published_at = data["last_published_at"]
|
||||
self.latest_revision_created_at = data["latest_revision_created_at"]
|
||||
self.search_description = data["search_description"]
|
||||
|
||||
# debug fields
|
||||
self.wp_post_id = data["wp_post_id"]
|
||||
self.wp_post_type = data["wp_post_type"]
|
||||
self.wp_link = data["wp_link"]
|
||||
self.wp_raw_content = data["wp_raw_content"]
|
||||
self.wp_block_json = data["wp_block_json"]
|
||||
self.wp_processed_content = data["wp_processed_content"]
|
||||
self.wp_normalized_styles = data["wp_normalized_styles"]
|
||||
self.wp_post_meta = data["wp_post_meta"]
|
||||
|
||||
# own model fields
|
||||
self.body = data["body"] or ""
|
||||
|
||||
# categories (organizers and event types)
|
||||
wp_categories = data["wp_categories"]
|
||||
|
||||
# organizers
|
||||
organizer_cats = [x for x in wp_categories if x["domain"] == "event_organizer"]
|
||||
organizers = []
|
||||
for x in organizer_cats:
|
||||
try:
|
||||
organizer = EventOrganizer.objects.get(slug=x["nicename"])
|
||||
except EventOrganizer.DoesNotExist:
|
||||
organizer = EventOrganizer.objects.create(name=x["name"], slug=x["nicename"])
|
||||
organizers.append(organizer)
|
||||
|
||||
self.organizer_links.set(
|
||||
[EventOrganizerLink(event=self, organizer=organizer) for organizer in organizers]
|
||||
)
|
||||
|
||||
## event types
|
||||
# type_cats = [x for x in wp_categories if x["domain"] == "event_type"]
|
||||
# event_categories = []
|
||||
# for x in type_cats:
|
||||
# try:
|
||||
# event_category = EventCategory.objects.get(slug=x["nicename"])
|
||||
# except EventCategory.DoesNotExist:
|
||||
# event_category = EventCategory.objects.create(
|
||||
# name=x["name"], slug=x["nicename"], show_in_filters=False
|
||||
# )
|
||||
# event_categories.append(event_category)
|
||||
# self.categories.set(event_categories)
|
||||
|
||||
meta = data["wp_post_meta"]
|
||||
|
||||
start_ts = meta.get("neuf_events_starttime") or 1337
|
||||
end_ts = meta.get("neuf_events_endtime")
|
||||
tz = ZoneInfo("Europe/Oslo")
|
||||
start = start_ts and datetime.datetime.fromtimestamp(start_ts, datetime.UTC).replace(
|
||||
tzinfo=tz
|
||||
)
|
||||
end = end_ts and datetime.datetime.fromtimestamp(end_ts, datetime.UTC).replace(tzinfo=tz)
|
||||
venue_id = meta.get("neuf_events_venue_id")
|
||||
venue_custom = meta.get("neuf_events_venue")
|
||||
venue = None
|
||||
if venue_id:
|
||||
venue = VenuePage.objects.get(wp_post_id=venue_id)
|
||||
venue_custom = ""
|
||||
else:
|
||||
venue_custom = venue_custom or ""
|
||||
|
||||
occurrence = EventOccurrence(
|
||||
event=self, start=start, end=end, venue=venue, venue_custom=venue_custom
|
||||
)
|
||||
self.occurrences.set([occurrence])
|
||||
|
||||
self.ticket_url = fix_url(meta.get("neuf_events_bs_url")) or ""
|
||||
self.facebook_url = fix_url(meta.get("neuf_events_fb_url")) or ""
|
||||
|
||||
def parse_price(price):
|
||||
if price is None:
|
||||
return ""
|
||||
if type(price) is int:
|
||||
return price
|
||||
p = price.strip()
|
||||
if p == "":
|
||||
return ""
|
||||
try:
|
||||
return int(p)
|
||||
except ValueError:
|
||||
pass
|
||||
free = ["gratis", "free", "gratis/free", "free/gratis"]
|
||||
if p.lower() in free:
|
||||
return 0
|
||||
return price
|
||||
|
||||
price_regular = parse_price(meta.get("neuf_events_price_regular"))
|
||||
price_member = parse_price(meta.get("neuf_events_price_member"))
|
||||
if not price_regular and not price_member:
|
||||
self.free = True
|
||||
else:
|
||||
self.price_regular = parse_price(meta.get("neuf_events_price_regular"))
|
||||
self.price_member = parse_price(meta.get("neuf_events_price_member"))
|
||||
|
||||
|
||||
class EventOccurrence(Orderable):
|
||||
objects = WPAwareManager()
|
||||
|
||||
event = ParentalKey(EventPage, on_delete=models.CASCADE, related_name="occurrences")
|
||||
start = models.DateTimeField()
|
||||
end = models.DateTimeField(null=True, blank=True)
|
||||
@@ -544,22 +467,24 @@ class EventOccurrence(Orderable):
|
||||
blank=True,
|
||||
max_length=128,
|
||||
help_text=mark_safe(
|
||||
"Bruk denne <em>om ingen av lokalene som kan velges til venstre</em> passer. "
|
||||
"F.eks. <em>Frederikkeplassen</em> eller <em>Sirkusteltet</em>."
|
||||
_(
|
||||
"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>."
|
||||
)
|
||||
),
|
||||
)
|
||||
|
||||
panels = [
|
||||
FieldRowPanel(
|
||||
children=[
|
||||
FieldPanel("start", heading="Start"),
|
||||
FieldPanel("end", heading="Slutt"),
|
||||
FieldPanel("start", heading=_("Start")),
|
||||
FieldPanel("end", heading=_("End")),
|
||||
],
|
||||
),
|
||||
FieldRowPanel(
|
||||
children=[
|
||||
FieldPanel("venue", heading="Lokale"),
|
||||
FieldPanel("venue_custom", heading="Lokale som fritekst"),
|
||||
FieldPanel("venue", heading=_("Venue"), widget=forms.Select),
|
||||
FieldPanel("venue_custom", heading=_("Venue as free text")),
|
||||
],
|
||||
),
|
||||
]
|
||||
@@ -572,140 +497,29 @@ 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(
|
||||
{"venue_custom": "Du kan ikke både velge et lokale og skrive noe i dette feltet."}
|
||||
{
|
||||
"venue_custom": _(
|
||||
"You can't both pick a venue and write something in this field."
|
||||
)
|
||||
}
|
||||
)
|
||||
if not self.venue and not self.venue_custom:
|
||||
raise ValidationError({"venue": "Lokale er påkrevd."})
|
||||
raise ValidationError({"venue": _("Venue is required.")})
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.start}--{self.end}"
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Forekomst"
|
||||
verbose_name_plural = "Forekomster"
|
||||
|
||||
|
||||
sample_legacy_event_json = """
|
||||
{
|
||||
"id": 64573,
|
||||
"date": "2023-12-27T11:28:34",
|
||||
"date_gmt": "2023-12-27T10:28:34",
|
||||
"guid": {
|
||||
"rendered": "https://studentersamfundet.no/?post_type=event&p=64573"
|
||||
},
|
||||
"modified": "2023-12-27T11:44:11",
|
||||
"modified_gmt": "2023-12-27T10:44:11",
|
||||
"slug": "quiz-147-2-2-2-2-2-2-2-2-2-2-2-2-2-2-2-2-2-2-2-2-2-3-2-2-2-2-2-2-2-2-2-2-2-2-2-2-2-2-2-2-2-2-2-2-2-2-2-2-2-2-2-2-2-2-3-2-2-2-2-2-2-2-2-2-2-2-2-2",
|
||||
"status": "publish",
|
||||
"type": "event",
|
||||
"link": "https://studentersamfundet.no/arrangement/quiz-147-2-2-2-2-2-2-2-2-2-2-2-2-2-2-2-2-2-2-2-2-2-3-2-2-2-2-2-2-2-2-2-2-2-2-2-2-2-2-2-2-2-2-2-2-2-2-2-2-2-2-2-2-2-2-3-2-2-2-2-2-2-2-2-2-2-2-2-2/",
|
||||
"title": {
|
||||
"rendered": "QUIZ",
|
||||
"decoded": "QUIZ"
|
||||
},
|
||||
"content": {
|
||||
"rendered": "\n<p>Det Norske Studentersamfund inviterer til quiz hver tirsdag kl. 19:00.</p>\n\n\n\n<p>Vi serverer 50 spørsmål som kan spenne seg fra one hit wonders fra 80-tallet, universets uendelighet, dyrelivets merkverdigheter og mye, mye mer!</p>\n\n\n\n<p>Quiz på Chateau Neuf er åpent for alle. Vinnere og “lucky losers” vil bli utnevnt hver kveld. Lag som er over seks personer er tillatt, men da trekkes dere for ett poeng per deltaker per runde.</p>\n\n\n\n<p>For de som ønsker å være med på sammenlagtkonkurransen for høsten vil den regnes ut for de tolv beste prestasjonene laget leverer. Så det vil fremdeles være god sjanse for å vinne sammenlagt selv dere må droppe en quiz eller to for eksamener eller andre forpliktelser.</p>\n\n\n\n<p>Velkommen quizglade mennesker!</p>\n\n\n\n<p>Gratis inngang!</p>\n",
|
||||
"protected": false
|
||||
},
|
||||
"excerpt": {
|
||||
"rendered": "<p>Det Norske Studentersamfund inviterer til quiz hver tirsdag kl. 19:00. Vi serverer 50 spørsmål som kan spenne seg fra one hit wonders fra 80-tallet, universets uendelighet, dyrelivets merkverdigheter og mye, mye mer! Quiz på Chateau Neuf er åpent for alle. Vinnere og “lucky losers” vil bli utnevnt hver kveld. Lag som er over seks personer […]</p>\n",
|
||||
"protected": false
|
||||
},
|
||||
"author": 2150,
|
||||
"featured_media": 64585,
|
||||
"template": "",
|
||||
"meta": [],
|
||||
"event_types": [13],
|
||||
"event_organizers": [390, 322],
|
||||
"facebook_url": "https://fb.me/e/2RDR5pZdr",
|
||||
"ticket_url": "",
|
||||
"price_regular": "",
|
||||
"price_member": "",
|
||||
"start_time": "2024-05-07T17:00:00+00:00",
|
||||
"end_time": "2024-05-07T20:00:00+00:00",
|
||||
"venue": "Glassbaren",
|
||||
"venue_id": "55063",
|
||||
"thumbnail": {
|
||||
"thumbnail": "https://studentersamfundet.no/wp/wp-content/uploads/2023/12/quiz-header-1-150x150.png",
|
||||
"medium": "https://studentersamfundet.no/wp/wp-content/uploads/2023/12/quiz-header-1-300x169.png",
|
||||
"medium_large": "https://studentersamfundet.no/wp/wp-content/uploads/2023/12/quiz-header-1-768x433.png",
|
||||
"large": "https://studentersamfundet.no/wp/wp-content/uploads/2023/12/quiz-header-1-1280x720.png",
|
||||
"1536x1536": "https://studentersamfundet.no/wp/wp-content/uploads/2023/12/quiz-header-1-1536x865.png",
|
||||
"2048x2048": "https://studentersamfundet.no/wp/wp-content/uploads/2023/12/quiz-header-1.png",
|
||||
"four-column": "https://studentersamfundet.no/wp/wp-content/uploads/2023/12/quiz-header-1-393x342.png",
|
||||
"six-column": "https://studentersamfundet.no/wp/wp-content/uploads/2023/12/quiz-header-1-608x342.png",
|
||||
"extra-large": "https://studentersamfundet.no/wp/wp-content/uploads/2023/12/quiz-header-1-1600x901.png",
|
||||
"newsletter-half": "https://studentersamfundet.no/wp/wp-content/uploads/2023/12/quiz-header-1-320x190.png",
|
||||
"newsletter-third": "https://studentersamfundet.no/wp/wp-content/uploads/2023/12/quiz-header-1-213x126.png",
|
||||
"featured": "https://studentersamfundet.no/wp/wp-content/uploads/2023/12/quiz-header-1-1200x480.png"
|
||||
},
|
||||
"_links": {
|
||||
"self": [
|
||||
{
|
||||
"href": "https://studentersamfundet.no/wp-json/wp/v2/events/64573"
|
||||
}
|
||||
],
|
||||
"collection": [
|
||||
{
|
||||
"href": "https://studentersamfundet.no/wp-json/wp/v2/events"
|
||||
}
|
||||
],
|
||||
"about": [
|
||||
{
|
||||
"href": "https://studentersamfundet.no/wp-json/wp/v2/types/event"
|
||||
}
|
||||
],
|
||||
"author": [
|
||||
{
|
||||
"embeddable": true,
|
||||
"href": "https://studentersamfundet.no/wp-json/wp/v2/users/2150"
|
||||
}
|
||||
],
|
||||
"version-history": [
|
||||
{
|
||||
"count": 1,
|
||||
"href": "https://studentersamfundet.no/wp-json/wp/v2/events/64573/revisions"
|
||||
}
|
||||
],
|
||||
"predecessor-version": [
|
||||
{
|
||||
"id": 64574,
|
||||
"href": "https://studentersamfundet.no/wp-json/wp/v2/events/64573/revisions/64574"
|
||||
}
|
||||
],
|
||||
"wp:featuredmedia": [
|
||||
{
|
||||
"embeddable": true,
|
||||
"href": "https://studentersamfundet.no/wp-json/wp/v2/media/64585"
|
||||
}
|
||||
],
|
||||
"wp:attachment": [
|
||||
{
|
||||
"href": "https://studentersamfundet.no/wp-json/wp/v2/media?parent=64573"
|
||||
}
|
||||
],
|
||||
"wp:term": [
|
||||
{
|
||||
"taxonomy": "event_type",
|
||||
"embeddable": true,
|
||||
"href": "https://studentersamfundet.no/wp-json/wp/v2/event_types?post=64573"
|
||||
},
|
||||
{
|
||||
"taxonomy": "event_organizer",
|
||||
"embeddable": true,
|
||||
"href": "https://studentersamfundet.no/wp-json/wp/v2/event_organizers?post=64573"
|
||||
}
|
||||
],
|
||||
"curies": [
|
||||
{
|
||||
"name": "wp",
|
||||
"href": "https://api.w.org/{rel}",
|
||||
"templated": true
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
"""
|
||||
verbose_name = _("occurrence")
|
||||
verbose_name_plural = _("occurrences")
|
||||
|
||||
@@ -0,0 +1,119 @@
|
||||
{
|
||||
"id": 64573,
|
||||
"date": "2023-12-27T11:28:34",
|
||||
"date_gmt": "2023-12-27T10:28:34",
|
||||
"guid": {
|
||||
"rendered": "https://studentersamfundet.no/?post_type=event&p=64573"
|
||||
},
|
||||
"modified": "2023-12-27T11:44:11",
|
||||
"modified_gmt": "2023-12-27T10:44:11",
|
||||
"slug": "quiz-147-2-2-2-2-2-2-2-2-2-2-2-2-2-2-2-2-2-2-2-2-2-3-2-2-2-2-2-2-2-2-2-2-2-2-2-2-2-2-2-2-2-2-2-2-2-2-2-2-2-2-2-2-2-2-3-2-2-2-2-2-2-2-2-2-2-2-2-2",
|
||||
"status": "publish",
|
||||
"type": "event",
|
||||
"link": "https://studentersamfundet.no/arrangement/quiz-147-2-2-2-2-2-2-2-2-2-2-2-2-2-2-2-2-2-2-2-2-2-3-2-2-2-2-2-2-2-2-2-2-2-2-2-2-2-2-2-2-2-2-2-2-2-2-2-2-2-2-2-2-2-2-3-2-2-2-2-2-2-2-2-2-2-2-2-2/",
|
||||
"title": {
|
||||
"rendered": "QUIZ",
|
||||
"decoded": "QUIZ"
|
||||
},
|
||||
"content": {
|
||||
"rendered": "\n<p>Det Norske Studentersamfund inviterer til quiz hver tirsdag kl. 19:00.</p>\n\n\n\n<p>Vi serverer 50 spørsmål som kan spenne seg fra one hit wonders fra 80-tallet, universets uendelighet, dyrelivets merkverdigheter og mye, mye mer!</p>\n\n\n\n<p>Quiz på Chateau Neuf er åpent for alle. Vinnere og “lucky losers” vil bli utnevnt hver kveld. Lag som er over seks personer er tillatt, men da trekkes dere for ett poeng per deltaker per runde.</p>\n\n\n\n<p>For de som ønsker å være med på sammenlagtkonkurransen for høsten vil den regnes ut for de tolv beste prestasjonene laget leverer. Så det vil fremdeles være god sjanse for å vinne sammenlagt selv dere må droppe en quiz eller to for eksamener eller andre forpliktelser.</p>\n\n\n\n<p>Velkommen quizglade mennesker!</p>\n\n\n\n<p>Gratis inngang!</p>\n",
|
||||
"protected": false
|
||||
},
|
||||
"excerpt": {
|
||||
"rendered": "<p>Det Norske Studentersamfund inviterer til quiz hver tirsdag kl. 19:00. Vi serverer 50 spørsmål som kan spenne seg fra one hit wonders fra 80-tallet, universets uendelighet, dyrelivets merkverdigheter og mye, mye mer! Quiz på Chateau Neuf er åpent for alle. Vinnere og “lucky losers” vil bli utnevnt hver kveld. Lag som er over seks personer […]</p>\n",
|
||||
"protected": false
|
||||
},
|
||||
"author": 2150,
|
||||
"featured_media": 64585,
|
||||
"template": "",
|
||||
"meta": [],
|
||||
"event_types": [13],
|
||||
"event_organizers": [390, 322],
|
||||
"facebook_url": "https://fb.me/e/2RDR5pZdr",
|
||||
"ticket_url": "",
|
||||
"price_regular": "",
|
||||
"price_member": "",
|
||||
"start_time": "2024-05-07T17:00:00+00:00",
|
||||
"end_time": "2024-05-07T20:00:00+00:00",
|
||||
"venue": "Glassbaren",
|
||||
"venue_id": "55063",
|
||||
"thumbnail": {
|
||||
"thumbnail": "https://studentersamfundet.no/wp/wp-content/uploads/2023/12/quiz-header-1-150x150.png",
|
||||
"medium": "https://studentersamfundet.no/wp/wp-content/uploads/2023/12/quiz-header-1-300x169.png",
|
||||
"medium_large": "https://studentersamfundet.no/wp/wp-content/uploads/2023/12/quiz-header-1-768x433.png",
|
||||
"large": "https://studentersamfundet.no/wp/wp-content/uploads/2023/12/quiz-header-1-1280x720.png",
|
||||
"1536x1536": "https://studentersamfundet.no/wp/wp-content/uploads/2023/12/quiz-header-1-1536x865.png",
|
||||
"2048x2048": "https://studentersamfundet.no/wp/wp-content/uploads/2023/12/quiz-header-1.png",
|
||||
"four-column": "https://studentersamfundet.no/wp/wp-content/uploads/2023/12/quiz-header-1-393x342.png",
|
||||
"six-column": "https://studentersamfundet.no/wp/wp-content/uploads/2023/12/quiz-header-1-608x342.png",
|
||||
"extra-large": "https://studentersamfundet.no/wp/wp-content/uploads/2023/12/quiz-header-1-1600x901.png",
|
||||
"newsletter-half": "https://studentersamfundet.no/wp/wp-content/uploads/2023/12/quiz-header-1-320x190.png",
|
||||
"newsletter-third": "https://studentersamfundet.no/wp/wp-content/uploads/2023/12/quiz-header-1-213x126.png",
|
||||
"featured": "https://studentersamfundet.no/wp/wp-content/uploads/2023/12/quiz-header-1-1200x480.png"
|
||||
},
|
||||
"_links": {
|
||||
"self": [
|
||||
{
|
||||
"href": "https://studentersamfundet.no/wp-json/wp/v2/events/64573"
|
||||
}
|
||||
],
|
||||
"collection": [
|
||||
{
|
||||
"href": "https://studentersamfundet.no/wp-json/wp/v2/events"
|
||||
}
|
||||
],
|
||||
"about": [
|
||||
{
|
||||
"href": "https://studentersamfundet.no/wp-json/wp/v2/types/event"
|
||||
}
|
||||
],
|
||||
"author": [
|
||||
{
|
||||
"embeddable": true,
|
||||
"href": "https://studentersamfundet.no/wp-json/wp/v2/users/2150"
|
||||
}
|
||||
],
|
||||
"version-history": [
|
||||
{
|
||||
"count": 1,
|
||||
"href": "https://studentersamfundet.no/wp-json/wp/v2/events/64573/revisions"
|
||||
}
|
||||
],
|
||||
"predecessor-version": [
|
||||
{
|
||||
"id": 64574,
|
||||
"href": "https://studentersamfundet.no/wp-json/wp/v2/events/64573/revisions/64574"
|
||||
}
|
||||
],
|
||||
"wp:featuredmedia": [
|
||||
{
|
||||
"embeddable": true,
|
||||
"href": "https://studentersamfundet.no/wp-json/wp/v2/media/64585"
|
||||
}
|
||||
],
|
||||
"wp:attachment": [
|
||||
{
|
||||
"href": "https://studentersamfundet.no/wp-json/wp/v2/media?parent=64573"
|
||||
}
|
||||
],
|
||||
"wp:term": [
|
||||
{
|
||||
"taxonomy": "event_type",
|
||||
"embeddable": true,
|
||||
"href": "https://studentersamfundet.no/wp-json/wp/v2/event_types?post=64573"
|
||||
},
|
||||
{
|
||||
"taxonomy": "event_organizer",
|
||||
"embeddable": true,
|
||||
"href": "https://studentersamfundet.no/wp-json/wp/v2/event_organizers?post=64573"
|
||||
}
|
||||
],
|
||||
"curies": [
|
||||
{
|
||||
"name": "wp",
|
||||
"href": "https://api.w.org/{rel}",
|
||||
"templated": true
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
{% extends "wagtailadmin/generic/base.html" %}
|
||||
{% load i18n wagtailadmin_tags %}
|
||||
|
||||
{% block main_content %}
|
||||
<table class="listing">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{% trans "Outcome" %}</th>
|
||||
<th>{% trans "ID" %}</th>
|
||||
<th>{% trans "Name" %}</th>
|
||||
<th>{% trans "Slug" %}</th>
|
||||
<th>{% trans "Association" %}</th>
|
||||
<th>{% trans "External URL" %}</th>
|
||||
<th>{% trans "Events" %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><strong>{% trans "Survives" %}</strong></td>
|
||||
<td>{{ survivor.pk }}</td>
|
||||
<td>{{ survivor.name }}</td>
|
||||
<td>{{ survivor.slug }}</td>
|
||||
<td>{{ survivor.association|default:"—" }}</td>
|
||||
<td>{{ survivor.external_url|default:"—" }}</td>
|
||||
<td>{{ survivor_event_count }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>{% trans "Is deleted" %}</strong></td>
|
||||
<td>{{ loser.pk }}</td>
|
||||
<td>{{ loser.name }}</td>
|
||||
<td>{{ loser.slug }}</td>
|
||||
<td>{{ loser.association|default:"—" }}</td>
|
||||
<td>{{ loser.external_url|default:"—" }}</td>
|
||||
<td>{{ loser_event_count }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<p class="help-block help-warning">
|
||||
{% blocktrans with loser_name=loser.name survivor_name=survivor.name trimmed %}
|
||||
Events organized by “{{ loser_name }}” will be moved to “{{ survivor_name }}”,
|
||||
and “{{ loser_name }}” will be deleted. This cannot be undone.
|
||||
{% endblocktrans %}
|
||||
</p>
|
||||
|
||||
<form method="post" novalidate>
|
||||
{% csrf_token %}
|
||||
<ul class="fields">
|
||||
{% for field in form.visible_fields %}
|
||||
<li>{% formattedfield field %}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
<button type="submit" class="button serious">{% trans "Merge organizers" %}</button>
|
||||
</form>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,30 @@
|
||||
{% extends "wagtailadmin/generic/base.html" %}
|
||||
{% load i18n wagtailadmin_tags %}
|
||||
|
||||
{% block main_content %}
|
||||
<p>
|
||||
{% trans "Pick the two organizers to merge. The one with the lowest ID survives; the other one is deleted after its events have been moved over." %}
|
||||
</p>
|
||||
<form method="post" novalidate>
|
||||
{% csrf_token %}
|
||||
{% if form.non_field_errors %}
|
||||
<div class="help-block help-critical">{{ form.non_field_errors }}</div>
|
||||
{% endif %}
|
||||
<ul class="fields">
|
||||
{% for field in form.visible_fields %}
|
||||
<li>{% formattedfield field %}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
<button type="submit" class="button">{% trans "Continue" %}</button>
|
||||
</form>
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_js %}
|
||||
{{ block.super }}
|
||||
{{ form.media.js }}
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_css %}
|
||||
{{ block.super }}
|
||||
{{ form.media.css }}
|
||||
{% endblock %}
|
||||
@@ -1,2 +0,0 @@
|
||||
|
||||
# Create your tests here.
|
||||
+70
-5
@@ -1,15 +1,80 @@
|
||||
from django import forms
|
||||
from django.db.models import Q
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from wagtail.admin.forms import WagtailAdminModelForm
|
||||
from wagtail.admin.forms.choosers import BaseFilterForm
|
||||
from wagtail.admin.ui.tables import UpdatedAtColumn
|
||||
from wagtail.admin.views.generic.chooser import ChooseResultsView, ChooseView
|
||||
from wagtail.admin.viewsets.chooser import ChooserViewSet
|
||||
from wagtail.snippets.views.snippets import SnippetViewSet
|
||||
|
||||
from dnscms.utils import slugify
|
||||
from events.models import EventOrganizer
|
||||
|
||||
|
||||
class EventOrganizerSearchForm(BaseFilterForm):
|
||||
"""Substring search on name/slug"""
|
||||
|
||||
q = forms.CharField(
|
||||
label=_("Search term"),
|
||||
widget=forms.TextInput(attrs={"placeholder": _("Search")}),
|
||||
required=False,
|
||||
)
|
||||
|
||||
def filter(self, objects):
|
||||
objects = super().filter(objects)
|
||||
search_query = self.cleaned_data.get("q")
|
||||
if search_query:
|
||||
objects = objects.filter(
|
||||
Q(name__icontains=search_query) | Q(slug__icontains=search_query)
|
||||
)
|
||||
self.is_searching = True
|
||||
self.search_query = search_query
|
||||
return objects
|
||||
|
||||
|
||||
class EventOrganizerChooseView(ChooseView):
|
||||
filter_form_class = EventOrganizerSearchForm
|
||||
|
||||
|
||||
class EventOrganizerChooseResultsView(ChooseResultsView):
|
||||
filter_form_class = EventOrganizerSearchForm
|
||||
|
||||
|
||||
class EventOrganizerSnippetViewSet(SnippetViewSet):
|
||||
model = EventOrganizer
|
||||
icon = "group"
|
||||
list_display = ["name", "slug", UpdatedAtColumn()]
|
||||
# unset the search backend so the listing search filters on icontains instead
|
||||
search_backend_name = None
|
||||
search_fields = ["name", "slug"]
|
||||
|
||||
|
||||
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"
|
||||
icon = "group"
|
||||
per_page = 30
|
||||
page_title = "Choose organizers"
|
||||
choose_one_text = "Choose an organizer"
|
||||
choose_another_text = "Choose another organizer"
|
||||
edit_item_text = "Edit this organizer"
|
||||
form_fields = ["name", "association", "external_url"]
|
||||
page_title = _("Choose organizers")
|
||||
choose_one_text = _("Choose an organizer")
|
||||
choose_another_text = _("Choose another organizer")
|
||||
edit_item_text = _("Edit this organizer")
|
||||
creation_form_class = EventOrganizerCreationForm
|
||||
choose_view_class = EventOrganizerChooseView
|
||||
choose_results_view_class = EventOrganizerChooseResultsView
|
||||
|
||||
|
||||
event_organizer_chooser_viewset = EventOrganizerChooserViewSet("event_organizer_chooser")
|
||||
|
||||
@@ -1,8 +1,60 @@
|
||||
from django.urls import path, reverse_lazy
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from wagtail import hooks
|
||||
from wagtail.admin.auth import user_passes_test
|
||||
from wagtail.admin.menu import MenuItem
|
||||
from wagtail.snippets.models import register_snippet
|
||||
|
||||
from .views import event_organizer_chooser_viewset
|
||||
from .admin import event_sidebar_viewset, event_explorer_viewset
|
||||
from .merge_organizers import MergeOrganizersConfirmView, MergeOrganizersSelectView
|
||||
from .views import EventOrganizerSnippetViewSet, event_organizer_chooser_viewset
|
||||
|
||||
register_snippet(EventOrganizerSnippetViewSet)
|
||||
|
||||
superuser_only = user_passes_test(lambda user: user.is_superuser)
|
||||
|
||||
|
||||
class SuperuserMenuItem(MenuItem):
|
||||
def is_shown(self, request):
|
||||
return request.user.is_superuser
|
||||
|
||||
|
||||
@hooks.register("register_admin_urls")
|
||||
def register_merge_organizers_urls():
|
||||
return [
|
||||
path(
|
||||
"merge-organizers/",
|
||||
superuser_only(MergeOrganizersSelectView.as_view()),
|
||||
name="events_merge_organizers",
|
||||
),
|
||||
path(
|
||||
"merge-organizers/confirm/",
|
||||
superuser_only(MergeOrganizersConfirmView.as_view()),
|
||||
name="events_merge_organizers_confirm",
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
@hooks.register("register_settings_menu_item")
|
||||
def register_merge_organizers_menu_item():
|
||||
return SuperuserMenuItem(
|
||||
_("Merge organizers"),
|
||||
reverse_lazy("events_merge_organizers"),
|
||||
icon_name="group",
|
||||
order=1100,
|
||||
)
|
||||
|
||||
|
||||
@hooks.register("register_admin_viewset")
|
||||
def register_viewset():
|
||||
return event_organizer_chooser_viewset
|
||||
|
||||
|
||||
@hooks.register("register_admin_viewset")
|
||||
def register_event_sidebar_viewset():
|
||||
return event_sidebar_viewset
|
||||
|
||||
|
||||
@hooks.register("register_admin_viewset")
|
||||
def register_event_explorer_viewset():
|
||||
return event_explorer_viewset
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
# Generated by Django 6.0.5 on 2026-07-02 14:30
|
||||
|
||||
import wagtail.fields
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('generic', '0026_alter_genericpage_body'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='genericpage',
|
||||
name='body',
|
||||
field=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'), ('studioNote', 'Note (STUDiO)'), ('studioCalendar', 'Kalender (STUDiO)'), ('studioGuitar', 'Gitar (STUDiO)'), ('studioDisco', 'Discokule (STUDiO)'), ('studioMole', 'Muldvarpen (STUDiO)')], '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)]], {})}),
|
||||
),
|
||||
]
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -4,13 +4,14 @@ from wagtail.admin.panels import FieldPanel
|
||||
from wagtail.fields import RichTextField, StreamField
|
||||
from wagtail.models import Page
|
||||
from wagtail.search import index
|
||||
from wagtail_headless_preview.models import HeadlessMixin
|
||||
|
||||
from dnscms.blocks import PageSectionBlock
|
||||
from dnscms.fields import BASE_BLOCKS
|
||||
from dnscms.options import ALL_PIGS
|
||||
|
||||
|
||||
class GenericPage(Page):
|
||||
class GenericPage(HeadlessMixin, Page):
|
||||
subpage_types = ["generic.GenericPage"]
|
||||
show_in_menus = True
|
||||
|
||||
|
||||
@@ -10,9 +10,10 @@ from wagtail.admin.panels import (
|
||||
PageChooserPanel,
|
||||
)
|
||||
from wagtail.models import Orderable, Page
|
||||
from wagtail_headless_preview.models import HeadlessMixin
|
||||
|
||||
|
||||
class HomePage(Page):
|
||||
class HomePage(HeadlessMixin, Page):
|
||||
max_count = 1
|
||||
|
||||
content_panels = Page.content_panels + [
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
{% extends "base.html" %}
|
||||
{% load static %}
|
||||
|
||||
{% block body_class %}template-homepage{% endblock %}
|
||||
|
||||
{% block extra_css %}
|
||||
|
||||
{% comment %}
|
||||
Delete the line below if you're just getting started and want to remove the welcome screen!
|
||||
{% endcomment %}
|
||||
<link rel="stylesheet" href="{% static 'css/welcome_page.css' %}">
|
||||
{% endblock extra_css %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
{% comment %}
|
||||
Delete the line below if you're just getting started and want to remove the welcome screen!
|
||||
{% endcomment %}
|
||||
{% include 'home/welcome_page.html' %}
|
||||
|
||||
{% endblock content %}
|
||||
@@ -1,52 +0,0 @@
|
||||
{% load i18n wagtailcore_tags %}
|
||||
|
||||
<header class="header">
|
||||
<div class="logo">
|
||||
<a href="https://wagtail.org/">
|
||||
<svg class="figure-logo" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 342.5 126.2"><title>{% trans "Visit the Wagtail website" %}</title><path fill="#FFF" d="M84 1.9v5.7s-10.2-3.8-16.8 3.1c-4.8 5-5.2 10.6-3 18.1 21.6 0 25 12.1 25 12.1L87 27l6.8-8.3c0-9.8-8.1-16.3-9.8-16.8z"/><circle cx="85.9" cy="15.9" r="2.6"/><path d="M89.2 40.9s-3.3-16.6-24.9-12.1c-2.2-7.5-1.8-13 3-18.1C73.8 3.8 84 7.6 84 7.6V1.9C80.4.3 77 0 73.2 0 59.3 0 51.6 10.4 48.3 17.4L9.2 89.3l11-2.1-20.2 39 14.1-2.5L24.9 93c30.6 0 69.8-11 64.3-52.1z"/><path d="M102.4 27l-8.6-8.3L87 27z"/><path fill="#FFF" d="M30 84.1s1-.2 2.8-.6c1.8-.4 4.3-1 7.3-1.8 1.5-.4 3.1-.9 4.8-1.5 1.7-.6 3.5-1.2 5.2-2 1.8-.7 3.6-1.6 5.4-2.6 1.8-1 3.5-2.1 5.1-3.4.4-.3.8-.6 1.2-1l1.2-1c.7-.7 1.5-1.4 2.2-2.2.7-.7 1.3-1.5 1.9-2.3l.9-1.2.4-.6.4-.6c.2-.4.5-.8.7-1.2.2-.4.4-.8.7-1.2l.3-.6.3-.6c.2-.4.4-.8.5-1.2l.9-2.4c.2-.8.5-1.6.7-2.3.2-.7.3-1.5.5-2.1.1-.7.2-1.3.3-2 .1-.6.2-1.2.2-1.7.1-.5.1-1 .2-1.5.1-1.8.1-2.8.1-2.8l1.6.1s-.1 1.1-.2 2.9c-.1.5-.1 1-.2 1.5-.1.6-.1 1.2-.3 1.8-.1.6-.3 1.3-.4 2-.2.7-.4 1.4-.6 2.2-.2.8-.5 1.5-.8 2.4-.3.8-.6 1.6-1 2.5l-.6 1.2-.3.6-.3.6c-.2.4-.5.8-.7 1.3-.3.4-.5.8-.8 1.2-.1.2-.3.4-.4.6l-.4.6-.9 1.2c-.7.8-1.3 1.6-2.1 2.3-.7.8-1.5 1.4-2.3 2.2l-1.2 1c-.4.3-.8.6-1.3.9-1.7 1.2-3.5 2.3-5.3 3.3-1.8.9-3.7 1.8-5.5 2.5-1.8.7-3.6 1.3-5.3 1.8-1.7.5-3.3 1-4.9 1.3-3 .7-5.6 1.3-7.4 1.6-1.6.6-2.6.8-2.6.8z"/><g fill="#231F20"><path d="M127 83.9h-8.8l-12.6-36.4h7.9l9 27.5 9-27.5h7.9l9 27.5 9-27.5h7.9L153 83.9h-8.8L135.6 59 127 83.9zM200.1 83.9h-7V79c-3 3.6-7 5.4-12.1 5.4-3.8 0-6.9-1.1-9.4-3.2s-3.7-5-3.7-8.6c0-3.6 1.3-6.3 4-8 2.6-1.8 6.2-2.7 10.7-2.7h9.9v-1.4c0-4.8-2.7-7.3-8.1-7.3-3.4 0-6.9 1.2-10.5 3.7l-3.4-4.8c4.4-3.5 9.4-5.3 15.1-5.3 4.3 0 7.8 1.1 10.5 3.2 2.7 2.2 4.1 5.6 4.1 10.2v23.7zm-7.7-13.6v-3.1h-8.6c-5.5 0-8.3 1.7-8.3 5.2 0 1.8.7 3.1 2.1 4.1 1.4.9 3.3 1.4 5.7 1.4 2.4 0 4.6-.7 6.4-2.1 1.8-1.3 2.7-3.1 2.7-5.5zM241.7 47.5v31.7c0 6.4-1.7 11.3-5.2 14.5-3.5 3.2-8 4.8-13.4 4.8-5.5 0-10.4-1.7-14.8-5.1l3.6-5.8c3.6 2.7 7.1 4 10.8 4 3.6 0 6.5-.9 8.6-2.8 2.1-1.9 3.2-4.9 3.2-9v-4.7c-1.1 2.1-2.8 3.9-4.9 5.1-2.1 1.3-4.5 1.9-7.1 1.9-4.8 0-8.8-1.7-11.9-5.1-3.1-3.4-4.7-7.6-4.7-12.6s1.6-9.2 4.7-12.6c3.1-3.4 7.1-5.1 11.9-5.1 4.8 0 8.7 2 11.7 6v-5.4h7.5zm-28.4 16.8c0 3 .9 5.6 2.8 7.7 1.8 2.2 4.3 3.2 7.5 3.2 3.1 0 5.7-1 7.6-3.1 1.9-2.1 2.9-4.7 2.9-7.8 0-3.1-1-5.8-2.9-7.9-2-2.2-4.5-3.2-7.6-3.2-3.1 0-5.6 1.1-7.4 3.4-2 2.1-2.9 4.7-2.9 7.7zM260.9 53.6v18.5c0 1.7.5 3.1 1.4 4.1.9 1 2.2 1.5 3.8 1.5 1.6 0 3.2-.8 4.7-2.4l3.1 5.4c-2.7 2.4-5.7 3.6-8.9 3.6-3.3 0-6-1.1-8.3-3.4-2.3-2.3-3.5-5.3-3.5-9.1V53.6h-4.6v-6.2h4.6V36.1h7.7v11.4h9.6v6.2h-9.6zM309.5 83.9h-7V79c-3 3.6-7 5.4-12.1 5.4-3.8 0-6.9-1.1-9.4-3.2s-3.7-5-3.7-8.6c0-3.6 1.3-6.3 4-8 2.6-1.8 6.2-2.7 10.7-2.7h9.9v-1.4c0-4.8-2.7-7.3-8.1-7.3-3.4 0-6.9 1.2-10.5 3.7l-3.4-4.8c4.4-3.5 9.4-5.3 15.1-5.3 4.3 0 7.8 1.1 10.5 3.2 2.7 2.2 4.1 5.6 4.1 10.2v23.7zm-7.7-13.6v-3.1h-8.6c-5.5 0-8.3 1.7-8.3 5.2 0 1.8.7 3.1 2.1 4.1 1.4.9 3.3 1.4 5.7 1.4 2.4 0 4.6-.7 6.4-2.1 1.8-1.3 2.7-3.1 2.7-5.5zM319.3 40.2c-1-1-1.4-2.1-1.4-3.4 0-1.3.5-2.5 1.4-3.4 1-1 2.1-1.4 3.4-1.4 1.3 0 2.5.5 3.4 1.4 1 1 1.4 2.1 1.4 3.4 0 1.3-.5 2.5-1.4 3.4s-2.1 1.4-3.4 1.4c-1.3.1-2.4-.4-3.4-1.4zm7.2 43.7h-7.7V47.5h7.7v36.4zM342.5 83.9h-7.7V33.1h7.7v50.8z"/></g></svg>
|
||||
</a>
|
||||
</div>
|
||||
<div class="header-link">
|
||||
{% comment %}
|
||||
This works for all cases but prerelease versions:
|
||||
{% endcomment %}
|
||||
<a href="{% wagtail_documentation_path %}/releases/{% wagtail_release_notes_path %}">
|
||||
{% trans "View the release notes" %}
|
||||
</a>
|
||||
</div>
|
||||
</header>
|
||||
<main class="main">
|
||||
<div class="figure">
|
||||
<svg class="figure-space" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 300 300" aria-hidden="true">
|
||||
<path class="egg" fill="currentColor" d="M150 250c-42.741 0-75-32.693-75-90s42.913-110 75-110c32.088 0 75 52.693 75 110s-32.258 90-75 90z"/>
|
||||
<ellipse fill="#ddd" cx="150" cy="270" rx="40" ry="7"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="main-text">
|
||||
<h1>{% trans "Welcome to your new Wagtail site!" %}</h1>
|
||||
<p>{% trans 'Please feel free to <a href="https://github.com/wagtail/wagtail/wiki/Slack">join our community on Slack</a>, or get started with one of the links below.' %}</p>
|
||||
</div>
|
||||
</main>
|
||||
<footer class="footer" role="contentinfo">
|
||||
<a class="option option-one" href="{% wagtail_documentation_path %}/">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" aria-hidden="true"><path d="M9 21c0 .5.4 1 1 1h4c.6 0 1-.5 1-1v-1H9v1zm3-19C8.1 2 5 5.1 5 9c0 2.4 1.2 4.5 3 5.7V17c0 .5.4 1 1 1h6c.6 0 1-.5 1-1v-2.3c1.8-1.3 3-3.4 3-5.7 0-3.9-3.1-7-7-7zm2.9 11.1l-.9.6V16h-4v-2.3l-.9-.6C7.8 12.2 7 10.6 7 9c0-2.8 2.2-5 5-5s5 2.2 5 5c0 1.6-.8 3.2-2.1 4.1z"/></svg>
|
||||
<div>
|
||||
<h2>{% trans "Wagtail Documentation" %}</h2>
|
||||
<p>{% trans "Topics, references, & how-tos" %}</p>
|
||||
</div>
|
||||
</a>
|
||||
<a class="option option-two" href="{% wagtail_documentation_path %}/getting_started/tutorial.html">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" aria-hidden="true"><path d="M0 0h24v24H0V0z" fill="none"/><path d="M9.4 16.6L4.8 12l4.6-4.6L8 6l-6 6 6 6 1.4-1.4zm5.2 0l4.6-4.6-4.6-4.6L16 6l6 6-6 6-1.4-1.4z"/></svg>
|
||||
<div>
|
||||
<h2>{% trans "Tutorial" %}</h2>
|
||||
<p>{% trans "Build your first Wagtail site" %}</p>
|
||||
</div>
|
||||
</a>
|
||||
<a class="option option-three" href="{% url 'wagtailadmin_home' %}">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" aria-hidden="true"><path d="M0 0h24v24H0z" fill="none"/><path d="M16.5 13c-1.2 0-3.07.34-4.5 1-1.43-.67-3.3-1-4.5-1C5.33 13 1 14.08 1 16.25V19h22v-2.75c0-2.17-4.33-3.25-6.5-3.25zm-4 4.5h-10v-1.25c0-.54 2.56-1.75 5-1.75s5 1.21 5 1.75v1.25zm9 0H14v-1.25c0-.46-.2-.86-.52-1.22.88-.3 1.96-.53 3.02-.53 2.44 0 5 1.21 5 1.75v1.25zM7.5 12c1.93 0 3.5-1.57 3.5-3.5S9.43 5 7.5 5 4 6.57 4 8.5 5.57 12 7.5 12zm0-5.5c1.1 0 2 .9 2 2s-.9 2-2 2-2-.9-2-2 .9-2 2-2zm9 5.5c1.93 0 3.5-1.57 3.5-3.5S18.43 5 16.5 5 13 6.57 13 8.5s1.57 3.5 3.5 3.5zm0-5.5c1.1 0 2 .9 2 2s-.9 2-2 2-2-.9-2-2 .9-2 2-2z"/></svg>
|
||||
<div>
|
||||
<h2>{% trans "Admin Interface" %}</h2>
|
||||
<p>{% trans "Create your superuser first!" %}</p>
|
||||
</div>
|
||||
</a>
|
||||
</footer>
|
||||
Binary file not shown.
@@ -0,0 +1,359 @@
|
||||
# SOME DESCRIPTIVE TITLE.
|
||||
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
|
||||
# This file is distributed under the same license as the PACKAGE package.
|
||||
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: dnscms\n"
|
||||
"Report-Msgid-Bugs-To: \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"
|
||||
|
||||
msgid "Title"
|
||||
msgstr "Tittel"
|
||||
|
||||
msgid "Type"
|
||||
msgstr "Type"
|
||||
|
||||
msgid "Updated"
|
||||
msgstr "Oppdatert"
|
||||
|
||||
msgid "Status"
|
||||
msgstr "Status"
|
||||
|
||||
msgid "Associations"
|
||||
msgstr "Foreninger"
|
||||
|
||||
msgid "Lead"
|
||||
msgstr "Ingress"
|
||||
|
||||
msgid "Content"
|
||||
msgstr "Innhold"
|
||||
|
||||
msgid "association index"
|
||||
msgstr "foreningsoversikt"
|
||||
|
||||
msgid "association indexes"
|
||||
msgstr "foreningsoversikter"
|
||||
|
||||
msgid "Association"
|
||||
msgstr "Forening"
|
||||
|
||||
msgid "Committee"
|
||||
msgstr "Utvalg"
|
||||
|
||||
msgid "Excerpt"
|
||||
msgstr "Utdrag"
|
||||
|
||||
msgid "A very short summary of the content below. Used in listing views."
|
||||
msgstr ""
|
||||
"En veldig kort oppsummering av innholdet nedenfor. Brukes i listevisninger."
|
||||
|
||||
msgid "Website"
|
||||
msgstr "Nettsted"
|
||||
|
||||
msgid "association"
|
||||
msgstr "forening"
|
||||
|
||||
msgid "associations"
|
||||
msgstr "foreninger"
|
||||
|
||||
msgid "Choose an association"
|
||||
msgstr "Velg en forening"
|
||||
|
||||
msgid "Choose another association"
|
||||
msgstr "Velg en annen forening"
|
||||
|
||||
msgid "Edit this association"
|
||||
msgstr "Rediger denne foreningen"
|
||||
|
||||
msgid "%Y-%m-%d at %H:%M"
|
||||
msgstr "%Y-%m-%d kl %H:%M"
|
||||
|
||||
#, python-format
|
||||
msgid "%(count)d occurrence"
|
||||
msgid_plural "%(count)d occurrences"
|
||||
msgstr[0] "%(count)d forekomst"
|
||||
msgstr[1] "%(count)d forekomster"
|
||||
|
||||
msgid "Date"
|
||||
msgstr "Dato"
|
||||
|
||||
msgid "Organizers"
|
||||
msgstr "Arrangører"
|
||||
|
||||
msgid "Events"
|
||||
msgstr "Arrangementer"
|
||||
|
||||
msgid "slug"
|
||||
msgstr "permalenke"
|
||||
|
||||
msgid "The name of the category as it will appear in URLs."
|
||||
msgstr "Navnet på kategorien slik det vil vises i URL-er."
|
||||
|
||||
msgid "Should this category be available as a filter in the event programme?"
|
||||
msgstr "Skal denne kategorien være mulig å filtrere på i programmet?"
|
||||
|
||||
msgid "None"
|
||||
msgstr "Ingen"
|
||||
|
||||
msgid "Default pig for events of this kind."
|
||||
msgstr "Standardgris for arrangementer av denne typen."
|
||||
|
||||
msgid "Pig"
|
||||
msgstr "Gris"
|
||||
|
||||
msgid "event category"
|
||||
msgstr "arrangementskategori"
|
||||
|
||||
msgid "event categories"
|
||||
msgstr "arrangementskategorier"
|
||||
|
||||
msgid "organizer"
|
||||
msgstr "arrangør"
|
||||
|
||||
msgid "organizers"
|
||||
msgstr "arrangører"
|
||||
|
||||
msgid "The name of the organizer as it will appear in URLs."
|
||||
msgstr "Navnet på arrangøren slik det vil vises i URL-er."
|
||||
|
||||
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."
|
||||
|
||||
msgid "Link to the external organizer's website"
|
||||
msgstr "Lenke til nettstedet til ekstern arrangør"
|
||||
|
||||
msgid "Internal organizer"
|
||||
msgstr "Intern arrangør"
|
||||
|
||||
msgid "External organizer"
|
||||
msgstr "Ekstern arrangør"
|
||||
|
||||
msgid "Leave this empty if the organizer exists in the list above."
|
||||
msgstr "La denne stå tom om arrangøren finnes i lista over."
|
||||
|
||||
msgid "event organizer"
|
||||
msgstr "arrangør"
|
||||
|
||||
msgid "event organizers"
|
||||
msgstr "arrangører"
|
||||
|
||||
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 "
|
||||
"cover uncritically!"
|
||||
msgstr ""
|
||||
"Velg et bilde til bruk i programmet og andre visningsflater. Bør være et "
|
||||
"bilde eller en illustrasjon uten for mye tekst – ikke gjenbruk et Facebook-"
|
||||
"cover ukritisk!"
|
||||
|
||||
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."
|
||||
msgstr ""
|
||||
"En kort tekst som kommer rett under tittelen. La denne gjerne stå tom om du "
|
||||
"fikk plass til det meste i hovedtittelen."
|
||||
|
||||
msgid "Automatic"
|
||||
msgstr "Automatisk"
|
||||
|
||||
msgid ""
|
||||
"The pig that hangs out on the event page. Automatic causes one to be chosen "
|
||||
"based on the event's category."
|
||||
msgstr ""
|
||||
"Grisen som henger på arrangementssiden. Automatisk fører til at en velges "
|
||||
"basert på arrangementets kategori."
|
||||
|
||||
msgid "Direct link to ticket purchase, e.g. TicketCo, Billetto or Ticketmaster"
|
||||
msgstr ""
|
||||
"Lenke direkte til billettkjøp, f.eks. TicketCo, Billetto eller Ticketmaster"
|
||||
|
||||
msgid "Direct link to the event on Facebook"
|
||||
msgstr "Lenke direkte til arrangementet på Facebook"
|
||||
|
||||
msgid "Free"
|
||||
msgstr "Gratis"
|
||||
|
||||
msgid "Is this event free for everyone?"
|
||||
msgstr "Er dette arrangementet gratis for alle?"
|
||||
|
||||
msgid "Regular price"
|
||||
msgstr "Ordinær pris"
|
||||
|
||||
msgid "Price for students"
|
||||
msgstr "Pris for studenter"
|
||||
|
||||
msgid "Price for DNS members"
|
||||
msgstr "Pris for medlemmer av DNS"
|
||||
|
||||
msgid ""
|
||||
"Write <strong>0</strong> for free. An empty field hides the price category. "
|
||||
"If possible, write digits only."
|
||||
msgstr ""
|
||||
"Skriv <strong>0</strong> om gratis. Tomt felt skjuler priskategorien. Om "
|
||||
"mulig, skriv kun tall."
|
||||
|
||||
msgid "Ticket purchase link"
|
||||
msgstr "Billettkjøpslenke"
|
||||
|
||||
msgid "Subtitle"
|
||||
msgstr "Undertittel"
|
||||
|
||||
msgid "Who is behind the event?"
|
||||
msgstr "Hvem står bak arrangementet?"
|
||||
|
||||
msgid "Organizer"
|
||||
msgstr "Arrangør"
|
||||
|
||||
msgid "Facebook link"
|
||||
msgstr "Facebook-lenke"
|
||||
|
||||
msgid "Direct link to the event on Facebook."
|
||||
msgstr "Lenke direkte til arrangementet på Facebook."
|
||||
|
||||
msgid "Pricing and tickets"
|
||||
msgstr "Priser og billettkjøp"
|
||||
|
||||
msgid "Date, time and venue"
|
||||
msgstr "Dato, tid og lokale"
|
||||
|
||||
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."
|
||||
|
||||
msgid "Occurrence"
|
||||
msgstr "Forekomst"
|
||||
|
||||
msgid "event"
|
||||
msgstr "arrangement"
|
||||
|
||||
msgid "events"
|
||||
msgstr "arrangementer"
|
||||
|
||||
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>."
|
||||
msgstr ""
|
||||
"Bruk denne <em>om ingen av lokalene som kan velges til venstre</em> passer. "
|
||||
"F.eks. <em>Frederikkeplassen</em> eller <em>Sirkusteltet</em>."
|
||||
|
||||
msgid "Start"
|
||||
msgstr "Start"
|
||||
|
||||
msgid "End"
|
||||
msgstr "Slutt"
|
||||
|
||||
msgid "Venue"
|
||||
msgstr "Lokale"
|
||||
|
||||
msgid "Venue as free text"
|
||||
msgstr "Lokale som fritekst"
|
||||
|
||||
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."
|
||||
|
||||
msgid "Venue is required."
|
||||
msgstr "Lokale er påkrevd."
|
||||
|
||||
msgid "occurrence"
|
||||
msgstr "forekomst"
|
||||
|
||||
msgid "occurrences"
|
||||
msgstr "forekomster"
|
||||
|
||||
msgid "Choose organizers"
|
||||
msgstr "Velg arrangører"
|
||||
|
||||
msgid "Choose an organizer"
|
||||
msgstr "Velg en arrangør"
|
||||
|
||||
msgid "Choose another organizer"
|
||||
msgstr "Velg en annen arrangør"
|
||||
|
||||
msgid "Edit this organizer"
|
||||
msgstr "Rediger denne arrangøren"
|
||||
|
||||
msgid "image"
|
||||
msgstr "bilde"
|
||||
|
||||
msgid "images"
|
||||
msgstr "bilder"
|
||||
|
||||
msgid "First published"
|
||||
msgstr "Først publisert"
|
||||
|
||||
msgid "News"
|
||||
msgstr "Nyheter"
|
||||
|
||||
msgid "news index"
|
||||
msgstr "nyhetsoversikt"
|
||||
|
||||
msgid "news indexes"
|
||||
msgstr "nyhetsoversikter"
|
||||
|
||||
msgid ""
|
||||
"Choose an image for use on the front page and other surfaces. Should be a "
|
||||
"photo or an illustration without too much text."
|
||||
msgstr ""
|
||||
"Velg et bilde til bruk på forsiden og andre visningsflater. Bør være et "
|
||||
"bilde eller en illustrasjon uten for mye tekst."
|
||||
|
||||
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."
|
||||
|
||||
msgid ""
|
||||
"A brief, introductory paragraph that summarizes the main content of the "
|
||||
"article."
|
||||
msgstr ""
|
||||
"Et kortfattet, innledende avsnitt som oppsummerer hovedinnholdet i "
|
||||
"artikkelen."
|
||||
|
||||
msgid "news article"
|
||||
msgstr "nyhetsartikkel"
|
||||
|
||||
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"
|
||||
+8
-3
@@ -1,3 +1,8 @@
|
||||
[tools]
|
||||
python = "3.14"
|
||||
uv = "latest"
|
||||
# Translation tasks
|
||||
[tasks.compilemessages]
|
||||
description = "Compile translation files for Norwegian Bokmal"
|
||||
run = "uv run manage.py compilemessages -l nb"
|
||||
|
||||
[tasks.makemessages]
|
||||
description = "Extract translation strings for Norwegian Bokmal"
|
||||
run = "uv run manage.py makemessages -l nb"
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
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, PageViewSet
|
||||
|
||||
from dnscms.admin import ListingRedirectChooseParentView
|
||||
from news.models import NewsPage
|
||||
|
||||
|
||||
class NewsChooseParentView(ListingRedirectChooseParentView):
|
||||
listing_url_name = "news:index"
|
||||
|
||||
|
||||
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"),
|
||||
sort_key="latest_revision_created_at",
|
||||
width="10%",
|
||||
),
|
||||
PageStatusColumn("status", label=_("Status"), sort_key="live", width="10%"),
|
||||
]
|
||||
|
||||
|
||||
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()
|
||||
@@ -0,0 +1,28 @@
|
||||
# Generated by Django 6.0.5 on 2026-05-19 19:55
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('images', '0005_customimage_description'),
|
||||
('news', '0018_newspage_wp_block_json_newspage_wp_link_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name='newsindex',
|
||||
options={'verbose_name': 'news index', 'verbose_name_plural': 'news indexes'},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='newspage',
|
||||
options={'verbose_name': 'news article', 'verbose_name_plural': 'news articles'},
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='newspage',
|
||||
name='featured_image',
|
||||
field=models.ForeignKey(blank=True, help_text='Choose an image for use on the front page and other surfaces. Should be a photo or an illustration without too much text.', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='images.customimage'),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,19 @@
|
||||
# Generated by Django 6.0.5 on 2026-07-02 14:53
|
||||
|
||||
import wagtail.fields
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('news', '0019_alter_news_options'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='newspage',
|
||||
name='body',
|
||||
field=wagtail.fields.StreamField([('paragraph', 0), ('image', 4), ('image_slider', 8), ('horizontal_rule', 10), ('featured', 18), ('page_section_navigation', 19), ('accordion', 23), ('fact_box', 26), ('schedule', 33), ('embed', 34), ('raw_html', 35)], 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.blocks.CharBlock', (), {'help_text': 'Overskrift for timeplanen (valgfritt)', 'label': 'Tittel', 'max_length': 64, 'required': False}), 28: ('wagtail.blocks.CharBlock', (), {'help_text': 'Fritekst, f.eks. "13:00", "20:00 – 23:00" eller "23.06"', 'label': 'Tidspunkt/dato', 'max_length': 64, 'required': True}), 29: ('wagtail.blocks.RichTextBlock', (), {'features': ['bold', 'italic', 'link'], 'help_text': 'Kort tittel på én linje. Du kan legge inn lenker.', 'label': 'Tittel', 'required': True}), 30: ('wagtail.blocks.RichTextBlock', (), {'features': ['bold', 'italic', 'link'], 'help_text': 'Valgfri tilleggsinformasjon.', 'label': 'Beskrivelse', 'required': False}), 31: ('wagtail.blocks.StructBlock', [[('time', 28), ('title', 29), ('description', 30)]], {}), 32: ('wagtail.blocks.ListBlock', (31,), {'label': 'Punkter', 'min_num': 1}), 33: ('wagtail.blocks.StructBlock', [[('title', 27), ('items', 32)]], {}), 34: ('wagtail.embeds.blocks.EmbedBlock', (), {}), 35: ('wagtail.blocks.RawHTMLBlock', (), {})}, default=[('paragraph', '')]),
|
||||
),
|
||||
]
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,19 @@
|
||||
# Generated by Django 6.0.5 on 2026-07-04 22:29
|
||||
|
||||
import wagtail.fields
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('news', '0021_alter_newspage_body'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='newspage',
|
||||
name='body',
|
||||
field=wagtail.fields.StreamField([('paragraph', 0), ('image', 4), ('image_slider', 8), ('horizontal_rule', 10), ('featured', 18), ('page_section_navigation', 19), ('accordion', 23), ('fact_box', 26), ('schedule', 33), ('embed', 34), ('raw_html', 35)], 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.blocks.CharBlock', (), {'help_text': 'Overskrift for timeplanen (valgfritt)', 'label': 'Tittel', 'max_length': 64, 'required': False}), 28: ('wagtail.blocks.CharBlock', (), {'help_text': 'Fritekst, f.eks. "13:00", "20:00 – 23:00" eller "23.06"', 'label': 'Tidspunkt/dato', 'max_length': 64, 'required': True}), 29: ('wagtail.blocks.RichTextBlock', (), {'features': ['bold', 'italic', 'link'], 'help_text': 'Kort tittel på én linje. Du kan legge inn lenker.', 'label': 'Tittel', 'required': True}), 30: ('wagtail.blocks.RichTextBlock', (), {'features': ['bold', 'italic', 'link'], 'help_text': 'Valgfri tilleggsinformasjon.', 'label': 'Beskrivelse', 'required': False}), 31: ('wagtail.blocks.StructBlock', [[('time', 28), ('title', 29), ('description', 30)]], {}), 32: ('wagtail.blocks.ListBlock', (31,), {'label': 'Punkter', 'min_num': 1}), 33: ('wagtail.blocks.StructBlock', [[('title', 27), ('items', 32)]], {}), 34: ('wagtail.embeds.blocks.EmbedBlock', (), {}), 35: ('wagtail.blocks.RawHTMLBlock', (), {})}, default=[('paragraph', '')]),
|
||||
),
|
||||
]
|
||||
+32
-60
@@ -1,24 +1,30 @@
|
||||
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, GraphQLRichText, GraphQLStreamfield, GraphQLString
|
||||
from wagtail.admin.panels import FieldPanel
|
||||
from wagtail.fields import RichTextField
|
||||
from wagtail.models import Page
|
||||
from wagtail.models import Page, PageManager
|
||||
from wagtail.search import index
|
||||
from wagtail_headless_preview.models import HeadlessMixin
|
||||
|
||||
from dnscms.fields import CommonStreamField
|
||||
from dnscms.wordpress.models import WPImportedPageMixin
|
||||
from dnscms.wordpress.models import DeferWPFieldsManagerMixin, WPImportedPageMixin
|
||||
|
||||
|
||||
class NewsPageManager(DeferWPFieldsManagerMixin, PageManager):
|
||||
pass
|
||||
|
||||
|
||||
@register_singular_query_field("newsIndex")
|
||||
class NewsIndex(Page):
|
||||
class NewsIndex(HeadlessMixin, Page):
|
||||
max_count = 1
|
||||
subpage_types = ["news.NewsPage"]
|
||||
|
||||
lead = RichTextField(features=["italic", "link"], blank=True)
|
||||
|
||||
content_panels = Page.content_panels + [
|
||||
FieldPanel("lead", heading="Ingress"),
|
||||
FieldPanel("lead", heading=_("Lead")),
|
||||
]
|
||||
|
||||
graphql_fields = [
|
||||
@@ -27,12 +33,18 @@ class NewsIndex(Page):
|
||||
|
||||
search_fields = []
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("news index")
|
||||
verbose_name_plural = _("news indexes")
|
||||
|
||||
class NewsPage(WPImportedPageMixin, Page):
|
||||
|
||||
class NewsPage(HeadlessMixin, WPImportedPageMixin, Page):
|
||||
subpage_types = []
|
||||
parent_page_types = ["news.NewsIndex"]
|
||||
show_in_menus = False
|
||||
|
||||
objects = NewsPageManager()
|
||||
|
||||
excerpt = models.TextField(max_length=512, blank=False)
|
||||
lead = RichTextField(features=["italic", "link"], blank=True)
|
||||
body = CommonStreamField
|
||||
@@ -42,23 +54,28 @@ class NewsPage(WPImportedPageMixin, Page):
|
||||
blank=True,
|
||||
on_delete=models.SET_NULL,
|
||||
related_name="+",
|
||||
help_text=(
|
||||
"Velg et bilde til bruk i på forsiden og andre visningsflater. "
|
||||
"Bør være et bilde eller en illustrasjon uten tekst."
|
||||
help_text=_(
|
||||
"Choose an image for use on the front page and other surfaces. "
|
||||
"Should be a photo or an illustration without too much text."
|
||||
),
|
||||
)
|
||||
|
||||
content_panels = Page.content_panels + [
|
||||
FieldPanel(
|
||||
"excerpt",
|
||||
heading="Utdrag",
|
||||
help_text="En veldig kort oppsummering av innholdet i artikkelen. Brukes på forsiden og i artikkeloversikten.",
|
||||
heading=_("Excerpt"),
|
||||
help_text=_(
|
||||
"A very short summary of the article's content. "
|
||||
"Used on the front page and in the article listing."
|
||||
),
|
||||
),
|
||||
FieldPanel("featured_image"),
|
||||
FieldPanel(
|
||||
"lead",
|
||||
heading="Ingress",
|
||||
help_text="Et kortfattet, innledende avsnitt som oppsummerer hovedinnholdet i artikkelen.",
|
||||
heading=_("Lead"),
|
||||
help_text=_(
|
||||
"A brief, introductory paragraph that summarizes the main content of the article."
|
||||
),
|
||||
),
|
||||
FieldPanel("body"),
|
||||
]
|
||||
@@ -76,51 +93,6 @@ class NewsPage(WPImportedPageMixin, Page):
|
||||
index.SearchField("body"),
|
||||
]
|
||||
|
||||
def import_wordpress_data(self, data):
|
||||
import html
|
||||
|
||||
from bs4 import BeautifulSoup
|
||||
|
||||
def generate_excerpt(html_content):
|
||||
soup = BeautifulSoup(html_content, features="lxml")
|
||||
VALID_TAGS = ["div", "p"]
|
||||
|
||||
for tag in soup.findAll("p"):
|
||||
if tag.name not in VALID_TAGS:
|
||||
tag.remove()
|
||||
|
||||
text = soup.get_text().strip()
|
||||
words = text.split(" ")
|
||||
if len(words) < 26:
|
||||
return text
|
||||
return " ".join(words[:25]) + " [...]"
|
||||
|
||||
# Wagtail page model fields
|
||||
self.title = html.unescape(data["title"])
|
||||
self.slug = data["slug"]
|
||||
self.first_published_at = data["first_published_at"]
|
||||
self.last_published_at = data["last_published_at"]
|
||||
self.latest_revision_created_at = data["latest_revision_created_at"]
|
||||
self.search_description = data["search_description"]
|
||||
|
||||
# debug fields
|
||||
self.wp_post_id = data["wp_post_id"]
|
||||
self.wp_post_type = data["wp_post_type"]
|
||||
self.wp_link = data["wp_link"]
|
||||
self.wp_raw_content = data["wp_raw_content"]
|
||||
self.wp_block_json = data["wp_block_json"]
|
||||
self.wp_processed_content = data["wp_processed_content"]
|
||||
self.wp_normalized_styles = data["wp_normalized_styles"]
|
||||
self.wp_post_meta = data["wp_post_meta"]
|
||||
|
||||
# own model fields
|
||||
self.body = data["body"] or ""
|
||||
|
||||
meta = data["wp_post_meta"]
|
||||
|
||||
written_excerpt = meta.get("excerpt_encoded")
|
||||
generated_excerpt = ""
|
||||
if not written_excerpt:
|
||||
generated_excerpt = generate_excerpt(self.wp_processed_content)
|
||||
|
||||
self.excerpt = written_excerpt or generated_excerpt or "[...]"
|
||||
class Meta:
|
||||
verbose_name = _("news article")
|
||||
verbose_name_plural = _("news articles")
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
from wagtail import hooks
|
||||
|
||||
from .admin import news_sidebar_viewset, news_explorer_viewset
|
||||
|
||||
|
||||
@hooks.register("register_admin_viewset")
|
||||
def register_news_sidebar_viewset():
|
||||
return news_sidebar_viewset
|
||||
|
||||
|
||||
@hooks.register("register_admin_viewset")
|
||||
def register_news_explorer_viewset():
|
||||
return news_explorer_viewset
|
||||
@@ -1,3 +0,0 @@
|
||||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
||||
+29
-9
@@ -4,20 +4,24 @@ version = "0.1.0"
|
||||
description = ""
|
||||
authors = [{ name = "EDB", email = "edb@neuf.no" }]
|
||||
requires-python = ">=3.14, <3.15"
|
||||
readme = "README.md"
|
||||
dependencies = [
|
||||
"wagtail>=7.3.1",
|
||||
"wagtail-grapple>=0.29.0",
|
||||
"django>=6.0.3",
|
||||
"django-extensions>=4.1",
|
||||
"psycopg2-binary>=2.9.11,<3",
|
||||
"gunicorn>=25.1.0",
|
||||
"whitenoise>=6.12.0",
|
||||
"wagtail>=7.4.2,<8",
|
||||
"wagtail-grapple>=0.31.0,<0.32",
|
||||
"wagtail-headless-preview>=0.9,<0.10",
|
||||
"django>=6.0.6,<7",
|
||||
"django-extensions>=4.1,<5",
|
||||
"psycopg2-binary>=2.9.12,<3",
|
||||
"gunicorn>=26.0.0,<27",
|
||||
"whitenoise>=6.12.0,<7",
|
||||
]
|
||||
|
||||
[dependency-groups]
|
||||
dev = [
|
||||
"ruff>=0.15.1",
|
||||
"ruff>=0.15.20,<0.16",
|
||||
"pytest>=9.1.1,<10",
|
||||
"pytest-cov>=7.0.0,<8",
|
||||
"pytest-django>=4.12.0,<5",
|
||||
"wagtail-factories>=4.5.0,<5",
|
||||
]
|
||||
|
||||
[tool.uv]
|
||||
@@ -34,3 +38,19 @@ line-length = 99
|
||||
select = ["F", "E", "W", "Q", "UP", "DJ"]
|
||||
ignore = []
|
||||
exclude = ["**/migrations/*.py"]
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
DJANGO_SETTINGS_MODULE = "dnscms.settings.test"
|
||||
python_files = ["test_*.py"]
|
||||
testpaths = ["tests"]
|
||||
addopts = "--cov=. --cov-report=term-missing"
|
||||
|
||||
[tool.coverage.run]
|
||||
omit = [
|
||||
"*/migrations/*",
|
||||
"tests/*",
|
||||
"manage.py",
|
||||
"dnscms/settings/*",
|
||||
"dnscms/wsgi.py",
|
||||
"dnscms/asgi.py",
|
||||
]
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
# Generated by Django 6.0.5 on 2026-07-02 14:53
|
||||
|
||||
import wagtail.fields
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('sponsors', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='sponsorspage',
|
||||
name='body',
|
||||
field=wagtail.fields.StreamField([('paragraph', 0), ('image', 4), ('image_slider', 8), ('horizontal_rule', 10), ('featured', 18), ('page_section_navigation', 19), ('accordion', 23), ('fact_box', 26), ('schedule', 33), ('embed', 34), ('raw_html', 35)], 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.blocks.CharBlock', (), {'help_text': 'Overskrift for timeplanen (valgfritt)', 'label': 'Tittel', 'max_length': 64, 'required': False}), 28: ('wagtail.blocks.CharBlock', (), {'help_text': 'Fritekst, f.eks. "13:00", "20:00 – 23:00" eller "23.06"', 'label': 'Tidspunkt/dato', 'max_length': 64, 'required': True}), 29: ('wagtail.blocks.RichTextBlock', (), {'features': ['bold', 'italic', 'link'], 'help_text': 'Kort tittel på én linje. Du kan legge inn lenker.', 'label': 'Tittel', 'required': True}), 30: ('wagtail.blocks.RichTextBlock', (), {'features': ['bold', 'italic', 'link'], 'help_text': 'Valgfri tilleggsinformasjon.', 'label': 'Beskrivelse', 'required': False}), 31: ('wagtail.blocks.StructBlock', [[('time', 28), ('title', 29), ('description', 30)]], {}), 32: ('wagtail.blocks.ListBlock', (31,), {'label': 'Punkter', 'min_num': 1}), 33: ('wagtail.blocks.StructBlock', [[('title', 27), ('items', 32)]], {}), 34: ('wagtail.embeds.blocks.EmbedBlock', (), {}), 35: ('wagtail.blocks.RawHTMLBlock', (), {})}),
|
||||
),
|
||||
]
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,19 @@
|
||||
# Generated by Django 6.0.5 on 2026-07-04 22:29
|
||||
|
||||
import wagtail.fields
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('sponsors', '0003_alter_sponsorspage_body'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='sponsorspage',
|
||||
name='body',
|
||||
field=wagtail.fields.StreamField([('paragraph', 0), ('image', 4), ('image_slider', 8), ('horizontal_rule', 10), ('featured', 18), ('page_section_navigation', 19), ('accordion', 23), ('fact_box', 26), ('schedule', 33), ('embed', 34), ('raw_html', 35)], 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.blocks.CharBlock', (), {'help_text': 'Overskrift for timeplanen (valgfritt)', 'label': 'Tittel', 'max_length': 64, 'required': False}), 28: ('wagtail.blocks.CharBlock', (), {'help_text': 'Fritekst, f.eks. "13:00", "20:00 – 23:00" eller "23.06"', 'label': 'Tidspunkt/dato', 'max_length': 64, 'required': True}), 29: ('wagtail.blocks.RichTextBlock', (), {'features': ['bold', 'italic', 'link'], 'help_text': 'Kort tittel på én linje. Du kan legge inn lenker.', 'label': 'Tittel', 'required': True}), 30: ('wagtail.blocks.RichTextBlock', (), {'features': ['bold', 'italic', 'link'], 'help_text': 'Valgfri tilleggsinformasjon.', 'label': 'Beskrivelse', 'required': False}), 31: ('wagtail.blocks.StructBlock', [[('time', 28), ('title', 29), ('description', 30)]], {}), 32: ('wagtail.blocks.ListBlock', (31,), {'label': 'Punkter', 'min_num': 1}), 33: ('wagtail.blocks.StructBlock', [[('title', 27), ('items', 32)]], {}), 34: ('wagtail.embeds.blocks.EmbedBlock', (), {}), 35: ('wagtail.blocks.RawHTMLBlock', (), {})}),
|
||||
),
|
||||
]
|
||||
@@ -8,6 +8,7 @@ from wagtail.fields import RichTextField, StreamField
|
||||
from wagtail.images.blocks import ImageChooserBlock
|
||||
from wagtail.models import Page
|
||||
from wagtail.search import index
|
||||
from wagtail_headless_preview.models import HeadlessMixin
|
||||
|
||||
from dnscms.blocks import BASE_BLOCKS
|
||||
|
||||
@@ -34,7 +35,7 @@ class SponsorBlock(blocks.StructBlock):
|
||||
|
||||
|
||||
@register_singular_query_field("sponsorsPage")
|
||||
class SponsorsPage(Page):
|
||||
class SponsorsPage(HeadlessMixin, Page):
|
||||
max_count = 1
|
||||
subpage_types = []
|
||||
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class StudioConfig(AppConfig):
|
||||
default_auto_field = "django.db.models.BigAutoField"
|
||||
name = "studio"
|
||||
@@ -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',),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,19 @@
|
||||
# Generated by Django 6.0.5 on 2026-07-02 14:30
|
||||
|
||||
import wagtail.fields
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('studio', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='studiopage',
|
||||
name='body',
|
||||
field=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'), ('studioNote', 'Note (STUDiO)'), ('studioCalendar', 'Kalender (STUDiO)'), ('studioGuitar', 'Gitar (STUDiO)'), ('studioDisco', 'Discokule (STUDiO)'), ('studioMole', 'Muldvarpen (STUDiO)')], '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)]], {})}),
|
||||
),
|
||||
]
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,64 @@
|
||||
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 wagtail_headless_preview.models import HeadlessMixin
|
||||
|
||||
from dnscms.blocks import BASE_BLOCKS, PageSectionBlock
|
||||
from dnscms.options import ALL_PIGS
|
||||
|
||||
|
||||
@register_singular_query_field("studioPage")
|
||||
class StudioPage(HeadlessMixin, 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"),
|
||||
]
|
||||
@@ -0,0 +1,144 @@
|
||||
import json
|
||||
|
||||
import factory
|
||||
import pytest
|
||||
import wagtail_factories
|
||||
from wagtail.models import Page
|
||||
|
||||
from associations.models import AssociationIndex, AssociationPage
|
||||
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
|
||||
|
||||
|
||||
class CustomImageFactory(wagtail_factories.ImageFactory):
|
||||
class Meta:
|
||||
model = CustomImage
|
||||
|
||||
|
||||
class AssociationIndexFactory(wagtail_factories.PageFactory):
|
||||
title = factory.Sequence(lambda n: f"Associations {n}")
|
||||
lead = "<p>Foreninger og utvalg.</p>"
|
||||
|
||||
class Meta:
|
||||
model = AssociationIndex
|
||||
|
||||
|
||||
class AssociationPageFactory(wagtail_factories.PageFactory):
|
||||
title = factory.Sequence(lambda n: f"Association {n}")
|
||||
excerpt = "Et utdrag."
|
||||
|
||||
class Meta:
|
||||
model = AssociationPage
|
||||
|
||||
|
||||
class EventIndexFactory(wagtail_factories.PageFactory):
|
||||
title = factory.Sequence(lambda n: f"Events {n}")
|
||||
|
||||
class Meta:
|
||||
model = EventIndex
|
||||
|
||||
|
||||
class EventPageFactory(wagtail_factories.PageFactory):
|
||||
title = factory.Sequence(lambda n: f"Event {n}")
|
||||
|
||||
class Meta:
|
||||
model = EventPage
|
||||
|
||||
|
||||
class GenericPageFactory(wagtail_factories.PageFactory):
|
||||
title = factory.Sequence(lambda n: f"Page {n}")
|
||||
lead = "<p>Ingress.</p>"
|
||||
|
||||
class Meta:
|
||||
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}")
|
||||
|
||||
class Meta:
|
||||
model = NewsIndex
|
||||
|
||||
|
||||
class NewsPageFactory(wagtail_factories.PageFactory):
|
||||
title = factory.Sequence(lambda n: f"Article {n}")
|
||||
excerpt = "Et utdrag."
|
||||
|
||||
class Meta:
|
||||
model = NewsPage
|
||||
|
||||
|
||||
class VenueIndexFactory(wagtail_factories.PageFactory):
|
||||
title = factory.Sequence(lambda n: f"Venues {n}")
|
||||
|
||||
class Meta:
|
||||
model = VenueIndex
|
||||
|
||||
|
||||
class VenuePageFactory(wagtail_factories.PageFactory):
|
||||
title = factory.Sequence(lambda n: f"Venue {n}")
|
||||
|
||||
class Meta:
|
||||
model = VenuePage
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def root_page(db):
|
||||
return Page.objects.get(depth=1)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def home_page(root_page):
|
||||
# Wagtail's initial migration creates a default "Welcome" page at depth=2.
|
||||
# Reuse it so we don't fight slug collisions across tests.
|
||||
return root_page.get_children().first() or root_page.add_child(
|
||||
instance=Page(title="Home", slug="home")
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def event_index(home_page):
|
||||
return EventIndexFactory(parent=home_page)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def news_index(home_page):
|
||||
return NewsIndexFactory(parent=home_page)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def association_index(home_page):
|
||||
return AssociationIndexFactory(parent=home_page)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def venue(home_page):
|
||||
venue_index = VenueIndexFactory(parent=home_page)
|
||||
return VenuePageFactory(parent=venue_index)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def graphql_post(client):
|
||||
def _post(query, variables=None):
|
||||
payload = {"query": query}
|
||||
if variables is not None:
|
||||
payload["variables"] = variables
|
||||
response = client.post(
|
||||
"/api/graphql/",
|
||||
data=json.dumps(payload),
|
||||
content_type="application/json",
|
||||
)
|
||||
return response, response.json()
|
||||
|
||||
return _post
|
||||
@@ -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
|
||||
@@ -0,0 +1,603 @@
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
import pytest
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db import connection
|
||||
from django.test.utils import CaptureQueriesContext
|
||||
from django.urls import reverse
|
||||
from django.utils import timezone
|
||||
|
||||
from events.admin import EventDateColumn, OrganizersColumn
|
||||
from events.models import (
|
||||
EventCategory,
|
||||
EventOccurrence,
|
||||
EventOrganizer,
|
||||
EventOrganizerLink,
|
||||
EventPage,
|
||||
)
|
||||
from events.views import EventOrganizerCreationForm
|
||||
from tests.conftest import (
|
||||
AssociationPageFactory,
|
||||
CustomImageFactory,
|
||||
EventPageFactory,
|
||||
)
|
||||
|
||||
|
||||
def test_eventpage_clean_unsets_specific_pricing_when_free():
|
||||
page = EventPage(
|
||||
title="Free event",
|
||||
slug="free-event",
|
||||
free=True,
|
||||
price_regular="100",
|
||||
price_student="50",
|
||||
price_member="25",
|
||||
)
|
||||
|
||||
page.clean()
|
||||
|
||||
assert page.price_regular == ""
|
||||
assert page.price_student == ""
|
||||
assert page.price_member == ""
|
||||
|
||||
|
||||
def test_eventpage_clean_keeps_specific_pricing_when_not_free():
|
||||
page = EventPage(
|
||||
title="Paid event",
|
||||
slug="paid-event",
|
||||
free=False,
|
||||
price_regular="100",
|
||||
price_student="50",
|
||||
price_member="25",
|
||||
)
|
||||
|
||||
page.clean()
|
||||
|
||||
assert page.price_regular == "100"
|
||||
assert page.price_student == "50"
|
||||
assert page.price_member == "25"
|
||||
|
||||
|
||||
def test_eventpage_clean_dedupes_organizers_by_name(event_index):
|
||||
org_a = EventOrganizer.objects.create(name="DNS", slug="dns-a")
|
||||
org_b = EventOrganizer.objects.create(name="DNS", slug="dns-b")
|
||||
|
||||
event = EventPageFactory(parent=event_index)
|
||||
EventOrganizerLink.objects.create(event=event, organizer=org_a)
|
||||
EventOrganizerLink.objects.create(event=event, organizer=org_b)
|
||||
|
||||
event = EventPage.objects.get(pk=event.pk)
|
||||
assert event.organizer_links.count() == 2
|
||||
|
||||
event.clean()
|
||||
|
||||
assert event.organizer_links.count() == 1
|
||||
|
||||
|
||||
def test_eventpage_clean_dedupes_three_duplicates_and_keeps_distinct(event_index):
|
||||
dup_1 = EventOrganizer.objects.create(name="DNS", slug="dns-1")
|
||||
dup_2 = EventOrganizer.objects.create(name="DNS", slug="dns-2")
|
||||
dup_3 = EventOrganizer.objects.create(name="DNS", slug="dns-3")
|
||||
distinct = EventOrganizer.objects.create(name="Studentersamfundet", slug="ss")
|
||||
|
||||
event = EventPageFactory(parent=event_index)
|
||||
for organizer in (dup_1, dup_2, dup_3, distinct):
|
||||
EventOrganizerLink.objects.create(event=event, organizer=organizer)
|
||||
|
||||
event = EventPage.objects.get(pk=event.pk)
|
||||
assert event.organizer_links.count() == 4
|
||||
|
||||
event.clean()
|
||||
|
||||
names = sorted(link.organizer.name for link in event.organizer_links.all())
|
||||
assert names == ["DNS", "Studentersamfundet"]
|
||||
|
||||
|
||||
def test_eventoccurrence_clean_rejects_both_venue_and_venue_custom(event_index, venue):
|
||||
event = EventPageFactory(parent=event_index)
|
||||
occurrence = EventOccurrence(
|
||||
event=event,
|
||||
start=timezone.now(),
|
||||
venue=venue,
|
||||
venue_custom="Frederikkeplassen",
|
||||
)
|
||||
|
||||
with pytest.raises(ValidationError) as exc:
|
||||
occurrence.clean()
|
||||
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())
|
||||
|
||||
with pytest.raises(ValidationError) as exc:
|
||||
occurrence.clean()
|
||||
assert "venue" in exc.value.message_dict
|
||||
|
||||
|
||||
def test_eventpage_manager_future_filters_past_and_annotates(event_index):
|
||||
now = timezone.now()
|
||||
|
||||
past = EventPageFactory(parent=event_index, title="Past")
|
||||
EventOccurrence.objects.create(event=past, start=now - timedelta(days=7), venue_custom="Old")
|
||||
|
||||
future = EventPageFactory(parent=event_index, title="Future")
|
||||
EventOccurrence.objects.create(event=future, start=now + timedelta(days=7), venue_custom="New")
|
||||
|
||||
results = list(EventPage.objects.live().future().order_by("next_occurrence"))
|
||||
|
||||
assert [p.pk for p in results] == [future.pk]
|
||||
assert results[0].next_occurrence is not None
|
||||
|
||||
|
||||
def test_future_includes_occurrence_late_today(event_index):
|
||||
today_start = timezone.localtime(timezone.now()).replace(
|
||||
hour=0, minute=0, second=0, microsecond=0
|
||||
)
|
||||
late_today = today_start + timedelta(hours=23, minutes=59)
|
||||
|
||||
event = EventPageFactory(parent=event_index, title="Late today")
|
||||
EventOccurrence.objects.create(event=event, start=late_today, venue_custom="X")
|
||||
|
||||
assert event.pk in EventPage.objects.future().values_list("pk", flat=True)
|
||||
|
||||
|
||||
def test_future_excludes_occurrence_just_before_today(event_index):
|
||||
today_start = timezone.localtime(timezone.now()).replace(
|
||||
hour=0, minute=0, second=0, microsecond=0
|
||||
)
|
||||
just_before_today = today_start - timedelta(seconds=1)
|
||||
|
||||
event = EventPageFactory(parent=event_index, title="Just past")
|
||||
EventOccurrence.objects.create(event=event, start=just_before_today, venue_custom="X")
|
||||
|
||||
assert event.pk not in EventPage.objects.future().values_list("pk", flat=True)
|
||||
|
||||
|
||||
def test_future_next_occurrence_picks_earliest_future_ignoring_past(event_index):
|
||||
now = timezone.now()
|
||||
soonest_future = now + timedelta(days=3)
|
||||
|
||||
event = EventPageFactory(parent=event_index, title="With history")
|
||||
EventOccurrence.objects.create(event=event, start=now - timedelta(days=30), venue_custom="X")
|
||||
EventOccurrence.objects.create(event=event, start=soonest_future, venue_custom="X")
|
||||
EventOccurrence.objects.create(event=event, start=now + timedelta(days=10), venue_custom="X")
|
||||
|
||||
annotated = EventPage.objects.future().filter(pk=event.pk).first()
|
||||
assert annotated is not None
|
||||
assert abs((annotated.next_occurrence - soonest_future).total_seconds()) < 1
|
||||
|
||||
|
||||
def test_graphql_event_index_future_events_query(event_index, graphql_post):
|
||||
upcoming = EventPageFactory(parent=event_index, title="Upcoming gig")
|
||||
EventOccurrence.objects.create(
|
||||
event=upcoming,
|
||||
start=timezone.now() + timedelta(days=3),
|
||||
venue_custom="Storsalen",
|
||||
)
|
||||
|
||||
response, body = graphql_post(
|
||||
"""
|
||||
query {
|
||||
eventIndex {
|
||||
futureEvents { title }
|
||||
}
|
||||
}
|
||||
"""
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert "errors" not in body, body
|
||||
titles = [e["title"] for e in body["data"]["eventIndex"]["futureEvents"]]
|
||||
assert "Upcoming gig" in titles
|
||||
|
||||
|
||||
def test_future_events_does_not_have_n_plus_one_queries(
|
||||
event_index, venue, association_index, graphql_post
|
||||
):
|
||||
"""Regression test: query count for futureEvents stays bounded as events grow."""
|
||||
konsert = EventCategory.objects.create(name="Konsert", slug="konsert")
|
||||
association = AssociationPageFactory(parent=association_index, title="DNS")
|
||||
org = EventOrganizer.objects.create(name="Forening", slug="forening", association=association)
|
||||
image = CustomImageFactory(title="Cover")
|
||||
now = timezone.now()
|
||||
|
||||
for i in range(5):
|
||||
event = EventPageFactory(
|
||||
parent=event_index,
|
||||
title=f"Event {i}",
|
||||
body=[("paragraph", "<p>x</p>")],
|
||||
featured_image=image,
|
||||
)
|
||||
event.categories.add(konsert)
|
||||
EventOrganizerLink.objects.create(event=event, organizer=org)
|
||||
EventOccurrence.objects.create(
|
||||
event=event,
|
||||
start=now + timedelta(days=i + 1),
|
||||
venue=venue,
|
||||
)
|
||||
|
||||
home_query = """
|
||||
query {
|
||||
eventIndex {
|
||||
futureEvents {
|
||||
id
|
||||
title
|
||||
subtitle
|
||||
body { blockType }
|
||||
featuredImage { url }
|
||||
occurrences { start end venueCustom venue { title } }
|
||||
categories { name slug }
|
||||
organizers { name slug association { title } }
|
||||
}
|
||||
}
|
||||
}
|
||||
"""
|
||||
|
||||
with CaptureQueriesContext(connection) as ctx:
|
||||
response, body = graphql_post(home_query)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert "errors" not in body, body
|
||||
assert len(body["data"]["eventIndex"]["futureEvents"]) == 5
|
||||
|
||||
# Bump only alongside an intentional resolver change.
|
||||
max_queries = 6
|
||||
assert len(ctx) <= max_queries, (
|
||||
f"futureEvents took {len(ctx)} queries for 5 events — likely N+1. "
|
||||
f"Captured queries:\n"
|
||||
+ "\n".join(f" {i + 1}. {q['sql'][:120]}" for i, q in enumerate(ctx.captured_queries))
|
||||
)
|
||||
|
||||
|
||||
def test_future_events_does_not_load_wp_import_fields(event_index, graphql_post):
|
||||
"""wp_* columns must stay deferred and lazy-load on explicit access."""
|
||||
event = EventPageFactory(parent=event_index, wp_raw_content="marker")
|
||||
EventOccurrence.objects.create(
|
||||
event=event, start=timezone.now() + timedelta(days=1), venue_custom="X"
|
||||
)
|
||||
|
||||
with CaptureQueriesContext(connection) as ctx:
|
||||
response, body = graphql_post("{ eventIndex { futureEvents { id } } }")
|
||||
|
||||
assert response.status_code == 200 and "errors" not in body, body
|
||||
sql = "\n".join(q["sql"] for q in ctx.captured_queries)
|
||||
assert "wp_raw_content" not in sql, f"wp_* must be deferred. SQL:\n{sql}"
|
||||
assert EventPage.objects.get(pk=event.pk).wp_raw_content == "marker"
|
||||
|
||||
|
||||
def test_graphql_event_index_future_events_ordered_by_next_occurrence(event_index, graphql_post):
|
||||
now = timezone.now()
|
||||
|
||||
later = EventPageFactory(parent=event_index, title="Later gig")
|
||||
EventOccurrence.objects.create(event=later, start=now + timedelta(days=10), venue_custom="X")
|
||||
|
||||
sooner = EventPageFactory(parent=event_index, title="Sooner gig")
|
||||
EventOccurrence.objects.create(event=sooner, start=now + timedelta(days=3), venue_custom="X")
|
||||
|
||||
response, body = graphql_post(
|
||||
"""
|
||||
query {
|
||||
eventIndex {
|
||||
futureEvents { title }
|
||||
}
|
||||
}
|
||||
"""
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert "errors" not in body, body
|
||||
titles = [e["title"] for e in body["data"]["eventIndex"]["futureEvents"]]
|
||||
assert titles.index("Sooner gig") < titles.index("Later gig")
|
||||
|
||||
|
||||
def test_event_date_column_no_occurrences(event_index):
|
||||
event = EventPageFactory(parent=event_index)
|
||||
column = EventDateColumn("event_date")
|
||||
|
||||
assert column.get_value(event) == "—"
|
||||
|
||||
|
||||
def test_event_date_column_single_occurrence(event_index):
|
||||
event = EventPageFactory(parent=event_index)
|
||||
start = timezone.make_aware(datetime(2025, 7, 22, 19, 30))
|
||||
EventOccurrence.objects.create(event=event, start=start, venue_custom="X")
|
||||
column = EventDateColumn("event_date")
|
||||
|
||||
assert column.get_value(event) == "2025-07-22 kl 19:30"
|
||||
|
||||
|
||||
def test_event_date_column_multiple_occurrences_shows_count(event_index):
|
||||
event = EventPageFactory(parent=event_index)
|
||||
now = timezone.now()
|
||||
EventOccurrence.objects.create(event=event, start=now, venue_custom="X")
|
||||
EventOccurrence.objects.create(event=event, start=now + timedelta(days=1), venue_custom="X")
|
||||
EventOccurrence.objects.create(event=event, start=now + timedelta(days=2), venue_custom="X")
|
||||
column = EventDateColumn("event_date")
|
||||
|
||||
assert column.get_value(event) == "3 forekomster"
|
||||
|
||||
|
||||
def test_organizers_column_no_organizers(event_index):
|
||||
event = EventPageFactory(parent=event_index)
|
||||
column = OrganizersColumn("organizers")
|
||||
|
||||
assert column.get_value(event) == "—"
|
||||
|
||||
|
||||
def test_organizers_column_single_organizer_shows_name(event_index):
|
||||
org = EventOrganizer.objects.create(name="Forening A", slug="forening-a")
|
||||
event = EventPageFactory(parent=event_index)
|
||||
EventOrganizerLink.objects.create(event=event, organizer=org)
|
||||
column = OrganizersColumn("organizers")
|
||||
|
||||
assert column.get_value(event) == "Forening A"
|
||||
|
||||
|
||||
def test_organizers_column_multiple_organizers_truncates_with_count(event_index):
|
||||
org_a = EventOrganizer.objects.create(name="Forening A", slug="forening-a")
|
||||
org_b = EventOrganizer.objects.create(name="Forening B", slug="forening-b")
|
||||
org_c = EventOrganizer.objects.create(name="Forening C", slug="forening-c")
|
||||
event = EventPageFactory(parent=event_index)
|
||||
EventOrganizerLink.objects.create(event=event, organizer=org_a, sort_order=0)
|
||||
EventOrganizerLink.objects.create(event=event, organizer=org_b, sort_order=1)
|
||||
EventOrganizerLink.objects.create(event=event, organizer=org_c, sort_order=2)
|
||||
column = OrganizersColumn("organizers")
|
||||
|
||||
assert column.get_value(event) == "Forening A (+2)"
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def comprehensive_event(event_index, venue, association_index):
|
||||
"""A fully-populated paid EventPage exercising every field exposed via GraphQL."""
|
||||
image = CustomImageFactory(
|
||||
title="Cover",
|
||||
alt="Et fotografi av en gris med solbriller",
|
||||
attribution="Foto: Test",
|
||||
)
|
||||
|
||||
konsert = EventCategory.objects.create(
|
||||
name="Konsert", slug="konsert", show_in_filters=True, pig="pigHeadLogo"
|
||||
)
|
||||
klubb = EventCategory.objects.create(name="Klubb", slug="klubb")
|
||||
|
||||
association = AssociationPageFactory(
|
||||
parent=association_index,
|
||||
title="Internal",
|
||||
association_type="forening",
|
||||
)
|
||||
internal_org = EventOrganizer.objects.create(
|
||||
name="Internal", slug="internal", association=association
|
||||
)
|
||||
external_org = EventOrganizer.objects.create(
|
||||
name="External",
|
||||
slug="external",
|
||||
external_url="https://external.example.com",
|
||||
)
|
||||
|
||||
event = EventPageFactory(
|
||||
parent=event_index,
|
||||
title="Et arrangement",
|
||||
slug="et-arrangement",
|
||||
subtitle="En undertekst",
|
||||
lead="<p>Ingress.</p>",
|
||||
body=[("paragraph", "<p>Body content.</p>")],
|
||||
pig="automatic",
|
||||
free=False,
|
||||
price_regular="150",
|
||||
price_student="100",
|
||||
price_member="75",
|
||||
ticket_url="https://example.com/tickets",
|
||||
facebook_url="https://facebook.com/example",
|
||||
featured_image=image,
|
||||
)
|
||||
event.categories.add(konsert, klubb)
|
||||
EventOrganizerLink.objects.create(event=event, organizer=internal_org)
|
||||
EventOrganizerLink.objects.create(event=event, organizer=external_org)
|
||||
|
||||
now = timezone.now()
|
||||
EventOccurrence.objects.create(
|
||||
event=event,
|
||||
start=now + timedelta(days=5),
|
||||
end=now + timedelta(days=5, hours=3),
|
||||
venue=venue,
|
||||
)
|
||||
EventOccurrence.objects.create(
|
||||
event=event,
|
||||
start=now + timedelta(days=12),
|
||||
end=now + timedelta(days=12, hours=2),
|
||||
venue_custom="Frederikkeplassen",
|
||||
)
|
||||
|
||||
event.save()
|
||||
return event
|
||||
|
||||
|
||||
def test_graphql_event_index_returns_all_fields_for_comprehensive_event(
|
||||
comprehensive_event, graphql_post
|
||||
):
|
||||
response, body = graphql_post(
|
||||
"""
|
||||
query {
|
||||
eventIndex {
|
||||
futureEvents {
|
||||
title
|
||||
slug
|
||||
subtitle
|
||||
lead
|
||||
body {
|
||||
blockType
|
||||
field
|
||||
... on RichTextBlock {
|
||||
value
|
||||
}
|
||||
}
|
||||
pig
|
||||
free
|
||||
priceRegular
|
||||
priceStudent
|
||||
priceMember
|
||||
ticketUrl
|
||||
facebookUrl
|
||||
featuredImage {
|
||||
alt
|
||||
attribution
|
||||
}
|
||||
categories {
|
||||
name
|
||||
slug
|
||||
showInFilters
|
||||
pig
|
||||
}
|
||||
organizers {
|
||||
name
|
||||
slug
|
||||
externalUrl
|
||||
association {
|
||||
title
|
||||
}
|
||||
}
|
||||
occurrences {
|
||||
start
|
||||
end
|
||||
venueCustom
|
||||
venue {
|
||||
title
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
"""
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert "errors" not in body, body
|
||||
|
||||
events = body["data"]["eventIndex"]["futureEvents"]
|
||||
event = next(e for e in events if e["title"] == "Et arrangement")
|
||||
|
||||
assert event["slug"] == "et-arrangement"
|
||||
assert event["subtitle"] == "En undertekst"
|
||||
assert "Ingress." in event["lead"]
|
||||
assert event["pig"] == "automatic"
|
||||
assert event["free"] is False
|
||||
assert event["priceRegular"] == "150"
|
||||
assert event["priceStudent"] == "100"
|
||||
assert event["priceMember"] == "75"
|
||||
assert event["ticketUrl"] == "https://example.com/tickets"
|
||||
assert event["facebookUrl"] == "https://facebook.com/example"
|
||||
|
||||
assert event["featuredImage"]["alt"] == "Et fotografi av en gris med solbriller"
|
||||
assert event["featuredImage"]["attribution"] == "Foto: Test"
|
||||
|
||||
assert event["body"][0]["blockType"] == "RichTextBlock"
|
||||
assert "Body content." in event["body"][0]["value"]
|
||||
|
||||
categories_by_name = {c["name"]: c for c in event["categories"]}
|
||||
assert set(categories_by_name) == {"Konsert", "Klubb"}
|
||||
assert categories_by_name["Konsert"]["slug"] == "konsert"
|
||||
assert categories_by_name["Konsert"]["showInFilters"] is True
|
||||
assert categories_by_name["Konsert"]["pig"] == "pigHeadLogo"
|
||||
assert categories_by_name["Klubb"]["showInFilters"] is False
|
||||
|
||||
organizers_by_name = {o["name"]: o for o in event["organizers"]}
|
||||
assert set(organizers_by_name) == {"Internal", "External"}
|
||||
assert organizers_by_name["Internal"]["association"]["title"] == "Internal"
|
||||
assert organizers_by_name["Internal"]["externalUrl"] == ""
|
||||
assert organizers_by_name["External"]["association"] is None
|
||||
assert organizers_by_name["External"]["externalUrl"] == "https://external.example.com"
|
||||
|
||||
assert len(event["occurrences"]) == 2
|
||||
venue_occ = next(o for o in event["occurrences"] if o["venue"] is not None)
|
||||
custom_occ = next(o for o in event["occurrences"] if o["venueCustom"])
|
||||
assert venue_occ["venueCustom"] == ""
|
||||
assert venue_occ["venue"]["title"]
|
||||
assert custom_occ["venue"] is None
|
||||
assert custom_occ["venueCustom"] == "Frederikkeplassen"
|
||||
|
||||
venue_occ_db = comprehensive_event.occurrences.exclude(venue=None).get()
|
||||
custom_occ_db = comprehensive_event.occurrences.exclude(venue_custom="").get()
|
||||
assert datetime.fromisoformat(venue_occ["start"]) == venue_occ_db.start
|
||||
assert datetime.fromisoformat(venue_occ["end"]) == venue_occ_db.end
|
||||
assert datetime.fromisoformat(custom_occ["start"]) == custom_occ_db.start
|
||||
assert datetime.fromisoformat(custom_occ["end"]) == custom_occ_db.end
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def searchable_organizers(db):
|
||||
return [
|
||||
EventOrganizer.objects.create(name="Arrangementsutvalget", slug="arrangementsutvalget"),
|
||||
EventOrganizer.objects.create(name="Cinema Neuf", slug="kino"),
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"url_name",
|
||||
["event_organizer_chooser:choose", "wagtailsnippets_events_eventorganizer:list"],
|
||||
)
|
||||
def test_organizer_search_matches_substrings_of_name_and_slug(
|
||||
admin_client, searchable_organizers, url_name
|
||||
):
|
||||
url = reverse(url_name)
|
||||
|
||||
# mid-word match inside a compound name
|
||||
content = admin_client.get(url, {"q": "utvalget"}).content.decode()
|
||||
assert "Arrangementsutvalget" in content
|
||||
assert "Cinema Neuf" not in content
|
||||
|
||||
# slug-only match
|
||||
content = admin_client.get(url, {"q": "kino"}).content.decode()
|
||||
assert "Cinema Neuf" in content
|
||||
assert "Arrangementsutvalget" not in content
|
||||
@@ -0,0 +1,71 @@
|
||||
from generic.models import GenericPage
|
||||
from tests.conftest import GenericPageFactory
|
||||
|
||||
|
||||
def test_generic_page_persists_via_factory(home_page):
|
||||
page = GenericPageFactory(
|
||||
parent=home_page,
|
||||
title="Om oss",
|
||||
slug="om-oss",
|
||||
lead="<p>Ingress.</p>",
|
||||
body=[("paragraph", "<p>Body content.</p>")],
|
||||
pig="drink",
|
||||
)
|
||||
|
||||
reloaded = GenericPage.objects.get(pk=page.pk)
|
||||
assert reloaded.title == "Om oss"
|
||||
assert reloaded.slug == "om-oss"
|
||||
assert "Ingress." in reloaded.lead
|
||||
assert reloaded.pig == "drink"
|
||||
assert reloaded.body[0].block_type == "paragraph"
|
||||
|
||||
|
||||
def test_generic_page_allows_recursive_children(home_page):
|
||||
parent = GenericPageFactory(parent=home_page, title="Parent", slug="parent")
|
||||
child = GenericPageFactory(parent=parent, title="Child", slug="child")
|
||||
|
||||
assert child.get_parent().specific == parent
|
||||
assert list(parent.get_children().specific()) == [child]
|
||||
|
||||
|
||||
def test_graphql_generic_page_query(home_page, graphql_post):
|
||||
GenericPageFactory(
|
||||
parent=home_page,
|
||||
title="Om oss",
|
||||
slug="om-oss",
|
||||
lead="<p>Ingress text.</p>",
|
||||
body=[("paragraph", "<p>Body content.</p>")],
|
||||
pig="drink",
|
||||
)
|
||||
|
||||
response, body = graphql_post(
|
||||
"""
|
||||
query {
|
||||
page(slug: "om-oss", contentType: "generic.GenericPage") {
|
||||
title
|
||||
slug
|
||||
... on GenericPage {
|
||||
lead
|
||||
pig
|
||||
body {
|
||||
blockType
|
||||
field
|
||||
... on RichTextBlock {
|
||||
value
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
"""
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert "errors" not in body, body
|
||||
data = body["data"]["page"]
|
||||
assert data["title"] == "Om oss"
|
||||
assert data["slug"] == "om-oss"
|
||||
assert "Ingress text." in data["lead"]
|
||||
assert data["pig"] == "drink"
|
||||
assert data["body"][0]["blockType"] == "RichTextBlock"
|
||||
assert "Body content." in data["body"][0]["value"]
|
||||
@@ -0,0 +1,6 @@
|
||||
def test_graphql_endpoint_responds(db, graphql_post):
|
||||
response, body = graphql_post("{ __schema { queryType { name } } }")
|
||||
|
||||
assert response.status_code == 200
|
||||
assert "errors" not in body
|
||||
assert body["data"]["__schema"]["queryType"]["name"] == "Query"
|
||||
@@ -0,0 +1,275 @@
|
||||
import pytest
|
||||
from django.contrib.auth.models import Permission
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.urls import reverse
|
||||
from wagtail.models import ReferenceIndex
|
||||
|
||||
from events.merge_organizers import CUSTOM_VALUE, merge_event_organizers
|
||||
from events.models import EventOrganizer, EventOrganizerLink, EventPage
|
||||
from tests.conftest import AssociationPageFactory, EventPageFactory
|
||||
|
||||
pytestmark = pytest.mark.django_db
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def organizers(db):
|
||||
keeper = EventOrganizer.objects.create(name="Kulturutvalget", slug="kulturutvalget")
|
||||
goner = EventOrganizer.objects.create(name="KU", slug="ku")
|
||||
return keeper, goner
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def editor_client(client, django_user_model):
|
||||
user = django_user_model.objects.create_user(username="editor", password="pw")
|
||||
user.user_permissions.add(Permission.objects.get(codename="access_admin"))
|
||||
client.force_login(user)
|
||||
return client
|
||||
|
||||
|
||||
def test_merge_repoints_event_links_and_deletes_loser(event_index, organizers):
|
||||
keeper, goner = organizers
|
||||
event = EventPageFactory(parent=event_index)
|
||||
EventOrganizerLink.objects.create(event=event, organizer=goner)
|
||||
|
||||
merged = merge_event_organizers(keeper, goner)
|
||||
|
||||
assert merged.pk == keeper.pk
|
||||
assert not EventOrganizer.objects.filter(pk=goner.pk).exists()
|
||||
assert list(event.organizer_links.values_list("organizer_id", flat=True)) == [keeper.pk]
|
||||
|
||||
|
||||
def test_lowest_pk_survives_regardless_of_argument_order(organizers):
|
||||
keeper, goner = organizers
|
||||
|
||||
merged = merge_event_organizers(goner, keeper)
|
||||
|
||||
assert merged.pk == keeper.pk
|
||||
assert not EventOrganizer.objects.filter(pk=goner.pk).exists()
|
||||
|
||||
|
||||
def test_merge_applies_chosen_name_and_slug(organizers):
|
||||
keeper, goner = organizers
|
||||
|
||||
merged = merge_event_organizers(keeper, goner, name="KU", slug="ku")
|
||||
|
||||
assert merged.name == "KU"
|
||||
assert merged.slug == "ku"
|
||||
|
||||
|
||||
def test_merge_keeps_survivor_name_and_slug_by_default(organizers):
|
||||
keeper, goner = organizers
|
||||
|
||||
merged = merge_event_organizers(keeper, goner)
|
||||
|
||||
assert merged.name == "Kulturutvalget"
|
||||
assert merged.slug == "kulturutvalget"
|
||||
|
||||
|
||||
def test_merge_collapses_duplicate_links_on_same_event(event_index, organizers):
|
||||
keeper, goner = organizers
|
||||
event = EventPageFactory(parent=event_index)
|
||||
EventOrganizerLink.objects.create(event=event, organizer=keeper)
|
||||
EventOrganizerLink.objects.create(event=event, organizer=goner)
|
||||
|
||||
merge_event_organizers(keeper, goner)
|
||||
|
||||
assert list(event.organizer_links.values_list("organizer_id", flat=True)) == [keeper.pk]
|
||||
|
||||
|
||||
def test_merge_fills_blank_fields_from_loser(association_index, organizers):
|
||||
keeper, goner = organizers
|
||||
association = AssociationPageFactory(parent=association_index)
|
||||
goner.association = association
|
||||
goner.external_url = "https://example.com"
|
||||
goner.save()
|
||||
|
||||
merged = merge_event_organizers(keeper, goner)
|
||||
|
||||
assert merged.association_id == association.pk
|
||||
assert merged.external_url == "https://example.com"
|
||||
|
||||
|
||||
def test_merge_keeps_survivor_fields_when_set(organizers):
|
||||
keeper, goner = organizers
|
||||
keeper.external_url = "https://keeper.example.com"
|
||||
keeper.save()
|
||||
goner.external_url = "https://goner.example.com"
|
||||
goner.save()
|
||||
|
||||
merged = merge_event_organizers(keeper, goner)
|
||||
|
||||
assert merged.external_url == "https://keeper.example.com"
|
||||
|
||||
|
||||
def test_merge_rewrites_event_page_revisions(event_index, organizers):
|
||||
keeper, goner = organizers
|
||||
event = EventPageFactory(parent=event_index)
|
||||
EventOrganizerLink.objects.create(event=event, organizer=goner)
|
||||
revision = EventPage.objects.get(pk=event.pk).save_revision()
|
||||
assert [link["organizer"] for link in revision.content["organizer_links"]] == [goner.pk]
|
||||
|
||||
merge_event_organizers(keeper, goner)
|
||||
|
||||
revision.refresh_from_db()
|
||||
assert [link["organizer"] for link in revision.content["organizer_links"]] == [keeper.pk]
|
||||
|
||||
|
||||
def test_merge_dedupes_organizers_within_revisions(event_index, organizers):
|
||||
keeper, goner = organizers
|
||||
event = EventPageFactory(parent=event_index)
|
||||
EventOrganizerLink.objects.create(event=event, organizer=keeper)
|
||||
EventOrganizerLink.objects.create(event=event, organizer=goner)
|
||||
revision = EventPage.objects.get(pk=event.pk).save_revision()
|
||||
|
||||
merge_event_organizers(keeper, goner)
|
||||
|
||||
revision.refresh_from_db()
|
||||
assert [link["organizer"] for link in revision.content["organizer_links"]] == [keeper.pk]
|
||||
|
||||
|
||||
def test_merge_updates_reference_index(event_index, organizers):
|
||||
keeper, goner = organizers
|
||||
event = EventPageFactory(parent=event_index)
|
||||
EventOrganizerLink.objects.create(event=event, organizer=goner)
|
||||
ReferenceIndex.create_or_update_for_object(EventPage.objects.get(pk=event.pk))
|
||||
organizer_ct = ContentType.objects.get_for_model(EventOrganizer)
|
||||
assert ReferenceIndex.objects.filter(
|
||||
to_content_type=organizer_ct, to_object_id=str(goner.pk)
|
||||
).exists()
|
||||
|
||||
merge_event_organizers(keeper, goner)
|
||||
|
||||
assert not ReferenceIndex.objects.filter(
|
||||
to_content_type=organizer_ct, to_object_id=str(goner.pk)
|
||||
).exists()
|
||||
assert ReferenceIndex.objects.filter(
|
||||
to_content_type=organizer_ct, to_object_id=str(keeper.pk)
|
||||
).exists()
|
||||
|
||||
|
||||
def test_merge_refuses_same_organizer(organizers):
|
||||
keeper, _ = organizers
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
merge_event_organizers(keeper, keeper)
|
||||
|
||||
|
||||
def test_merge_views_require_superuser(editor_client, organizers):
|
||||
keeper, goner = organizers
|
||||
confirm_url = reverse("events_merge_organizers_confirm")
|
||||
urls = [
|
||||
reverse("events_merge_organizers"),
|
||||
f"{confirm_url}?organizer_a={keeper.pk}&organizer_b={goner.pk}",
|
||||
]
|
||||
|
||||
for url in urls:
|
||||
response = editor_client.get(url)
|
||||
assert response.status_code == 302
|
||||
assert response.url == reverse("wagtailadmin_home")
|
||||
assert EventOrganizer.objects.filter(pk=goner.pk).exists()
|
||||
|
||||
|
||||
def test_select_view_redirects_to_confirm(admin_client, organizers):
|
||||
keeper, goner = organizers
|
||||
|
||||
response = admin_client.post(
|
||||
reverse("events_merge_organizers"),
|
||||
{"organizer_a": keeper.pk, "organizer_b": goner.pk},
|
||||
)
|
||||
|
||||
assert response.status_code == 302
|
||||
confirm_url = reverse("events_merge_organizers_confirm")
|
||||
assert response.url == f"{confirm_url}?organizer_a={keeper.pk}&organizer_b={goner.pk}"
|
||||
|
||||
|
||||
def test_select_view_rejects_same_organizer_twice(admin_client, organizers):
|
||||
keeper, _ = organizers
|
||||
|
||||
response = admin_client.post(
|
||||
reverse("events_merge_organizers"),
|
||||
{"organizer_a": keeper.pk, "organizer_b": keeper.pk},
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert "Pick two different organizers." in response.content.decode()
|
||||
|
||||
|
||||
def test_confirm_view_shows_both_organizers(admin_client, organizers):
|
||||
keeper, goner = organizers
|
||||
confirm_url = reverse("events_merge_organizers_confirm")
|
||||
|
||||
response = admin_client.get(f"{confirm_url}?organizer_a={goner.pk}&organizer_b={keeper.pk}")
|
||||
|
||||
content = response.content.decode()
|
||||
assert response.status_code == 200
|
||||
assert "Kulturutvalget" in content
|
||||
assert "KU" in content
|
||||
|
||||
|
||||
def test_confirm_view_rejects_missing_or_equal_organizers(admin_client, organizers):
|
||||
keeper, _ = organizers
|
||||
confirm_url = reverse("events_merge_organizers_confirm")
|
||||
queries = [
|
||||
"",
|
||||
f"?organizer_a={keeper.pk}&organizer_b={keeper.pk}",
|
||||
f"?organizer_a=abc&organizer_b={keeper.pk}",
|
||||
f"?organizer_a={keeper.pk}&organizer_b=999999",
|
||||
]
|
||||
|
||||
for query in queries:
|
||||
response = admin_client.get(confirm_url + query)
|
||||
assert response.status_code == 302
|
||||
assert response.url == reverse("events_merge_organizers")
|
||||
|
||||
|
||||
def test_confirm_view_merges_with_picked_values(admin_client, event_index, organizers):
|
||||
keeper, goner = organizers
|
||||
event = EventPageFactory(parent=event_index)
|
||||
EventOrganizerLink.objects.create(event=event, organizer=goner)
|
||||
confirm_url = reverse("events_merge_organizers_confirm")
|
||||
|
||||
response = admin_client.post(
|
||||
f"{confirm_url}?organizer_a={keeper.pk}&organizer_b={goner.pk}",
|
||||
{"name": goner.name, "name_custom": "", "slug": goner.slug, "slug_custom": ""},
|
||||
)
|
||||
|
||||
assert response.status_code == 302
|
||||
assert response.url == reverse("wagtailsnippets_events_eventorganizer:list")
|
||||
keeper.refresh_from_db()
|
||||
assert keeper.name == "KU"
|
||||
assert keeper.slug == "ku"
|
||||
assert not EventOrganizer.objects.filter(pk=goner.pk).exists()
|
||||
assert list(event.organizer_links.values_list("organizer_id", flat=True)) == [keeper.pk]
|
||||
|
||||
|
||||
def test_confirm_view_merges_with_custom_values(admin_client, organizers):
|
||||
keeper, goner = organizers
|
||||
confirm_url = reverse("events_merge_organizers_confirm")
|
||||
|
||||
response = admin_client.post(
|
||||
f"{confirm_url}?organizer_a={keeper.pk}&organizer_b={goner.pk}",
|
||||
{
|
||||
"name": CUSTOM_VALUE,
|
||||
"name_custom": "Kulturutvalget (KU)",
|
||||
"slug": CUSTOM_VALUE,
|
||||
"slug_custom": "Kulturutvalget KU",
|
||||
},
|
||||
)
|
||||
|
||||
assert response.status_code == 302
|
||||
keeper.refresh_from_db()
|
||||
assert keeper.name == "Kulturutvalget (KU)"
|
||||
assert keeper.slug == "kulturutvalget-ku"
|
||||
|
||||
|
||||
def test_confirm_view_requires_custom_value_when_picked(admin_client, organizers):
|
||||
keeper, goner = organizers
|
||||
confirm_url = reverse("events_merge_organizers_confirm")
|
||||
|
||||
response = admin_client.post(
|
||||
f"{confirm_url}?organizer_a={keeper.pk}&organizer_b={goner.pk}",
|
||||
{"name": CUSTOM_VALUE, "name_custom": "", "slug": keeper.slug, "slug_custom": ""},
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert EventOrganizer.objects.filter(pk=goner.pk).exists()
|
||||
@@ -0,0 +1,32 @@
|
||||
from news.admin import NewsSidebarViewSet
|
||||
from news.models import NewsPage
|
||||
from tests.conftest import NewsPageFactory
|
||||
|
||||
|
||||
def test_news_page_persists_via_factory(news_index):
|
||||
page = NewsPageFactory(parent=news_index, title="Big news", excerpt="Short summary")
|
||||
|
||||
reloaded = NewsPage.objects.get(pk=page.pk)
|
||||
assert reloaded.title == "Big news"
|
||||
assert reloaded.excerpt == "Short summary"
|
||||
|
||||
|
||||
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):
|
||||
response, body = graphql_post(
|
||||
"""
|
||||
query {
|
||||
newsIndex {
|
||||
title
|
||||
}
|
||||
}
|
||||
"""
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert "errors" not in body, body
|
||||
assert body["data"]["newsIndex"]["title"] == news_index.title
|
||||
@@ -0,0 +1,91 @@
|
||||
import datetime
|
||||
|
||||
import pytest
|
||||
|
||||
from openinghours.models import OpeningHoursItem, OpeningHoursSet
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def opening_hours_set(db):
|
||||
return OpeningHoursSet.objects.create(
|
||||
name="Vanlige åpningstider",
|
||||
effective_from=datetime.date(2025, 1, 1),
|
||||
)
|
||||
|
||||
|
||||
def test_opening_hours_set_str_with_end_date():
|
||||
ohs = OpeningHoursSet(
|
||||
name="Sommer",
|
||||
effective_from=datetime.date(2025, 6, 1),
|
||||
effective_to=datetime.date(2025, 8, 31),
|
||||
)
|
||||
assert str(ohs) == "Sommer (2025-06-01 - 2025-08-31)"
|
||||
|
||||
|
||||
def test_opening_hours_set_str_uses_infinity_when_open_ended():
|
||||
ohs = OpeningHoursSet(
|
||||
name="Forever",
|
||||
effective_from=datetime.date(2025, 1, 1),
|
||||
effective_to=None,
|
||||
)
|
||||
assert str(ohs) == "Forever (2025-01-01 - ∞)"
|
||||
|
||||
|
||||
def test_opening_hours_streamfield_week_roundtrip(opening_hours_set):
|
||||
OpeningHoursItem.objects.create(
|
||||
opening_hours_set=opening_hours_set,
|
||||
function="glassbaren",
|
||||
week=[
|
||||
(
|
||||
"week",
|
||||
{
|
||||
"monday": {
|
||||
"time_from": datetime.time(15, 0),
|
||||
"time_to": datetime.time(23, 0),
|
||||
"custom": "",
|
||||
},
|
||||
"tuesday": {"time_from": None, "time_to": None, "custom": "Stengt"},
|
||||
"wednesday": {"time_from": None, "time_to": None, "custom": ""},
|
||||
"thursday": {"time_from": None, "time_to": None, "custom": ""},
|
||||
"friday": {"time_from": None, "time_to": None, "custom": ""},
|
||||
"saturday": {"time_from": None, "time_to": None, "custom": ""},
|
||||
"sunday": {"time_from": None, "time_to": None, "custom": ""},
|
||||
},
|
||||
),
|
||||
],
|
||||
)
|
||||
|
||||
reloaded = OpeningHoursSet.objects.get(pk=opening_hours_set.pk)
|
||||
item = reloaded.items.get()
|
||||
assert item.function == "glassbaren"
|
||||
|
||||
week_block = item.week[0]
|
||||
assert week_block.block_type == "week"
|
||||
assert week_block.value["monday"]["time_from"] == datetime.time(15, 0)
|
||||
assert week_block.value["monday"]["time_to"] == datetime.time(23, 0)
|
||||
assert week_block.value["tuesday"]["custom"] == "Stengt"
|
||||
|
||||
|
||||
def test_graphql_opening_hours_sets_query(db, graphql_post):
|
||||
OpeningHoursSet.objects.create(
|
||||
name="Sommer 2025",
|
||||
effective_from=datetime.date(2025, 6, 1),
|
||||
effective_to=datetime.date(2025, 8, 31),
|
||||
)
|
||||
|
||||
response, body = graphql_post(
|
||||
"""
|
||||
query {
|
||||
openingHoursSets {
|
||||
name
|
||||
effectiveFrom
|
||||
effectiveTo
|
||||
}
|
||||
}
|
||||
"""
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert "errors" not in body, body
|
||||
names = [s["name"] for s in body["data"]["openingHoursSets"]]
|
||||
assert "Sommer 2025" in names
|
||||
@@ -0,0 +1,37 @@
|
||||
"""Round-trip tests for wagtail-headless-preview token resolution via grapple."""
|
||||
|
||||
from generic.models import GenericPage
|
||||
from tests.conftest import GenericPageFactory
|
||||
|
||||
|
||||
def test_generic_page_preview_token_resolves_draft(home_page, graphql_post):
|
||||
"""A minted preview token returns the unsaved draft via grapple's page(token: …)."""
|
||||
# Publish a baseline so there's a live revision to diverge from.
|
||||
page = GenericPageFactory(parent=home_page, title="Original title", slug="generic-preview")
|
||||
|
||||
# Mutate in-memory to simulate unsaved editor state, then mint a token.
|
||||
# create_page_preview() snapshots the current to_json() into a PagePreview row.
|
||||
page.title = "Edited title (draft)"
|
||||
preview = page.create_page_preview()
|
||||
|
||||
response, body = graphql_post(
|
||||
"""
|
||||
query previewPage($token: String!) {
|
||||
page: page(token: $token) {
|
||||
__typename
|
||||
... on GenericPage {
|
||||
title
|
||||
}
|
||||
}
|
||||
}
|
||||
""",
|
||||
variables={"token": preview.token},
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert "errors" not in body, body
|
||||
assert body["data"]["page"]["__typename"] == "GenericPage"
|
||||
assert body["data"]["page"]["title"] == "Edited title (draft)"
|
||||
|
||||
# Live revision is unchanged — token short-circuits the published query.
|
||||
assert GenericPage.objects.get(pk=page.pk).title == "Original title"
|
||||
@@ -0,0 +1,127 @@
|
||||
import logging
|
||||
import urllib.error
|
||||
|
||||
import pytest
|
||||
|
||||
from dnscms import revalidation
|
||||
from events.models import EventCategory
|
||||
from tests.conftest import GenericPageFactory
|
||||
|
||||
WEBHOOK_URL = "https://frontend.example.com/api/revalidate"
|
||||
|
||||
|
||||
class InlineThread:
|
||||
def __init__(self, target=None, args=(), kwargs=None, daemon=None):
|
||||
self._target = target
|
||||
self._args = args
|
||||
self._kwargs = kwargs or {}
|
||||
|
||||
def start(self):
|
||||
self._target(*self._args, **self._kwargs)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def webhook_calls(monkeypatch, settings):
|
||||
settings.REVALIDATE_WEBHOOK_SECRET = "s3cret"
|
||||
settings.FRONTEND_BASE_URL = "https://frontend.example.com"
|
||||
calls = []
|
||||
monkeypatch.setattr(revalidation.threading, "Thread", InlineThread)
|
||||
monkeypatch.setattr(
|
||||
revalidation, "_send_webhook", lambda url, secret, reason: calls.append((url, secret))
|
||||
)
|
||||
return calls
|
||||
|
||||
|
||||
def test_page_publish_triggers_webhook(
|
||||
home_page, webhook_calls, django_capture_on_commit_callbacks
|
||||
):
|
||||
page = GenericPageFactory(parent=home_page)
|
||||
with django_capture_on_commit_callbacks(execute=True):
|
||||
page.save_revision().publish()
|
||||
|
||||
assert webhook_calls == [(WEBHOOK_URL, "s3cret")]
|
||||
|
||||
|
||||
def test_page_unpublish_triggers_webhook(
|
||||
home_page, webhook_calls, django_capture_on_commit_callbacks
|
||||
):
|
||||
page = GenericPageFactory(parent=home_page)
|
||||
page.save_revision().publish()
|
||||
webhook_calls.clear()
|
||||
|
||||
with django_capture_on_commit_callbacks(execute=True):
|
||||
page.unpublish()
|
||||
|
||||
assert webhook_calls == [(WEBHOOK_URL, "s3cret")]
|
||||
|
||||
|
||||
def test_page_delete_triggers_webhook(
|
||||
home_page, webhook_calls, django_capture_on_commit_callbacks
|
||||
):
|
||||
# live=False isolates the post_delete path: deleting a live page would
|
||||
# also fire page_unpublished (a harmless duplicate purge in production)
|
||||
page = GenericPageFactory(parent=home_page, live=False)
|
||||
webhook_calls.clear()
|
||||
|
||||
with django_capture_on_commit_callbacks(execute=True):
|
||||
page.delete()
|
||||
|
||||
assert webhook_calls == [(WEBHOOK_URL, "s3cret")]
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_snippet_save_triggers_webhook(webhook_calls, django_capture_on_commit_callbacks):
|
||||
with django_capture_on_commit_callbacks(execute=True):
|
||||
EventCategory.objects.create(name="Konsert", slug="konsert")
|
||||
|
||||
assert webhook_calls == [(WEBHOOK_URL, "s3cret")]
|
||||
|
||||
|
||||
def test_unset_secret_skips_and_warns_once(settings, monkeypatch, caplog):
|
||||
settings.REVALIDATE_WEBHOOK_SECRET = ""
|
||||
monkeypatch.setattr(revalidation, "_warned_unconfigured", False)
|
||||
|
||||
with caplog.at_level(logging.WARNING, logger="dnscms.revalidation"):
|
||||
revalidation.trigger_frontend_revalidation("test")
|
||||
revalidation.trigger_frontend_revalidation("test")
|
||||
|
||||
warnings = [r for r in caplog.records if r.levelno == logging.WARNING]
|
||||
assert len(warnings) == 1
|
||||
|
||||
|
||||
def test_send_webhook_failure_is_swallowed(monkeypatch, caplog):
|
||||
def raise_urlerror(request, timeout):
|
||||
raise urllib.error.URLError("connection refused")
|
||||
|
||||
monkeypatch.setattr(revalidation.urllib.request, "urlopen", raise_urlerror)
|
||||
|
||||
with caplog.at_level(logging.ERROR, logger="dnscms.revalidation"):
|
||||
revalidation._send_webhook(WEBHOOK_URL, "s3cret", "test")
|
||||
|
||||
assert any("frontend revalidation failed" in r.message for r in caplog.records)
|
||||
|
||||
|
||||
def test_send_webhook_request_shape(monkeypatch):
|
||||
seen = {}
|
||||
|
||||
class FakeResponse:
|
||||
status = 200
|
||||
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
def __exit__(self, *exc):
|
||||
return False
|
||||
|
||||
def fake_urlopen(request, timeout):
|
||||
seen["request"] = request
|
||||
seen["timeout"] = timeout
|
||||
return FakeResponse()
|
||||
|
||||
monkeypatch.setattr(revalidation.urllib.request, "urlopen", fake_urlopen)
|
||||
revalidation._send_webhook(WEBHOOK_URL, "s3cret", "test")
|
||||
|
||||
request = seen["request"]
|
||||
assert request.get_method() == "POST"
|
||||
assert request.get_header("X-revalidate-secret") == "s3cret"
|
||||
assert seen["timeout"] == revalidation.WEBHOOK_TIMEOUT_SECONDS
|
||||
@@ -0,0 +1,131 @@
|
||||
from wagtail.search.backends import get_search_backend
|
||||
|
||||
from tests.conftest import EventPageFactory, GenericPageFactory
|
||||
|
||||
|
||||
SEARCH_QUERY = """
|
||||
query Search($query: String) {
|
||||
results: search(query: $query) {
|
||||
__typename
|
||||
... on PageInterface {
|
||||
title
|
||||
}
|
||||
}
|
||||
}
|
||||
"""
|
||||
|
||||
|
||||
def _index(page):
|
||||
# Wagtail's post_save signal enqueues indexing via django-tasks, which isn't
|
||||
# drained synchronously in tests. Call the backend directly so the page is
|
||||
# findable through the live search code path.
|
||||
get_search_backend().add(page)
|
||||
|
||||
|
||||
def _titles_for(body, typename):
|
||||
return [r["title"] for r in body["data"]["results"] if r["__typename"] == typename]
|
||||
|
||||
|
||||
def test_search_returns_live_generic_page(home_page, graphql_post):
|
||||
page = GenericPageFactory(
|
||||
parent=home_page,
|
||||
title="PublishedGenericSearchToken",
|
||||
slug="published-generic-search",
|
||||
)
|
||||
_index(page)
|
||||
|
||||
response, body = graphql_post(SEARCH_QUERY, {"query": "PublishedGenericSearchToken"})
|
||||
|
||||
assert response.status_code == 200
|
||||
assert "errors" not in body, body
|
||||
assert "PublishedGenericSearchToken" in _titles_for(body, "GenericPage")
|
||||
|
||||
|
||||
def test_search_excludes_draft_generic_page(home_page, graphql_post):
|
||||
page = GenericPageFactory(
|
||||
parent=home_page,
|
||||
title="DraftGenericSearchToken",
|
||||
slug="draft-generic-search",
|
||||
live=False,
|
||||
)
|
||||
_index(page)
|
||||
|
||||
response, body = graphql_post(SEARCH_QUERY, {"query": "DraftGenericSearchToken"})
|
||||
|
||||
assert response.status_code == 200
|
||||
assert "errors" not in body, body
|
||||
assert "DraftGenericSearchToken" not in _titles_for(body, "GenericPage")
|
||||
|
||||
|
||||
def test_search_returns_live_event_page(home_page, event_index, graphql_post):
|
||||
page = EventPageFactory(
|
||||
parent=event_index,
|
||||
title="PublishedEventSearchToken",
|
||||
slug="published-event-search",
|
||||
)
|
||||
_index(page)
|
||||
|
||||
response, body = graphql_post(SEARCH_QUERY, {"query": "PublishedEventSearchToken"})
|
||||
|
||||
assert response.status_code == 200
|
||||
assert "errors" not in body, body
|
||||
assert "PublishedEventSearchToken" in _titles_for(body, "EventPage")
|
||||
|
||||
|
||||
def test_search_excludes_draft_event_page(home_page, event_index, graphql_post):
|
||||
page = EventPageFactory(
|
||||
parent=event_index,
|
||||
title="DraftEventSearchToken",
|
||||
slug="draft-event-search",
|
||||
live=False,
|
||||
)
|
||||
_index(page)
|
||||
|
||||
response, body = graphql_post(SEARCH_QUERY, {"query": "DraftEventSearchToken"})
|
||||
|
||||
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,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="<p>Ingress.</p>",
|
||||
body=[("paragraph", "<p>Body content.</p>")],
|
||||
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="<p>Ingress text.</p>",
|
||||
body=[("paragraph", "<p>Body content.</p>")],
|
||||
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"]
|
||||
Generated
+230
-64
@@ -62,6 +62,54 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/8a/1f/f041989e93b001bc4e44bb1669ccdcf54d3f00e628229a85b08d330615c5/charset_normalizer-3.4.3-py3-none-any.whl", hash = "sha256:ce571ab16d890d23b5c278547ba694193a45011ff86a9162a71307ed9f86759a", size = 53175, upload-time = "2025-08-09T07:57:26.864Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "colorama"
|
||||
version = "0.4.6"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "coverage"
|
||||
version = "7.14.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/23/7f/d0720730a397a999ffc0fd3f5bebef347338e3a47b727da66fbb228e2ff2/coverage-7.14.0.tar.gz", hash = "sha256:057a6af2f160a85384cde4ab36f0d2777bae1057bae255f95413cdd382aa5c74", size = 919489, upload-time = "2026-05-10T18:02:31.397Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/1c/18/b9a6586d73992807c26f9a5f274131be3d76b56b18a82b9392e2a25d2e45/coverage-7.14.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:9aed9fa983514ca032790f3fe0d1c0e42ca7e16b42432af1706b50a9a46bef5d", size = 220036, upload-time = "2026-05-10T18:01:33.057Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f3/9b/4165a1d56ddc302a0e2d518fd9d412a4fd0b57562618c78c5f21c57194f5/coverage-7.14.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ba3b8390db29296dbbf49e91b6fe08f990743a90c8f447ba4c2ffc29670dfa63", size = 220368, upload-time = "2026-05-10T18:01:34.705Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/69/aa/c12e52a5ba148d9995229d557e3be6e554fe469addc0e9241b2f0956d8ea/coverage-7.14.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3a5d8e876dfa2f102e970b183863d6dedd023d3c0eeca1fe7a9787bc5f28b212", size = 251417, upload-time = "2026-05-10T18:01:36.949Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d7/51/ec641c26e6dca1b25a7d2035ba6ecb7c884ef1a100a9e42fbe4ce4405139/coverage-7.14.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:5ebb8f4614a3787d567e610bbfdf96a4798dd69a1afb1bd8ad228d4111fe6ff3", size = 253924, upload-time = "2026-05-10T18:01:38.985Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/33/c4/59c3de0bd1b538824173fd518fed51c1ce740ca5ed68e74545983f4053a9/coverage-7.14.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b9bf47223dd8db3d4c4b2e443b02bace480d428f0822c3f991600448a176c97", size = 255269, upload-time = "2026-05-10T18:01:40.957Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7b/a9/36dfa153a62040296f6e7febfdb20a5720622f6ef5a81a41e8237b9a5344/coverage-7.14.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3485a836550b303d006d57cc06e3d5afaabc642c77050b7c985a97b13e3776b8", size = 257583, upload-time = "2026-05-10T18:01:42.607Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/26/7b/cc2c048d4114d9ab1c2409e9ee365e5ae10736df6dffcfc9444effa6c708/coverage-7.14.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3e7e88110bae996d199d1693ca8ec3fd52441d426401ae963437598667b4c5eb", size = 251434, upload-time = "2026-05-10T18:01:44.537Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ee/df/6770eaa576e604575e9a78055313250faef5faa84bd6f71a39fece519c43/coverage-7.14.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:15228a6800ce7bdf1b74800595e56db7138cecb338fdbf044806e10dcf182dfe", size = 253280, upload-time = "2026-05-10T18:01:46.175Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ad/9e/1c0264514a3f98259a6d64765a397b2c8373e3ba59ee722a4802d3ec0c61/coverage-7.14.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:9d26ac7f5398bafc5b57421ad994e8a4749e8a7a0e62d05ec7d53014d5963bfa", size = 251241, upload-time = "2026-05-10T18:01:48.732Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/64/16/4efdf3e3c4079cdbf0ece56a2fea872df9e8a3e15a13a0af4400e1075944/coverage-7.14.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:2fb73254ff43c911c967a899e1359bc5049b4b115d6e8fbdde4937d0a2246cd5", size = 255516, upload-time = "2026-05-10T18:01:50.819Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/93/69/b1de96346603881b3d1bc8d6447c83200e1c9700ffbaff926ba01ff5724c/coverage-7.14.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:454a380af72c6adada298ed270d38c7a391288198dbfb8467f786f588751a90c", size = 251059, upload-time = "2026-05-10T18:01:52.773Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a4/66/2881853e0363a5e0a724d1103e53650795367471b6afb234f8b49e713bc6/coverage-7.14.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:65c86fb646d2bd2972e96bd1a8b45817ed907cee68655d6295fe7ec031d04cca", size = 252716, upload-time = "2026-05-10T18:01:54.506Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/55/5c/0d3305d002c41dcde873dbe456491e663dc55152ca526b630b5c47efd62f/coverage-7.14.0-cp314-cp314-win32.whl", hash = "sha256:6a6516b02a6101398e19a3f44820f69bab2590697f7def4331f668b14adaf828", size = 222788, upload-time = "2026-05-10T18:01:56.487Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f9/58/6e1b8f52fdc3184b47dc5037f5070d83a3d11042db1594b02d2a44d786c8/coverage-7.14.0-cp314-cp314-win_amd64.whl", hash = "sha256:45e0f79d8351fa76e256716df91eab12890d32678b9590df7ae1042e4bd4cf5d", size = 223600, upload-time = "2026-05-10T18:01:58.497Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/00/70/a18c408e674bc26281cadaedc7351f929bd2094e191e4b15271c30b084cc/coverage-7.14.0-cp314-cp314-win_arm64.whl", hash = "sha256:4b899594a8b2d81e5cc064a0d7f9cac2081fed91049456cae7676787e41549c9", size = 222168, upload-time = "2026-05-10T18:02:00.411Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3d/89/2681f071d238b62aff8dfc2ab44fc24cfdb38d1c01f391a80522ff5d3a16/coverage-7.14.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:f580f8c80acd94ac72e863efe2cab791d8c38d153e0b463b92dfa000d5c84cd1", size = 220766, upload-time = "2026-05-10T18:02:02.313Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bd/c7/c987babafd9207ffa1995e1ef1f9b26762cf4963aa768a66b6f0501e4616/coverage-7.14.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a2bd259c442cd43c49b30fbafc51776eb19ea396faf159d26a83e6a0a5f13b0c", size = 221035, upload-time = "2026-05-10T18:02:04.017Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5a/e9/d6a5ac3b333088143d6fc877d398a9a674dc03124a2f776e131f03864823/coverage-7.14.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:a706b908dfa85538863504c624b237a3cc34232bf403c057414ebfdb3b4d9f84", size = 262405, upload-time = "2026-05-10T18:02:05.915Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/38/b1/e70838d29a7c08e22d44398a46db90815bbcbf28de06992bd9210d1a8d8e/coverage-7.14.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:7333cd944ee4393b9b3d3c1b598c936d4fc8d70573a4c7dacfec5590dd50e436", size = 264530, upload-time = "2026-05-10T18:02:07.582Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6b/73/5c31ef97763288d03d9995152b96d5475b527c63d91c84b01caea894b83a/coverage-7.14.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f162bc9a15b82d947b02651b0c7e1609d6f7a8735ca330cfadec8481dd97d5a", size = 266932, upload-time = "2026-05-10T18:02:09.401Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e1/76/dd56d80f29c5f05b4d76f7e7c6d47cafacae017189c75c5759d24f9ff0cc/coverage-7.14.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:362cb78e01a5dc82009d88004cf60f2e6b6d6fcbfdec05b05af73b0abf40118f", size = 268062, upload-time = "2026-05-10T18:02:11.399Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6e/c7/27ba85cd5b95614f159ff93ebff1901584a8d192e2e5e24c4943a7453f59/coverage-7.14.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:acebd068fca5512c3a6fde9c045f901613478781a73f0e82b307b214daef23fb", size = 261504, upload-time = "2026-05-10T18:02:13.257Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/13/2e/e8149f60ab5d5684c6eee881bdf34b127115cddbb958b196768dd9d63473/coverage-7.14.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:29fe3da551dface75deb2ccbf87b6b66e2e7ef38f6d89050b428be94afff3490", size = 264398, upload-time = "2026-05-10T18:02:15.063Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d9/7f/1261b025285323225f4b4abffa5a643649dfd67e25ddca7ebcbdea3b7cb3/coverage-7.14.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:b4cc4fce8672fffcb09b0eafc167b396b3ba53c4a7230f54b7aaffbf6c835fa9", size = 262000, upload-time = "2026-05-10T18:02:16.756Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d3/dc/829c54f60b9d08389439c00f813c752781c496fc5788c78d8006db4b4f2b/coverage-7.14.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:5d4a51aad8ba8bdcd2b8bd8f03d4aca19693fa2327a3470e4718a25b03481020", size = 265732, upload-time = "2026-05-10T18:02:18.817Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ed/b0/70bd1419941652fa062689cba9c3eeafb8f5e6fbb890bce41c3bdda5dbd6/coverage-7.14.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:9f323af3e1e4f68b60b7b247e37b8515563a61375518fa59de1af48ba28a3db6", size = 260847, upload-time = "2026-05-10T18:02:20.528Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f2/73/be40b2390656c654d35ea0015ea7ba3d945769cf80790ad5e0bb2d56d2ba/coverage-7.14.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:1a0abc7342ea9711c469dd8b821c6c311e6bc6aac1442e5fbd6b27fae0a8f3db", size = 263166, upload-time = "2026-05-10T18:02:22.337Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/29/55/4a643f712fcf7cf2881f8ec1e0ccb7b164aff3108f69b51801246c8799f2/coverage-7.14.0-cp314-cp314t-win32.whl", hash = "sha256:a9f864ef57b7172e2db87a096642dd51e179e085ab6b2c371c29e885f65c8fb2", size = 223573, upload-time = "2026-05-10T18:02:24.11Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/27/96/3acae5da0953be042c0b4dea6d6789d2f080701c77b88e44d5bd41b9219b/coverage-7.14.0-cp314-cp314t-win_amd64.whl", hash = "sha256:29943e552fdc08e082eb51400fb2f58e118a83b5542bd06531214e084399b644", size = 224680, upload-time = "2026-05-10T18:02:25.896Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/93/3d/6ab5d2dd8325d838737c6f8d83d62eb6230e0d70b87b51b57bbfd08fa767/coverage-7.14.0-cp314-cp314t-win_arm64.whl", hash = "sha256:742a73ea621953b012f2c4c2219b512180dd84489acf5b1596b0aafc55b9100b", size = 222703, upload-time = "2026-05-10T18:02:27.822Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/61/e8/cb8e80d6f9f55b99588625062822bf946cf03ed06315df4bd8397f5632a1/coverage-7.14.0-py3-none-any.whl", hash = "sha256:8de5b61163aee3d05c8a2beab6f47913df7981dad1baf82c414d99158c286ab1", size = 211764, upload-time = "2026-05-10T18:02:29.538Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "defusedxml"
|
||||
version = "0.7.1"
|
||||
@@ -73,16 +121,16 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "django"
|
||||
version = "6.0.3"
|
||||
version = "6.0.6"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "asgiref" },
|
||||
{ name = "sqlparse" },
|
||||
{ name = "tzdata", marker = "sys_platform == 'win32'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/80/e1/894115c6bd70e2c8b66b0c40a3c367d83a5a48c034a4d904d31b62f7c53a/django-6.0.3.tar.gz", hash = "sha256:90be765ee756af8a6cbd6693e56452404b5ad15294f4d5e40c0a55a0f4870fe1", size = 10872701, upload-time = "2026-03-03T13:55:15.026Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/78/29/ac41e16097af67066d97a7d5775c5d8e7efc5d0284f6b0a159e07b9adb92/django-6.0.6.tar.gz", hash = "sha256:ad03916ba59523d781ae5c3f631960c23d69a9d9c43cecda52fc23b47e953713", size = 10905525, upload-time = "2026-06-03T13:02:46.503Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/72/b1/23f2556967c45e34d3d3cf032eb1bd3ef925ee458667fb99052a0b3ea3a6/django-6.0.3-py3-none-any.whl", hash = "sha256:2e5974441491ddb34c3f13d5e7a9f97b07ba03bf70234c0a9c68b79bbb235bc3", size = 8358527, upload-time = "2026-03-03T13:55:10.552Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/eb/50/23f9dc45483419a3cc2085b498b25adfbf10642b2941c73e6d2dfaffc9ab/django-6.0.6-py3-none-any.whl", hash = "sha256:25148b1194c47c2e685e5f5e9c5d59c78b075dfd282cb9618861ba6c1708f4d2", size = 8373354, upload-time = "2026-06-03T13:02:41.72Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -111,14 +159,14 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "django-modelcluster"
|
||||
version = "6.4.1"
|
||||
version = "6.5"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "django" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/5b/f7/51efcea27d74665230be07b63e631e16204893ef32338a0e671c5ee0cd40/django_modelcluster-6.4.1.tar.gz", hash = "sha256:e736fcee925f83b63218dbf9c869ab50618b0f5e98869a5aa497f7a5331aa263", size = 29029, upload-time = "2025-12-04T12:21:41.907Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/34/0f/c5fd0c280a10224d619325783bfaab5a54903f82340e41ae21ab3127ebc9/django_modelcluster-6.5.tar.gz", hash = "sha256:459cbf0fb3bbc8c15ea9a2a062abc0d1ef935a38e04aa8ac3cb241c826bbc6dd", size = 29707, upload-time = "2026-05-01T12:25:33.524Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/a5/e4/ec99d52aa04e204e938564b603f4591e2e82e236ed59af664fee35179e75/django_modelcluster-6.4.1-py2.py3-none-any.whl", hash = "sha256:ccc190cd9e22c24900ea2410bff64d444d48f43f0f4aedeed0f6cd94e2536698", size = 29315, upload-time = "2025-12-04T12:21:39.911Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cd/57/cdcdea9a56d114e9217d52ce9a8af19ad7fe8a0996faf8480d3f286c02e0/django_modelcluster-6.5-py3-none-any.whl", hash = "sha256:469b380a294027d5211d0e5d5746e0381f445b058d2229977a58fe0f1c58a596", size = 29865, upload-time = "2026-05-01T12:25:31.886Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -174,14 +222,14 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "django-treebeard"
|
||||
version = "4.7.1"
|
||||
version = "5.1.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "django" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/bb/24/eaccbce17355380cb3a8fe6ad92a85b76453dc1f0ecd04f48bfe8929065b/django-treebeard-4.7.1.tar.gz", hash = "sha256:846e462904b437155f76e04907ba4e48480716855f88b898df4122bdcfbd6e98", size = 294139, upload-time = "2024-01-31T16:35:19.751Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/f7/16/aa732ea1033586e4f946d8e47be9116ffb22b050485b179a7cafb82dfc34/django_treebeard-5.1.0.tar.gz", hash = "sha256:8ac2ba41307469a679c98188124933bc3baade36b02480343d1a301a1fca0700", size = 302339, upload-time = "2026-05-12T02:06:37.002Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/87/79/259966820614746cc4d81762edf97a53bf1e3b8e74797c010d310c6f4a8f/django_treebeard-4.7.1-py3-none-any.whl", hash = "sha256:995c7120153ab999898fe3043bbdcd8a0fc77cc106eb94de7350e9d02c885135", size = 93210, upload-time = "2024-01-31T16:35:17.843Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/42/d3/311b9c43950238744f946540c48ea7068bf9e55bea976908a7be13b73082/django_treebeard-5.1.0-py3-none-any.whl", hash = "sha256:d0f68fcf1b49158e38d2322aa62a37bc0b89452d66a16f88e58dbe076d043c28", size = 78591, upload-time = "2026-05-12T02:06:35.62Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -207,27 +255,39 @@ dependencies = [
|
||||
{ name = "psycopg2-binary" },
|
||||
{ name = "wagtail" },
|
||||
{ name = "wagtail-grapple" },
|
||||
{ name = "wagtail-headless-preview" },
|
||||
{ name = "whitenoise" },
|
||||
]
|
||||
|
||||
[package.dev-dependencies]
|
||||
dev = [
|
||||
{ name = "pytest" },
|
||||
{ name = "pytest-cov" },
|
||||
{ name = "pytest-django" },
|
||||
{ name = "ruff" },
|
||||
{ name = "wagtail-factories" },
|
||||
]
|
||||
|
||||
[package.metadata]
|
||||
requires-dist = [
|
||||
{ name = "django", specifier = ">=6.0.3" },
|
||||
{ name = "django-extensions", specifier = ">=4.1" },
|
||||
{ name = "gunicorn", specifier = ">=25.1.0" },
|
||||
{ name = "psycopg2-binary", specifier = ">=2.9.11,<3" },
|
||||
{ name = "wagtail", specifier = ">=7.3.1" },
|
||||
{ name = "wagtail-grapple", specifier = ">=0.29.0" },
|
||||
{ name = "whitenoise", specifier = ">=6.12.0" },
|
||||
{ name = "django", specifier = ">=6.0.6,<7" },
|
||||
{ name = "django-extensions", specifier = ">=4.1,<5" },
|
||||
{ name = "gunicorn", specifier = ">=26.0.0,<27" },
|
||||
{ name = "psycopg2-binary", specifier = ">=2.9.12,<3" },
|
||||
{ name = "wagtail", specifier = ">=7.4.2,<8" },
|
||||
{ name = "wagtail-grapple", specifier = ">=0.31.0,<0.32" },
|
||||
{ name = "wagtail-headless-preview", specifier = ">=0.9,<0.10" },
|
||||
{ name = "whitenoise", specifier = ">=6.12.0,<7" },
|
||||
]
|
||||
|
||||
[package.metadata.requires-dev]
|
||||
dev = [{ name = "ruff", specifier = ">=0.15.1" }]
|
||||
dev = [
|
||||
{ name = "pytest", specifier = ">=9.1.1,<10" },
|
||||
{ name = "pytest-cov", specifier = ">=7.0.0,<8" },
|
||||
{ name = "pytest-django", specifier = ">=4.12.0,<5" },
|
||||
{ name = "ruff", specifier = ">=0.15.20,<0.16" },
|
||||
{ name = "wagtail-factories", specifier = ">=4.5.0,<5" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "draftjs-exporter"
|
||||
@@ -247,6 +307,30 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/c1/8b/5fe2cc11fee489817272089c4203e679c63b570a5aaeb18d852ae3cbba6a/et_xmlfile-2.0.0-py3-none-any.whl", hash = "sha256:7a91720bc756843502c3b7504c77b8fe44217c85c537d85037f0f536151b2caa", size = 18059, upload-time = "2024-10-25T17:25:39.051Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "factory-boy"
|
||||
version = "3.3.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "faker" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/ba/98/75cacae9945f67cfe323829fc2ac451f64517a8a330b572a06a323997065/factory_boy-3.3.3.tar.gz", hash = "sha256:866862d226128dfac7f2b4160287e899daf54f2612778327dd03d0e2cb1e3d03", size = 164146, upload-time = "2025-02-03T09:49:04.433Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/27/8d/2bc5f5546ff2ccb3f7de06742853483ab75bf74f36a92254702f8baecc79/factory_boy-3.3.3-py2.py3-none-any.whl", hash = "sha256:1c39e3289f7e667c4285433f305f8d506efc2fe9c73aaea4151ebd5cdea394fc", size = 37036, upload-time = "2025-02-03T09:49:01.659Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "faker"
|
||||
version = "40.18.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "tzdata", marker = "sys_platform == 'win32'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/18/06/70886e82d8f1d2b73454f3a7c1b7405300128df22e70d85a828951366932/faker-40.18.0.tar.gz", hash = "sha256:2207575c0e8f90e6ccd6dbef764de875c614d16d3db4eee9712d9a00087f2e70", size = 1968243, upload-time = "2026-05-14T16:43:04.834Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/84/0b/5c0b2d3a4b7a715f1835dd3f963bfbe841a02ae5cad1df8ee0325dfad235/faker-40.18.0-py3-none-any.whl", hash = "sha256:61a6b94b74605ddb090a065deb197a1c585ae7a874c094cf6693671d271e6083", size = 2006355, upload-time = "2026-05-14T16:43:02.489Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "filetype"
|
||||
version = "1.2.0"
|
||||
@@ -311,14 +395,14 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "gunicorn"
|
||||
version = "25.1.0"
|
||||
version = "26.0.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "packaging" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/66/13/ef67f59f6a7896fdc2c1d62b5665c5219d6b0a9a1784938eb9a28e55e128/gunicorn-25.1.0.tar.gz", hash = "sha256:1426611d959fa77e7de89f8c0f32eed6aa03ee735f98c01efba3e281b1c47616", size = 594377, upload-time = "2026-02-13T11:09:58.989Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/6d/b7/a4a3f632f823e432ce6bc65f62961b7980c898c77f075a2f7118cb3846fe/gunicorn-26.0.0.tar.gz", hash = "sha256:ca9346f85e3a4aeeb64d491045c16b9a35647abd37ea15efe53080eb8b090baf", size = 727286, upload-time = "2026-05-05T06:38:25.529Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/da/73/4ad5b1f6a2e21cf1e85afdaad2b7b1a933985e2f5d679147a1953aaa192c/gunicorn-25.1.0-py3-none-any.whl", hash = "sha256:d0b1236ccf27f72cfe14bce7caadf467186f19e865094ca84221424e839b8b8b", size = 197067, upload-time = "2026-02-13T11:09:57.146Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e6/40/9c2384fc2be4ad25dd4a49decd5ad9ea5a3639814c11bd40ab77cb9f0a14/gunicorn-26.0.0-py3-none-any.whl", hash = "sha256:40233d26a5f0d1872916188c276e21641155111c2853f0c2cd55260aec0d24fc", size = 212009, upload-time = "2026-05-05T06:38:23.007Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -330,6 +414,15 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442, upload-time = "2024-09-15T18:07:37.964Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "iniconfig"
|
||||
version = "2.3.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "laces"
|
||||
version = "0.1.2"
|
||||
@@ -344,15 +437,15 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "modelsearch"
|
||||
version = "1.1.1"
|
||||
version = "1.3.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "django" },
|
||||
{ name = "django-tasks" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/3f/49/751b8872bb9c1ec667d0d312ab90022f0426326109163ade4719466c2e4d/modelsearch-1.1.1.tar.gz", hash = "sha256:25f329c4d93572729c931f65c46cedb5cfc32d368690ebdabc223aa6205251d6", size = 87514, upload-time = "2025-11-10T14:29:57.223Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/dd/25/81121175512af4a4b152f6db3ea749535659f5890f0987cd481646527dff/modelsearch-1.3.1.tar.gz", hash = "sha256:f3b2e304dbef9f7c838423f591cfe0c60f4cef584bae8793d2aa8ac35a3c46e0", size = 103935, upload-time = "2026-04-27T23:37:08.841Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/88/4b/e3eb1f4e4f7ca4bfa9b71cea497352c1ce13261254d59cb33a36bcbff335/modelsearch-1.1.1-py3-none-any.whl", hash = "sha256:d2580790af76c3a6404f651c9d8ca8695b284551583bb8ca6ddeb17eca0cfb52", size = 106987, upload-time = "2025-11-10T14:29:55.946Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0f/f1/63cf88da72bc557594a543dd19a2c7e219acccc70cb2b1e2639204580fe6/modelsearch-1.3.1-py3-none-any.whl", hash = "sha256:a92847f01788d0d615e8715fabb8e823029288840297fb9e7235ee03ad49b6a8", size = 127463, upload-time = "2026-04-27T23:37:07.651Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -424,6 +517,15 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/6b/3c/63cbbe3a6a54e3ec925d2433bb431a4bf61695f34b5f1e689db642fb20c1/pillow_heif-1.1.0-cp314-cp314-win_amd64.whl", hash = "sha256:514c856230995dc2f918fb12d83906885d42829e911868e23035e2d916c4c7c5", size = 5573890, upload-time = "2025-08-02T09:58:05.049Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pluggy"
|
||||
version = "1.6.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "promise"
|
||||
version = "2.3"
|
||||
@@ -435,21 +537,72 @@ sdist = { url = "https://files.pythonhosted.org/packages/cf/9c/fb5d48abfe5d791cd
|
||||
|
||||
[[package]]
|
||||
name = "psycopg2-binary"
|
||||
version = "2.9.11"
|
||||
version = "2.9.12"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/ac/6c/8767aaa597ba424643dc87348c6f1754dd9f48e80fdc1b9f7ca5c3a7c213/psycopg2-binary-2.9.11.tar.gz", hash = "sha256:b6aed9e096bf63f9e75edf2581aa9a7e7186d97ab5c177aa6c87797cd591236c", size = 379620, upload-time = "2025-10-10T11:14:48.041Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/2a/60/a3624f79acea344c16fbef3a94d28b89a8042ddfb8f3e4ca83f538671409/psycopg2_binary-2.9.12.tar.gz", hash = "sha256:5ac9444edc768c02a6b6a591f070b8aae28ff3a99be57560ac996001580f294c", size = 379686, upload-time = "2026-04-21T09:40:34.304Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/64/12/93ef0098590cf51d9732b4f139533732565704f45bdc1ffa741b7c95fb54/psycopg2_binary-2.9.11-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:92e3b669236327083a2e33ccfa0d320dd01b9803b3e14dd986a4fc54aa00f4e1", size = 3756567, upload-time = "2025-10-10T11:13:11.885Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7c/a9/9d55c614a891288f15ca4b5209b09f0f01e3124056924e17b81b9fa054cc/psycopg2_binary-2.9.11-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:e0deeb03da539fa3577fcb0b3f2554a97f7e5477c246098dbb18091a4a01c16f", size = 3864755, upload-time = "2025-10-10T11:13:17.727Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/13/1e/98874ce72fd29cbde93209977b196a2edae03f8490d1bd8158e7f1daf3a0/psycopg2_binary-2.9.11-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:9b52a3f9bb540a3e4ec0f6ba6d31339727b2950c9772850d6545b7eae0b9d7c5", size = 4411646, upload-time = "2025-10-10T11:13:24.432Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5a/bd/a335ce6645334fb8d758cc358810defca14a1d19ffbc8a10bd38a2328565/psycopg2_binary-2.9.11-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:db4fd476874ccfdbb630a54426964959e58da4c61c9feba73e6094d51303d7d8", size = 4468701, upload-time = "2025-10-10T11:13:29.266Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/44/d6/c8b4f53f34e295e45709b7568bf9b9407a612ea30387d35eb9fa84f269b4/psycopg2_binary-2.9.11-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:47f212c1d3be608a12937cc131bd85502954398aaa1320cb4c14421a0ffccf4c", size = 4166293, upload-time = "2025-10-10T11:13:33.336Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4b/e0/f8cc36eadd1b716ab36bb290618a3292e009867e5c97ce4aba908cb99644/psycopg2_binary-2.9.11-cp314-cp314-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e35b7abae2b0adab776add56111df1735ccc71406e56203515e228a8dc07089f", size = 3983184, upload-time = "2025-10-30T02:55:32.483Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/53/3e/2a8fe18a4e61cfb3417da67b6318e12691772c0696d79434184a511906dc/psycopg2_binary-2.9.11-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fcf21be3ce5f5659daefd2b3b3b6e4727b028221ddc94e6c1523425579664747", size = 3652650, upload-time = "2025-10-10T11:13:38.181Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/76/36/03801461b31b29fe58d228c24388f999fe814dfc302856e0d17f97d7c54d/psycopg2_binary-2.9.11-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:9bd81e64e8de111237737b29d68039b9c813bdf520156af36d26819c9a979e5f", size = 3298663, upload-time = "2025-10-10T11:13:44.878Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/97/77/21b0ea2e1a73aa5fa9222b2a6b8ba325c43c3a8d54272839c991f2345656/psycopg2_binary-2.9.11-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:32770a4d666fbdafab017086655bcddab791d7cb260a16679cc5a7338b64343b", size = 3044737, upload-time = "2025-10-30T02:55:35.69Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/67/69/f36abe5f118c1dca6d3726ceae164b9356985805480731ac6712a63f24f0/psycopg2_binary-2.9.11-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c3cb3a676873d7506825221045bd70e0427c905b9c8ee8d6acd70cfcbd6e576d", size = 3347643, upload-time = "2025-10-10T11:13:53.499Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e1/36/9c0c326fe3a4227953dfb29f5d0c8ae3b8eb8c1cd2967aa569f50cb3c61f/psycopg2_binary-2.9.11-cp314-cp314-win_amd64.whl", hash = "sha256:4012c9c954dfaccd28f94e84ab9f94e12df76b4afb22331b1f0d3154893a6316", size = 2803913, upload-time = "2025-10-10T11:13:57.058Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/13/1b/708c0dca874acfad6d65314271859899a79007686f3a1f74e82a2ed4b645/psycopg2_binary-2.9.12-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:6f3b3de8a74ef8db215f22edffb19e32dc6fa41340456de7ec99efdc8a7b3ec2", size = 3712428, upload-time = "2026-04-20T23:35:23.453Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d6/39/ddbea9d4b4de6aca9431b6ed253f530f8a02d3b8f9bcfd0dbfe2b3de6fe4/psycopg2_binary-2.9.12-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1006fb62f0f0bc5ce256a832356c6262e91be43f5e4eb15b5eaf38079464caf2", size = 3823184, upload-time = "2026-04-20T23:35:25.92Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bf/a0/bc2fef74b106fa345567122a0659e6d94512ed7dc0131ec44c9e5aba3725/psycopg2_binary-2.9.12-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:840066105706cd2eb29b9a1c2329620056582a4bf3e8169dec5c447042d0869f", size = 4579157, upload-time = "2026-04-20T23:35:28.542Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/57/d7/d4e3b2005d3de607ca4fbb0e8742e248056e52184a6b94ebda3c1c2c329b/psycopg2_binary-2.9.12-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:863f5d12241ebe1c76a72a04c2113b6dc905f90b9cef0e9be0efd994affd9354", size = 4274970, upload-time = "2026-04-20T23:35:30.418Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2e/42/c9853f8db3967fe08bcde11f53d53b85d351750cae726ce001cb68afa9c1/psycopg2_binary-2.9.12-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a99eaab34a9010f1a086b126de467466620a750634d114d20455f3a824aae033", size = 5895175, upload-time = "2026-04-20T23:35:33.584Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/eb/fd/b82b5601a97630308bef079f545ffec481bbbc795c2ba5ec416a01d03f60/psycopg2_binary-2.9.12-cp314-cp314-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ffdd7dc5463ccd61845ac37b7012d0f35a1548df9febe14f8dd549be4a0bc81e", size = 4110658, upload-time = "2026-04-20T23:35:35.638Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/62/8c/32ca69b0389ef25dd22937bf9e8fbe2ce27aea20b05ded48c4ce4cb42475/psycopg2_binary-2.9.12-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:54a0dfecab1b48731f934e06139dfe11e24219fb6d0ceb32177cf0375f14c7b5", size = 3656251, upload-time = "2026-04-20T23:35:37.854Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c4/29/96992a2b59e3b9d730fcf9612d0a387305025dc867a9fc490a9e496e074e/psycopg2_binary-2.9.12-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:96937c9c5d891f772430f418a7a8b4691a90c3e6b93cf72b5bd7cad8cbca32a5", size = 3301810, upload-time = "2026-04-20T23:35:39.927Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/56/ad/44b06659949b243ae10112cd3b20a197f9bf3e81d5651379b9eb889bfaad/psycopg2_binary-2.9.12-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:77b348775efd4cdab410ec6609d81ccecd1139c90265fa583a7255c8064bc03d", size = 3048977, upload-time = "2026-04-20T23:35:41.806Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1d/f2/10a1bcebadb6aa55e280e1f58975c36a7b560ea525184c7aa4064c466633/psycopg2_binary-2.9.12-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:527e6342b3e44c2f0544f6b8e927d60de7f163f5723b8f1dfa7d2a84298738cd", size = 3351466, upload-time = "2026-04-20T23:35:43.993Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/20/be/b732c8418ffa5bcfda002890f5dc4c869fc17db66ff11f53b17cfe44afc0/psycopg2_binary-2.9.12-cp314-cp314-win_amd64.whl", hash = "sha256:f12ae41fcafadb39b2785e64a40f9db05d6de2ac114077457e0e7c597f3af980", size = 2848762, upload-time = "2026-04-20T23:35:46.421Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pygments"
|
||||
version = "2.20.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991, upload-time = "2026-03-29T13:29:33.898Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pytest"
|
||||
version = "9.1.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "colorama", marker = "sys_platform == 'win32'" },
|
||||
{ name = "iniconfig" },
|
||||
{ name = "packaging" },
|
||||
{ name = "pluggy" },
|
||||
{ name = "pygments" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/e4/47/b9efed96c114afcfa3c9d3fe98a76a1d14c74a9e266d397cf6eb64be5e01/pytest-9.1.1.tar.gz", hash = "sha256:1088fbde8f2b49d95a549a195707afa7a76a3ce9bcadc26b6d71f0ffda5fe313", size = 1636369, upload-time = "2026-06-19T10:58:32.857Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/24/25/1de2678b631f5a49215c6c96fff41ba892b0a34df68d6d80292b1b48aa7f/pytest-9.1.1-py3-none-any.whl", hash = "sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c", size = 386536, upload-time = "2026-06-19T10:58:31.347Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pytest-cov"
|
||||
version = "7.1.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "coverage" },
|
||||
{ name = "pluggy" },
|
||||
{ name = "pytest" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/b1/51/a849f96e117386044471c8ec2bd6cfebacda285da9525c9106aeb28da671/pytest_cov-7.1.0.tar.gz", hash = "sha256:30674f2b5f6351aa09702a9c8c364f6a01c27aae0c1366ae8016160d1efc56b2", size = 55592, upload-time = "2026-03-21T20:11:16.284Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/9d/7a/d968e294073affff457b041c2be9868a40c1c71f4a35fcc1e45e5493067b/pytest_cov-7.1.0-py3-none-any.whl", hash = "sha256:a0461110b7865f9a271aa1b51e516c9a95de9d696734a2f71e3e78f46e1d4678", size = 22876, upload-time = "2026-03-21T20:11:14.438Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pytest-django"
|
||||
version = "4.12.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "pytest" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/13/2b/db9a193df89e5660137f5428063bcc2ced7ad790003b26974adf5c5ceb3b/pytest_django-4.12.0.tar.gz", hash = "sha256:df94ec819a83c8979c8f6de13d9cdfbe76e8c21d39473cfe2b40c9fc9be3c758", size = 91156, upload-time = "2026-02-14T18:40:49.235Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/83/a5/41d091f697c09609e7ef1d5d61925494e0454ebf51de7de05f0f0a728f1d/pytest_django-4.12.0-py3-none-any.whl", hash = "sha256:3ff300c49f8350ba2953b90297d23bf5f589db69545f56f1ec5f8cff5da83e85", size = 26123, upload-time = "2026-02-14T18:40:47.381Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -481,27 +634,27 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "ruff"
|
||||
version = "0.15.1"
|
||||
version = "0.15.20"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/04/dc/4e6ac71b511b141cf626357a3946679abeba4cf67bc7cc5a17920f31e10d/ruff-0.15.1.tar.gz", hash = "sha256:c590fe13fb57c97141ae975c03a1aedb3d3156030cabd740d6ff0b0d601e203f", size = 4540855, upload-time = "2026-02-12T23:09:09.998Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/43/dc/35b341fc554ba02f217fc10da57d1a75168cfbcf75b0ef2202176d4c4f2d/ruff-0.15.20.tar.gz", hash = "sha256:1416eb04349192646b54de98f146c4f59afe37d0decfc02c3cbbf396f3a28566", size = 4755489, upload-time = "2026-06-25T17:20:37.578Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/23/bf/e6e4324238c17f9d9120a9d60aa99a7daaa21204c07fcd84e2ef03bb5fd1/ruff-0.15.1-py3-none-linux_armv6l.whl", hash = "sha256:b101ed7cf4615bda6ffe65bdb59f964e9f4a0d3f85cbf0e54f0ab76d7b90228a", size = 10367819, upload-time = "2026-02-12T23:09:03.598Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b3/ea/c8f89d32e7912269d38c58f3649e453ac32c528f93bb7f4219258be2e7ed/ruff-0.15.1-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:939c995e9277e63ea632cc8d3fae17aa758526f49a9a850d2e7e758bfef46602", size = 10798618, upload-time = "2026-02-12T23:09:22.928Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5e/0f/1d0d88bc862624247d82c20c10d4c0f6bb2f346559d8af281674cf327f15/ruff-0.15.1-py3-none-macosx_11_0_arm64.whl", hash = "sha256:1d83466455fdefe60b8d9c8df81d3c1bbb2115cede53549d3b522ce2bc703899", size = 10148518, upload-time = "2026-02-12T23:08:58.339Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f5/c8/291c49cefaa4a9248e986256df2ade7add79388fe179e0691be06fae6f37/ruff-0.15.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a9457e3c3291024866222b96108ab2d8265b477e5b1534c7ddb1810904858d16", size = 10518811, upload-time = "2026-02-12T23:09:31.865Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c3/1a/f5707440e5ae43ffa5365cac8bbb91e9665f4a883f560893829cf16a606b/ruff-0.15.1-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:92c92b003e9d4f7fbd33b1867bb15a1b785b1735069108dfc23821ba045b29bc", size = 10196169, upload-time = "2026-02-12T23:09:17.306Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2a/ff/26ddc8c4da04c8fd3ee65a89c9fb99eaa5c30394269d424461467be2271f/ruff-0.15.1-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1fe5c41ab43e3a06778844c586251eb5a510f67125427625f9eb2b9526535779", size = 10990491, upload-time = "2026-02-12T23:09:25.503Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fc/00/50920cb385b89413f7cdb4bb9bc8fc59c1b0f30028d8bccc294189a54955/ruff-0.15.1-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:66a6dd6df4d80dc382c6484f8ce1bcceb55c32e9f27a8b94c32f6c7331bf14fb", size = 11843280, upload-time = "2026-02-12T23:09:19.88Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5d/6d/2f5cad8380caf5632a15460c323ae326f1e1a2b5b90a6ee7519017a017ca/ruff-0.15.1-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6a4a42cbb8af0bda9bcd7606b064d7c0bc311a88d141d02f78920be6acb5aa83", size = 11274336, upload-time = "2026-02-12T23:09:14.907Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a3/1d/5f56cae1d6c40b8a318513599b35ea4b075d7dc1cd1d04449578c29d1d75/ruff-0.15.1-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4ab064052c31dddada35079901592dfba2e05f5b1e43af3954aafcbc1096a5b2", size = 11137288, upload-time = "2026-02-12T23:09:07.475Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cd/20/6f8d7d8f768c93b0382b33b9306b3b999918816da46537d5a61635514635/ruff-0.15.1-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:5631c940fe9fe91f817a4c2ea4e81f47bee3ca4aa646134a24374f3c19ad9454", size = 11070681, upload-time = "2026-02-12T23:08:55.43Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9a/67/d640ac76069f64cdea59dba02af2e00b1fa30e2103c7f8d049c0cff4cafd/ruff-0.15.1-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:68138a4ba184b4691ccdc39f7795c66b3c68160c586519e7e8444cf5a53e1b4c", size = 10486401, upload-time = "2026-02-12T23:09:27.927Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/65/3d/e1429f64a3ff89297497916b88c32a5cc88eeca7e9c787072d0e7f1d3e1e/ruff-0.15.1-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:518f9af03bfc33c03bdb4cb63fabc935341bb7f54af500f92ac309ecfbba6330", size = 10197452, upload-time = "2026-02-12T23:09:12.147Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/78/83/e2c3bade17dad63bf1e1c2ffaf11490603b760be149e1419b07049b36ef2/ruff-0.15.1-py3-none-musllinux_1_2_i686.whl", hash = "sha256:da79f4d6a826caaea95de0237a67e33b81e6ec2e25fc7e1993a4015dffca7c61", size = 10693900, upload-time = "2026-02-12T23:09:34.418Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a1/27/fdc0e11a813e6338e0706e8b39bb7a1d61ea5b36873b351acee7e524a72a/ruff-0.15.1-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:3dd86dccb83cd7d4dcfac303ffc277e6048600dfc22e38158afa208e8bf94a1f", size = 11227302, upload-time = "2026-02-12T23:09:36.536Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f6/58/ac864a75067dcbd3b95be5ab4eb2b601d7fbc3d3d736a27e391a4f92a5c1/ruff-0.15.1-py3-none-win32.whl", hash = "sha256:660975d9cb49b5d5278b12b03bb9951d554543a90b74ed5d366b20e2c57c2098", size = 10462555, upload-time = "2026-02-12T23:09:29.899Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e0/5e/d4ccc8a27ecdb78116feac4935dfc39d1304536f4296168f91ed3ec00cd2/ruff-0.15.1-py3-none-win_amd64.whl", hash = "sha256:c820fef9dd5d4172a6570e5721704a96c6679b80cf7be41659ed439653f62336", size = 11599956, upload-time = "2026-02-12T23:09:01.157Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2a/07/5bda6a85b220c64c65686bc85bd0bbb23b29c62b3a9f9433fa55f17cda93/ruff-0.15.1-py3-none-win_arm64.whl", hash = "sha256:5ff7d5f0f88567850f45081fac8f4ec212be8d0b963e385c3f7d0d2eb4899416", size = 10874604, upload-time = "2026-02-12T23:09:05.515Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/94/d9/2d5014f0253ba541d2061d9fa7193f48e941c8b21bb88a7ff9bbe0bd0596/ruff-0.15.20-py3-none-linux_armv6l.whl", hash = "sha256:00e188c53e499c3c1637f73c91dcf2fb56d576cab76ce1be50a27c4e80e37078", size = 10839665, upload-time = "2026-06-25T17:19:44.702Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c6/d3/ac1798ba64f670698867fcfc591d50e7e421bef137db564858f619a30fcf/ruff-0.15.20-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:9ebd1fd9b9c95fc0bd7b2761aebec1f030013d2e193a2901b224af68fe47251b", size = 11208649, upload-time = "2026-06-25T17:19:48.787Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/47/47/d3ac899991202095dfcf3d5176be4272642be3cf981a2f1a30f72a2afb95/ruff-0.15.20-py3-none-macosx_11_0_arm64.whl", hash = "sha256:c5b16cdd67ca108185cd36dce98c576350c03b1660a751de725fb049193a0632", size = 10622638, upload-time = "2026-06-25T17:19:51.354Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/33/13/4e043fe30aa94d4ff5213a9881fc296d12960f5971b234a5263fdc225312/ruff-0.15.20-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3413bb3c3d2ca6a8208f1f4809cd2dca3c6de6d0b491c0e70847672bde6e6efd", size = 10984227, upload-time = "2026-06-25T17:19:54.044Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/76/e6/92e7bf40388bc5800073b96564f56264f7e48bfd1a498f5ced6ae6d5a769/ruff-0.15.20-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bd7ec42b3bb3da066488db093308a69c4ac5ee6d2af333a86ba6e2eb2e7dd44b", size = 10622882, upload-time = "2026-06-25T17:19:57.037Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/13/7a/43460be3f24495a3aa46d4b16873e2c4941b3b5f0b00cf88c03b7b94b339/ruff-0.15.20-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e1a36ad0eb77fba9aabfb69ede54de6f376d04ac18ebea022847046d340a8267", size = 11474808, upload-time = "2026-06-25T17:20:00.357Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/27/a0/f37077884873221c6b33b4ab49eb18f9f88e54a16a25a5bca59bef46dd66/ruff-0.15.20-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b6df3b1e4610432f0386dba04d853b5f08cbbc903410c6fcc02f620f05aff53c", size = 12293094, upload-time = "2026-06-25T17:20:03.446Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a6/74/165545b60256a9704c21ac0ec4a0d07933b320812f9584836c9f4aca4292/ruff-0.15.20-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e89f198a1ea6ef0d727c1cf16088bc91a6cb0ab947dedc966715691647186eae", size = 11526176, upload-time = "2026-06-25T17:20:06.301Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/86/b1/a976a136d40ade83ce743578399865f57001003a409acadc0ecbb3051082/ruff-0.15.20-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:309809086c2acb67624950a3c8133e80f32d0d3e27106c0cd60ff26657c9f24b", size = 11520767, upload-time = "2026-06-25T17:20:09.191Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/19/0f/f032696cb01c9b54c0263fa393474d7758f1cdc021a01b04e3cbc2500999/ruff-0.15.20-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:2d2374caa2f2c2f9e2b7da0a50802cfb8b79f55a9b5e49379f564544fbf56487", size = 11500132, upload-time = "2026-06-25T17:20:13.602Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4b/f4/51b1a14bc69e8c224b15dab9cce8e99b425e0455d462caa2b3c9be2b6a8e/ruff-0.15.20-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:a1ed17b65293e0c2f22fc387bc13198a5de94bf4429589b0ff6946b0feaf21a3", size = 10943828, upload-time = "2026-06-25T17:20:16.635Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/71/4b/fe267640783cd02bf6c5cc290b1df1051be2ec294c678b5c15fe19e52343/ruff-0.15.20-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:f701305e66b38ea6c91882490eb73459796808e4c6362a1b765255e0cdcd4053", size = 10645418, upload-time = "2026-06-25T17:20:19.4Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b0/c0/a65aa4ec2f5e87a1df32dc3ec1fede434fe3dfd5cbcf3b503cafc676ab54/ruff-0.15.20-py3-none-musllinux_1_2_i686.whl", hash = "sha256:5b9c0c367ad8e5d0d5b5b8537864c469a0a0e55417aadfbeca41fa61333be9f4", size = 11211770, upload-time = "2026-06-25T17:20:22.033Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5a/a4/0caa331d954ae2723d729d351c989cb4ca8b6077d5c6c2cb6de75e98c041/ruff-0.15.20-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:01cc00dd58f0df339d0e902219dd53990ea99996a0344e5d9cc8d45d5307e460", size = 11618698, upload-time = "2026-06-25T17:20:25.259Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/10/9b/5f14927848d2fd4aa891fd88d883788c5a7baba561c7874732364045708c/ruff-0.15.20-py3-none-win32.whl", hash = "sha256:ed65ef510e43a137207e0f01cfcf998aeddb1aeeda5c9d35023e910284d7cf21", size = 10857322, upload-time = "2026-06-25T17:20:28.612Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fa/f0/fe47c501f9dea92a26d788ff98bb5d92ed4cb4c88792c5c88af6b697dc8e/ruff-0.15.20-py3-none-win_amd64.whl", hash = "sha256:a525c81c70fb0380344dd1d8745d8cc1c890b7fc94a58d5a07bd8eb9557b8415", size = 11993274, upload-time = "2026-06-25T17:20:31.871Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d7/2b/9555445e1201d92b3195f45cdb153a0b68f24e0a4273f6e3d5ab46e212bb/ruff-0.15.20-py3-none-win_arm64.whl", hash = "sha256:2f5b2a6d614e8700388806a14996c40fab2c47b819ef57d790a34878858ed9ca", size = 11343498, upload-time = "2026-06-25T17:20:35.03Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -578,7 +731,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "wagtail"
|
||||
version = "7.3.1"
|
||||
version = "7.4.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "anyascii" },
|
||||
@@ -600,35 +753,48 @@ dependencies = [
|
||||
{ name = "telepath" },
|
||||
{ name = "willow", extra = ["heif"] },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/4e/f9/e28a1b87ea61c68b74990c9f5c8cb11da9a689e07c8b769acc89121f8523/wagtail-7.3.1.tar.gz", hash = "sha256:2ce131d9a4e7d55fdb5b592d320a758a189174b2cc3966b70a34a1b3dc56f449", size = 6855119, upload-time = "2026-03-03T15:54:48.523Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/91/83/a644e4657aba0f6d56e225d5a46f6471e44dab7fbfcac48af1d1c4f6a70b/wagtail-7.4.2.tar.gz", hash = "sha256:6e261b50693abfbe2b738717bedc5bd0915e2e08efb9400102f4098c0c03aaab", size = 6811456, upload-time = "2026-06-15T15:36:21.187Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/bb/0e/5efc903966b966df2261a66cce8cb88909e4ade86f1173a156aadbbd1a06/wagtail-7.3.1-py3-none-any.whl", hash = "sha256:eab131e15ab9edc7ed24143d44271e92af79239e105bc3e173d26c95d2b489b3", size = 9479191, upload-time = "2026-03-03T15:54:42.644Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9e/2e/9b0903d637ccfcbf1f8847d0f649d9d29eb46b329135313abff7835c0610/wagtail-7.4.2-py3-none-any.whl", hash = "sha256:ae80a6d536a4098f7c9d37e169ba0f14c29c9769b50b6d92546f41e5cad201d8", size = 9463486, upload-time = "2026-06-15T15:36:15.799Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wagtail-factories"
|
||||
version = "4.5.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "factory-boy" },
|
||||
{ name = "wagtail" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/0c/b8/0a6e91ea32467acd96c5769face88c6ac79f2f0990f822d7396d054bbc15/wagtail_factories-4.5.0.tar.gz", hash = "sha256:d981ceaaf7715b2325f9debb1be1ffc07e338ae8957e9527f4f9175fcdff2c56", size = 10131, upload-time = "2026-06-04T15:29:25.031Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/8f/18/36e0be763870e22b1f0f87361cc768a7bfcc842f6504a6bafdddf932fd7d/wagtail_factories-4.5.0-py3-none-any.whl", hash = "sha256:4ad0e9f396fca7f7d349a8752c49f68e18deeffee9c857897b849ff08c6c33a6", size = 10753, upload-time = "2026-06-04T15:29:24.087Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wagtail-grapple"
|
||||
version = "0.29.0"
|
||||
version = "0.31.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "graphene-django" },
|
||||
{ name = "wagtail" },
|
||||
{ name = "wagtail-headless-preview" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/c0/66/abe1b5ad7c335ff3969190c38ac259285b9f107d8273a18cc8c0ba0d36c5/wagtail_grapple-0.29.0.tar.gz", hash = "sha256:56b023dcfdce72532fba00610507b2705bf3a0be068946275c7658314de5431a", size = 39112, upload-time = "2025-07-02T10:25:19.811Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/9c/29/8cab682abde7f3a8d78a29aac6e441b7a9a20d9baf850c7fc438ad086c48/wagtail_grapple-0.31.0.tar.gz", hash = "sha256:5a70e88dca5188181e42306b207b1e38cf5df4452aa62e764b9e997e2ea19797", size = 39240, upload-time = "2026-04-21T09:24:28.195Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/48/ae/a7a26e331f9207ed864ebea292aac6af1e19a221391d2f833cd4fbe70f68/wagtail_grapple-0.29.0-py3-none-any.whl", hash = "sha256:d9c8c76bd9ba8c52ba1796b86043360d5ab083b24db6caaf2b1e6e41a7a2503e", size = 49124, upload-time = "2025-07-02T10:25:18.345Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/08/05/d80a7675e359fa62d45b92f14e42850fcb5f42c4fec3950951585b0a0975/wagtail_grapple-0.31.0-py3-none-any.whl", hash = "sha256:fed9205df68861d0ec6c5be67ac5aeb00775ebabde6baa38d582e723076befc0", size = 49251, upload-time = "2026-04-21T09:24:26.18Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wagtail-headless-preview"
|
||||
version = "0.8.0"
|
||||
version = "0.9.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "wagtail" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/b4/28/87fda34fd89999bee03519a614ddca608def6d07b69d93365833c434b9c5/wagtail_headless_preview-0.8.0.tar.gz", hash = "sha256:581d8419cd1ef1f7de88235445e9695e5591d46259283d56bfe814e8620fa1d5", size = 13957, upload-time = "2024-02-23T15:53:44.986Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/46/c4/5fe6d6c0df1a11be144d6cc9e0f951fb871490208b93237eeda75f8a1bd5/wagtail_headless_preview-0.9.0.tar.gz", hash = "sha256:dbabbb614bd1689f048738c46034c557a3ee426e0ef80feb4831bcfc93a24e15", size = 14887, upload-time = "2026-06-13T10:49:37.597Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/62/57/bb6c10a11f7a0ec0d8e10ec99b4c7b5a6036e8846e61d145966b6c72113d/wagtail_headless_preview-0.8.0-py3-none-any.whl", hash = "sha256:91b305c36573490856e212f9e5645ada81f72ad37f1bb00e6feddc5537252358", size = 12301, upload-time = "2024-02-23T15:53:43.904Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/69/80/0cc32c5192f875d5556487cc71e7280bbfd734b5d6bf24156c5f15e23c92/wagtail_headless_preview-0.9.0-py3-none-any.whl", hash = "sha256:9047701345323d77938c07685279770646c92545bed851c6608f026ac645f16d", size = 12781, upload-time = "2026-06-13T10:49:35.969Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
@@ -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'},
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,29 @@
|
||||
# Generated by Django 6.0.5 on 2026-07-02 14:53
|
||||
|
||||
import wagtail.fields
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('venues', '0025_verbose_names'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='venueindex',
|
||||
name='body',
|
||||
field=wagtail.fields.StreamField([('paragraph', 0), ('image', 4), ('image_slider', 8), ('horizontal_rule', 10), ('featured', 18), ('page_section_navigation', 19), ('accordion', 23), ('fact_box', 26), ('schedule', 33), ('embed', 34), ('raw_html', 35)], 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.blocks.CharBlock', (), {'help_text': 'Overskrift for timeplanen (valgfritt)', 'label': 'Tittel', 'max_length': 64, 'required': False}), 28: ('wagtail.blocks.CharBlock', (), {'help_text': 'Fritekst, f.eks. "13:00", "20:00 – 23:00" eller "23.06"', 'label': 'Tidspunkt/dato', 'max_length': 64, 'required': True}), 29: ('wagtail.blocks.RichTextBlock', (), {'features': ['bold', 'italic', 'link'], 'help_text': 'Kort tittel på én linje. Du kan legge inn lenker.', 'label': 'Tittel', 'required': True}), 30: ('wagtail.blocks.RichTextBlock', (), {'features': ['bold', 'italic', 'link'], 'help_text': 'Valgfri tilleggsinformasjon.', 'label': 'Beskrivelse', 'required': False}), 31: ('wagtail.blocks.StructBlock', [[('time', 28), ('title', 29), ('description', 30)]], {}), 32: ('wagtail.blocks.ListBlock', (31,), {'label': 'Punkter', 'min_num': 1}), 33: ('wagtail.blocks.StructBlock', [[('title', 27), ('items', 32)]], {}), 34: ('wagtail.embeds.blocks.EmbedBlock', (), {}), 35: ('wagtail.blocks.RawHTMLBlock', (), {})}, default=[('paragraph', '')]),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='venuepage',
|
||||
name='body',
|
||||
field=wagtail.fields.StreamField([('paragraph', 0), ('image', 4), ('image_slider', 8), ('horizontal_rule', 10), ('featured', 18), ('page_section_navigation', 19), ('accordion', 23), ('fact_box', 26), ('schedule', 33), ('embed', 34), ('raw_html', 35)], 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.blocks.CharBlock', (), {'help_text': 'Overskrift for timeplanen (valgfritt)', 'label': 'Tittel', 'max_length': 64, 'required': False}), 28: ('wagtail.blocks.CharBlock', (), {'help_text': 'Fritekst, f.eks. "13:00", "20:00 – 23:00" eller "23.06"', 'label': 'Tidspunkt/dato', 'max_length': 64, 'required': True}), 29: ('wagtail.blocks.RichTextBlock', (), {'features': ['bold', 'italic', 'link'], 'help_text': 'Kort tittel på én linje. Du kan legge inn lenker.', 'label': 'Tittel', 'required': True}), 30: ('wagtail.blocks.RichTextBlock', (), {'features': ['bold', 'italic', 'link'], 'help_text': 'Valgfri tilleggsinformasjon.', 'label': 'Beskrivelse', 'required': False}), 31: ('wagtail.blocks.StructBlock', [[('time', 28), ('title', 29), ('description', 30)]], {}), 32: ('wagtail.blocks.ListBlock', (31,), {'label': 'Punkter', 'min_num': 1}), 33: ('wagtail.blocks.StructBlock', [[('title', 27), ('items', 32)]], {}), 34: ('wagtail.embeds.blocks.EmbedBlock', (), {}), 35: ('wagtail.blocks.RawHTMLBlock', (), {})}, default=[('paragraph', '')]),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='venuerentalindex',
|
||||
name='body',
|
||||
field=wagtail.fields.StreamField([('paragraph', 0), ('image', 4), ('image_slider', 8), ('horizontal_rule', 10), ('featured', 18), ('page_section_navigation', 19), ('accordion', 23), ('fact_box', 26), ('schedule', 33), ('embed', 34), ('raw_html', 35)], 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.blocks.CharBlock', (), {'help_text': 'Overskrift for timeplanen (valgfritt)', 'label': 'Tittel', 'max_length': 64, 'required': False}), 28: ('wagtail.blocks.CharBlock', (), {'help_text': 'Fritekst, f.eks. "13:00", "20:00 – 23:00" eller "23.06"', 'label': 'Tidspunkt/dato', 'max_length': 64, 'required': True}), 29: ('wagtail.blocks.RichTextBlock', (), {'features': ['bold', 'italic', 'link'], 'help_text': 'Kort tittel på én linje. Du kan legge inn lenker.', 'label': 'Tittel', 'required': True}), 30: ('wagtail.blocks.RichTextBlock', (), {'features': ['bold', 'italic', 'link'], 'help_text': 'Valgfri tilleggsinformasjon.', 'label': 'Beskrivelse', 'required': False}), 31: ('wagtail.blocks.StructBlock', [[('time', 28), ('title', 29), ('description', 30)]], {}), 32: ('wagtail.blocks.ListBlock', (31,), {'label': 'Punkter', 'min_num': 1}), 33: ('wagtail.blocks.StructBlock', [[('title', 27), ('items', 32)]], {}), 34: ('wagtail.embeds.blocks.EmbedBlock', (), {}), 35: ('wagtail.blocks.RawHTMLBlock', (), {})}, default=[('paragraph', '')]),
|
||||
),
|
||||
]
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,29 @@
|
||||
# Generated by Django 6.0.5 on 2026-07-04 22:29
|
||||
|
||||
import wagtail.fields
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('venues', '0027_alter_venueindex_body_alter_venuepage_body_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='venueindex',
|
||||
name='body',
|
||||
field=wagtail.fields.StreamField([('paragraph', 0), ('image', 4), ('image_slider', 8), ('horizontal_rule', 10), ('featured', 18), ('page_section_navigation', 19), ('accordion', 23), ('fact_box', 26), ('schedule', 33), ('embed', 34), ('raw_html', 35)], 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.blocks.CharBlock', (), {'help_text': 'Overskrift for timeplanen (valgfritt)', 'label': 'Tittel', 'max_length': 64, 'required': False}), 28: ('wagtail.blocks.CharBlock', (), {'help_text': 'Fritekst, f.eks. "13:00", "20:00 – 23:00" eller "23.06"', 'label': 'Tidspunkt/dato', 'max_length': 64, 'required': True}), 29: ('wagtail.blocks.RichTextBlock', (), {'features': ['bold', 'italic', 'link'], 'help_text': 'Kort tittel på én linje. Du kan legge inn lenker.', 'label': 'Tittel', 'required': True}), 30: ('wagtail.blocks.RichTextBlock', (), {'features': ['bold', 'italic', 'link'], 'help_text': 'Valgfri tilleggsinformasjon.', 'label': 'Beskrivelse', 'required': False}), 31: ('wagtail.blocks.StructBlock', [[('time', 28), ('title', 29), ('description', 30)]], {}), 32: ('wagtail.blocks.ListBlock', (31,), {'label': 'Punkter', 'min_num': 1}), 33: ('wagtail.blocks.StructBlock', [[('title', 27), ('items', 32)]], {}), 34: ('wagtail.embeds.blocks.EmbedBlock', (), {}), 35: ('wagtail.blocks.RawHTMLBlock', (), {})}, default=[('paragraph', '')]),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='venuepage',
|
||||
name='body',
|
||||
field=wagtail.fields.StreamField([('paragraph', 0), ('image', 4), ('image_slider', 8), ('horizontal_rule', 10), ('featured', 18), ('page_section_navigation', 19), ('accordion', 23), ('fact_box', 26), ('schedule', 33), ('embed', 34), ('raw_html', 35)], 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.blocks.CharBlock', (), {'help_text': 'Overskrift for timeplanen (valgfritt)', 'label': 'Tittel', 'max_length': 64, 'required': False}), 28: ('wagtail.blocks.CharBlock', (), {'help_text': 'Fritekst, f.eks. "13:00", "20:00 – 23:00" eller "23.06"', 'label': 'Tidspunkt/dato', 'max_length': 64, 'required': True}), 29: ('wagtail.blocks.RichTextBlock', (), {'features': ['bold', 'italic', 'link'], 'help_text': 'Kort tittel på én linje. Du kan legge inn lenker.', 'label': 'Tittel', 'required': True}), 30: ('wagtail.blocks.RichTextBlock', (), {'features': ['bold', 'italic', 'link'], 'help_text': 'Valgfri tilleggsinformasjon.', 'label': 'Beskrivelse', 'required': False}), 31: ('wagtail.blocks.StructBlock', [[('time', 28), ('title', 29), ('description', 30)]], {}), 32: ('wagtail.blocks.ListBlock', (31,), {'label': 'Punkter', 'min_num': 1}), 33: ('wagtail.blocks.StructBlock', [[('title', 27), ('items', 32)]], {}), 34: ('wagtail.embeds.blocks.EmbedBlock', (), {}), 35: ('wagtail.blocks.RawHTMLBlock', (), {})}, default=[('paragraph', '')]),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='venuerentalindex',
|
||||
name='body',
|
||||
field=wagtail.fields.StreamField([('paragraph', 0), ('image', 4), ('image_slider', 8), ('horizontal_rule', 10), ('featured', 18), ('page_section_navigation', 19), ('accordion', 23), ('fact_box', 26), ('schedule', 33), ('embed', 34), ('raw_html', 35)], 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.blocks.CharBlock', (), {'help_text': 'Overskrift for timeplanen (valgfritt)', 'label': 'Tittel', 'max_length': 64, 'required': False}), 28: ('wagtail.blocks.CharBlock', (), {'help_text': 'Fritekst, f.eks. "13:00", "20:00 – 23:00" eller "23.06"', 'label': 'Tidspunkt/dato', 'max_length': 64, 'required': True}), 29: ('wagtail.blocks.RichTextBlock', (), {'features': ['bold', 'italic', 'link'], 'help_text': 'Kort tittel på én linje. Du kan legge inn lenker.', 'label': 'Tittel', 'required': True}), 30: ('wagtail.blocks.RichTextBlock', (), {'features': ['bold', 'italic', 'link'], 'help_text': 'Valgfri tilleggsinformasjon.', 'label': 'Beskrivelse', 'required': False}), 31: ('wagtail.blocks.StructBlock', [[('time', 28), ('title', 29), ('description', 30)]], {}), 32: ('wagtail.blocks.ListBlock', (31,), {'label': 'Punkter', 'min_num': 1}), 33: ('wagtail.blocks.StructBlock', [[('title', 27), ('items', 32)]], {}), 34: ('wagtail.embeds.blocks.EmbedBlock', (), {}), 35: ('wagtail.blocks.RawHTMLBlock', (), {})}, default=[('paragraph', '')]),
|
||||
),
|
||||
]
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user