Compare commits
78 Commits
sponsors
...
e3a58556f7
| Author | SHA1 | Date | |
|---|---|---|---|
|
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 | |||
| d0a886a4ae | |||
| cc5b53011d | |||
| f769898698 | |||
| e71db3a0cc | |||
| 355c0b38a5 | |||
| bba98fe868 | |||
| 978aae4fc3 | |||
| ffc3a583b2 | |||
| 12aea04779 | |||
|
6bcf9bbfbd
|
|||
|
2e4ca34f5c
|
|||
|
d76b16781d
|
|||
| 8942bcc9da | |||
| 196f000a2d | |||
| 4655f67a9e | |||
| 8506fd1c2d | |||
| ca341f5f22 | |||
| 1431b8d6ff | |||
| 3c225aa68a | |||
| 16629a2fc0 | |||
| b18f9ec54f | |||
| 2a00d21717 | |||
| 09e69a7093 | |||
| d3f8b8f0bb | |||
| fcd5231c28 |
@@ -1,3 +1,4 @@
|
|||||||
.vscode
|
.vscode
|
||||||
.DS_Store
|
.DS_Store
|
||||||
*.swp
|
*.swp
|
||||||
|
scratch/
|
||||||
|
|||||||
@@ -0,0 +1,36 @@
|
|||||||
|
# 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
|
||||||
|
```
|
||||||
@@ -9,6 +9,7 @@ __pycache__
|
|||||||
.DS_Store
|
.DS_Store
|
||||||
*.swp
|
*.swp
|
||||||
/venv/
|
/venv/
|
||||||
|
/.venv/
|
||||||
/tmp/
|
/tmp/
|
||||||
/.vagrant/
|
/.vagrant/
|
||||||
/Vagrantfile.local
|
/Vagrantfile.local
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
.DS_Store
|
.DS_Store
|
||||||
*.swp
|
*.swp
|
||||||
.vscode/
|
.vscode/
|
||||||
|
.coverage
|
||||||
/venv/
|
/venv/
|
||||||
/.venv/
|
/.venv/
|
||||||
/static/
|
/static/
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
FROM python:3.12-slim-bullseye
|
FROM python:3.14-slim-trixie
|
||||||
COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/
|
COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/
|
||||||
|
|
||||||
RUN useradd wagtail
|
RUN useradd wagtail
|
||||||
@@ -37,4 +37,4 @@ USER wagtail
|
|||||||
ENV PATH="/app/.venv/bin:$PATH"
|
ENV PATH="/app/.venv/bin:$PATH"
|
||||||
RUN python manage.py collectstatic --noinput --clear
|
RUN python manage.py collectstatic --noinput --clear
|
||||||
|
|
||||||
CMD ["sh", "-c", "set -xe; python manage.py migrate --noinput && gunicorn dnscms.wsgi:application"]
|
CMD ["sh", "-c", "set -xe; python manage.py migrate --noinput && gunicorn dnscms.wsgi:application --workers 3"]
|
||||||
|
|||||||
@@ -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),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
from django.db import models
|
from django.db import models
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
from grapple.helpers import register_singular_query_field
|
from grapple.helpers import register_singular_query_field
|
||||||
from grapple.models import (
|
from grapple.models import (
|
||||||
GraphQLImage,
|
GraphQLImage,
|
||||||
@@ -8,15 +9,20 @@ from grapple.models import (
|
|||||||
)
|
)
|
||||||
from wagtail.admin.panels import FieldPanel
|
from wagtail.admin.panels import FieldPanel
|
||||||
from wagtail.fields import RichTextField
|
from wagtail.fields import RichTextField
|
||||||
from wagtail.models import Page
|
from wagtail.models import Page, PageManager
|
||||||
from wagtail.search import index
|
from wagtail.search import index
|
||||||
|
from wagtail_headless_preview.models import HeadlessMixin
|
||||||
|
|
||||||
from dnscms.fields import CommonStreamField
|
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")
|
@register_singular_query_field("associationIndex")
|
||||||
class AssociationIndex(Page):
|
class AssociationIndex(HeadlessMixin, Page):
|
||||||
max_count = 1
|
max_count = 1
|
||||||
subpage_types = ["associations.AssociationPage"]
|
subpage_types = ["associations.AssociationPage"]
|
||||||
|
|
||||||
@@ -25,8 +31,8 @@ class AssociationIndex(Page):
|
|||||||
body = CommonStreamField
|
body = CommonStreamField
|
||||||
|
|
||||||
content_panels = Page.content_panels + [
|
content_panels = Page.content_panels + [
|
||||||
FieldPanel("lead", heading="Ingress"),
|
FieldPanel("lead", heading=_("Lead")),
|
||||||
FieldPanel("body", heading="Innhold"),
|
FieldPanel("body", heading=_("Content")),
|
||||||
]
|
]
|
||||||
|
|
||||||
graphql_fields = [
|
graphql_fields = [
|
||||||
@@ -36,15 +42,21 @@ class AssociationIndex(Page):
|
|||||||
|
|
||||||
search_fields = Page.search_fields
|
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 = []
|
subpage_types = []
|
||||||
parent_page_types = ["associations.AssociationIndex"]
|
parent_page_types = ["associations.AssociationIndex"]
|
||||||
show_in_menus = False
|
show_in_menus = False
|
||||||
|
|
||||||
|
objects = AssociationPageManager()
|
||||||
|
|
||||||
class AssociationType(models.TextChoices):
|
class AssociationType(models.TextChoices):
|
||||||
FORENING = "forening", "Forening"
|
FORENING = "forening", _("Association")
|
||||||
UTVALG = "utvalg", "Utvalg"
|
UTVALG = "utvalg", _("Committee")
|
||||||
|
|
||||||
excerpt = models.TextField(max_length=512, blank=False)
|
excerpt = models.TextField(max_length=512, blank=False)
|
||||||
lead = RichTextField(features=["italic", "link"], blank=True)
|
lead = RichTextField(features=["italic", "link"], blank=True)
|
||||||
@@ -64,14 +76,14 @@ class AssociationPage(WPImportedPageMixin, Page):
|
|||||||
content_panels = Page.content_panels + [
|
content_panels = Page.content_panels + [
|
||||||
FieldPanel(
|
FieldPanel(
|
||||||
"excerpt",
|
"excerpt",
|
||||||
heading="Utdrag",
|
heading=_("Excerpt"),
|
||||||
help_text="En veldig kort oppsummering av innholdet nedenfor. Brukes i listevisninger.",
|
help_text=_("A very short summary of the content below. Used in listing views."),
|
||||||
),
|
),
|
||||||
FieldPanel("lead", heading="Ingress"),
|
FieldPanel("lead", heading=_("Lead")),
|
||||||
FieldPanel("body", heading="Innhold"),
|
FieldPanel("body", heading=_("Content")),
|
||||||
FieldPanel("logo"),
|
FieldPanel("logo"),
|
||||||
FieldPanel("association_type", heading="Type"),
|
FieldPanel("association_type", heading=_("Type")),
|
||||||
FieldPanel("website_url", heading="Nettside"),
|
FieldPanel("website_url", heading=_("Website")),
|
||||||
]
|
]
|
||||||
|
|
||||||
graphql_fields = [
|
graphql_fields = [
|
||||||
@@ -88,32 +100,6 @@ class AssociationPage(WPImportedPageMixin, Page):
|
|||||||
index.SearchField("body"),
|
index.SearchField("body"),
|
||||||
]
|
]
|
||||||
|
|
||||||
def import_wordpress_data(self, data):
|
class Meta:
|
||||||
import html
|
verbose_name = _("association")
|
||||||
|
verbose_name_plural = _("associations")
|
||||||
# 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"
|
|
||||||
|
|||||||
@@ -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
|
from wagtail.admin.viewsets.chooser import ChooserViewSet
|
||||||
|
|
||||||
|
|
||||||
class AssociationChooserViewSet(ChooserViewSet):
|
class AssociationChooserViewSet(ChooserViewSet):
|
||||||
model = "associations.AssociationPage"
|
model = "associations.AssociationPage"
|
||||||
icon = "group"
|
icon = "group"
|
||||||
choose_one_text = "Choose an association"
|
choose_one_text = _("Choose an association")
|
||||||
choose_another_text = "Choose another association"
|
choose_another_text = _("Choose another association")
|
||||||
edit_item_text = "Edit this association"
|
edit_item_text = _("Edit this association")
|
||||||
# form_fields = ["name"]
|
# form_fields = ["name"]
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,19 @@
|
|||||||
from wagtail import hooks
|
from wagtail import hooks
|
||||||
|
|
||||||
|
from .admin import association_sidebar_viewset, association_explorer_viewset
|
||||||
from .views import association_chooser_viewset
|
from .views import association_chooser_viewset
|
||||||
|
|
||||||
|
|
||||||
@hooks.register("register_admin_viewset")
|
@hooks.register("register_admin_viewset")
|
||||||
def register_viewset():
|
def register_viewset():
|
||||||
return association_chooser_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
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ from wagtail.fields import RichTextField, StreamField
|
|||||||
from wagtail.models import Page
|
from wagtail.models import Page
|
||||||
from wagtail.search import index
|
from wagtail.search import index
|
||||||
from wagtail.snippets.models import register_snippet
|
from wagtail.snippets.models import register_snippet
|
||||||
|
from wagtail_headless_preview.models import HeadlessMixin
|
||||||
|
|
||||||
from contacts.blocks import ContactSectionBlock
|
from contacts.blocks import ContactSectionBlock
|
||||||
from dnscms.blocks import BASE_BLOCKS
|
from dnscms.blocks import BASE_BLOCKS
|
||||||
@@ -22,7 +23,7 @@ PHONE_REGEX_VALIDATOR = RegexValidator(
|
|||||||
|
|
||||||
|
|
||||||
@register_singular_query_field("contactIndex")
|
@register_singular_query_field("contactIndex")
|
||||||
class ContactIndex(Page):
|
class ContactIndex(HeadlessMixin, Page):
|
||||||
max_count = 1
|
max_count = 1
|
||||||
subpage_types = []
|
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,8 @@
|
|||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class DnsCmsConfig(AppConfig):
|
||||||
|
name = "dnscms"
|
||||||
|
|
||||||
|
def ready(self):
|
||||||
|
from dnscms import signals # noqa: F401
|
||||||
@@ -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, ...)
|
# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
|
||||||
import os
|
import os
|
||||||
import re
|
|
||||||
|
|
||||||
PROJECT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
PROJECT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||||
BASE_DIR = os.path.dirname(PROJECT_DIR)
|
BASE_DIR = os.path.dirname(PROJECT_DIR)
|
||||||
@@ -37,9 +36,11 @@ INSTALLED_APPS = [
|
|||||||
"news",
|
"news",
|
||||||
"openinghours",
|
"openinghours",
|
||||||
"sponsors",
|
"sponsors",
|
||||||
|
"studio",
|
||||||
# end cms apps
|
# end cms apps
|
||||||
"grapple",
|
"grapple",
|
||||||
"graphene_django",
|
"graphene_django",
|
||||||
|
"wagtail_headless_preview",
|
||||||
"wagtail.contrib.forms",
|
"wagtail.contrib.forms",
|
||||||
"wagtail.contrib.redirects",
|
"wagtail.contrib.redirects",
|
||||||
"wagtail.contrib.settings",
|
"wagtail.contrib.settings",
|
||||||
@@ -61,6 +62,7 @@ INSTALLED_APPS = [
|
|||||||
"django.contrib.sessions",
|
"django.contrib.sessions",
|
||||||
"django.contrib.messages",
|
"django.contrib.messages",
|
||||||
"django.contrib.staticfiles",
|
"django.contrib.staticfiles",
|
||||||
|
"django.contrib.postgres",
|
||||||
]
|
]
|
||||||
|
|
||||||
MIDDLEWARE = [
|
MIDDLEWARE = [
|
||||||
@@ -141,6 +143,8 @@ USE_L10N = True
|
|||||||
|
|
||||||
USE_TZ = True
|
USE_TZ = True
|
||||||
|
|
||||||
|
LOCALE_PATHS = [os.path.join(BASE_DIR, "locale")]
|
||||||
|
|
||||||
|
|
||||||
# Static files (CSS, JavaScript, Images)
|
# Static files (CSS, JavaScript, Images)
|
||||||
# https://docs.djangoproject.com/en/4.2/howto/static-files/
|
# https://docs.djangoproject.com/en/4.2/howto/static-files/
|
||||||
@@ -169,9 +173,13 @@ MEDIA_URL = "/media/"
|
|||||||
# Wagtail settings
|
# Wagtail settings
|
||||||
|
|
||||||
WAGTAIL_SITE_NAME = "dnscms"
|
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_IMAGE_MODEL = "images.CustomImage"
|
||||||
|
WAGTAILIMAGES_EXTENSIONS = ["avif", "gif", "jpg", "jpeg", "png", "webp", "svg"]
|
||||||
|
|
||||||
# Search
|
# Search
|
||||||
# https://docs.wagtail.org/en/stable/topics/search/backends.html
|
# https://docs.wagtail.org/en/stable/topics/search/backends.html
|
||||||
@@ -182,11 +190,25 @@ WAGTAILSEARCH_BACKENDS = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
# Base URL to use when referring to full URLs within the Wagtail admin backend -
|
# 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
|
# e.g. in notification emails. Don't include '/admin' or a trailing slash.
|
||||||
WAGTAILADMIN_BASE_URL = "http://example.com"
|
# 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
|
# Public URL of the Next.js frontend. Used to direct preview iframes and to
|
||||||
BASE_URL = "http://example.com"
|
# 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("/")
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
# GraphQL
|
# GraphQL
|
||||||
GRAPHENE = {"SCHEMA": "grapple.schema.schema"}
|
GRAPHENE = {"SCHEMA": "grapple.schema.schema"}
|
||||||
@@ -202,77 +224,9 @@ GRAPPLE = {
|
|||||||
"news",
|
"news",
|
||||||
"openinghours",
|
"openinghours",
|
||||||
"sponsors",
|
"sponsors",
|
||||||
|
"studio",
|
||||||
],
|
],
|
||||||
"EXPOSE_GRAPHIQL": True,
|
"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"
|
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:
|
try:
|
||||||
from .local import *
|
from .local import *
|
||||||
except ImportError:
|
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,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,9 @@
|
|||||||
from django.contrib.admin.utils import quote
|
|
||||||
from django.templatetags.static import static
|
from django.templatetags.static import static
|
||||||
from django.urls import reverse
|
|
||||||
from django.utils.html import format_html
|
from django.utils.html import format_html
|
||||||
|
from grapple.registry import registry as grapple_registry
|
||||||
from wagtail import hooks
|
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
|
|
||||||
|
|
||||||
|
|
||||||
@hooks.register("register_rich_text_features")
|
@hooks.register("register_rich_text_features")
|
||||||
@@ -15,31 +11,38 @@ def enable_additional_rich_text_features(features):
|
|||||||
features.default_features.extend(["h5", "h6", "blockquote"])
|
features.default_features.extend(["h5", "h6", "blockquote"])
|
||||||
|
|
||||||
|
|
||||||
@hooks.register("register_admin_menu_item")
|
@hooks.register("register_schema_query")
|
||||||
def register_events_menu_item():
|
def override_search_resolver(query_mixins):
|
||||||
page = EventIndex.objects.first()
|
"""
|
||||||
events_url = "#"
|
Override Grapple's `search` resolver. Two fixes vs. the upstream version:
|
||||||
if page:
|
1. Restrict pages to live + public so drafts and access-restricted pages
|
||||||
events_url = reverse("wagtailadmin_explore", args=(quote(page.pk),))
|
don't leak via the public API.
|
||||||
return MenuItem("Arrangementer", events_url, icon_name="date", order=1)
|
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")
|
class SearchOverrideMixin:
|
||||||
def register_associations_menu_item():
|
def resolve_search(self, info, **kwargs):
|
||||||
page = AssociationIndex.objects.first()
|
query = kwargs.get("query")
|
||||||
associations_url = "#"
|
if not query:
|
||||||
if page:
|
return None
|
||||||
associations_url = reverse("wagtailadmin_explore", args=(quote(page.pk),))
|
s = get_search_backend()
|
||||||
return MenuItem("Foreninger", associations_url, icon_name="group", order=2)
|
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("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)
|
|
||||||
|
|
||||||
|
|
||||||
@hooks.register("construct_page_action_menu")
|
@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 django.db import models
|
||||||
from wagtail.models import Page
|
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
|
# 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):
|
class WPImportedPageMixin(Page):
|
||||||
wp_post_id = models.IntegerField(blank=True, null=True)
|
wp_post_id = models.IntegerField(blank=True, null=True)
|
||||||
wp_post_type = models.CharField(max_length=255, 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)
|
wp_link = models.TextField(blank=True, null=True) # noqa: DJ001
|
||||||
wp_raw_content = models.TextField(blank=True, null=True)
|
wp_raw_content = models.TextField(blank=True, null=True) # noqa: DJ001
|
||||||
wp_processed_content = models.TextField(blank=True, null=True)
|
wp_processed_content = models.TextField(blank=True, null=True) # noqa: DJ001
|
||||||
wp_block_json = models.TextField(blank=True, null=True)
|
wp_block_json = models.TextField(blank=True, null=True) # noqa: DJ001
|
||||||
wp_normalized_styles = models.TextField(blank=True, null=True)
|
wp_normalized_styles = models.TextField(blank=True, null=True) # noqa: DJ001
|
||||||
wp_post_meta = models.JSONField(blank=True, null=True)
|
wp_post_meta = models.JSONField(blank=True, null=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
abstract = True
|
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,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),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
from django import forms
|
from django import forms
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
from django.db import models
|
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 import timezone
|
||||||
from django.utils.html import mark_safe
|
from django.utils.html import mark_safe
|
||||||
from django.utils.translation import gettext_lazy as _
|
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.models import Orderable, Page, PageManager, PageQuerySet
|
||||||
from wagtail.search import index
|
from wagtail.search import index
|
||||||
from wagtail.snippets.models import register_snippet
|
from wagtail.snippets.models import register_snippet
|
||||||
|
from wagtail_headless_preview.models import HeadlessMixin
|
||||||
|
|
||||||
from associations.widgets import AssociationChooserWidget
|
from associations.widgets import AssociationChooserWidget
|
||||||
from dnscms.fields import CommonStreamField
|
from dnscms.fields import CommonStreamField
|
||||||
from dnscms.options import ALL_PIGS
|
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
|
from venues.models import VenuePage
|
||||||
|
|
||||||
|
|
||||||
@register_singular_query_field("eventIndex")
|
@register_singular_query_field("eventIndex")
|
||||||
class EventIndex(Page):
|
class EventIndex(HeadlessMixin, Page):
|
||||||
max_count = 1
|
max_count = 1
|
||||||
subpage_types = ["events.EventPage"]
|
subpage_types = ["events.EventPage"]
|
||||||
|
|
||||||
def future_events(self, info, **kwargs):
|
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 = [
|
graphql_fields = [
|
||||||
GraphQLCollection(
|
GraphQLCollection(
|
||||||
@@ -74,11 +95,12 @@ class EventCategory(models.Model):
|
|||||||
help_text=_("The name of the category as it will appear in URLs."),
|
help_text=_("The name of the category as it will appear in URLs."),
|
||||||
)
|
)
|
||||||
show_in_filters = models.BooleanField(
|
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 = [
|
PIG_CHOICES = [
|
||||||
("", "Ingen"),
|
("", _("None")),
|
||||||
] + ALL_PIGS
|
] + ALL_PIGS
|
||||||
|
|
||||||
pig = models.CharField(
|
pig = models.CharField(
|
||||||
@@ -86,14 +108,14 @@ class EventCategory(models.Model):
|
|||||||
choices=PIG_CHOICES,
|
choices=PIG_CHOICES,
|
||||||
default="",
|
default="",
|
||||||
blank=True,
|
blank=True,
|
||||||
help_text="Standardgris for arrangementer av denne typen.",
|
help_text=_("Default pig for events of this kind."),
|
||||||
)
|
)
|
||||||
|
|
||||||
panels = [
|
panels = [
|
||||||
TitleFieldPanel("name"),
|
TitleFieldPanel("name"),
|
||||||
FieldPanel("slug"),
|
FieldPanel("slug"),
|
||||||
FieldPanel("show_in_filters"),
|
FieldPanel("show_in_filters"),
|
||||||
FieldPanel("pig", heading="Gris"),
|
FieldPanel("pig", heading=_("Pig")),
|
||||||
]
|
]
|
||||||
|
|
||||||
graphql_fields = [
|
graphql_fields = [
|
||||||
@@ -104,8 +126,8 @@ class EventCategory(models.Model):
|
|||||||
]
|
]
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
verbose_name = "Event category"
|
verbose_name = _("event category")
|
||||||
verbose_name_plural = "Event categories"
|
verbose_name_plural = _("event categories")
|
||||||
ordering = ["name"]
|
ordering = ["name"]
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
@@ -133,8 +155,8 @@ class EventOrganizerLink(Orderable):
|
|||||||
return f"{self.organizer.name}"
|
return f"{self.organizer.name}"
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
verbose_name = "Arrangør"
|
verbose_name = _("organizer")
|
||||||
verbose_name_plural = "Arrangører"
|
verbose_name_plural = _("organizers")
|
||||||
constraints = [
|
constraints = [
|
||||||
UniqueConstraint(
|
UniqueConstraint(
|
||||||
"event", "organizer", name="event_organizer_link_event_organizer_unique"
|
"event", "organizer", name="event_organizer_link_event_organizer_unique"
|
||||||
@@ -144,7 +166,9 @@ class EventOrganizerLink(Orderable):
|
|||||||
|
|
||||||
@register_snippet
|
@register_snippet
|
||||||
@register_query_field("eventOrganizer", "eventOrganizers")
|
@register_query_field("eventOrganizer", "eventOrganizers")
|
||||||
class EventOrganizer(ClusterableModel):
|
class EventOrganizer(index.Indexed, ClusterableModel):
|
||||||
|
objects = WPAwareManager()
|
||||||
|
|
||||||
name = models.CharField(
|
name = models.CharField(
|
||||||
max_length=100,
|
max_length=100,
|
||||||
null=False,
|
null=False,
|
||||||
@@ -162,13 +186,13 @@ class EventOrganizer(ClusterableModel):
|
|||||||
blank=True,
|
blank=True,
|
||||||
on_delete=models.PROTECT,
|
on_delete=models.PROTECT,
|
||||||
related_name="organizers",
|
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(
|
external_url = models.URLField(
|
||||||
blank=True,
|
blank=True,
|
||||||
max_length=512,
|
max_length=512,
|
||||||
help_text="Lenke til nettstedet til ekstern arrangør",
|
help_text=_("Link to the external organizer's website"),
|
||||||
)
|
)
|
||||||
|
|
||||||
panels = [
|
panels = [
|
||||||
@@ -177,15 +201,15 @@ class EventOrganizer(ClusterableModel):
|
|||||||
FieldPanel(
|
FieldPanel(
|
||||||
"association",
|
"association",
|
||||||
widget=AssociationChooserWidget(linked_fields={"association": "#id_association"}),
|
widget=AssociationChooserWidget(linked_fields={"association": "#id_association"}),
|
||||||
heading="Intern arrangør",
|
heading=_("Internal organizer"),
|
||||||
),
|
),
|
||||||
MultiFieldPanel(
|
MultiFieldPanel(
|
||||||
heading="Ekstern arrangør",
|
heading=_("External organizer"),
|
||||||
children=[
|
children=[
|
||||||
FieldPanel(
|
FieldPanel(
|
||||||
"external_url",
|
"external_url",
|
||||||
heading="Nettsted",
|
heading=_("Website"),
|
||||||
help_text="La denne stå tom om arrangøren finnes i lista over.",
|
help_text=_("Leave this empty if the organizer exists in the list above."),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -198,9 +222,14 @@ class EventOrganizer(ClusterableModel):
|
|||||||
GraphQLString("external_url"),
|
GraphQLString("external_url"),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
search_fields = [
|
||||||
|
index.SearchField("name"),
|
||||||
|
index.AutocompleteField("name"),
|
||||||
|
]
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
verbose_name = "Event organizer"
|
verbose_name = _("event organizer")
|
||||||
verbose_name_plural = "Event organizers"
|
verbose_name_plural = _("event organizers")
|
||||||
ordering = ["name"]
|
ordering = ["name"]
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
@@ -209,15 +238,22 @@ class EventOrganizer(ClusterableModel):
|
|||||||
|
|
||||||
class EventPageQuerySet(PageQuerySet):
|
class EventPageQuerySet(PageQuerySet):
|
||||||
def future(self):
|
def future(self):
|
||||||
today = timezone.localtime(timezone.now()).date()
|
now = timezone.now()
|
||||||
next_occurrence = Min("occurrences__start", filter=Q(occurrences__start__gte=today))
|
today_start = timezone.localtime(now).replace(hour=0, minute=0, second=0, microsecond=0)
|
||||||
return self.filter(occurrences__start__gte=today).annotate(next_occurrence=next_occurrence)
|
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 = []
|
subpage_types = []
|
||||||
parent_page_types = ["events.EventIndex"]
|
parent_page_types = ["events.EventIndex"]
|
||||||
show_in_menus = False
|
show_in_menus = False
|
||||||
@@ -230,19 +266,19 @@ class EventPage(WPImportedPageMixin, Page):
|
|||||||
blank=True,
|
blank=True,
|
||||||
on_delete=models.SET_NULL,
|
on_delete=models.SET_NULL,
|
||||||
related_name="+",
|
related_name="+",
|
||||||
help_text=(
|
help_text=_(
|
||||||
"Velg et bilde til bruk i programmet og andre visningsflater. "
|
"Choose an image for use in the programme and other surfaces. "
|
||||||
"Bør være et bilde eller en illustrasjon uten tekst "
|
"Should be a photo or an illustration without too much text "
|
||||||
"– ikke gjenbruk et Facebook-cover ukritisk!"
|
"– don't reuse a Facebook cover uncritically!"
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
subtitle = models.CharField(
|
subtitle = models.CharField(
|
||||||
blank=True,
|
blank=True,
|
||||||
max_length=128,
|
max_length=128,
|
||||||
help_text=(
|
help_text=_(
|
||||||
"En kort tekst som kommer rett under under tittelen. "
|
"A short text that appears right below the title. "
|
||||||
"La denne gjerne stå tom om du fikk plass til det meste i tittelen."
|
"Feel free to leave it empty if you fit most of it in the main title."
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
lead = RichTextField(features=["italic", "link"], blank=True)
|
lead = RichTextField(features=["italic", "link"], blank=True)
|
||||||
@@ -258,8 +294,8 @@ class EventPage(WPImportedPageMixin, Page):
|
|||||||
)
|
)
|
||||||
|
|
||||||
PIG_CHOICES = [
|
PIG_CHOICES = [
|
||||||
("", "Ingen"),
|
("", _("None")),
|
||||||
("automatic", "Automatisk"),
|
("automatic", _("Automatic")),
|
||||||
] + ALL_PIGS
|
] + ALL_PIGS
|
||||||
|
|
||||||
pig = models.CharField(
|
pig = models.CharField(
|
||||||
@@ -267,21 +303,21 @@ class EventPage(WPImportedPageMixin, Page):
|
|||||||
choices=PIG_CHOICES,
|
choices=PIG_CHOICES,
|
||||||
default="automatic",
|
default="automatic",
|
||||||
blank=True,
|
blank=True,
|
||||||
help_text=(
|
help_text=_(
|
||||||
"Grisen som henger på arrangementssiden. "
|
"The pig that hangs out on the event page. "
|
||||||
"Automatisk fører til at en velges basert på arrangementets kategori."
|
"Automatic causes one to be chosen based on the event's category."
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
ticket_url = models.URLField(
|
ticket_url = models.URLField(
|
||||||
blank=True,
|
blank=True,
|
||||||
max_length=1024,
|
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(
|
facebook_url = models.URLField(
|
||||||
blank=True,
|
blank=True,
|
||||||
max_length=1024,
|
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)
|
free = models.BooleanField(null=False, default=False)
|
||||||
@@ -290,62 +326,65 @@ class EventPage(WPImportedPageMixin, Page):
|
|||||||
price_member = models.CharField(max_length=32, blank=True)
|
price_member = models.CharField(max_length=32, blank=True)
|
||||||
|
|
||||||
ticket_panels = [
|
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(
|
MultiFieldPanel(
|
||||||
children=[
|
children=[
|
||||||
FieldRowPanel(
|
FieldRowPanel(
|
||||||
children=[
|
children=[
|
||||||
FieldPanel("price_regular", heading="Ordinær pris"),
|
FieldPanel("price_regular", heading=_("Regular price")),
|
||||||
FieldPanel("price_student", heading="Pris for studenter"),
|
FieldPanel("price_student", heading=_("Price for students")),
|
||||||
FieldPanel("price_member", heading="Pris for medlemmer av DNS"),
|
FieldPanel("price_member", heading=_("Price for DNS members")),
|
||||||
],
|
],
|
||||||
help_text="",
|
help_text="",
|
||||||
),
|
),
|
||||||
HelpPanel(
|
HelpPanel(
|
||||||
content=mark_safe(
|
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"},
|
attrs={"id": "specific_pricing_panel"},
|
||||||
),
|
),
|
||||||
FieldPanel("ticket_url", heading="Billettkjøpslenke"),
|
FieldPanel("ticket_url", heading=_("Ticket purchase link")),
|
||||||
]
|
]
|
||||||
|
|
||||||
content_panels = Page.content_panels + [
|
content_panels = Page.content_panels + [
|
||||||
FieldPanel("subtitle", heading="Undertittel"),
|
FieldPanel("subtitle", heading=_("Subtitle")),
|
||||||
FieldPanel("featured_image"),
|
FieldPanel("featured_image"),
|
||||||
FieldPanel("lead", heading="Ingress"),
|
FieldPanel("lead", heading=_("Lead")),
|
||||||
FieldPanel("body"),
|
FieldPanel("body"),
|
||||||
FieldPanel("categories", widget=forms.CheckboxSelectMultiple),
|
FieldPanel("categories", widget=forms.CheckboxSelectMultiple),
|
||||||
MultiFieldPanel(
|
MultiFieldPanel(
|
||||||
heading="Arrangører",
|
heading=_("Organizers"),
|
||||||
children=[
|
children=[
|
||||||
HelpPanel(
|
HelpPanel(
|
||||||
content=("Hvem står bak arrangementet?"),
|
content=_("Who is behind the event?"),
|
||||||
),
|
),
|
||||||
MultipleChooserPanel(
|
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(
|
FieldPanel(
|
||||||
"facebook_url",
|
"facebook_url",
|
||||||
heading="Facebook-lenke",
|
heading=_("Facebook link"),
|
||||||
help_text="Lenke direkte til arrangementet på Facebook.",
|
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(
|
MultiFieldPanel(
|
||||||
heading="Dato, tid og lokale",
|
heading=_("Date, time and venue"),
|
||||||
children=[
|
children=[
|
||||||
HelpPanel(
|
HelpPanel(
|
||||||
content=(
|
content=_(
|
||||||
"Om arrangementet går over flere dager, "
|
"If the event spans several days, add each day as a separate occurrence."
|
||||||
"legg inn hver dag som en egen forekomst."
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
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"),
|
GraphQLImage("featured_image"),
|
||||||
GraphQLRichText("lead"),
|
GraphQLRichText("lead"),
|
||||||
GraphQLStreamfield("body"),
|
GraphQLStreamfield("body"),
|
||||||
GraphQLString("pig"),
|
GraphQLString("pig", required=True),
|
||||||
GraphQLString("ticket_url"),
|
GraphQLString("ticket_url"),
|
||||||
GraphQLString("facebook_url"),
|
GraphQLString("facebook_url"),
|
||||||
GraphQLBoolean("free"),
|
GraphQLBoolean("free"),
|
||||||
@@ -387,6 +426,10 @@ class EventPage(WPImportedPageMixin, Page):
|
|||||||
|
|
||||||
search_fields = Page.search_fields + [index.SearchField("body")]
|
search_fields = Page.search_fields + [index.SearchField("body")]
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = _("event")
|
||||||
|
verbose_name_plural = _("events")
|
||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
super().clean()
|
super().clean()
|
||||||
|
|
||||||
@@ -406,130 +449,10 @@ class EventPage(WPImportedPageMixin, Page):
|
|||||||
self.price_student = ""
|
self.price_student = ""
|
||||||
self.price_member = ""
|
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):
|
class EventOccurrence(Orderable):
|
||||||
|
objects = WPAwareManager()
|
||||||
|
|
||||||
event = ParentalKey(EventPage, on_delete=models.CASCADE, related_name="occurrences")
|
event = ParentalKey(EventPage, on_delete=models.CASCADE, related_name="occurrences")
|
||||||
start = models.DateTimeField()
|
start = models.DateTimeField()
|
||||||
end = models.DateTimeField(null=True, blank=True)
|
end = models.DateTimeField(null=True, blank=True)
|
||||||
@@ -544,22 +467,24 @@ class EventOccurrence(Orderable):
|
|||||||
blank=True,
|
blank=True,
|
||||||
max_length=128,
|
max_length=128,
|
||||||
help_text=mark_safe(
|
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 = [
|
panels = [
|
||||||
FieldRowPanel(
|
FieldRowPanel(
|
||||||
children=[
|
children=[
|
||||||
FieldPanel("start", heading="Start"),
|
FieldPanel("start", heading=_("Start")),
|
||||||
FieldPanel("end", heading="Slutt"),
|
FieldPanel("end", heading=_("End")),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
FieldRowPanel(
|
FieldRowPanel(
|
||||||
children=[
|
children=[
|
||||||
FieldPanel("venue", heading="Lokale"),
|
FieldPanel("venue", heading=_("Venue"), widget=forms.Select),
|
||||||
FieldPanel("venue_custom", heading="Lokale som fritekst"),
|
FieldPanel("venue_custom", heading=_("Venue as free text")),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
@@ -572,140 +497,29 @@ class EventOccurrence(Orderable):
|
|||||||
]
|
]
|
||||||
|
|
||||||
def clean(self):
|
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:
|
if self.venue and self.venue_custom:
|
||||||
raise ValidationError(
|
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:
|
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):
|
def __str__(self):
|
||||||
return f"{self.start}--{self.end}"
|
return f"{self.start}--{self.end}"
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
verbose_name = "Forekomst"
|
verbose_name = _("occurrence")
|
||||||
verbose_name_plural = "Forekomster"
|
verbose_name_plural = _("occurrences")
|
||||||
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
|
|
||||||
# Create your tests here.
|
|
||||||
@@ -1,15 +1,34 @@
|
|||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
from wagtail.admin.forms import WagtailAdminModelForm
|
||||||
from wagtail.admin.viewsets.chooser import ChooserViewSet
|
from wagtail.admin.viewsets.chooser import ChooserViewSet
|
||||||
|
|
||||||
|
from dnscms.utils import slugify
|
||||||
|
from events.models import EventOrganizer
|
||||||
|
|
||||||
|
|
||||||
|
class EventOrganizerCreationForm(WagtailAdminModelForm):
|
||||||
|
class Meta:
|
||||||
|
model = EventOrganizer
|
||||||
|
fields = ["name", "association", "external_url"]
|
||||||
|
|
||||||
|
def save(self, commit=True):
|
||||||
|
instance = super().save(commit=False)
|
||||||
|
if not instance.slug:
|
||||||
|
instance.slug = slugify(instance.name)
|
||||||
|
if commit:
|
||||||
|
instance.save()
|
||||||
|
return instance
|
||||||
|
|
||||||
|
|
||||||
class EventOrganizerChooserViewSet(ChooserViewSet):
|
class EventOrganizerChooserViewSet(ChooserViewSet):
|
||||||
model = "events.EventOrganizer"
|
model = "events.EventOrganizer"
|
||||||
icon = "group"
|
icon = "group"
|
||||||
per_page = 30
|
per_page = 30
|
||||||
page_title = "Choose organizers"
|
page_title = _("Choose organizers")
|
||||||
choose_one_text = "Choose an organizer"
|
choose_one_text = _("Choose an organizer")
|
||||||
choose_another_text = "Choose another organizer"
|
choose_another_text = _("Choose another organizer")
|
||||||
edit_item_text = "Edit this organizer"
|
edit_item_text = _("Edit this organizer")
|
||||||
form_fields = ["name", "association", "external_url"]
|
creation_form_class = EventOrganizerCreationForm
|
||||||
|
|
||||||
|
|
||||||
event_organizer_chooser_viewset = EventOrganizerChooserViewSet("event_organizer_chooser")
|
event_organizer_chooser_viewset = EventOrganizerChooserViewSet("event_organizer_chooser")
|
||||||
|
|||||||
@@ -1,8 +1,19 @@
|
|||||||
from wagtail import hooks
|
from wagtail import hooks
|
||||||
|
|
||||||
|
from .admin import event_sidebar_viewset, event_explorer_viewset
|
||||||
from .views import event_organizer_chooser_viewset
|
from .views import event_organizer_chooser_viewset
|
||||||
|
|
||||||
|
|
||||||
@hooks.register("register_admin_viewset")
|
@hooks.register("register_admin_viewset")
|
||||||
def register_viewset():
|
def register_viewset():
|
||||||
return event_organizer_chooser_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
|
||||||
|
|||||||
@@ -4,13 +4,14 @@ from wagtail.admin.panels import FieldPanel
|
|||||||
from wagtail.fields import RichTextField, StreamField
|
from wagtail.fields import RichTextField, StreamField
|
||||||
from wagtail.models import Page
|
from wagtail.models import Page
|
||||||
from wagtail.search import index
|
from wagtail.search import index
|
||||||
|
from wagtail_headless_preview.models import HeadlessMixin
|
||||||
|
|
||||||
from dnscms.blocks import PageSectionBlock
|
from dnscms.blocks import PageSectionBlock
|
||||||
from dnscms.fields import BASE_BLOCKS
|
from dnscms.fields import BASE_BLOCKS
|
||||||
from dnscms.options import ALL_PIGS
|
from dnscms.options import ALL_PIGS
|
||||||
|
|
||||||
|
|
||||||
class GenericPage(Page):
|
class GenericPage(HeadlessMixin, Page):
|
||||||
subpage_types = ["generic.GenericPage"]
|
subpage_types = ["generic.GenericPage"]
|
||||||
show_in_menus = True
|
show_in_menus = True
|
||||||
|
|
||||||
|
|||||||
@@ -10,9 +10,10 @@ from wagtail.admin.panels import (
|
|||||||
PageChooserPanel,
|
PageChooserPanel,
|
||||||
)
|
)
|
||||||
from wagtail.models import Orderable, Page
|
from wagtail.models import Orderable, Page
|
||||||
|
from wagtail_headless_preview.models import HeadlessMixin
|
||||||
|
|
||||||
|
|
||||||
class HomePage(Page):
|
class HomePage(HeadlessMixin, Page):
|
||||||
max_count = 1
|
max_count = 1
|
||||||
|
|
||||||
content_panels = Page.content_panels + [
|
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>
|
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
# Generated by Django 5.2.5 on 2025-08-12 21:00
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('images', '0004_alter_customimage_alt_alter_customimage_attribution'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='customimage',
|
||||||
|
name='description',
|
||||||
|
field=models.CharField(blank=True, default='', max_length=255, verbose_name='description'),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -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"
|
||||||
@@ -1,3 +1,8 @@
|
|||||||
[tools]
|
# Translation tasks
|
||||||
python = "3.13"
|
[tasks.compilemessages]
|
||||||
uv = "latest"
|
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'),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -1,24 +1,30 @@
|
|||||||
from django.db import models
|
from django.db import models
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
from grapple.helpers import register_singular_query_field
|
from grapple.helpers import register_singular_query_field
|
||||||
from grapple.models import GraphQLImage, GraphQLRichText, GraphQLStreamfield, GraphQLString
|
from grapple.models import GraphQLImage, GraphQLRichText, GraphQLStreamfield, GraphQLString
|
||||||
from wagtail.admin.panels import FieldPanel
|
from wagtail.admin.panels import FieldPanel
|
||||||
from wagtail.fields import RichTextField
|
from wagtail.fields import RichTextField
|
||||||
from wagtail.models import Page
|
from wagtail.models import Page, PageManager
|
||||||
from wagtail.search import index
|
from wagtail.search import index
|
||||||
|
from wagtail_headless_preview.models import HeadlessMixin
|
||||||
|
|
||||||
from dnscms.fields import CommonStreamField
|
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")
|
@register_singular_query_field("newsIndex")
|
||||||
class NewsIndex(Page):
|
class NewsIndex(HeadlessMixin, Page):
|
||||||
max_count = 1
|
max_count = 1
|
||||||
subpage_types = ["news.NewsPage"]
|
subpage_types = ["news.NewsPage"]
|
||||||
|
|
||||||
lead = RichTextField(features=["italic", "link"], blank=True)
|
lead = RichTextField(features=["italic", "link"], blank=True)
|
||||||
|
|
||||||
content_panels = Page.content_panels + [
|
content_panels = Page.content_panels + [
|
||||||
FieldPanel("lead", heading="Ingress"),
|
FieldPanel("lead", heading=_("Lead")),
|
||||||
]
|
]
|
||||||
|
|
||||||
graphql_fields = [
|
graphql_fields = [
|
||||||
@@ -27,12 +33,18 @@ class NewsIndex(Page):
|
|||||||
|
|
||||||
search_fields = []
|
search_fields = []
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = _("news index")
|
||||||
|
verbose_name_plural = _("news indexes")
|
||||||
|
|
||||||
class NewsPage(WPImportedPageMixin, Page):
|
|
||||||
|
class NewsPage(HeadlessMixin, WPImportedPageMixin, Page):
|
||||||
subpage_types = []
|
subpage_types = []
|
||||||
parent_page_types = ["news.NewsIndex"]
|
parent_page_types = ["news.NewsIndex"]
|
||||||
show_in_menus = False
|
show_in_menus = False
|
||||||
|
|
||||||
|
objects = NewsPageManager()
|
||||||
|
|
||||||
excerpt = models.TextField(max_length=512, blank=False)
|
excerpt = models.TextField(max_length=512, blank=False)
|
||||||
lead = RichTextField(features=["italic", "link"], blank=True)
|
lead = RichTextField(features=["italic", "link"], blank=True)
|
||||||
body = CommonStreamField
|
body = CommonStreamField
|
||||||
@@ -42,23 +54,28 @@ class NewsPage(WPImportedPageMixin, Page):
|
|||||||
blank=True,
|
blank=True,
|
||||||
on_delete=models.SET_NULL,
|
on_delete=models.SET_NULL,
|
||||||
related_name="+",
|
related_name="+",
|
||||||
help_text=(
|
help_text=_(
|
||||||
"Velg et bilde til bruk i på forsiden og andre visningsflater. "
|
"Choose an image for use on the front page and other surfaces. "
|
||||||
"Bør være et bilde eller en illustrasjon uten tekst."
|
"Should be a photo or an illustration without too much text."
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
content_panels = Page.content_panels + [
|
content_panels = Page.content_panels + [
|
||||||
FieldPanel(
|
FieldPanel(
|
||||||
"excerpt",
|
"excerpt",
|
||||||
heading="Utdrag",
|
heading=_("Excerpt"),
|
||||||
help_text="En veldig kort oppsummering av innholdet i artikkelen. Brukes på forsiden og i artikkeloversikten.",
|
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("featured_image"),
|
||||||
FieldPanel(
|
FieldPanel(
|
||||||
"lead",
|
"lead",
|
||||||
heading="Ingress",
|
heading=_("Lead"),
|
||||||
help_text="Et kortfattet, innledende avsnitt som oppsummerer hovedinnholdet i artikkelen.",
|
help_text=_(
|
||||||
|
"A brief, introductory paragraph that summarizes the main content of the article."
|
||||||
|
),
|
||||||
),
|
),
|
||||||
FieldPanel("body"),
|
FieldPanel("body"),
|
||||||
]
|
]
|
||||||
@@ -76,51 +93,6 @@ class NewsPage(WPImportedPageMixin, Page):
|
|||||||
index.SearchField("body"),
|
index.SearchField("body"),
|
||||||
]
|
]
|
||||||
|
|
||||||
def import_wordpress_data(self, data):
|
class Meta:
|
||||||
import html
|
verbose_name = _("news article")
|
||||||
|
verbose_name_plural = _("news articles")
|
||||||
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 "[...]"
|
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -6,7 +6,8 @@ from modelcluster.fields import ParentalKey
|
|||||||
from modelcluster.models import ClusterableModel
|
from modelcluster.models import ClusterableModel
|
||||||
from wagtail import blocks
|
from wagtail import blocks
|
||||||
from wagtail.admin.panels import FieldPanel, InlinePanel, MultiFieldPanel, TitleFieldPanel
|
from wagtail.admin.panels import FieldPanel, InlinePanel, MultiFieldPanel, TitleFieldPanel
|
||||||
from wagtail.models import Orderable, StreamField
|
from wagtail.fields import StreamField
|
||||||
|
from wagtail.models import Orderable
|
||||||
from wagtail.snippets.models import register_snippet
|
from wagtail.snippets.models import register_snippet
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +0,0 @@
|
|||||||
from django.test import TestCase
|
|
||||||
|
|
||||||
# Create your tests here.
|
|
||||||
@@ -3,20 +3,26 @@ name = "dnscms"
|
|||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
description = ""
|
description = ""
|
||||||
authors = [{ name = "EDB", email = "edb@neuf.no" }]
|
authors = [{ name = "EDB", email = "edb@neuf.no" }]
|
||||||
requires-python = ">=3.12, <3.13"
|
requires-python = ">=3.14, <3.15"
|
||||||
readme = "README.md"
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"wagtail>=6.1.3,<7",
|
"wagtail>=7.4.1,<8",
|
||||||
"django>=5.0.7,<6",
|
"wagtail-grapple>=0.31.0,<0.32",
|
||||||
"wagtail-grapple>=0.26.0,<0.27",
|
"wagtail-headless-preview>=0.8,<0.9",
|
||||||
"psycopg2-binary>=2.9.10,<3",
|
"django>=6.0.5,<7",
|
||||||
"django-extensions>=3.2.3,<4",
|
"django-extensions>=4.1,<5",
|
||||||
"whitenoise>=6.7.0,<7",
|
"psycopg2-binary>=2.9.12,<3",
|
||||||
"gunicorn>=23.0.0",
|
"gunicorn>=26.0.0,<27",
|
||||||
|
"whitenoise>=6.12.0,<7",
|
||||||
]
|
]
|
||||||
|
|
||||||
[dependency-groups]
|
[dependency-groups]
|
||||||
dev = ["ruff"]
|
dev = [
|
||||||
|
"ruff>=0.15.13,<0.16",
|
||||||
|
"pytest>=9.0.3,<10",
|
||||||
|
"pytest-cov>=7.0.0,<8",
|
||||||
|
"pytest-django>=4.12.0,<5",
|
||||||
|
"wagtail-factories>=4.4.0,<5",
|
||||||
|
]
|
||||||
|
|
||||||
[tool.uv]
|
[tool.uv]
|
||||||
package = false
|
package = false
|
||||||
@@ -32,3 +38,19 @@ line-length = 99
|
|||||||
select = ["F", "E", "W", "Q", "UP", "DJ"]
|
select = ["F", "E", "W", "Q", "UP", "DJ"]
|
||||||
ignore = []
|
ignore = []
|
||||||
exclude = ["**/migrations/*.py"]
|
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",
|
||||||
|
]
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ from wagtail.fields import RichTextField, StreamField
|
|||||||
from wagtail.images.blocks import ImageChooserBlock
|
from wagtail.images.blocks import ImageChooserBlock
|
||||||
from wagtail.models import Page
|
from wagtail.models import Page
|
||||||
from wagtail.search import index
|
from wagtail.search import index
|
||||||
|
from wagtail_headless_preview.models import HeadlessMixin
|
||||||
|
|
||||||
from dnscms.blocks import BASE_BLOCKS
|
from dnscms.blocks import BASE_BLOCKS
|
||||||
|
|
||||||
@@ -34,7 +35,7 @@ class SponsorBlock(blocks.StructBlock):
|
|||||||
|
|
||||||
|
|
||||||
@register_singular_query_field("sponsorsPage")
|
@register_singular_query_field("sponsorsPage")
|
||||||
class SponsorsPage(Page):
|
class SponsorsPage(HeadlessMixin, Page):
|
||||||
max_count = 1
|
max_count = 1
|
||||||
subpage_types = []
|
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,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,574 @@
|
|||||||
|
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.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
|
||||||
@@ -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,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,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"]
|
||||||
@@ -1,77 +1,113 @@
|
|||||||
version = 1
|
version = 1
|
||||||
revision = 2
|
revision = 3
|
||||||
requires-python = "==3.12.*"
|
requires-python = "==3.14.*"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "aniso8601"
|
|
||||||
version = "9.0.1"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/cb/72/be3db445b03944bfbb2b02b82d00cb2a2bcf96275c4543f14bf60fa79e12/aniso8601-9.0.1.tar.gz", hash = "sha256:72e3117667eedf66951bb2d93f4296a56b94b078a8a95905a052611fb3f1b973", size = 47345, upload-time = "2021-03-02T01:33:22.944Z" }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/e3/04/e97c12dc034791d7b504860acfcdd2963fa21ae61eaca1c9d31245f812c3/aniso8601-9.0.1-py2.py3-none-any.whl", hash = "sha256:1d2b7ef82963909e93c4f24ce48d4de9e66009a21bf1c1e1c85bdd0812fe412f", size = 52754, upload-time = "2021-03-02T01:33:20.669Z" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "anyascii"
|
name = "anyascii"
|
||||||
version = "0.3.2"
|
version = "0.3.3"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/9f/52/93b9ea99063f7cf37fb67f5e3f49480686cbe7f228c48b9d713326223b6e/anyascii-0.3.2.tar.gz", hash = "sha256:9d5d32ef844fe225b8bc7cba7f950534fae4da27a9bf3a6bea2cb0ea46ce4730", size = 214052, upload-time = "2023-03-16T00:24:42.431Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/db/ba/edebda727008390936da4a9bf677c19cd63b32d51e864656d2cbd1028e25/anyascii-0.3.3.tar.gz", hash = "sha256:c94e9dd9d47b3d9494eca305fef9447d00b4bf1a32aff85aa746fa3ec7fb95c3", size = 264680, upload-time = "2025-06-29T03:33:30.427Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/4f/7b/a9a747e0632271d855da379532b05a62c58e979813814a57fa3b3afeb3a4/anyascii-0.3.2-py3-none-any.whl", hash = "sha256:3b3beef6fc43d9036d3b0529050b0c48bfad8bc960e9e562d7223cfb94fe45d4", size = 289923, upload-time = "2023-03-16T00:24:39.649Z" },
|
{ url = "https://files.pythonhosted.org/packages/c2/76/783b75a21ce3563b8709050de030ae253853b147bd52e141edc1025aa268/anyascii-0.3.3-py3-none-any.whl", hash = "sha256:f5ab5e53c8781a36b5a40e1296a0eeda2f48c649ef10c3921c1381b1d00dee7a", size = 345090, upload-time = "2025-06-29T03:33:28.356Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "asgiref"
|
name = "asgiref"
|
||||||
version = "3.8.1"
|
version = "3.9.1"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/29/38/b3395cc9ad1b56d2ddac9970bc8f4141312dbaec28bc7c218b0dfafd0f42/asgiref-3.8.1.tar.gz", hash = "sha256:c343bd80a0bec947a9860adb4c432ffa7db769836c64238fc34bdc3fec84d590", size = 35186, upload-time = "2024-03-22T14:39:36.863Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/90/61/0aa957eec22ff70b830b22ff91f825e70e1ef732c06666a805730f28b36b/asgiref-3.9.1.tar.gz", hash = "sha256:a5ab6582236218e5ef1648f242fd9f10626cfd4de8dc377db215d5d5098e3142", size = 36870, upload-time = "2025-07-08T09:07:43.344Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/39/e3/893e8757be2612e6c266d9bb58ad2e3651524b5b40cf56761e985a28b13e/asgiref-3.8.1-py3-none-any.whl", hash = "sha256:3e1e3ecc849832fe52ccf2cb6686b7a55f82bb1d6aee72a58826471390335e47", size = 23828, upload-time = "2024-03-22T14:39:34.521Z" },
|
{ url = "https://files.pythonhosted.org/packages/7c/3c/0464dcada90d5da0e71018c04a140ad6349558afb30b3051b4264cc5b965/asgiref-3.9.1-py3-none-any.whl", hash = "sha256:f3bba7092a48005b5f5bacd747d36ee4a5a61f4a269a6df590b43144355ebd2c", size = 23790, upload-time = "2025-07-08T09:07:41.548Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "beautifulsoup4"
|
name = "beautifulsoup4"
|
||||||
version = "4.12.3"
|
version = "4.13.4"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "soupsieve" },
|
{ name = "soupsieve" },
|
||||||
|
{ name = "typing-extensions" },
|
||||||
]
|
]
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/b3/ca/824b1195773ce6166d388573fc106ce56d4a805bd7427b624e063596ec58/beautifulsoup4-4.12.3.tar.gz", hash = "sha256:74e3d1928edc070d21748185c46e3fb33490f22f52a3addee9aee0f4f7781051", size = 581181, upload-time = "2024-01-17T16:53:17.902Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/d8/e4/0c4c39e18fd76d6a628d4dd8da40543d136ce2d1752bd6eeeab0791f4d6b/beautifulsoup4-4.13.4.tar.gz", hash = "sha256:dbb3c4e1ceae6aefebdaf2423247260cd062430a410e38c66f2baa50a8437195", size = 621067, upload-time = "2025-04-15T17:05:13.836Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/b1/fe/e8c672695b37eecc5cbf43e1d0638d88d66ba3a44c4d321c796f4e59167f/beautifulsoup4-4.12.3-py3-none-any.whl", hash = "sha256:b80878c9f40111313e55da8ba20bdba06d8fa3969fc68304167741bbf9e082ed", size = 147925, upload-time = "2024-01-17T16:53:12.779Z" },
|
{ url = "https://files.pythonhosted.org/packages/50/cd/30110dc0ffcf3b131156077b90e9f60ed75711223f306da4db08eff8403b/beautifulsoup4-4.13.4-py3-none-any.whl", hash = "sha256:9bbbb14bfde9d79f38b8cd5f8c7c85f4b8f2523190ebed90e950a8dea4cb1c4b", size = 187285, upload-time = "2025-04-15T17:05:12.221Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "certifi"
|
name = "certifi"
|
||||||
version = "2024.8.30"
|
version = "2025.8.3"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/b0/ee/9b19140fe824b367c04c5e1b369942dd754c4c5462d5674002f75c4dedc1/certifi-2024.8.30.tar.gz", hash = "sha256:bec941d2aa8195e248a60b31ff9f0558284cf01a52591ceda73ea9afffd69fd9", size = 168507, upload-time = "2024-08-30T01:55:04.365Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/dc/67/960ebe6bf230a96cda2e0abcf73af550ec4f090005363542f0765df162e0/certifi-2025.8.3.tar.gz", hash = "sha256:e564105f78ded564e3ae7c923924435e1daa7463faeab5bb932bc53ffae63407", size = 162386, upload-time = "2025-08-03T03:07:47.08Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/12/90/3c9ff0512038035f59d279fddeb79f5f1eccd8859f06d6163c58798b9487/certifi-2024.8.30-py3-none-any.whl", hash = "sha256:922820b53db7a7257ffbda3f597266d435245903d80737e34f8a45ff3e3230d8", size = 167321, upload-time = "2024-08-30T01:55:02.591Z" },
|
{ url = "https://files.pythonhosted.org/packages/e5/48/1549795ba7742c948d2ad169c1c8cdbae65bc450d6cd753d124b17c8cd32/certifi-2025.8.3-py3-none-any.whl", hash = "sha256:f6c12493cfb1b06ba2ff328595af9350c65d6644968e5d3a2ffd78699af217a5", size = 161216, upload-time = "2025-08-03T03:07:45.777Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "charset-normalizer"
|
name = "charset-normalizer"
|
||||||
version = "3.3.2"
|
version = "3.4.3"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/63/09/c1bc53dab74b1816a00d8d030de5bf98f724c52c1635e07681d312f20be8/charset-normalizer-3.3.2.tar.gz", hash = "sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5", size = 104809, upload-time = "2023-11-01T04:04:59.997Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/83/2d/5fd176ceb9b2fc619e63405525573493ca23441330fcdaee6bef9460e924/charset_normalizer-3.4.3.tar.gz", hash = "sha256:6fce4b8500244f6fcb71465d4a4930d132ba9ab8e71a7859e6a5d59851068d14", size = 122371, upload-time = "2025-08-09T07:57:28.46Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/d1/b2/fcedc8255ec42afee97f9e6f0145c734bbe104aac28300214593eb326f1d/charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:0b2b64d2bb6d3fb9112bafa732def486049e63de9618b5843bcdd081d8144cd8", size = 192892, upload-time = "2023-11-01T04:03:24.135Z" },
|
{ url = "https://files.pythonhosted.org/packages/8e/91/b5a06ad970ddc7a0e513112d40113e834638f4ca1120eb727a249fb2715e/charset_normalizer-3.4.3-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3cd35b7e8aedeb9e34c41385fda4f73ba609e561faedfae0a9e75e44ac558a15", size = 204342, upload-time = "2025-08-09T07:56:38.687Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/2e/7d/2259318c202f3d17f3fe6438149b3b9e706d1070fe3fcbb28049730bb25c/charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:ddbb2551d7e0102e7252db79ba445cdab71b26640817ab1e3e3648dad515003b", size = 122213, upload-time = "2023-11-01T04:03:25.66Z" },
|
{ url = "https://files.pythonhosted.org/packages/ce/ec/1edc30a377f0a02689342f214455c3f6c2fbedd896a1d2f856c002fc3062/charset_normalizer-3.4.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b89bc04de1d83006373429975f8ef9e7932534b8cc9ca582e4db7d20d91816db", size = 145995, upload-time = "2025-08-09T07:56:40.048Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/3a/52/9f9d17c3b54dc238de384c4cb5a2ef0e27985b42a0e5cc8e8a31d918d48d/charset_normalizer-3.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6", size = 119404, upload-time = "2023-11-01T04:03:27.04Z" },
|
{ url = "https://files.pythonhosted.org/packages/17/e5/5e67ab85e6d22b04641acb5399c8684f4d37caf7558a53859f0283a650e9/charset_normalizer-3.4.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2001a39612b241dae17b4687898843f254f8748b796a2e16f1051a17078d991d", size = 158640, upload-time = "2025-08-09T07:56:41.311Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/99/b0/9c365f6d79a9f0f3c379ddb40a256a67aa69c59609608fe7feb6235896e1/charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f4a014bc36d3c57402e2977dada34f9c12300af536839dc38c0beab8878f38a", size = 137275, upload-time = "2023-11-01T04:03:28.466Z" },
|
{ url = "https://files.pythonhosted.org/packages/f1/e5/38421987f6c697ee3722981289d554957c4be652f963d71c5e46a262e135/charset_normalizer-3.4.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8dcfc373f888e4fb39a7bc57e93e3b845e7f462dacc008d9749568b1c4ece096", size = 156636, upload-time = "2025-08-09T07:56:43.195Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/91/33/749df346e93d7a30cdcb90cbfdd41a06026317bfbfb62cd68307c1a3c543/charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a10af20b82360ab00827f916a6058451b723b4e65030c5a18577c8b2de5b3389", size = 147518, upload-time = "2023-11-01T04:03:29.82Z" },
|
{ url = "https://files.pythonhosted.org/packages/a0/e4/5a075de8daa3ec0745a9a3b54467e0c2967daaaf2cec04c845f73493e9a1/charset_normalizer-3.4.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:18b97b8404387b96cdbd30ad660f6407799126d26a39ca65729162fd810a99aa", size = 150939, upload-time = "2025-08-09T07:56:44.819Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/72/1a/641d5c9f59e6af4c7b53da463d07600a695b9824e20849cb6eea8a627761/charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8d756e44e94489e49571086ef83b2bb8ce311e730092d2c34ca8f7d925cb20aa", size = 140182, upload-time = "2023-11-01T04:03:31.511Z" },
|
{ url = "https://files.pythonhosted.org/packages/02/f7/3611b32318b30974131db62b4043f335861d4d9b49adc6d57c1149cc49d4/charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ccf600859c183d70eb47e05a44cd80a4ce77394d1ac0f79dbd2dd90a69a3a049", size = 148580, upload-time = "2025-08-09T07:56:46.684Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/ee/fb/14d30eb4956408ee3ae09ad34299131fb383c47df355ddb428a7331cfa1e/charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b", size = 141869, upload-time = "2023-11-01T04:03:32.887Z" },
|
{ url = "https://files.pythonhosted.org/packages/7e/61/19b36f4bd67f2793ab6a99b979b4e4f3d8fc754cbdffb805335df4337126/charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:53cd68b185d98dde4ad8990e56a58dea83a4162161b1ea9272e5c9182ce415e0", size = 159870, upload-time = "2025-08-09T07:56:47.941Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/df/3e/a06b18788ca2eb6695c9b22325b6fde7dde0f1d1838b1792a0076f58fe9d/charset_normalizer-3.3.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6ac7ffc7ad6d040517be39eb591cac5ff87416c2537df6ba3cba3bae290c0fed", size = 144042, upload-time = "2023-11-01T04:03:34.412Z" },
|
{ url = "https://files.pythonhosted.org/packages/06/57/84722eefdd338c04cf3030ada66889298eaedf3e7a30a624201e0cbe424a/charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:30a96e1e1f865f78b030d65241c1ee850cdf422d869e9028e2fc1d5e4db73b92", size = 157797, upload-time = "2025-08-09T07:56:49.756Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/45/59/3d27019d3b447a88fe7e7d004a1e04be220227760264cc41b405e863891b/charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:7ed9e526742851e8d5cc9e6cf41427dfc6068d4f5a3bb03659444b4cabf6bc26", size = 138275, upload-time = "2023-11-01T04:03:35.759Z" },
|
{ url = "https://files.pythonhosted.org/packages/72/2a/aff5dd112b2f14bcc3462c312dce5445806bfc8ab3a7328555da95330e4b/charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d716a916938e03231e86e43782ca7878fb602a125a91e7acb8b5112e2e96ac16", size = 152224, upload-time = "2025-08-09T07:56:51.369Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/7b/ef/5eb105530b4da8ae37d506ccfa25057961b7b63d581def6f99165ea89c7e/charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:8bdb58ff7ba23002a4c5808d608e4e6c687175724f54a5dade5fa8c67b604e4d", size = 144819, upload-time = "2023-11-01T04:03:37.216Z" },
|
{ url = "https://files.pythonhosted.org/packages/b7/8c/9839225320046ed279c6e839d51f028342eb77c91c89b8ef2549f951f3ec/charset_normalizer-3.4.3-cp314-cp314-win32.whl", hash = "sha256:c6dbd0ccdda3a2ba7c2ecd9d77b37f3b5831687d8dc1b6ca5f56a4880cc7b7ce", size = 100086, upload-time = "2025-08-09T07:56:52.722Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/a2/51/e5023f937d7f307c948ed3e5c29c4b7a3e42ed2ee0b8cdf8f3a706089bf0/charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:6b3251890fff30ee142c44144871185dbe13b11bab478a88887a639655be1068", size = 149415, upload-time = "2023-11-01T04:03:38.694Z" },
|
{ url = "https://files.pythonhosted.org/packages/ee/7a/36fbcf646e41f710ce0a563c1c9a343c6edf9be80786edeb15b6f62e17db/charset_normalizer-3.4.3-cp314-cp314-win_amd64.whl", hash = "sha256:73dc19b562516fc9bcf6e5d6e596df0b4eb98d87e4f79f3ae71840e6ed21361c", size = 107400, upload-time = "2025-08-09T07:56:55.172Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/24/9d/2e3ef673dfd5be0154b20363c5cdcc5606f35666544381bee15af3778239/charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:b4a23f61ce87adf89be746c8a8974fe1c823c891d8f86eb218bb957c924bb143", size = 141212, upload-time = "2023-11-01T04:03:40.07Z" },
|
{ 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" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/5b/ae/ce2c12fcac59cb3860b2e2d76dc405253a4475436b1861d95fe75bdea520/charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:efcb3f6676480691518c177e3b465bcddf57cea040302f9f4e6e191af91174d4", size = 142167, upload-time = "2023-11-01T04:03:41.491Z" },
|
]
|
||||||
{ url = "https://files.pythonhosted.org/packages/ed/3a/a448bf035dce5da359daf9ae8a16b8a39623cc395a2ffb1620aa1bce62b0/charset_normalizer-3.3.2-cp312-cp312-win32.whl", hash = "sha256:d965bba47ddeec8cd560687584e88cf699fd28f192ceb452d1d7ee807c5597b7", size = 93041, upload-time = "2023-11-01T04:03:42.836Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/b6/7c/8debebb4f90174074b827c63242c23851bdf00a532489fba57fef3416e40/charset_normalizer-3.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:96b02a3dc4381e5494fad39be677abcb5e6634bf7b4fa83a6dd3112607547001", size = 100397, upload-time = "2023-11-01T04:03:44.467Z" },
|
[[package]]
|
||||||
{ url = "https://files.pythonhosted.org/packages/28/76/e6222113b83e3622caa4bb41032d0b1bf785250607392e1b778aca0b8a7d/charset_normalizer-3.3.2-py3-none-any.whl", hash = "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc", size = 48543, upload-time = "2023-11-01T04:04:58.622Z" },
|
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]]
|
[[package]]
|
||||||
@@ -85,53 +121,52 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "django"
|
name = "django"
|
||||||
version = "5.1"
|
version = "6.0.5"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "asgiref" },
|
{ name = "asgiref" },
|
||||||
{ name = "sqlparse" },
|
{ name = "sqlparse" },
|
||||||
{ name = "tzdata", marker = "sys_platform == 'win32'" },
|
{ name = "tzdata", marker = "sys_platform == 'win32'" },
|
||||||
]
|
]
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/1e/0c/d854d25bb74a8a3b41e642bbd27fe6af12fadd0edfd07d487809cf0ef719/Django-5.1.tar.gz", hash = "sha256:848a5980e8efb76eea70872fb0e4bc5e371619c70fffbe48e3e1b50b2c09455d", size = 10681050, upload-time = "2024-08-07T13:34:10.857Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/5e/f1/bf85f0d29ef76abf901f193fe8fef4769d3da7794197832bc30151c071d8/django-6.0.5.tar.gz", hash = "sha256:bc6d6872e98a2864c836e42edd644b362db311147dd5aa8d5b82ba7a032f5269", size = 10924131, upload-time = "2026-05-05T13:54:39.329Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/28/b4/110532cebfea2244d76119904da98c6fa045ebb202aee9ec7cbf36ea3cad/Django-5.1-py3-none-any.whl", hash = "sha256:d3b811bf5371a26def053d7ee42a9df1267ef7622323fe70a601936725aa4557", size = 8246099, upload-time = "2024-08-07T13:33:52.959Z" },
|
{ url = "https://files.pythonhosted.org/packages/94/5b/1328f8b84fce040c404f76822bf8c57d254e368e8cbd8bd67ec2b26d75f5/django-6.0.5-py3-none-any.whl", hash = "sha256:9d58a7cb49244e74c8e161d5e403a46d6209f1009ba40f5a66d6aa0d0786a8f0", size = 8368680, upload-time = "2026-05-05T13:54:33.532Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "django-extensions"
|
name = "django-extensions"
|
||||||
version = "3.2.3"
|
version = "4.1"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "django" },
|
{ name = "django" },
|
||||||
]
|
]
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/8a/f1/318684c9466968bf9a9c221663128206e460c1a67f595055be4b284cde8a/django-extensions-3.2.3.tar.gz", hash = "sha256:44d27919d04e23b3f40231c4ab7af4e61ce832ef46d610cc650d53e68328410a", size = 277216, upload-time = "2023-06-05T17:09:01.447Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/6d/b3/ed0f54ed706ec0b54fd251cc0364a249c6cd6c6ec97f04dc34be5e929eac/django_extensions-4.1.tar.gz", hash = "sha256:7b70a4d28e9b840f44694e3f7feb54f55d495f8b3fa6c5c0e5e12bcb2aa3cdeb", size = 283078, upload-time = "2025-04-11T01:15:39.617Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/a7/7e/ba12b9660642663f5273141018d2bec0a1cae1711f4f6d1093920e157946/django_extensions-3.2.3-py3-none-any.whl", hash = "sha256:9600b7562f79a92cbf1fde6403c04fee314608fefbb595502e34383ae8203401", size = 229868, upload-time = "2023-06-05T17:08:58.197Z" },
|
{ url = "https://files.pythonhosted.org/packages/64/96/d967ca440d6a8e3861120f51985d8e5aec79b9a8bdda16041206adfe7adc/django_extensions-4.1-py3-none-any.whl", hash = "sha256:0699a7af28f2523bf8db309a80278519362cd4b6e1fd0a8cd4bf063e1e023336", size = 232980, upload-time = "2025-04-11T01:15:37.701Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "django-filter"
|
name = "django-filter"
|
||||||
version = "24.3"
|
version = "25.1"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "django" },
|
{ name = "django" },
|
||||||
]
|
]
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/50/bc/dc19ae39c235332926dd0efe0951f663fa1a9fc6be8430737ff7fd566b20/django_filter-24.3.tar.gz", hash = "sha256:d8ccaf6732afd21ca0542f6733b11591030fa98669f8d15599b358e24a2cd9c3", size = 144444, upload-time = "2024-08-02T13:27:58.132Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/b5/40/c702a6fe8cccac9bf426b55724ebdf57d10a132bae80a17691d0cf0b9bac/django_filter-25.1.tar.gz", hash = "sha256:1ec9eef48fa8da1c0ac9b411744b16c3f4c31176c867886e4c48da369c407153", size = 143021, upload-time = "2025-02-14T16:30:53.238Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/09/b1/92f1c30b47c1ebf510c35a2ccad9448f73437e5891bbd2b4febe357cc3de/django_filter-24.3-py3-none-any.whl", hash = "sha256:c4852822928ce17fb699bcfccd644b3574f1a2d80aeb2b4ff4f16b02dd49dc64", size = 95011, upload-time = "2024-08-02T13:27:55.616Z" },
|
{ url = "https://files.pythonhosted.org/packages/07/a6/70dcd68537c434ba7cb9277d403c5c829caf04f35baf5eb9458be251e382/django_filter-25.1-py3-none-any.whl", hash = "sha256:4fa48677cf5857b9b1347fed23e355ea792464e0fe07244d1fdfb8a806215b80", size = 94114, upload-time = "2025-02-14T16:30:50.435Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "django-modelcluster"
|
name = "django-modelcluster"
|
||||||
version = "6.3"
|
version = "6.5"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "django" },
|
{ name = "django" },
|
||||||
{ name = "pytz" },
|
|
||||||
]
|
]
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/3d/dd/db1ab2e256f9fda6796ac312686561cbe2740d960c920f74410559ef7739/django-modelcluster-6.3.tar.gz", hash = "sha256:0caed8a0e889f3abb92f144670878a466ef954ffa6c4c7b9c80e6426b720a49d", size = 28903, upload-time = "2024-02-26T19:26:53.185Z" }
|
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 = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/ee/6d/8deb2dfde2e1177412b759a1c32713be19b1d3af1da686e44e64c165e8c0/django_modelcluster-6.3-py2.py3-none-any.whl", hash = "sha256:a8783d6565a0663f41cd6003ea361c3a5711e8a2a326160f1ec1eceb3e973d4f", size = 29039, upload-time = "2024-02-26T19:26:50.903Z" },
|
{ 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]]
|
[[package]]
|
||||||
@@ -146,40 +181,67 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/e6/5b/216157ff053f955b15b9dcdc13bfb6e406666445164fee9e332e141be96d/django_permissionedforms-0.1-py2.py3-none-any.whl", hash = "sha256:d341a961a27cc77fde8cc42141c6ab55cc1f0cb886963cc2d6967b9674fa47d6", size = 5744, upload-time = "2022-02-28T19:40:26.337Z" },
|
{ url = "https://files.pythonhosted.org/packages/e6/5b/216157ff053f955b15b9dcdc13bfb6e406666445164fee9e332e141be96d/django_permissionedforms-0.1-py2.py3-none-any.whl", hash = "sha256:d341a961a27cc77fde8cc42141c6ab55cc1f0cb886963cc2d6967b9674fa47d6", size = 5744, upload-time = "2022-02-28T19:40:26.337Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "django-stubs-ext"
|
||||||
|
version = "5.2.2"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "django" },
|
||||||
|
{ name = "typing-extensions" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/fc/06/5e94715d103e6cc72380cb0d0b6682a7d5ad2c366cee478c94d77aad777d/django_stubs_ext-5.2.2.tar.gz", hash = "sha256:d9d151b919fe2438760f5bd938f03e1cb08c84d0651f9e5917f1313907e42683", size = 6244, upload-time = "2025-07-17T08:34:35.054Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/4e/38/2903676f97f7902ee31984a06756b0e8836e897f4b617e1a03be4a43eb4f/django_stubs_ext-5.2.2-py3-none-any.whl", hash = "sha256:8833bbe32405a2a0ce168d3f75a87168f61bd16939caf0e8bf173bccbd8a44c5", size = 8816, upload-time = "2025-07-17T08:34:33.715Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "django-taggit"
|
name = "django-taggit"
|
||||||
version = "5.0.1"
|
version = "6.1.0"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "django" },
|
{ name = "django" },
|
||||||
]
|
]
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/10/eb/269501702e231b552f68d813d0a07ac1ea81fd25a3c92fb0f249818f0f39/django-taggit-5.0.1.tar.gz", hash = "sha256:edcd7db1e0f35c304e082a2f631ddac2e16ef5296029524eb792af7430cab4cc", size = 60372, upload-time = "2023-10-29T11:57:20.124Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/34/a6/f1beaf8f552fe90c153cc039316ebab942c23dfbc88588dde081fefca816/django_taggit-6.1.0.tar.gz", hash = "sha256:c4d1199e6df34125dd36db5eb0efe545b254dec3980ce5dd80e6bab3e78757c3", size = 38151, upload-time = "2024-09-29T08:07:39.477Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/07/18/94a45c8c50592d5cf7d73a9111053917c3684fda031c988041396bb3e588/django_taggit-5.0.1-py3-none-any.whl", hash = "sha256:a0ca8a28b03c4b26c2630fd762cb76ec39b5e41abf727a7b66f897a625c5e647", size = 61141, upload-time = "2023-10-29T11:56:57.954Z" },
|
{ url = "https://files.pythonhosted.org/packages/6b/34/4185c345530b91d05cb82e05d07148f481a5eb5dc2ac44e092b3daa6f206/django_taggit-6.1.0-py3-none-any.whl", hash = "sha256:ab776264bbc76cb3d7e49e1bf9054962457831bd21c3a42db9138b41956e4cf0", size = 75749, upload-time = "2024-09-29T08:07:14.612Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "django-tasks"
|
||||||
|
version = "0.9.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "django" },
|
||||||
|
{ name = "django-stubs-ext" },
|
||||||
|
{ name = "typing-extensions" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/60/b1/064645bf246a1f5b46d9638755b1869ea44a6d05e7c7c12841fddebb71f6/django_tasks-0.9.0.tar.gz", hash = "sha256:971b3829efeee68147f7deced8d21b907131b11ec7953af83eb94b11f128a24d", size = 32343, upload-time = "2025-10-17T16:21:08.58Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/71/e1/ce539ce1e21be71696649f84b6dbd508b2c0b89b559a6e620478bb126f7c/django_tasks-0.9.0-py3-none-any.whl", hash = "sha256:fddc344934a605d9eafa08ac8ba32c0cde9da23ef534e03a41f09fa0417b535a", size = 44057, upload-time = "2025-10-17T16:21:07.283Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "django-treebeard"
|
name = "django-treebeard"
|
||||||
version = "4.7.1"
|
version = "5.1.0"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "django" },
|
{ 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 = [
|
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]]
|
[[package]]
|
||||||
name = "djangorestframework"
|
name = "djangorestframework"
|
||||||
version = "3.15.2"
|
version = "3.16.1"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "django" },
|
{ name = "django" },
|
||||||
]
|
]
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/2c/ce/31482eb688bdb4e271027076199e1aa8d02507e530b6d272ab8b4481557c/djangorestframework-3.15.2.tar.gz", hash = "sha256:36fe88cd2d6c6bec23dca9804bab2ba5517a8bb9d8f47ebc68981b56840107ad", size = 1067420, upload-time = "2024-06-19T07:59:32.891Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/8a/95/5376fe618646fde6899b3cdc85fd959716bb67542e273a76a80d9f326f27/djangorestframework-3.16.1.tar.gz", hash = "sha256:166809528b1aced0a17dc66c24492af18049f2c9420dbd0be29422029cfc3ff7", size = 1089735, upload-time = "2025-08-06T17:50:53.251Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/7c/b6/fa99d8f05eff3a9310286ae84c4059b08c301ae4ab33ae32e46e8ef76491/djangorestframework-3.15.2-py3-none-any.whl", hash = "sha256:2b8871b062ba1aefc2de01f773875441a961fefbf79f5eed1e32b2f096944b20", size = 1071235, upload-time = "2024-06-19T07:59:26.106Z" },
|
{ url = "https://files.pythonhosted.org/packages/b0/ce/bf8b9d3f415be4ac5588545b5fcdbbb841977db1c1d923f7568eeabe1689/djangorestframework-3.16.1-py3-none-any.whl", hash = "sha256:33a59f47fb9c85ede792cbf88bde71893bcda0667bc573f784649521f1102cec", size = 1080442, upload-time = "2025-08-06T17:50:50.667Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -193,44 +255,80 @@ dependencies = [
|
|||||||
{ name = "psycopg2-binary" },
|
{ name = "psycopg2-binary" },
|
||||||
{ name = "wagtail" },
|
{ name = "wagtail" },
|
||||||
{ name = "wagtail-grapple" },
|
{ name = "wagtail-grapple" },
|
||||||
|
{ name = "wagtail-headless-preview" },
|
||||||
{ name = "whitenoise" },
|
{ name = "whitenoise" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.dev-dependencies]
|
[package.dev-dependencies]
|
||||||
dev = [
|
dev = [
|
||||||
|
{ name = "pytest" },
|
||||||
|
{ name = "pytest-cov" },
|
||||||
|
{ name = "pytest-django" },
|
||||||
{ name = "ruff" },
|
{ name = "ruff" },
|
||||||
|
{ name = "wagtail-factories" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.metadata]
|
[package.metadata]
|
||||||
requires-dist = [
|
requires-dist = [
|
||||||
{ name = "django", specifier = ">=5.0.7,<6" },
|
{ name = "django", specifier = ">=6.0.5,<7" },
|
||||||
{ name = "django-extensions", specifier = ">=3.2.3,<4" },
|
{ name = "django-extensions", specifier = ">=4.1,<5" },
|
||||||
{ name = "gunicorn", specifier = ">=23.0.0" },
|
{ name = "gunicorn", specifier = ">=26.0.0,<27" },
|
||||||
{ name = "psycopg2-binary", specifier = ">=2.9.10,<3" },
|
{ name = "psycopg2-binary", specifier = ">=2.9.12,<3" },
|
||||||
{ name = "wagtail", specifier = ">=6.1.3,<7" },
|
{ name = "wagtail", specifier = ">=7.4.1,<8" },
|
||||||
{ name = "wagtail-grapple", specifier = ">=0.26.0,<0.27" },
|
{ name = "wagtail-grapple", specifier = ">=0.31.0,<0.32" },
|
||||||
{ name = "whitenoise", specifier = ">=6.7.0,<7" },
|
{ name = "wagtail-headless-preview", specifier = ">=0.8,<0.9" },
|
||||||
|
{ name = "whitenoise", specifier = ">=6.12.0,<7" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.metadata.requires-dev]
|
[package.metadata.requires-dev]
|
||||||
dev = [{ name = "ruff" }]
|
dev = [
|
||||||
|
{ name = "pytest", specifier = ">=9.0.3,<10" },
|
||||||
|
{ name = "pytest-cov", specifier = ">=7.0.0,<8" },
|
||||||
|
{ name = "pytest-django", specifier = ">=4.12.0,<5" },
|
||||||
|
{ name = "ruff", specifier = ">=0.15.13,<0.16" },
|
||||||
|
{ name = "wagtail-factories", specifier = ">=4.4.0,<5" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "draftjs-exporter"
|
name = "draftjs-exporter"
|
||||||
version = "5.0.0"
|
version = "5.1.0"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/29/5b/7729b1a95d48d2f51b03feacda1d5b17058e1caec524592e59d0df801a8c/draftjs_exporter-5.0.0.tar.gz", hash = "sha256:2efee45d4bb4c0aaacc3e5ea2983a29a29381e02037f3f92a6b12706d7b87e1e", size = 33271, upload-time = "2022-04-03T22:00:47.094Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/d1/52/8b98525ab5477410bdbaf279c5fe0a99108211d40818bb769460784c1c41/draftjs_exporter-5.1.0.tar.gz", hash = "sha256:9f44b8dcecb702540e3aab24af2fad8683aec910fe0034c12cfab5d716ac5f84", size = 33500, upload-time = "2025-02-21T17:32:57.175Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/58/f4/95ad9b91194156f0e87877d640428b1c7be70a90979796ecdcb77adcfb4f/draftjs_exporter-5.0.0-py3-none-any.whl", hash = "sha256:8cb9d2d51284233decfe274802f1c53e257158c62b9f53ed2399de3fa80ac561", size = 26320, upload-time = "2022-04-03T22:00:44.647Z" },
|
{ url = "https://files.pythonhosted.org/packages/dc/99/26d5524aaa3e89266e0af19332053aa9f8a61b1c39b29c0dc709f43fbb29/draftjs_exporter-5.1.0-py3-none-any.whl", hash = "sha256:c32932b7933b994fd5ea74c1decf47b5c41e13ba06363f5b69b76ef10137d4e9", size = 26302, upload-time = "2025-02-21T17:32:54.4Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "et-xmlfile"
|
name = "et-xmlfile"
|
||||||
version = "1.1.0"
|
version = "2.0.0"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/3d/5d/0413a31d184a20c763ad741cc7852a659bf15094c24840c5bdd1754765cd/et_xmlfile-1.1.0.tar.gz", hash = "sha256:8eb9e2bc2f8c97e37a2dc85a09ecdcdec9d8a396530a6d5a33b30b9a92da0c5c", size = 3218, upload-time = "2021-04-26T13:26:05.068Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/d3/38/af70d7ab1ae9d4da450eeec1fa3918940a5fafb9055e934af8d6eb0c2313/et_xmlfile-2.0.0.tar.gz", hash = "sha256:dab3f4764309081ce75662649be815c4c9081e88f0837825f90fd28317d4da54", size = 17234, upload-time = "2024-10-25T17:25:40.039Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/96/c2/3dd434b0108730014f1b96fd286040dc3bcb70066346f7e01ec2ac95865f/et_xmlfile-1.1.0-py3-none-any.whl", hash = "sha256:a2ba85d1d6a74ef63837eed693bcb89c3f752169b0e3e7ae5b16ca5e1b3deada", size = 4688, upload-time = "2021-04-26T13:26:03.429Z" },
|
{ 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]]
|
[[package]]
|
||||||
@@ -244,21 +342,22 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "graphene"
|
name = "graphene"
|
||||||
version = "3.3"
|
version = "3.4.3"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "aniso8601" },
|
|
||||||
{ name = "graphql-core" },
|
{ name = "graphql-core" },
|
||||||
{ name = "graphql-relay" },
|
{ name = "graphql-relay" },
|
||||||
|
{ name = "python-dateutil" },
|
||||||
|
{ name = "typing-extensions" },
|
||||||
]
|
]
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/82/75/02875858c7c09fc156840181cdee27b23408fac75720a2e1e9128f3d48a5/graphene-3.3.tar.gz", hash = "sha256:529bf40c2a698954217d3713c6041d69d3f719ad0080857d7ee31327112446b0", size = 57893, upload-time = "2023-07-26T06:48:39.083Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/cc/f6/bf62ff950c317ed03e77f3f6ddd7e34aaa98fe89d79ebd660c55343d8054/graphene-3.4.3.tar.gz", hash = "sha256:2a3786948ce75fe7e078443d37f609cbe5bb36ad8d6b828740ad3b95ed1a0aaa", size = 44739, upload-time = "2024-11-09T20:44:25.757Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/24/70/96f6027cdfc9bb89fc07627b615cb43fb1c443c93498412beaeaf157e9f1/graphene-3.3-py2.py3-none-any.whl", hash = "sha256:bb3810be33b54cb3e6969506671eb72319e8d7ba0d5ca9c8066472f75bf35a38", size = 128227, upload-time = "2023-07-26T06:48:37.766Z" },
|
{ url = "https://files.pythonhosted.org/packages/66/e0/61d8e98007182e6b2aca7cf65904721fb2e4bce0192272ab9cb6f69d8812/graphene-3.4.3-py2.py3-none-any.whl", hash = "sha256:820db6289754c181007a150db1f7fff544b94142b556d12e3ebc777a7bf36c71", size = 114894, upload-time = "2024-11-09T20:44:23.851Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "graphene-django"
|
name = "graphene-django"
|
||||||
version = "3.2.2"
|
version = "3.2.3"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "django" },
|
{ name = "django" },
|
||||||
@@ -268,18 +367,18 @@ dependencies = [
|
|||||||
{ name = "promise" },
|
{ name = "promise" },
|
||||||
{ name = "text-unidecode" },
|
{ name = "text-unidecode" },
|
||||||
]
|
]
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/09/f1/4eb1c0ae79814aba03e2347f664cb0bef72816fbe99d4399378bb22bef47/graphene-django-3.2.2.tar.gz", hash = "sha256:059ccf25d9a5159f28d7ebf1a648c993ab34deb064e80b70ca096aa22a609556", size = 87591, upload-time = "2024-06-12T02:58:09.439Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/6f/7a/8aef131349329dcd167578720f6364412f1728bfda14bba22c1e7b5d8365/graphene-django-3.2.3.tar.gz", hash = "sha256:d831bfe8e9a6e77e477b7854faef4addb318f386119a69ee4c57b74560f3e07d", size = 88393, upload-time = "2025-03-13T08:33:03.949Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/da/71/6800d05144ee4fb57f471571dd80fc9c6b2196e6243d39ed3d3654082824/graphene_django-3.2.2-py2.py3-none-any.whl", hash = "sha256:0fd95c8c1cbe77ae2a5940045ce276803c3acbf200a156731e0c730f2776ae2c", size = 114204, upload-time = "2024-06-12T02:58:07.897Z" },
|
{ url = "https://files.pythonhosted.org/packages/18/35/ab9c668222f6271a0b71efb147c46816229dbb9fa5d15ee6b025eb08d4b2/graphene_django-3.2.3-py2.py3-none-any.whl", hash = "sha256:0c673a4dad315b26b4d18eb379ad0c7027fd6a36d23a1848b7c7c09a14a9271e", size = 114959, upload-time = "2025-03-13T08:33:02.453Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "graphql-core"
|
name = "graphql-core"
|
||||||
version = "3.2.3"
|
version = "3.2.6"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/ee/a6/94df9045ca1bac404c7b394094cd06713f63f49c7a4d54d99b773ae81737/graphql-core-3.2.3.tar.gz", hash = "sha256:06d2aad0ac723e35b1cb47885d3e5c45e956a53bc1b209a9fc5369007fe46676", size = 529552, upload-time = "2022-09-23T08:37:13.684Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/c4/16/7574029da84834349b60ed71614d66ca3afe46e9bf9c7b9562102acb7d4f/graphql_core-3.2.6.tar.gz", hash = "sha256:c08eec22f9e40f0bd61d805907e3b3b1b9a320bc606e23dc145eebca07c8fbab", size = 505353, upload-time = "2025-01-26T16:36:27.374Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/f8/39/e5143e7ec70939d2076c1165ae9d4a3815597019c4d797b7f959cf778600/graphql_core-3.2.3-py3-none-any.whl", hash = "sha256:5766780452bd5ec8ba133f8bf287dc92713e3868ddd83aee4faab9fc3e303dc3", size = 202921, upload-time = "2022-09-23T08:37:11.825Z" },
|
{ url = "https://files.pythonhosted.org/packages/ae/4f/7297663840621022bc73c22d7d9d80dbc78b4db6297f764b545cd5dd462d/graphql_core-3.2.6-py3-none-any.whl", hash = "sha256:78b016718c161a6fb20a7d97bbf107f331cd1afe53e45566c59f776ed7f0b45f", size = 203416, upload-time = "2025-01-26T16:36:24.868Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -296,48 +395,57 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "gunicorn"
|
name = "gunicorn"
|
||||||
version = "23.0.0"
|
version = "26.0.0"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "packaging" },
|
{ name = "packaging" },
|
||||||
]
|
]
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/34/72/9614c465dc206155d93eff0ca20d42e1e35afc533971379482de953521a4/gunicorn-23.0.0.tar.gz", hash = "sha256:f014447a0101dc57e294f6c18ca6b40227a4c90e9bdb586042628030cba004ec", size = 375031, upload-time = "2024-08-10T20:25:27.378Z" }
|
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 = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/cb/7d/6dac2a6e1eba33ee43f318edbed4ff29151a49b5d37f080aad1e6469bca4/gunicorn-23.0.0-py3-none-any.whl", hash = "sha256:ec400d38950de4dfd418cff8328b2c8faed0edb0d517d3394e457c317908ca4d", size = 85029, upload-time = "2024-08-10T20:25:24.996Z" },
|
{ 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]]
|
[[package]]
|
||||||
name = "idna"
|
name = "idna"
|
||||||
version = "3.8"
|
version = "3.10"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/e8/ac/e349c5e6d4543326c6883ee9491e3921e0d07b55fdf3cce184b40d63e72a/idna-3.8.tar.gz", hash = "sha256:d838c2c0ed6fced7693d5e8ab8e734d5f8fda53a039c0164afb0b82e771e3603", size = 189467, upload-time = "2024-08-23T16:01:51.339Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490, upload-time = "2024-09-15T18:07:39.745Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/22/7e/d71db821f177828df9dea8c42ac46473366f191be53080e552e628aad991/idna-3.8-py3-none-any.whl", hash = "sha256:050b4e5baadcd44d760cedbd2b8e639f2ff89bbc7a5730fcc662954303377aac", size = 66894, upload-time = "2024-08-23T16:01:49.963Z" },
|
{ 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]]
|
[[package]]
|
||||||
name = "l18n"
|
name = "iniconfig"
|
||||||
version = "2021.3"
|
version = "2.3.0"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
dependencies = [
|
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" }
|
||||||
{ name = "pytz" },
|
|
||||||
{ name = "six" },
|
|
||||||
]
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/ff/9c/13d732e16e8fbdef7e68f4f339f3b86618430d4003c7ffe82e15bf925d7c/l18n-2021.3.tar.gz", hash = "sha256:1956e890d673d17135cc20913253c154f6bc1c00266c22b7d503cc1a5a42d848", size = 50712, upload-time = "2021-11-12T09:32:36.255Z" }
|
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/6c/e7/dfa82d0bb2b314950e457a755463b429090127ffc0ab9d8d14ef4563ad44/l18n-2021.3-py3-none-any.whl", hash = "sha256:78495d1df95b6f7dcc694d1ba8994df709c463a1cbac1bf016e1b9a5ce7280b9", size = 51534, upload-time = "2021-11-12T09:32:34.296Z" },
|
{ 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]]
|
[[package]]
|
||||||
name = "laces"
|
name = "laces"
|
||||||
version = "0.1.1"
|
version = "0.1.2"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "django" },
|
{ name = "django" },
|
||||||
]
|
]
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/a2/cb/d69f2cbb248bd6baceb7597d8c6c229797689b546ac2a446fac15b8f455f/laces-0.1.1.tar.gz", hash = "sha256:e45159c46f6adca33010d34e9af869e57201b70675c6dc088e919b16c89456a4", size = 26889, upload-time = "2024-02-10T23:34:22.055Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/74/9a/9192d6a74e2c6db4f705dd98f56be488e47373172c13f4916aeabc4d68b8/laces-0.1.2.tar.gz", hash = "sha256:3218e09c1889ae5cf3fc7a82f5bb63ec0c7879889b6a9760bfc42323c694b84d", size = 29264, upload-time = "2025-01-14T04:37:34.805Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/e0/dc/ceadbdc5e14aec7bd01bdd6ef8d5bf704d6a12d1b5a25bcaaa066bdb820d/laces-0.1.1-py3-none-any.whl", hash = "sha256:ae2c575b9aaa46154e5518c61c9f86f5a9478f753a51e9c5547c7d275d361242", size = 21349, upload-time = "2024-02-10T23:34:20.37Z" },
|
{ url = "https://files.pythonhosted.org/packages/60/fe/31f76f5cb2579bdda208aa257ce5482653f22ab1bad3e128fe2f803fa2f1/laces-0.1.2-py3-none-any.whl", hash = "sha256:980cdaf9a31e883a2b8198132e2388931a4eb8814f5bfa5d8bba13ff9f657b7c", size = 22462, upload-time = "2025-01-14T04:37:30.636Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "modelsearch"
|
||||||
|
version = "1.3.1"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "django" },
|
||||||
|
{ name = "django-tasks" },
|
||||||
|
]
|
||||||
|
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/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]]
|
[[package]]
|
||||||
@@ -363,39 +471,59 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pillow"
|
name = "pillow"
|
||||||
version = "10.4.0"
|
version = "11.3.0"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/cd/74/ad3d526f3bf7b6d3f408b73fde271ec69dfac8b81341a318ce825f2b3812/pillow-10.4.0.tar.gz", hash = "sha256:166c1cd4d24309b30d61f79f4a9114b7b2313d7450912277855ff5dfd7cd4a06", size = 46555059, upload-time = "2024-07-01T09:48:43.583Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/f3/0d/d0d6dea55cd152ce3d6767bb38a8fc10e33796ba4ba210cbab9354b6d238/pillow-11.3.0.tar.gz", hash = "sha256:3828ee7586cd0b2091b6209e5ad53e20d0649bbe87164a459d0676e035e8f523", size = 47113069, upload-time = "2025-07-01T09:16:30.666Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/05/cb/0353013dc30c02a8be34eb91d25e4e4cf594b59e5a55ea1128fde1e5f8ea/pillow-10.4.0-cp312-cp312-macosx_10_10_x86_64.whl", hash = "sha256:673655af3eadf4df6b5457033f086e90299fdd7a47983a13827acf7459c15d94", size = 3509350, upload-time = "2024-07-01T09:46:17.177Z" },
|
{ url = "https://files.pythonhosted.org/packages/73/f4/04905af42837292ed86cb1b1dabe03dce1edc008ef14c473c5c7e1443c5d/pillow-11.3.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:d9da3df5f9ea2a89b81bb6087177fb1f4d1c7146d583a3fe5c672c0d94e55e12", size = 5278520, upload-time = "2025-07-01T09:15:17.429Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/e7/cf/5c558a0f247e0bf9cec92bff9b46ae6474dd736f6d906315e60e4075f737/pillow-10.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:866b6942a92f56300012f5fbac71f2d610312ee65e22f1aa2609e491284e5597", size = 3374980, upload-time = "2024-07-01T09:46:19.169Z" },
|
{ url = "https://files.pythonhosted.org/packages/41/b0/33d79e377a336247df6348a54e6d2a2b85d644ca202555e3faa0cf811ecc/pillow-11.3.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0b275ff9b04df7b640c59ec5a3cb113eefd3795a8df80bac69646ef699c6981a", size = 4686116, upload-time = "2025-07-01T09:15:19.423Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/84/48/6e394b86369a4eb68b8a1382c78dc092245af517385c086c5094e3b34428/pillow-10.4.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:29dbdc4207642ea6aad70fbde1a9338753d33fb23ed6956e706936706f52dd80", size = 4343799, upload-time = "2024-07-01T09:46:21.883Z" },
|
{ url = "https://files.pythonhosted.org/packages/49/2d/ed8bc0ab219ae8768f529597d9509d184fe8a6c4741a6864fea334d25f3f/pillow-11.3.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0743841cabd3dba6a83f38a92672cccbd69af56e3e91777b0ee7f4dba4385632", size = 5864597, upload-time = "2025-07-03T13:10:38.404Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/3b/f3/a8c6c11fa84b59b9df0cd5694492da8c039a24cd159f0f6918690105c3be/pillow-10.4.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bf2342ac639c4cf38799a44950bbc2dfcb685f052b9e262f446482afaf4bffca", size = 4459973, upload-time = "2024-07-01T09:46:24.321Z" },
|
{ url = "https://files.pythonhosted.org/packages/b5/3d/b932bb4225c80b58dfadaca9d42d08d0b7064d2d1791b6a237f87f661834/pillow-11.3.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2465a69cf967b8b49ee1b96d76718cd98c4e925414ead59fdf75cf0fd07df673", size = 7638246, upload-time = "2025-07-03T13:10:44.987Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/7d/1b/c14b4197b80150fb64453585247e6fb2e1d93761fa0fa9cf63b102fde822/pillow-10.4.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:f5b92f4d70791b4a67157321c4e8225d60b119c5cc9aee8ecf153aace4aad4ef", size = 4370054, upload-time = "2024-07-01T09:46:26.825Z" },
|
{ url = "https://files.pythonhosted.org/packages/09/b5/0487044b7c096f1b48f0d7ad416472c02e0e4bf6919541b111efd3cae690/pillow-11.3.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:41742638139424703b4d01665b807c6468e23e699e8e90cffefe291c5832b027", size = 5973336, upload-time = "2025-07-01T09:15:21.237Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/55/77/40daddf677897a923d5d33329acd52a2144d54a9644f2a5422c028c6bf2d/pillow-10.4.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:86dcb5a1eb778d8b25659d5e4341269e8590ad6b4e8b44d9f4b07f8d136c414a", size = 4539484, upload-time = "2024-07-01T09:46:29.355Z" },
|
{ url = "https://files.pythonhosted.org/packages/a8/2d/524f9318f6cbfcc79fbc004801ea6b607ec3f843977652fdee4857a7568b/pillow-11.3.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:93efb0b4de7e340d99057415c749175e24c8864302369e05914682ba642e5d77", size = 6642699, upload-time = "2025-07-01T09:15:23.186Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/40/54/90de3e4256b1207300fb2b1d7168dd912a2fb4b2401e439ba23c2b2cabde/pillow-10.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:780c072c2e11c9b2c7ca37f9a2ee8ba66f44367ac3e5c7832afcfe5104fd6d1b", size = 4477375, upload-time = "2024-07-01T09:46:31.756Z" },
|
{ url = "https://files.pythonhosted.org/packages/6f/d2/a9a4f280c6aefedce1e8f615baaa5474e0701d86dd6f1dede66726462bbd/pillow-11.3.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7966e38dcd0fa11ca390aed7c6f20454443581d758242023cf36fcb319b1a874", size = 6083789, upload-time = "2025-07-01T09:15:25.1Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/13/24/1bfba52f44193860918ff7c93d03d95e3f8748ca1de3ceaf11157a14cf16/pillow-10.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:37fb69d905be665f68f28a8bba3c6d3223c8efe1edf14cc4cfa06c241f8c81d9", size = 4608773, upload-time = "2024-07-01T09:46:33.73Z" },
|
{ url = "https://files.pythonhosted.org/packages/fe/54/86b0cd9dbb683a9d5e960b66c7379e821a19be4ac5810e2e5a715c09a0c0/pillow-11.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:98a9afa7b9007c67ed84c57c9e0ad86a6000da96eaa638e4f8abe5b65ff83f0a", size = 6720386, upload-time = "2025-07-01T09:15:27.378Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/55/04/5e6de6e6120451ec0c24516c41dbaf80cce1b6451f96561235ef2429da2e/pillow-10.4.0-cp312-cp312-win32.whl", hash = "sha256:7dfecdbad5c301d7b5bde160150b4db4c659cee2b69589705b6f8a0c509d9f42", size = 2235690, upload-time = "2024-07-01T09:46:36.587Z" },
|
{ url = "https://files.pythonhosted.org/packages/e7/95/88efcaf384c3588e24259c4203b909cbe3e3c2d887af9e938c2022c9dd48/pillow-11.3.0-cp314-cp314-win32.whl", hash = "sha256:02a723e6bf909e7cea0dac1b0e0310be9d7650cd66222a5f1c571455c0a45214", size = 6370911, upload-time = "2025-07-01T09:15:29.294Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/74/0a/d4ce3c44bca8635bd29a2eab5aa181b654a734a29b263ca8efe013beea98/pillow-10.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:1d846aea995ad352d4bdcc847535bd56e0fd88d36829d2c90be880ef1ee4668a", size = 2554951, upload-time = "2024-07-01T09:46:38.777Z" },
|
{ url = "https://files.pythonhosted.org/packages/2e/cc/934e5820850ec5eb107e7b1a72dd278140731c669f396110ebc326f2a503/pillow-11.3.0-cp314-cp314-win_amd64.whl", hash = "sha256:a418486160228f64dd9e9efcd132679b7a02a5f22c982c78b6fc7dab3fefb635", size = 7117383, upload-time = "2025-07-01T09:15:31.128Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/b5/ca/184349ee40f2e92439be9b3502ae6cfc43ac4b50bc4fc6b3de7957563894/pillow-10.4.0-cp312-cp312-win_arm64.whl", hash = "sha256:e553cad5179a66ba15bb18b353a19020e73a7921296a7979c4a2b7f6a5cd57f9", size = 2243427, upload-time = "2024-07-01T09:46:43.15Z" },
|
{ url = "https://files.pythonhosted.org/packages/d6/e9/9c0a616a71da2a5d163aa37405e8aced9a906d574b4a214bede134e731bc/pillow-11.3.0-cp314-cp314-win_arm64.whl", hash = "sha256:155658efb5e044669c08896c0c44231c5e9abcaadbc5cd3648df2f7c0b96b9a6", size = 2511385, upload-time = "2025-07-01T09:15:33.328Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/1a/33/c88376898aff369658b225262cd4f2659b13e8178e7534df9e6e1fa289f6/pillow-11.3.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:59a03cdf019efbfeeed910bf79c7c93255c3d54bc45898ac2a4140071b02b4ae", size = 5281129, upload-time = "2025-07-01T09:15:35.194Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/1f/70/d376247fb36f1844b42910911c83a02d5544ebd2a8bad9efcc0f707ea774/pillow-11.3.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f8a5827f84d973d8636e9dc5764af4f0cf2318d26744b3d902931701b0d46653", size = 4689580, upload-time = "2025-07-01T09:15:37.114Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/eb/1c/537e930496149fbac69efd2fc4329035bbe2e5475b4165439e3be9cb183b/pillow-11.3.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ee92f2fd10f4adc4b43d07ec5e779932b4eb3dbfbc34790ada5a6669bc095aa6", size = 5902860, upload-time = "2025-07-03T13:10:50.248Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/bd/57/80f53264954dcefeebcf9dae6e3eb1daea1b488f0be8b8fef12f79a3eb10/pillow-11.3.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c96d333dcf42d01f47b37e0979b6bd73ec91eae18614864622d9b87bbd5bbf36", size = 7670694, upload-time = "2025-07-03T13:10:56.432Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/70/ff/4727d3b71a8578b4587d9c276e90efad2d6fe0335fd76742a6da08132e8c/pillow-11.3.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4c96f993ab8c98460cd0c001447bff6194403e8b1d7e149ade5f00594918128b", size = 6005888, upload-time = "2025-07-01T09:15:39.436Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/05/ae/716592277934f85d3be51d7256f3636672d7b1abfafdc42cf3f8cbd4b4c8/pillow-11.3.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:41342b64afeba938edb034d122b2dda5db2139b9a4af999729ba8818e0056477", size = 6670330, upload-time = "2025-07-01T09:15:41.269Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e7/bb/7fe6cddcc8827b01b1a9766f5fdeb7418680744f9082035bdbabecf1d57f/pillow-11.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:068d9c39a2d1b358eb9f245ce7ab1b5c3246c7c8c7d9ba58cfa5b43146c06e50", size = 6114089, upload-time = "2025-07-01T09:15:43.13Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/8b/f5/06bfaa444c8e80f1a8e4bff98da9c83b37b5be3b1deaa43d27a0db37ef84/pillow-11.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a1bc6ba083b145187f648b667e05a2534ecc4b9f2784c2cbe3089e44868f2b9b", size = 6748206, upload-time = "2025-07-01T09:15:44.937Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f0/77/bc6f92a3e8e6e46c0ca78abfffec0037845800ea38c73483760362804c41/pillow-11.3.0-cp314-cp314t-win32.whl", hash = "sha256:118ca10c0d60b06d006be10a501fd6bbdfef559251ed31b794668ed569c87e12", size = 6377370, upload-time = "2025-07-01T09:15:46.673Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/4a/82/3a721f7d69dca802befb8af08b7c79ebcab461007ce1c18bd91a5d5896f9/pillow-11.3.0-cp314-cp314t-win_amd64.whl", hash = "sha256:8924748b688aa210d79883357d102cd64690e56b923a186f35a82cbc10f997db", size = 7121500, upload-time = "2025-07-01T09:15:48.512Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/89/c7/5572fa4a3f45740eaab6ae86fcdf7195b55beac1371ac8c619d880cfe948/pillow-11.3.0-cp314-cp314t-win_arm64.whl", hash = "sha256:79ea0d14d3ebad43ec77ad5272e6ff9bba5b679ef73375ea760261207fa8e0aa", size = 2512835, upload-time = "2025-07-01T09:15:50.399Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pillow-heif"
|
name = "pillow-heif"
|
||||||
version = "0.18.0"
|
version = "1.1.0"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "pillow" },
|
{ name = "pillow" },
|
||||||
]
|
]
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/c9/bb/e7797fe7f5cad447bb470f916ead38f0929e8d28bdf6bd5d9f31dfe1ac26/pillow_heif-0.18.0.tar.gz", hash = "sha256:70318dad9faa76121c6592ac0ab59881ff0dac6ab791a922e70d82c7706cce88", size = 16172675, upload-time = "2024-07-27T16:21:21.889Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/6f/d4/597cf8c54d1ed494a46cc12e358d2f73a993b5f139402b35681f56beffde/pillow_heif-1.1.0.tar.gz", hash = "sha256:6c0c5f81a780185bbddc56e0d5537c53aa6cb5fb6018f5a60534a47c53f5455d", size = 18271020, upload-time = "2025-08-02T09:58:32.54Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/5a/48/4bdce48d77c307b50bba0c77d6485e156f0fefcca273ce007351ad1deb40/pillow_heif-0.18.0-cp312-cp312-macosx_12_0_x86_64.whl", hash = "sha256:c795e7ccceea33e01e49ce536139f94cabb1bf017393666f76c05a9daebae2da", size = 5279030, upload-time = "2024-07-27T16:20:00.876Z" },
|
{ url = "https://files.pythonhosted.org/packages/cf/93/e32cd695bca9750f6fa166f28a4c84fd8a23209f99d56712359c0b42f2e6/pillow_heif-1.1.0-cp314-cp314-macosx_13_0_x86_64.whl", hash = "sha256:c47f12a30a86c43f714353d0fb8478dc4da7915d678642e0b6cf6f0fd53d711e", size = 3384304, upload-time = "2025-08-02T09:57:55.997Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/3c/d8/8f2f8f44b6fc3689ace680b21655a0eabb1393f8e66b8851b0f95b8aac1c/pillow_heif-0.18.0-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:4dd5b3ec09be45c1ef63be31773df90e18ee08e5e950018b0a349924b54a24ac", size = 3732529, upload-time = "2024-07-27T16:20:02.849Z" },
|
{ url = "https://files.pythonhosted.org/packages/62/88/3e7cda3383c165a5eebf9a0193450de2b810b7654dbc363c8c26a87be6f2/pillow_heif-1.1.0-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:3cc4a7a808b659f657c5144aeec8152d261d8f3c4a859eba66ef31dab26982d2", size = 2261529, upload-time = "2025-08-02T09:57:57.6Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/f8/61/f0aac50a7ad051e1cb71ed7a8d3a94b5e54ea8ed1acb03586f0fcdebd730/pillow_heif-0.18.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eb2eade59c2654c2643a3b637de37c19e75a77c66a3e9a5e0ae26210e4f48aee", size = 6759746, upload-time = "2024-07-27T16:20:04.776Z" },
|
{ url = "https://files.pythonhosted.org/packages/bf/f2/469d9ef5c64efecbc0559aa56ebe6539007f478d8aee83253037dc203567/pillow_heif-1.1.0-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f3b0df4a03f9c7f150d801f3644961df5af0739503d6fc5d3e0aeb540d16c3d", size = 5780880, upload-time = "2025-08-02T09:57:59.408Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/4b/71/a8b3684f64307b96ea0ae14594564b41fdf4d4a6f7df5ef73d180a71db5d/pillow_heif-0.18.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:35b59d599bfdb8454739db6b92f0841ecadbe887babb5ed5abd5299587843eef", size = 7586203, upload-time = "2024-07-27T16:20:06.811Z" },
|
{ url = "https://files.pythonhosted.org/packages/13/74/a5c13f96ba06f4ec7d85a6d343b91c2767bd5a3fd90c146cf3c59fbcb12c/pillow_heif-1.1.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:44fcd3a171337788bf672882484725f124143d2f290cfdbf029c22c113a15c79", size = 5502822, upload-time = "2025-08-02T09:58:00.841Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/d8/59/3b1c90f7d366fb653f5f072803ae6350e608bea0d0d0ea65d6e7764c470f/pillow_heif-0.18.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:26a0b8b8e899e7bcc876ee61fcadb0f0b849bd6a0d5c20f0e969c77a43b40568", size = 8119771, upload-time = "2024-07-27T16:20:08.491Z" },
|
{ url = "https://files.pythonhosted.org/packages/57/04/1a6a88647dbb0eec95fa8b9d5cac501f690cdd83479e7cacf56772f1189f/pillow_heif-1.1.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7f07f28e99ae74dffca936e24d8881f3e7e572233cf1f7bc16fe42fb08a795be", size = 6822779, upload-time = "2025-08-02T09:58:02.238Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/48/20/a72297dc260d0bf8fce8f209d36ceb498b2f1e8beb478a0ab3f565c9a3c9/pillow_heif-0.18.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0276a3e0c667677ed0c67f4512cdf2f674065018049307ba4de5cb4648b0a33e", size = 8850943, upload-time = "2024-07-27T16:20:10.262Z" },
|
{ url = "https://files.pythonhosted.org/packages/ae/37/8873625e681505940381cff6a4a191301f98d9a5c1499f60a2e892ad8c89/pillow_heif-1.1.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7a6de39cfba4037479aa5deb65a6bb0b27da0a11b95f740202f03b578ee8932f", size = 6429866, upload-time = "2025-08-02T09:58:03.637Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/12/98/2bcb2790618cfb8ce2054256a4d3bc2597288bf82444cc87fdc815484c38/pillow_heif-0.18.0-cp312-cp312-win_amd64.whl", hash = "sha256:5916fa31f2015626dd2372d14e24521ea6caed11b25be14faa9b9c67731087ce", size = 8547707, upload-time = "2024-07-27T16:20:12.594Z" },
|
{ 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]]
|
[[package]]
|
||||||
@@ -409,36 +537,89 @@ sdist = { url = "https://files.pythonhosted.org/packages/cf/9c/fb5d48abfe5d791cd
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "psycopg2-binary"
|
name = "psycopg2-binary"
|
||||||
version = "2.9.10"
|
version = "2.9.12"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/cb/0e/bdc8274dc0585090b4e3432267d7be4dfbfd8971c0fa59167c711105a6bf/psycopg2-binary-2.9.10.tar.gz", hash = "sha256:4b3df0e6990aa98acda57d983942eff13d824135fe2250e6522edaa782a06de2", size = 385764, upload-time = "2024-10-16T11:24:58.126Z" }
|
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 = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/49/7d/465cc9795cf76f6d329efdafca74693714556ea3891813701ac1fee87545/psycopg2_binary-2.9.10-cp312-cp312-macosx_12_0_x86_64.whl", hash = "sha256:880845dfe1f85d9d5f7c412efea7a08946a46894537e4e5d091732eb1d34d9a0", size = 3044771, upload-time = "2024-10-16T11:20:35.234Z" },
|
{ 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/8b/31/6d225b7b641a1a2148e3ed65e1aa74fc86ba3fee850545e27be9e1de893d/psycopg2_binary-2.9.10-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:9440fa522a79356aaa482aa4ba500b65f28e5d0e63b801abf6aa152a29bd842a", size = 3275336, upload-time = "2024-10-16T11:20:38.742Z" },
|
{ 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/30/b7/a68c2b4bff1cbb1728e3ec864b2d92327c77ad52edcd27922535a8366f68/psycopg2_binary-2.9.10-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e3923c1d9870c49a2d44f795df0c889a22380d36ef92440ff618ec315757e539", size = 2851637, upload-time = "2024-10-16T11:20:42.145Z" },
|
{ 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/0b/b1/cfedc0e0e6f9ad61f8657fd173b2f831ce261c02a08c0b09c652b127d813/psycopg2_binary-2.9.10-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7b2c956c028ea5de47ff3a8d6b3cc3330ab45cf0b7c3da35a2d6ff8420896526", size = 3082097, upload-time = "2024-10-16T11:20:46.185Z" },
|
{ 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/18/ed/0a8e4153c9b769f59c02fb5e7914f20f0b2483a19dae7bf2db54b743d0d0/psycopg2_binary-2.9.10-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f758ed67cab30b9a8d2833609513ce4d3bd027641673d4ebc9c067e4d208eec1", size = 3264776, upload-time = "2024-10-16T11:20:50.879Z" },
|
{ 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/10/db/d09da68c6a0cdab41566b74e0a6068a425f077169bed0946559b7348ebe9/psycopg2_binary-2.9.10-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8cd9b4f2cfab88ed4a9106192de509464b75a906462fb846b936eabe45c2063e", size = 3020968, upload-time = "2024-10-16T11:20:56.819Z" },
|
{ 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/94/28/4d6f8c255f0dfffb410db2b3f9ac5218d959a66c715c34cac31081e19b95/psycopg2_binary-2.9.10-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6dc08420625b5a20b53551c50deae6e231e6371194fa0651dbe0fb206452ae1f", size = 2872334, upload-time = "2024-10-16T11:21:02.411Z" },
|
{ 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/05/f7/20d7bf796593c4fea95e12119d6cc384ff1f6141a24fbb7df5a668d29d29/psycopg2_binary-2.9.10-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:d7cd730dfa7c36dbe8724426bf5612798734bff2d3c3857f36f2733f5bfc7c00", size = 2822722, upload-time = "2024-10-16T11:21:09.01Z" },
|
{ 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/4d/e4/0c407ae919ef626dbdb32835a03b6737013c3cc7240169843965cada2bdf/psycopg2_binary-2.9.10-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:155e69561d54d02b3c3209545fb08938e27889ff5a10c19de8d23eb5a41be8a5", size = 2920132, upload-time = "2024-10-16T11:21:16.339Z" },
|
{ 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/2d/70/aa69c9f69cf09a01da224909ff6ce8b68faeef476f00f7ec377e8f03be70/psycopg2_binary-2.9.10-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c3cc28a6fd5a4a26224007712e79b81dbaee2ffb90ff406256158ec4d7b52b47", size = 2959312, upload-time = "2024-10-16T11:21:25.584Z" },
|
{ 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/d3/bd/213e59854fafe87ba47814bf413ace0dcee33a89c8c8c814faca6bc7cf3c/psycopg2_binary-2.9.10-cp312-cp312-win32.whl", hash = "sha256:ec8a77f521a17506a24a5f626cb2aee7850f9b69a0afe704586f63a464f3cd64", size = 1025191, upload-time = "2024-10-16T11:21:29.912Z" },
|
{ 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" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/92/29/06261ea000e2dc1e22907dbbc483a1093665509ea586b29b8986a0e56733/psycopg2_binary-2.9.10-cp312-cp312-win_amd64.whl", hash = "sha256:18c5ee682b9c6dd3696dad6e54cc7ff3a1a9020df6a5c0f861ef8bfd338c3ca0", size = 1164031, upload-time = "2024-10-16T11:21:34.211Z" },
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pytz"
|
name = "pygments"
|
||||||
version = "2024.1"
|
version = "2.20.0"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/90/26/9f1f00a5d021fff16dee3de13d43e5e978f3d58928e129c3a62cf7eb9738/pytz-2024.1.tar.gz", hash = "sha256:2a29735ea9c18baf14b448846bde5a48030ed267578472d8955cd0e7443a9812", size = 316214, upload-time = "2024-02-02T01:18:41.693Z" }
|
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 = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/9c/3d/a121f284241f08268b21359bd425f7d4825cffc5ac5cd0e1b3d82ffd2b10/pytz-2024.1-py2.py3-none-any.whl", hash = "sha256:328171f4e3623139da4983451950b28e95ac706e13f3f2630a879749e7a8b319", size = 505474, upload-time = "2024-02-02T01:18:37.283Z" },
|
{ 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.0.3"
|
||||||
|
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/7d/0d/549bd94f1a0a402dc8cf64563a117c0f3765662e2e668477624baeec44d5/pytest-9.0.3.tar.gz", hash = "sha256:b86ada508af81d19edeb213c681b1d48246c1a91d304c6c81a427674c17eb91c", size = 1572165, upload-time = "2026-04-07T17:16:18.027Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-py3-none-any.whl", hash = "sha256:2c5efc453d45394fdd706ade797c0a81091eccd1d6e4bccfcd476e2b8e0ab5d9", size = 375249, upload-time = "2026-04-07T17:16:16.13Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[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]]
|
||||||
|
name = "python-dateutil"
|
||||||
|
version = "2.9.0.post0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "six" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "requests"
|
name = "requests"
|
||||||
version = "2.32.3"
|
version = "2.32.4"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "certifi" },
|
{ name = "certifi" },
|
||||||
@@ -446,61 +627,61 @@ dependencies = [
|
|||||||
{ name = "idna" },
|
{ name = "idna" },
|
||||||
{ name = "urllib3" },
|
{ name = "urllib3" },
|
||||||
]
|
]
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/63/70/2bf7780ad2d390a8d301ad0b550f1581eadbd9a20f896afe06353c2a2913/requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760", size = 131218, upload-time = "2024-05-29T15:37:49.536Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/e1/0a/929373653770d8a0d7ea76c37de6e41f11eb07559b103b1c02cafb3f7cf8/requests-2.32.4.tar.gz", hash = "sha256:27d0316682c8a29834d3264820024b62a36942083d52caf2f14c0591336d3422", size = 135258, upload-time = "2025-06-09T16:43:07.34Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/f9/9b/335f9764261e915ed497fcdeb11df5dfd6f7bf257d4a6a2a686d80da4d54/requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6", size = 64928, upload-time = "2024-05-29T15:37:47.027Z" },
|
{ url = "https://files.pythonhosted.org/packages/7c/e4/56027c4a6b4ae70ca9de302488c5ca95ad4a39e190093d6c1a8ace08341b/requests-2.32.4-py3-none-any.whl", hash = "sha256:27babd3cda2a6d50b30443204ee89830707d396671944c998b5975b031ac2b2c", size = 64847, upload-time = "2025-06-09T16:43:05.728Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ruff"
|
name = "ruff"
|
||||||
version = "0.12.4"
|
version = "0.15.13"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/9b/ce/8d7dbedede481245b489b769d27e2934730791a9a82765cb94566c6e6abd/ruff-0.12.4.tar.gz", hash = "sha256:13efa16df6c6eeb7d0f091abae50f58e9522f3843edb40d56ad52a5a4a4b6873", size = 5131435, upload-time = "2025-07-17T17:27:19.138Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/24/21/a7d5c126d5b557715ef81098f3db2fe20f622a039ff2e626af28d674ab80/ruff-0.15.13.tar.gz", hash = "sha256:f9d89f17f7ba7fb2ed42921f0df75da797a9a5d71bc39049e2c687cf2baf44b7", size = 4678180, upload-time = "2026-05-14T13:44:37.869Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/ae/9f/517bc5f61bad205b7f36684ffa5415c013862dee02f55f38a217bdbe7aa4/ruff-0.12.4-py3-none-linux_armv6l.whl", hash = "sha256:cb0d261dac457ab939aeb247e804125a5d521b21adf27e721895b0d3f83a0d0a", size = 10188824, upload-time = "2025-07-17T17:26:31.412Z" },
|
{ url = "https://files.pythonhosted.org/packages/c6/61/11d458dc6ac22504fd8e237b29dfd40504c7fbbcc8930402cfe51a8e63ed/ruff-0.15.13-py3-none-linux_armv6l.whl", hash = "sha256:444b580fc72fd6887e650acd3e575e18cdc79dbcf42fb4030b491057921f61f8", size = 10738279, upload-time = "2026-05-14T13:44:18.7Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/28/83/691baae5a11fbbde91df01c565c650fd17b0eabed259e8b7563de17c6529/ruff-0.12.4-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:55c0f4ca9769408d9b9bac530c30d3e66490bd2beb2d3dae3e4128a1f05c7442", size = 10884521, upload-time = "2025-07-17T17:26:35.084Z" },
|
{ url = "https://files.pythonhosted.org/packages/86/ca/caa871ee7be718c45256fada4e16a218ee3e33f0c4a46b729a60a24912e6/ruff-0.15.13-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:6590d009e7cb7ebf36f83dbdd44a3fa48a0994ff6f1cdc1b08006abe58f98dc7", size = 11124798, upload-time = "2026-05-14T13:44:06.427Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/d6/8d/756d780ff4076e6dd035d058fa220345f8c458391f7edfb1c10731eedc75/ruff-0.12.4-py3-none-macosx_11_0_arm64.whl", hash = "sha256:a8224cc3722c9ad9044da7f89c4c1ec452aef2cfe3904365025dd2f51daeae0e", size = 10277653, upload-time = "2025-07-17T17:26:37.897Z" },
|
{ url = "https://files.pythonhosted.org/packages/d3/19/43f5f2e568dddde567fc41f8471f9432c09563e19d3e617a48cfa52f8f0a/ruff-0.15.13-py3-none-macosx_11_0_arm64.whl", hash = "sha256:1c26d2f66163deeb6e08d8b39fbbe983ce3c71cea06a6d7591cfd1421793c629", size = 10460761, upload-time = "2026-05-14T13:44:04.375Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/8d/97/8eeee0f48ece153206dce730fc9e0e0ca54fd7f261bb3d99c0a4343a1892/ruff-0.12.4-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e9949d01d64fa3672449a51ddb5d7548b33e130240ad418884ee6efa7a229586", size = 10485993, upload-time = "2025-07-17T17:26:40.68Z" },
|
{ url = "https://files.pythonhosted.org/packages/99/df/cf938cd6de3003178f03ad7c1ea2a6c099468c03a35037985070b37e76be/ruff-0.15.13-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9dbd6f94b434f896308e4d57fb7bfde0d02b99f7a64b3bdab0fdfa6a864203a5", size = 10804451, upload-time = "2026-05-14T13:44:25.221Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/49/b8/22a43d23a1f68df9b88f952616c8508ea6ce4ed4f15353b8168c48b2d7e7/ruff-0.12.4-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:be0593c69df9ad1465e8a2d10e3defd111fdb62dcd5be23ae2c06da77e8fcffb", size = 10022824, upload-time = "2025-07-17T17:26:43.564Z" },
|
{ url = "https://files.pythonhosted.org/packages/c7/7d/5d0973129b154ded2225729169d7068f26b467760b146493fde138415f23/ruff-0.15.13-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bf3259f3be4d181bda591da5db2571aed6853c6a048157756448020bc6c5cd22", size = 10534285, upload-time = "2026-05-14T13:44:08.888Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/cd/70/37c234c220366993e8cffcbd6cadbf332bfc848cbd6f45b02bade17e0149/ruff-0.12.4-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a7dea966bcb55d4ecc4cc3270bccb6f87a337326c9dcd3c07d5b97000dbff41c", size = 11524414, upload-time = "2025-07-17T17:26:46.219Z" },
|
{ url = "https://files.pythonhosted.org/packages/1f/e3/6b999bbc66cd51e5f073842bc2a3995e99c5e0e72e16b15e7261f7abf57a/ruff-0.15.13-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ae9c17e5eb4430c154e76abc25d79a318190f5a997f38fb6b114416c5319ffc9", size = 11312063, upload-time = "2026-05-14T13:44:11.274Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/14/77/c30f9964f481b5e0e29dd6a1fae1f769ac3fd468eb76fdd5661936edd262/ruff-0.12.4-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:afcfa3ab5ab5dd0e1c39bf286d829e042a15e966b3726eea79528e2e24d8371a", size = 12419216, upload-time = "2025-07-17T17:26:48.883Z" },
|
{ url = "https://files.pythonhosted.org/packages/af/5a/642639e9f5db04f1e97fbd6e091c6fd20725bdf072fb114d00eefb9e6eb8/ruff-0.15.13-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2e2e39bff6c341f4b577a21b801326fab0b11847f48fcaa83f00a113c9b3cb55", size = 12183079, upload-time = "2026-05-14T13:44:01.634Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/6e/79/af7fe0a4202dce4ef62c5e33fecbed07f0178f5b4dd9c0d2fcff5ab4a47c/ruff-0.12.4-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c057ce464b1413c926cdb203a0f858cd52f3e73dcb3270a3318d1630f6395bb3", size = 11976756, upload-time = "2025-07-17T17:26:51.754Z" },
|
{ url = "https://files.pythonhosted.org/packages/19/4c/7585735f6b53b0f12de13618b2f7d250a844f018822efc899df2e7b8295f/ruff-0.15.13-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e8d9a8e08013542e94d3220bc5b62cc3e5ef87c5f74bff367d3fac14fab013e6", size = 11440833, upload-time = "2026-05-14T13:43:59.043Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/09/d1/33fb1fc00e20a939c305dbe2f80df7c28ba9193f7a85470b982815a2dc6a/ruff-0.12.4-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e64b90d1122dc2713330350626b10d60818930819623abbb56535c6466cce045", size = 11020019, upload-time = "2025-07-17T17:26:54.265Z" },
|
{ url = "https://files.pythonhosted.org/packages/e8/31/bf1a0803d077e679cfeee5f2f67290a0fa79c7385b5d9a8c17b9db2c48f0/ruff-0.15.13-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cc411dfebe5eebe55ce041c6ae080eb7668955e866daa2fbb16692a784f1c4ca", size = 11434486, upload-time = "2026-05-14T13:44:27.761Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/64/f4/e3cd7f7bda646526f09693e2e02bd83d85fff8a8222c52cf9681c0d30843/ruff-0.12.4-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2abc48f3d9667fdc74022380b5c745873499ff827393a636f7a59da1515e7c57", size = 11277890, upload-time = "2025-07-17T17:26:56.914Z" },
|
{ url = "https://files.pythonhosted.org/packages/e1/4e/62c9b999875d4f14db80f277c030578f5e249c9852d65b7ac7ad0b43c041/ruff-0.15.13-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:768494eb08b9cee54e2fd27969966f74db5a57f6eaa7a90fcb3306af34dfc4bd", size = 11385189, upload-time = "2026-05-14T13:44:13.704Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/5e/d0/69a85fb8b94501ff1a4f95b7591505e8983f38823da6941eb5b6badb1e3a/ruff-0.12.4-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:2b2449dc0c138d877d629bea151bee8c0ae3b8e9c43f5fcaafcd0c0d0726b184", size = 10348539, upload-time = "2025-07-17T17:26:59.381Z" },
|
{ url = "https://files.pythonhosted.org/packages/fc/89/7e959047a104df3eb12863447c110140191fc5b6c4f379ea2e803fcdb0e4/ruff-0.15.13-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:fb75f9a3a7e42ffe117d734494e6c5e5cb3565d66e12612cb63d0e572a41a5b6", size = 10781380, upload-time = "2026-05-14T13:43:56.734Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/16/a0/91372d1cb1678f7d42d4893b88c252b01ff1dffcad09ae0c51aa2542275f/ruff-0.12.4-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:56e45bb11f625db55f9b70477062e6a1a04d53628eda7784dce6e0f55fd549eb", size = 10009579, upload-time = "2025-07-17T17:27:02.462Z" },
|
{ url = "https://files.pythonhosted.org/packages/ff/52/5fd18f3b88cab63e88aa11516b3b4e1e5f720e5c330f8dbe5c26210f41f8/ruff-0.15.13-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:8cb74dd33bb2f6613faf7fc03b660053b5ac4f80e706d5788c6335e2a8048d51", size = 10540605, upload-time = "2026-05-14T13:44:20.748Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/23/1b/c4a833e3114d2cc0f677e58f1df6c3b20f62328dbfa710b87a1636a5e8eb/ruff-0.12.4-py3-none-musllinux_1_2_i686.whl", hash = "sha256:478fccdb82ca148a98a9ff43658944f7ab5ec41c3c49d77cd99d44da019371a1", size = 10942982, upload-time = "2025-07-17T17:27:05.343Z" },
|
{ url = "https://files.pythonhosted.org/packages/e8/e0/9e35f338990d3e41a82875ff7053ffe97541dae81c9d02143177f381d572/ruff-0.15.13-py3-none-musllinux_1_2_i686.whl", hash = "sha256:7ef823f817fcd191dc934e984be9cf4094f808effa16f2542ad8e821ba02bbf2", size = 11036554, upload-time = "2026-05-14T13:44:16.256Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/ff/ce/ce85e445cf0a5dd8842f2f0c6f0018eedb164a92bdf3eda51984ffd4d989/ruff-0.12.4-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:0fc426bec2e4e5f4c4f182b9d2ce6a75c85ba9bcdbe5c6f2a74fcb8df437df4b", size = 11343331, upload-time = "2025-07-17T17:27:08.652Z" },
|
{ url = "https://files.pythonhosted.org/packages/c2/13/070fb048c24080fba188f66371e2a92785be257ad02242066dc7255ac6e9/ruff-0.15.13-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:f345a13937bd7f09f6f5d19fa0721b0c103e00e7f62bc67089a8e5e037719e0b", size = 11528133, upload-time = "2026-05-14T13:44:22.808Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/35/cf/441b7fc58368455233cfb5b77206c849b6dfb48b23de532adcc2e50ccc06/ruff-0.12.4-py3-none-win32.whl", hash = "sha256:4de27977827893cdfb1211d42d84bc180fceb7b72471104671c59be37041cf93", size = 10267904, upload-time = "2025-07-17T17:27:11.814Z" },
|
{ url = "https://files.pythonhosted.org/packages/6b/8c/b1e1666aef7fc6555094d73ae6cd981701781ae85b97ceefc0eebd0b4668/ruff-0.15.13-py3-none-win32.whl", hash = "sha256:4044f94208b3b05ba0fc4a4abd0558cf4d6459bd18325eead7fd8cc66f909b41", size = 10721455, upload-time = "2026-05-14T13:44:35.697Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/ce/7e/20af4a0df5e1299e7368d5ea4350412226afb03d95507faae94c80f00afd/ruff-0.12.4-py3-none-win_amd64.whl", hash = "sha256:fe0b9e9eb23736b453143d72d2ceca5db323963330d5b7859d60d101147d461a", size = 11209038, upload-time = "2025-07-17T17:27:14.417Z" },
|
{ url = "https://files.pythonhosted.org/packages/ab/a6/870a3e8a50590bb92be184ad928c2922f088b00d9dc5c5ec7b924ee08c22/ruff-0.15.13-py3-none-win_amd64.whl", hash = "sha256:7064884d442b7d477b4e7473d12da7f08851d2b1982763c5d3f388a19468a1a4", size = 11900409, upload-time = "2026-05-14T13:44:30.389Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/11/02/8857d0dfb8f44ef299a5dfd898f673edefb71e3b533b3b9d2db4c832dd13/ruff-0.12.4-py3-none-win_arm64.whl", hash = "sha256:0618ec4442a83ab545e5b71202a5c0ed7791e8471435b94e655b570a5031a98e", size = 10469336, upload-time = "2025-07-17T17:27:16.913Z" },
|
{ url = "https://files.pythonhosted.org/packages/9b/36/9c015cd052fca743dae8cb2aeb16b551444787467db42ceab0fc968865af/ruff-0.15.13-py3-none-win_arm64.whl", hash = "sha256:2471da9bd1068c8c064b5fd9c0c4b6dddffd6369cb1cd68b29993b1709ff1b21", size = 11179336, upload-time = "2026-05-14T13:44:33.026Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "six"
|
name = "six"
|
||||||
version = "1.16.0"
|
version = "1.17.0"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/71/39/171f1c67cd00715f190ba0b100d606d440a28c93c7714febeca8b79af85e/six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926", size = 34041, upload-time = "2021-05-05T14:18:18.379Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/d9/5a/e7c31adbe875f2abbb91bd84cf2dc52d792b5a01506781dbcf25c91daf11/six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254", size = 11053, upload-time = "2021-05-05T14:18:17.237Z" },
|
{ url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "soupsieve"
|
name = "soupsieve"
|
||||||
version = "2.6"
|
version = "2.7"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/d7/ce/fbaeed4f9fb8b2daa961f90591662df6a86c1abf25c548329a86920aedfb/soupsieve-2.6.tar.gz", hash = "sha256:e2e68417777af359ec65daac1057404a3c8a5455bb8abc36f1a9866ab1a51abb", size = 101569, upload-time = "2024-08-13T13:39:12.166Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/3f/f4/4a80cd6ef364b2e8b65b15816a843c0980f7a5a2b4dc701fc574952aa19f/soupsieve-2.7.tar.gz", hash = "sha256:ad282f9b6926286d2ead4750552c8a6142bc4c783fd66b0293547c8fe6ae126a", size = 103418, upload-time = "2025-04-20T18:50:08.518Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/d1/c2/fe97d779f3ef3b15f05c94a2f1e3d21732574ed441687474db9d342a7315/soupsieve-2.6-py3-none-any.whl", hash = "sha256:e72c4ff06e4fb6e4b5a9f0f55fe6e81514581fca1515028625d0f299c602ccc9", size = 36186, upload-time = "2024-08-13T13:39:10.986Z" },
|
{ url = "https://files.pythonhosted.org/packages/e7/9c/0e6afc12c269578be5c0c1c9f4b49a8d32770a080260c333ac04cc1c832d/soupsieve-2.7-py3-none-any.whl", hash = "sha256:6e60cc5c1ffaf1cebcc12e8188320b72071e922c2e897f737cadce79ad5d30c4", size = 36677, upload-time = "2025-04-20T18:50:07.196Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sqlparse"
|
name = "sqlparse"
|
||||||
version = "0.5.1"
|
version = "0.5.3"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/73/82/dfa23ec2cbed08a801deab02fe7c904bfb00765256b155941d789a338c68/sqlparse-0.5.1.tar.gz", hash = "sha256:bb6b4df465655ef332548e24f08e205afc81b9ab86cb1c45657a7ff173a3a00e", size = 84502, upload-time = "2024-07-15T19:30:27.085Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/e5/40/edede8dd6977b0d3da179a342c198ed100dd2aba4be081861ee5911e4da4/sqlparse-0.5.3.tar.gz", hash = "sha256:09f67787f56a0b16ecdbde1bfc7f5d9c3371ca683cfeaa8e6ff60b4807ec9272", size = 84999, upload-time = "2024-12-10T12:05:30.728Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/5d/a5/b2860373aa8de1e626b2bdfdd6df4355f0565b47e51f7d0c54fe70faf8fe/sqlparse-0.5.1-py3-none-any.whl", hash = "sha256:773dcbf9a5ab44a090f3441e2180efe2560220203dc2f8c0b0fa141e18b505e4", size = 44156, upload-time = "2024-07-15T19:30:25.033Z" },
|
{ url = "https://files.pythonhosted.org/packages/a9/5c/bfd6bd0bf979426d405cc6e71eceb8701b148b16c21d2dc3c261efc61c7b/sqlparse-0.5.3-py3-none-any.whl", hash = "sha256:cf2196ed3418f3ba5de6af7e82c694a9fbdbfecccdfc72e281548517081f16ca", size = 44415, upload-time = "2024-12-10T12:05:27.824Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -522,26 +703,35 @@ wheels = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tzdata"
|
name = "typing-extensions"
|
||||||
version = "2024.1"
|
version = "4.14.1"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/74/5b/e025d02cb3b66b7b76093404392d4b44343c69101cc85f4d180dd5784717/tzdata-2024.1.tar.gz", hash = "sha256:2674120f8d891909751c38abcdfd386ac0a5a1127954fbc332af6b5ceae07efd", size = 190559, upload-time = "2024-02-11T23:22:40.2Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/98/5a/da40306b885cc8c09109dc2e1abd358d5684b1425678151cdaed4731c822/typing_extensions-4.14.1.tar.gz", hash = "sha256:38b39f4aeeab64884ce9f74c94263ef78f3c22467c8724005483154c26648d36", size = 107673, upload-time = "2025-07-04T13:28:34.16Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/65/58/f9c9e6be752e9fcb8b6a0ee9fb87e6e7a1f6bcab2cdc73f02bb7ba91ada0/tzdata-2024.1-py2.py3-none-any.whl", hash = "sha256:9068bc196136463f5245e51efda838afa15aaeca9903f49050dfa2679db4d252", size = 345370, upload-time = "2024-02-11T23:22:38.223Z" },
|
{ url = "https://files.pythonhosted.org/packages/b5/00/d631e67a838026495268c2f6884f3711a15a9a2a96cd244fdaea53b823fb/typing_extensions-4.14.1-py3-none-any.whl", hash = "sha256:d1e1e3b58374dc93031d6eda2420a48ea44a36c2b4766a4fdeb3710755731d76", size = 43906, upload-time = "2025-07-04T13:28:32.743Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tzdata"
|
||||||
|
version = "2025.2"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/95/32/1a225d6164441be760d75c2c42e2780dc0873fe382da3e98a2e1e48361e5/tzdata-2025.2.tar.gz", hash = "sha256:b60a638fcc0daffadf82fe0f57e53d06bdec2f36c4df66280ae79bce6bd6f2b9", size = 196380, upload-time = "2025-03-23T13:54:43.652Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/5c/23/c7abc0ca0a1526a0774eca151daeb8de62ec457e77262b66b359c3c7679e/tzdata-2025.2-py2.py3-none-any.whl", hash = "sha256:1a403fada01ff9221ca8044d701868fa132215d84beb92242d9acd2147f667a8", size = 347839, upload-time = "2025-03-23T13:54:41.845Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "urllib3"
|
name = "urllib3"
|
||||||
version = "2.2.2"
|
version = "2.5.0"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/43/6d/fa469ae21497ddc8bc93e5877702dca7cb8f911e337aca7452b5724f1bb6/urllib3-2.2.2.tar.gz", hash = "sha256:dd505485549a7a552833da5e6063639d0d177c04f23bc3864e41e5dc5f612168", size = 292266, upload-time = "2024-06-17T13:40:11.401Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/15/22/9ee70a2574a4f4599c47dd506532914ce044817c7752a79b6a51286319bc/urllib3-2.5.0.tar.gz", hash = "sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760", size = 393185, upload-time = "2025-06-18T14:07:41.644Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/ca/1c/89ffc63a9605b583d5df2be791a27bc1a42b7c32bab68d3c8f2f73a98cd4/urllib3-2.2.2-py3-none-any.whl", hash = "sha256:a448b2f64d686155468037e1ace9f2d2199776e17f0a46610480d311f73e3472", size = 121444, upload-time = "2024-06-17T13:40:07.795Z" },
|
{ url = "https://files.pythonhosted.org/packages/a7/c2/fe1e52489ae3122415c51f387e221dd0773709bad6c6cdaa599e8a2c5185/urllib3-2.5.0-py3-none-any.whl", hash = "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc", size = 129795, upload-time = "2025-06-18T14:07:40.39Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wagtail"
|
name = "wagtail"
|
||||||
version = "6.2.1"
|
version = "7.4.1"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "anyascii" },
|
{ name = "anyascii" },
|
||||||
@@ -551,34 +741,48 @@ dependencies = [
|
|||||||
{ name = "django-modelcluster" },
|
{ name = "django-modelcluster" },
|
||||||
{ name = "django-permissionedforms" },
|
{ name = "django-permissionedforms" },
|
||||||
{ name = "django-taggit" },
|
{ name = "django-taggit" },
|
||||||
|
{ name = "django-tasks" },
|
||||||
{ name = "django-treebeard" },
|
{ name = "django-treebeard" },
|
||||||
{ name = "djangorestframework" },
|
{ name = "djangorestframework" },
|
||||||
{ name = "draftjs-exporter" },
|
{ name = "draftjs-exporter" },
|
||||||
{ name = "l18n" },
|
|
||||||
{ name = "laces" },
|
{ name = "laces" },
|
||||||
|
{ name = "modelsearch" },
|
||||||
{ name = "openpyxl" },
|
{ name = "openpyxl" },
|
||||||
{ name = "pillow" },
|
{ name = "pillow" },
|
||||||
{ name = "requests" },
|
{ name = "requests" },
|
||||||
{ name = "telepath" },
|
{ name = "telepath" },
|
||||||
{ name = "willow", extra = ["heif"] },
|
{ name = "willow", extra = ["heif"] },
|
||||||
]
|
]
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/6d/97/97b59c6bcd913f394e9f475dd952466954e9959ca9e10a45d4b251141eae/wagtail-6.2.1.tar.gz", hash = "sha256:0f136ef23b157997a44fa46543a320a31437350951cf13add8ea8b69cc5e8385", size = 6516190, upload-time = "2024-08-20T15:40:59.778Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/81/da/c060bd9ec00335336e04a610cd1d6762e459ade9c4983f7ecddae15af36d/wagtail-7.4.1.tar.gz", hash = "sha256:c7232cbe16afa5842c236e069b2baead3b3e87e9dc90a7493330b54b69e18b83", size = 6944979, upload-time = "2026-05-19T17:26:18.762Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/ca/b0/942f6bca14bfc2fbe786ebbaa90a831b71cbe3592e3da3f34d6be9f775b1/wagtail-6.2.1-py3-none-any.whl", hash = "sha256:31d073ea8acdc973ef45c5719977a1bb122ad0fc3f01348f37e922128200b42a", size = 8976424, upload-time = "2024-08-20T15:40:54.365Z" },
|
{ url = "https://files.pythonhosted.org/packages/53/c8/162fff3c5ef9e158127c9f4aa1f0d38edd8d1fa242629ae1d8af3b6306c7/wagtail-7.4.1-py3-none-any.whl", hash = "sha256:a42dc18b0fb818649c0d704ea50a2dea06d1b75614abdf837e90766263645c76", size = 9597601, upload-time = "2026-05-19T17:26:12.53Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wagtail-factories"
|
||||||
|
version = "4.4.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "factory-boy" },
|
||||||
|
{ name = "wagtail" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/42/8c/2ece22c2757bb3f6ef34f36cf185246b136134cf594b7b1dde92cb0a331f/wagtail_factories-4.4.0.tar.gz", hash = "sha256:c77c13d438a2e999a9220ff1829f060116013aa519a81944577353f460ba8cba", size = 9939, upload-time = "2026-02-10T15:14:13.7Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/4c/85/bd5aeaa54a10b6defca927f75bc3890acb7c3a9a23df8e10f3aa42e75807/wagtail_factories-4.4.0-py2.py3-none-any.whl", hash = "sha256:be0abb96d36bf0e3c733d7520fc1944441a379ab06d93af447fc4e71b67e2a01", size = 10754, upload-time = "2026-02-10T15:14:12.773Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wagtail-grapple"
|
name = "wagtail-grapple"
|
||||||
version = "0.26.0"
|
version = "0.31.0"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "graphene-django" },
|
{ name = "graphene-django" },
|
||||||
{ name = "wagtail" },
|
{ name = "wagtail" },
|
||||||
{ name = "wagtail-headless-preview" },
|
{ name = "wagtail-headless-preview" },
|
||||||
]
|
]
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/fc/36/2ad789a09d78b786eccf359ff88a596bcacfff4b9dd6505b68ad3c7f62f5/wagtail_grapple-0.26.0.tar.gz", hash = "sha256:68be091f7feb95bfbe0377bd03c1814a64a6150ae964ec593670d995f11780e5", size = 38362, upload-time = "2024-06-26T10:39:30.511Z" }
|
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 = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/7a/81/189560b651d61d71c81841f56249dcea3ab441bfcb2adb5afe7b5e516085/wagtail_grapple-0.26.0-py3-none-any.whl", hash = "sha256:de9d3c336d151279976d67f781c758ac5efc7d2bb003c2e311e6fbeea1554c41", size = 47759, upload-time = "2024-06-26T10:39:28.849Z" },
|
{ 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]]
|
[[package]]
|
||||||
@@ -595,24 +799,24 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "whitenoise"
|
name = "whitenoise"
|
||||||
version = "6.7.0"
|
version = "6.12.0"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/64/b8/86451d63ef5e1a9c480b52759d9db25ba85c3420ebdaf039057ed152a4c1/whitenoise-6.7.0.tar.gz", hash = "sha256:58c7a6cd811e275a6c91af22e96e87da0b1109e9a53bb7464116ef4c963bf636", size = 24973, upload-time = "2024-06-19T16:20:09.11Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/cb/2a/55b3f3a4ec326cd077c1c3defeee656b9298372a69229134d930151acd01/whitenoise-6.12.0.tar.gz", hash = "sha256:f723ebb76a112e98816ff80fcea0a6c9b8ecde835f8ddda25df7a30a3c2db6ad", size = 26841, upload-time = "2026-02-27T00:05:42.028Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/b8/42/68400d8ad59f67a1f7e12c2f39089ce005f08f73333f3e215f3d5ed6453c/whitenoise-6.7.0-py3-none-any.whl", hash = "sha256:a1ae85e01fdc9815d12fa33f17765bc132ed2c54fa76daf9e39e879dd93566f6", size = 19905, upload-time = "2024-06-19T16:20:03.269Z" },
|
{ url = "https://files.pythonhosted.org/packages/db/eb/d5583a11486211f3ebd4b385545ae787f32363d453c19fffd81106c9c138/whitenoise-6.12.0-py3-none-any.whl", hash = "sha256:fc5e8c572e33ebf24795b47b6a7da8da3c00cff2349f5b04c02f28d0cc5a3cc2", size = 20302, upload-time = "2026-02-27T00:05:40.086Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "willow"
|
name = "willow"
|
||||||
version = "1.8.0"
|
version = "1.11.0"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "defusedxml" },
|
{ name = "defusedxml" },
|
||||||
{ name = "filetype" },
|
{ name = "filetype" },
|
||||||
]
|
]
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/77/c3/9d19db05d3abe0375e8d6bf2d3f7ea6f8df266d5ddabcc5a114c8a7cce1f/willow-1.8.0.tar.gz", hash = "sha256:ef3df6cde80d4914e719188147bef1d71c240edb118340e0c5957ecc8fe08315", size = 113313, upload-time = "2024-01-17T19:11:58.712Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/a3/bd/2a383be24c3e47423aa9b0aa5b4ca818ef193506b58800dd51e1b89d7bb3/willow-1.11.0.tar.gz", hash = "sha256:70292b2d0cd2d5bb4076f0b3d61308aeaa0b225f3970d00752f08a8fd386c3d1", size = 113827, upload-time = "2025-07-16T08:46:26.939Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/95/7a/a48cf90bbf226f793d830c3bbdb7f0c4aa8087c08ce84cc864a1c882a457/willow-1.8.0-py3-none-any.whl", hash = "sha256:48ccf5ce48ccd29c37a32497cd7af50983f8570543c4de2988b15d583efc66be", size = 119223, upload-time = "2024-01-17T19:11:56.316Z" },
|
{ url = "https://files.pythonhosted.org/packages/c1/05/b3f1b443c31ad871c48e19ea2be189681c2df4ccf594b1dd83d6775c032b/willow-1.11.0-py3-none-any.whl", hash = "sha256:0a4388dbf18726eef8f27449659047689c39b7023045ca5a8a75410d3864ee6f", size = 119459, upload-time = "2025-07-16T08:46:25.596Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.optional-dependencies]
|
[package.optional-dependencies]
|
||||||
|
|||||||
@@ -0,0 +1,58 @@
|
|||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
from wagtail.admin.ui.tables import BooleanColumn, DateColumn
|
||||||
|
from wagtail.admin.ui.tables.pages import PageStatusColumn, PageTitleColumn
|
||||||
|
from wagtail.admin.viewsets.pages import PageListingViewSet, PageViewSet
|
||||||
|
|
||||||
|
from dnscms.admin import ListingRedirectChooseParentView
|
||||||
|
from venues.models import VenuePage
|
||||||
|
|
||||||
|
|
||||||
|
class VenueChooseParentView(ListingRedirectChooseParentView):
|
||||||
|
listing_url_name = "venues:index"
|
||||||
|
|
||||||
|
|
||||||
|
class VenueListingMixin:
|
||||||
|
"""Shared model + columns for the standalone listing and the page explorer."""
|
||||||
|
|
||||||
|
model = VenuePage
|
||||||
|
icon = "home"
|
||||||
|
columns = [
|
||||||
|
PageTitleColumn("title", label=_("Title"), sort_key="title", classname="title"),
|
||||||
|
BooleanColumn(
|
||||||
|
"show_as_bookable",
|
||||||
|
label=_("Rentals page"),
|
||||||
|
sort_key="show_as_bookable",
|
||||||
|
width="10%",
|
||||||
|
),
|
||||||
|
BooleanColumn(
|
||||||
|
"show_in_overview",
|
||||||
|
label=_("Venue overview"),
|
||||||
|
sort_key="show_in_overview",
|
||||||
|
width="10%",
|
||||||
|
),
|
||||||
|
DateColumn(
|
||||||
|
"latest_revision_created_at",
|
||||||
|
label=_("Updated"),
|
||||||
|
sort_key="latest_revision_created_at",
|
||||||
|
width="10%",
|
||||||
|
),
|
||||||
|
PageStatusColumn("status", label=_("Status"), sort_key="live", width="10%"),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class VenueSidebarViewSet(VenueListingMixin, PageListingViewSet):
|
||||||
|
"""Standalone 'Venues' sidebar entry, reached independently of the page tree."""
|
||||||
|
|
||||||
|
choose_parent_view_class = VenueChooseParentView
|
||||||
|
menu_label = _("Venues")
|
||||||
|
menu_order = 4
|
||||||
|
add_to_admin_menu = True
|
||||||
|
ordering = "title"
|
||||||
|
|
||||||
|
|
||||||
|
class VenueExplorerViewSet(VenueListingMixin, PageViewSet):
|
||||||
|
"""Applies the same columns when navigating into VenueIndex via the page explorer."""
|
||||||
|
|
||||||
|
|
||||||
|
venue_sidebar_viewset = VenueSidebarViewSet("venues")
|
||||||
|
venue_explorer_viewset = VenueExplorerViewSet()
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
# Generated by Django 6.0.5 on 2026-05-25 23:40
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('venues', '0024_venuepage_show_in_overview_and_more'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name='venueindex',
|
||||||
|
options={'verbose_name': 'venue index', 'verbose_name_plural': 'venue indexes'},
|
||||||
|
),
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name='venuepage',
|
||||||
|
options={'verbose_name': 'venue', 'verbose_name_plural': 'venues'},
|
||||||
|
),
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name='venuerentalindex',
|
||||||
|
options={'verbose_name': 'rentals page', 'verbose_name_plural': 'rentals pages'},
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
from django.db import models
|
from django.db import models
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
from grapple.helpers import register_singular_query_field
|
from grapple.helpers import register_singular_query_field
|
||||||
from grapple.models import (
|
from grapple.models import (
|
||||||
GraphQLBoolean,
|
GraphQLBoolean,
|
||||||
@@ -9,16 +10,21 @@ from grapple.models import (
|
|||||||
)
|
)
|
||||||
from wagtail.admin.panels import FieldPanel, FieldRowPanel, MultiFieldPanel
|
from wagtail.admin.panels import FieldPanel, FieldRowPanel, MultiFieldPanel
|
||||||
from wagtail.fields import RichTextField, StreamField
|
from wagtail.fields import RichTextField, StreamField
|
||||||
from wagtail.models import Page
|
from wagtail.models import Page, PageManager
|
||||||
from wagtail.search import index
|
from wagtail.search import index
|
||||||
|
from wagtail_headless_preview.models import HeadlessMixin
|
||||||
|
|
||||||
from dnscms.blocks import ImageSliderBlock
|
from dnscms.blocks import ImageSliderBlock
|
||||||
from dnscms.fields import CommonStreamField
|
from dnscms.fields import CommonStreamField
|
||||||
from dnscms.wordpress.models import WPImportedPageMixin
|
from dnscms.wordpress.models import DeferWPFieldsManagerMixin, WPImportedPageMixin
|
||||||
|
|
||||||
|
|
||||||
|
class VenuePageManager(DeferWPFieldsManagerMixin, PageManager):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
@register_singular_query_field("venueIndex")
|
@register_singular_query_field("venueIndex")
|
||||||
class VenueIndex(Page):
|
class VenueIndex(HeadlessMixin, Page):
|
||||||
# there can only be one venue index page
|
# there can only be one venue index page
|
||||||
max_count = 1
|
max_count = 1
|
||||||
subpage_types = ["venues.VenuePage"]
|
subpage_types = ["venues.VenuePage"]
|
||||||
@@ -33,9 +39,13 @@ class VenueIndex(Page):
|
|||||||
|
|
||||||
graphql_fields = [GraphQLRichText("lead"), GraphQLStreamfield("body")]
|
graphql_fields = [GraphQLRichText("lead"), GraphQLStreamfield("body")]
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = _("venue index")
|
||||||
|
verbose_name_plural = _("venue indexes")
|
||||||
|
|
||||||
|
|
||||||
@register_singular_query_field("venueRentalIndex")
|
@register_singular_query_field("venueRentalIndex")
|
||||||
class VenueRentalIndex(Page):
|
class VenueRentalIndex(HeadlessMixin, Page):
|
||||||
# there can only be one venue index page
|
# there can only be one venue index page
|
||||||
max_count = 1
|
max_count = 1
|
||||||
subpage_types = []
|
subpage_types = []
|
||||||
@@ -50,14 +60,20 @@ class VenueRentalIndex(Page):
|
|||||||
|
|
||||||
graphql_fields = [GraphQLRichText("lead"), GraphQLStreamfield("body")]
|
graphql_fields = [GraphQLRichText("lead"), GraphQLStreamfield("body")]
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = _("rentals page")
|
||||||
|
verbose_name_plural = _("rentals pages")
|
||||||
|
|
||||||
class VenuePage(WPImportedPageMixin, Page):
|
|
||||||
|
class VenuePage(HeadlessMixin, WPImportedPageMixin, Page):
|
||||||
# no children
|
# no children
|
||||||
subpage_types = []
|
subpage_types = []
|
||||||
parent_page_types = ["venues.VenueIndex"]
|
parent_page_types = ["venues.VenueIndex"]
|
||||||
# should not be able to be shown in menus
|
# should not be able to be shown in menus
|
||||||
show_in_menus = False
|
show_in_menus = False
|
||||||
|
|
||||||
|
objects = VenuePageManager()
|
||||||
|
|
||||||
featured_image = models.ForeignKey(
|
featured_image = models.ForeignKey(
|
||||||
"images.CustomImage",
|
"images.CustomImage",
|
||||||
null=True,
|
null=True,
|
||||||
@@ -178,41 +194,6 @@ class VenuePage(WPImportedPageMixin, Page):
|
|||||||
index.SearchField("body"),
|
index.SearchField("body"),
|
||||||
]
|
]
|
||||||
|
|
||||||
def import_wordpress_data(self, data):
|
class Meta:
|
||||||
import html
|
verbose_name = _("venue")
|
||||||
|
verbose_name_plural = _("venues")
|
||||||
# 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.show_as_bookable = meta.get("neuf_venues_show_on_booking_page", False)
|
|
||||||
self.preposition = meta.get("neuf_venues_preposition") or ""
|
|
||||||
self.floor = meta.get("neuf_venues_floor") or ""
|
|
||||||
self.used_for = meta.get("neuf_venues_used_for") or ""
|
|
||||||
|
|
||||||
self.capability_bar = meta.get("neuf_venues_bar") or ""
|
|
||||||
self.capability_audio = meta.get("neuf_venues_audio") or ""
|
|
||||||
self.capability_lighting = meta.get("neuf_venues_lighting") or ""
|
|
||||||
self.capability_audio_video = meta.get("neuf_venues_audio_video") or ""
|
|
||||||
|
|
||||||
self.capacity_legal = meta.get("neuf_venues_capacity_legal") or ""
|
|
||||||
self.capacity_standing = meta.get("neuf_venues_capacity_standing") or ""
|
|
||||||
self.capacity_sitting = meta.get("neuf_venues_capacity_sitting") or ""
|
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
from wagtail import hooks
|
||||||
|
|
||||||
|
from .admin import venue_explorer_viewset, venue_sidebar_viewset
|
||||||
|
|
||||||
|
|
||||||
|
@hooks.register("register_admin_viewset")
|
||||||
|
def register_venue_sidebar_viewset():
|
||||||
|
return venue_sidebar_viewset
|
||||||
|
|
||||||
|
|
||||||
|
@hooks.register("register_admin_viewset")
|
||||||
|
def register_venue_explorer_viewset():
|
||||||
|
return venue_explorer_viewset
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
[tools]
|
||||||
|
python = "3.14"
|
||||||
|
uv = "latest"
|
||||||
|
node = "24"
|
||||||
|
prek = "latest"
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
exclude = '^web/src/gql/'
|
||||||
|
|
||||||
|
[[repos]]
|
||||||
|
repo = "https://github.com/pre-commit/pre-commit-hooks"
|
||||||
|
rev = "v6.0.0"
|
||||||
|
hooks = [
|
||||||
|
{ id = "end-of-file-fixer" },
|
||||||
|
{ id = "trailing-whitespace" },
|
||||||
|
{ id = "check-yaml" },
|
||||||
|
{ id = "check-toml" },
|
||||||
|
{ id = "check-merge-conflict" },
|
||||||
|
{ id = "check-added-large-files" },
|
||||||
|
{ id = "debug-statements" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[repos]]
|
||||||
|
repo = "https://github.com/astral-sh/ruff-pre-commit"
|
||||||
|
rev = "v0.15.13"
|
||||||
|
|
||||||
|
[[repos.hooks]]
|
||||||
|
id = "ruff-check"
|
||||||
|
args = ["--fix"]
|
||||||
|
files = '^dnscms/.*\.py$'
|
||||||
|
exclude = '/migrations/'
|
||||||
|
|
||||||
|
[[repos.hooks]]
|
||||||
|
id = "ruff-format"
|
||||||
|
files = '^dnscms/.*\.py$'
|
||||||
|
exclude = '/migrations/'
|
||||||
@@ -1,2 +1,2 @@
|
|||||||
GRAPHQL_ENDPOINT=https://cms.neuf.no/api/graphql/
|
WAGTAIL_BASE_URL=https://cms.neuf.no
|
||||||
URL=http://localhost:3000
|
URL=http://localhost:3000
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# Based on https://github.com/vercel/next.js/tree/canary/examples/with-docker
|
# Based on https://github.com/vercel/next.js/tree/canary/examples/with-docker
|
||||||
|
|
||||||
FROM node:22-alpine AS base
|
FROM node:24-alpine AS base
|
||||||
|
|
||||||
# Install dependencies only when needed
|
# Install dependencies only when needed
|
||||||
FROM base AS deps
|
FROM base AS deps
|
||||||
|
|||||||
@@ -1,15 +0,0 @@
|
|||||||
This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app).
|
|
||||||
|
|
||||||
## Development
|
|
||||||
|
|
||||||
Run the development server:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
npm run dev
|
|
||||||
```
|
|
||||||
|
|
||||||
Update GraphQL definitions from `http://127.0.0.1:8000/api/graphql/`:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
npm run codegen
|
|
||||||
```
|
|
||||||
@@ -3,13 +3,31 @@ import { CodegenConfig } from "@graphql-codegen/cli";
|
|||||||
import { loadEnvConfig } from "@next/env";
|
import { loadEnvConfig } from "@next/env";
|
||||||
loadEnvConfig(process.cwd());
|
loadEnvConfig(process.cwd());
|
||||||
|
|
||||||
|
const wagtailBaseUrl = process.env.WAGTAIL_BASE_URL;
|
||||||
|
if (!wagtailBaseUrl) {
|
||||||
|
throw new Error("WAGTAIL_BASE_URL is not set");
|
||||||
|
}
|
||||||
|
const graphqlEndpoint = `${wagtailBaseUrl.replace(/\/$/, "")}/api/graphql/`;
|
||||||
|
|
||||||
const config: CodegenConfig = {
|
const config: CodegenConfig = {
|
||||||
schema: process.env.GRAPHQL_ENDPOINT,
|
schema: graphqlEndpoint,
|
||||||
documents: ["src/**/*.tsx", "src/**/*.ts"],
|
documents: ["src/**/*.tsx", "src/**/*.ts"],
|
||||||
ignoreNoDocuments: true, // for better experience with the watcher
|
ignoreNoDocuments: true, // for better experience with the watcher
|
||||||
generates: {
|
generates: {
|
||||||
"./src/gql/": {
|
"./src/gql/": {
|
||||||
preset: "client",
|
preset: "client",
|
||||||
|
presetConfig: {
|
||||||
|
fragmentMasking: { unmaskFunctionName: "unmaskFragment" },
|
||||||
|
},
|
||||||
|
config: {
|
||||||
|
scalars: {
|
||||||
|
DateTime: "string",
|
||||||
|
JSONString: "string",
|
||||||
|
PositiveInt: "number",
|
||||||
|
RichText: "string",
|
||||||
|
UUID: "string",
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,2 +0,0 @@
|
|||||||
[tools]
|
|
||||||
node = "22"
|
|
||||||
@@ -1,4 +1,8 @@
|
|||||||
/** @type {import('next').NextConfig} */
|
/** @type {import('next').NextConfig} */
|
||||||
|
import path from "path";
|
||||||
|
import { fileURLToPath } from "url";
|
||||||
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||||
|
|
||||||
const nextConfig = {
|
const nextConfig = {
|
||||||
output: "standalone",
|
output: "standalone",
|
||||||
images: {
|
images: {
|
||||||
@@ -12,6 +16,11 @@ const nextConfig = {
|
|||||||
hostname: "**",
|
hostname: "**",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
formats: ["image/avif", "image/webp"],
|
||||||
|
dangerouslyAllowLocalIP: process.env.NODE_ENV === "development",
|
||||||
|
},
|
||||||
|
turbopack: {
|
||||||
|
root: __dirname,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -7,34 +7,46 @@
|
|||||||
"build": "next build",
|
"build": "next build",
|
||||||
"start": "next start",
|
"start": "next start",
|
||||||
"lint": "next lint",
|
"lint": "next lint",
|
||||||
"codegen": "graphql-codegen"
|
"codegen": "graphql-codegen",
|
||||||
|
"perf:build": "next build",
|
||||||
|
"perf:serve": "next start -p 3100",
|
||||||
|
"perf:lh:desktop": "lighthouse http://localhost:3100/ --preset=desktop --output=html --output=json --output-path=../scratch/lighthouse/home-desktop --only-categories=performance --chrome-flags=\"--headless=new\" --quiet",
|
||||||
|
"perf:lh:mobile": "lighthouse http://localhost:3100/ --output=html --output=json --output-path=../scratch/lighthouse/home-mobile --only-categories=performance --chrome-flags=\"--headless=new\" --quiet",
|
||||||
|
"perf:lh": "wait-on http://localhost:3100/ && npm run perf:lh:desktop && npm run perf:lh:mobile"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@graphql-codegen/cli": "^5.0.7",
|
"@graphql-codegen/cli": "^7.0.0",
|
||||||
"@graphql-codegen/client-preset": "^4.8.3",
|
"@graphql-codegen/client-preset": "^6.0.0",
|
||||||
"@parcel/watcher": "^2.5.1",
|
"@parcel/watcher": "^2.5.6",
|
||||||
"@sindresorhus/slugify": "^2.2.1",
|
"@sindresorhus/slugify": "^3.0.0",
|
||||||
"@urql/next": "^1.1.5",
|
"@urql/next": "^2.0.0",
|
||||||
"date-fns": "^4.1.0",
|
"date-fns": "^4.1.0",
|
||||||
"date-fns-tz": "^3.2.0",
|
"date-fns-tz": "^3.2.0",
|
||||||
"graphql": "^16.11.0",
|
"graphql": "^16.14.0",
|
||||||
"next": "^15.4.6",
|
"next": "^16.2.6",
|
||||||
"nuqs": "^2.4.3",
|
"nuqs": "^2.8.9",
|
||||||
"react": "^19.1.1",
|
"react": "19.2.6",
|
||||||
"react-dom": "^19.1.1",
|
"react-dom": "19.2.6",
|
||||||
"react-intersection-observer": "^9.16.0",
|
"react-intersection-observer": "^10.0.3",
|
||||||
"sass": "^1.90.0",
|
"sass": "^1.99.0",
|
||||||
"sharp": "^0.34.3",
|
"sharp": "^0.34.5",
|
||||||
"swiper": "^11.2.10",
|
"swiper": "^12.1.4",
|
||||||
"urql": "^4.2.2",
|
"urql": "^5.0.2",
|
||||||
"use-debounce": "^10.0.5"
|
"use-debounce": "^10.1.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "^22",
|
"@types/node": "^24",
|
||||||
"@types/react": "^19.1.9",
|
"@types/react": "19.2.14",
|
||||||
"@types/react-dom": "^19.1.7",
|
"@types/react-dom": "19.2.3",
|
||||||
|
"baseline-browser-mapping": "^2.10.29",
|
||||||
"eslint": "^9",
|
"eslint": "^9",
|
||||||
"eslint-config-next": "15.4.6",
|
"eslint-config-next": "16.2.6",
|
||||||
"typescript": "^5"
|
"lighthouse": "^13.3.0",
|
||||||
|
"typescript": "^6",
|
||||||
|
"wait-on": "^9.0.10"
|
||||||
|
},
|
||||||
|
"overrides": {
|
||||||
|
"@types/react": "19.2.14",
|
||||||
|
"@types/react-dom": "19.2.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
|
After Width: | Height: | Size: 221 KiB |
|
After Width: | Height: | Size: 432 KiB |
|
After Width: | Height: | Size: 95 KiB |
@@ -0,0 +1,51 @@
|
|||||||
|
grisemonster image variants
|
||||||
|
===========================
|
||||||
|
|
||||||
|
Source: grisemonster-lossless-original.png (2048x1024 RGBA PNG, 2.0M)
|
||||||
|
|
||||||
|
Files in use
|
||||||
|
------------
|
||||||
|
grisemonster-2048.png 443K desktop (>1280px viewport)
|
||||||
|
grisemonster-1280.png 227K tablet (601-1280px)
|
||||||
|
grisemonster-800.png 97K mobile (<=600px)
|
||||||
|
|
||||||
|
The previous single grisemonster.png was 624K served to every device.
|
||||||
|
Mobile now downloads 97K (-84%); desktop downloads 443K (-29%).
|
||||||
|
|
||||||
|
How they were generated
|
||||||
|
-----------------------
|
||||||
|
For each width W in {2048, 1280, 800}:
|
||||||
|
|
||||||
|
magick grisemonster-lossless-original.png -resize ${W}x base.png
|
||||||
|
pngquant 64 --quality=50-80 --speed 1 --output grisemonster-${W}.png base.png
|
||||||
|
oxipng -o max --strip safe grisemonster-${W}.png
|
||||||
|
|
||||||
|
pngquant reduces to a 64-colour palette (the image is a flat
|
||||||
|
illustration with very few distinct colours, so this is visually
|
||||||
|
lossless at the displayed scale). oxipng then does a lossless
|
||||||
|
re-encode pass to squeeze the PNG further.
|
||||||
|
|
||||||
|
Wired up in src/components/layout/footer.module.scss via two
|
||||||
|
min-width media queries on `.pigPattern`'s background-image.
|
||||||
|
|
||||||
|
Why PNG, not WebP/AVIF
|
||||||
|
----------------------
|
||||||
|
Tested at 2048 width:
|
||||||
|
|
||||||
|
PNG (pngquant 64 + oxipng) 443K <-- chosen
|
||||||
|
PNG (pngquant 256 + oxipng) 522K
|
||||||
|
AVIF q50 493K
|
||||||
|
AVIF q60 598K
|
||||||
|
AVIF q75 718K
|
||||||
|
WebP q75 829K larger than current PNG
|
||||||
|
WebP q85 903K larger than current PNG
|
||||||
|
WebP lossless 1.0M larger than current PNG
|
||||||
|
|
||||||
|
For this kind of content -- a flat, limited-palette illustration with
|
||||||
|
large transparent regions -- palette PNG is the smallest format.
|
||||||
|
WebP and AVIF are tuned for photographic content and lose to a
|
||||||
|
well-quantised PNG here. Visual quality of the three PNG/AVIF options
|
||||||
|
above was indistinguishable at the rendered scale.
|
||||||
|
|
||||||
|
Conclusion: the win came from responsive sizing, not from changing
|
||||||
|
format. Stayed on PNG to keep the CSS simple (no image-set() needed).
|
||||||
|
Before Width: | Height: | Size: 610 KiB |
|
After Width: | Height: | Size: 32 KiB |
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 15 KiB |
@@ -1,41 +1,15 @@
|
|||||||
import { graphql } from "@/gql";
|
import { graphql } from "@/gql";
|
||||||
import { GenericFragment } from "@/gql/graphql";
|
|
||||||
import { getClient } from "@/app/client";
|
import { getClient } from "@/app/client";
|
||||||
import { Metadata, ResolvingMetadata } from "next";
|
import { Metadata, ResolvingMetadata } from "next";
|
||||||
import { notFound } from "next/navigation";
|
import { notFound } from "next/navigation";
|
||||||
import { PageHeader } from "@/components/general/PageHeader";
|
import {
|
||||||
import { PageContent } from "@/components/general/PageContent";
|
GenericPageView,
|
||||||
import { BgPig } from "@/components/general/BgPig";
|
loadGenericPageProps,
|
||||||
|
} from "@/components/general/GenericPageView";
|
||||||
import { getSeoMetadata } from "@/lib/seo";
|
import { getSeoMetadata } from "@/lib/seo";
|
||||||
|
|
||||||
export const dynamicParams = false;
|
export const dynamicParams = false;
|
||||||
|
|
||||||
const GenericFragmentDefinition = graphql(`
|
|
||||||
fragment Generic on GenericPage {
|
|
||||||
__typename
|
|
||||||
id
|
|
||||||
urlPath
|
|
||||||
seoTitle
|
|
||||||
searchDescription
|
|
||||||
title
|
|
||||||
lead
|
|
||||||
pig
|
|
||||||
body {
|
|
||||||
...Blocks
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`);
|
|
||||||
|
|
||||||
const genericPageByUrlPathQuery = graphql(`
|
|
||||||
query genericPageByUrl($urlPath: String!) {
|
|
||||||
page: page(contentType: "generic.GenericPage", urlPath: $urlPath) {
|
|
||||||
... on GenericPage {
|
|
||||||
...Generic
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`);
|
|
||||||
|
|
||||||
function getWagtailUrlPath(url: string[]): string {
|
function getWagtailUrlPath(url: string[]): string {
|
||||||
// for the page /foo/bar we need to look for `/home/foo/bar/`
|
// for the page /foo/bar we need to look for `/home/foo/bar/`
|
||||||
return `/home/${url.join("/")}/`;
|
return `/home/${url.join("/")}/`;
|
||||||
@@ -78,46 +52,14 @@ export async function generateMetadata(
|
|||||||
parent: ResolvingMetadata
|
parent: ResolvingMetadata
|
||||||
): Promise<Metadata | null> {
|
): Promise<Metadata | null> {
|
||||||
const { url } = await params;
|
const { url } = await params;
|
||||||
const urlPath = getWagtailUrlPath(url);
|
const props = await loadGenericPageProps({ urlPath: getWagtailUrlPath(url) });
|
||||||
const { data, error } = await getClient().query(genericPageByUrlPathQuery, {
|
if (!props) return null;
|
||||||
urlPath: urlPath,
|
return getSeoMetadata(props.page, parent);
|
||||||
});
|
|
||||||
|
|
||||||
if (error) {
|
|
||||||
throw new Error(error.message);
|
|
||||||
}
|
|
||||||
if (!data?.page) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const page = data.page as GenericFragment;
|
|
||||||
const metadata = await getSeoMetadata(page, parent);
|
|
||||||
return metadata;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default async function Page({ params }: { params: Params }) {
|
export default async function Page({ params }: { params: Params }) {
|
||||||
const { url } = await params;
|
const { url } = await params;
|
||||||
const urlPath = getWagtailUrlPath(url);
|
const props = await loadGenericPageProps({ urlPath: getWagtailUrlPath(url) });
|
||||||
const { data, error } = await getClient().query(genericPageByUrlPathQuery, {
|
if (!props) return notFound();
|
||||||
urlPath: urlPath,
|
return <GenericPageView {...props} />;
|
||||||
});
|
|
||||||
if (error) {
|
|
||||||
throw new Error(error.message);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!data?.page) {
|
|
||||||
return notFound();
|
|
||||||
}
|
|
||||||
|
|
||||||
const page = data?.page as GenericFragment;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<main className="site-main" id="main">
|
|
||||||
<PageHeader heading={page.title} lead={page.lead} />
|
|
||||||
<PageContent blocks={page.body} />
|
|
||||||
</main>
|
|
||||||
{page.pig && <BgPig type={page.pig} color="white" />}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,24 +1,13 @@
|
|||||||
import { Metadata, ResolvingMetadata } from "next";
|
import { Metadata, ResolvingMetadata } from "next";
|
||||||
import { notFound } from "next/navigation";
|
import { notFound } from "next/navigation";
|
||||||
import { getClient } from "@/app/client";
|
import { getClient } from "@/app/client";
|
||||||
import { Breadcrumb } from "@/components/general/Breadcrumb";
|
import {
|
||||||
import { ImageFigure } from "@/components/general/Image";
|
NewsPageView,
|
||||||
import { PageContent } from "@/components/general/PageContent";
|
loadNewsPageProps,
|
||||||
|
} from "@/components/news/NewsPageView";
|
||||||
import { graphql } from "@/gql";
|
import { graphql } from "@/gql";
|
||||||
import { NewsFragment } from "@/gql/graphql";
|
|
||||||
import { formatDate } from "@/lib/date";
|
|
||||||
import { getSeoMetadata } from "@/lib/seo";
|
import { getSeoMetadata } from "@/lib/seo";
|
||||||
|
|
||||||
const newsBySlugQuery = graphql(`
|
|
||||||
query newsBySlug($slug: String!) {
|
|
||||||
news: page(contentType: "news.NewsPage", slug: $slug) {
|
|
||||||
... on NewsPage {
|
|
||||||
...News
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`);
|
|
||||||
|
|
||||||
export async function generateStaticParams() {
|
export async function generateStaticParams() {
|
||||||
const allNewsSlugsQuery = graphql(`
|
const allNewsSlugsQuery = graphql(`
|
||||||
query allNewsSlugs {
|
query allNewsSlugs {
|
||||||
@@ -51,62 +40,14 @@ export async function generateMetadata(
|
|||||||
parent: ResolvingMetadata
|
parent: ResolvingMetadata
|
||||||
): Promise<Metadata | null> {
|
): Promise<Metadata | null> {
|
||||||
const { slug } = await params;
|
const { slug } = await params;
|
||||||
const { data, error } = await getClient().query(newsBySlugQuery, {
|
const props = await loadNewsPageProps({ slug });
|
||||||
slug,
|
if (!props) return null;
|
||||||
});
|
return getSeoMetadata(props.news, parent);
|
||||||
if (error) {
|
|
||||||
throw new Error(error.message);
|
|
||||||
}
|
|
||||||
if (!data?.news) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
const news = data.news as NewsFragment;
|
|
||||||
const metadata = await getSeoMetadata(news, parent);
|
|
||||||
return metadata;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default async function Page({ params }: { params: Params }) {
|
export default async function Page({ params }: { params: Params }) {
|
||||||
const { slug } = await params;
|
const { slug } = await params;
|
||||||
const { data, error } = await getClient().query(newsBySlugQuery, {
|
const props = await loadNewsPageProps({ slug });
|
||||||
slug,
|
if (!props) return notFound();
|
||||||
});
|
return <NewsPageView {...props} />;
|
||||||
|
|
||||||
if (error) {
|
|
||||||
throw new Error(error.message);
|
|
||||||
}
|
|
||||||
if (!data?.news) {
|
|
||||||
return notFound();
|
|
||||||
}
|
|
||||||
|
|
||||||
const news = data?.news as NewsFragment;
|
|
||||||
const featuredImage: any = news.featuredImage;
|
|
||||||
return (
|
|
||||||
<main className="site-main" id="main">
|
|
||||||
<section className="news-header">
|
|
||||||
<Breadcrumb
|
|
||||||
link="/aktuelt"
|
|
||||||
text="Nyhet"
|
|
||||||
date={formatDate(news.firstPublishedAt, "d. MMMM yyyy")}
|
|
||||||
/>
|
|
||||||
<h1 className="news-title">{news.title}</h1>
|
|
||||||
{news.lead && (
|
|
||||||
<div
|
|
||||||
className="lead"
|
|
||||||
dangerouslySetInnerHTML={{ __html: news.lead }}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{featuredImage && (
|
|
||||||
<ImageFigure
|
|
||||||
src={featuredImage.url}
|
|
||||||
alt={featuredImage.alt ?? ""}
|
|
||||||
width={featuredImage.width}
|
|
||||||
height={featuredImage.height}
|
|
||||||
attribution={featuredImage.attribution}
|
|
||||||
sizes="100vw"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</section>
|
|
||||||
<PageContent blocks={news.body} />
|
|
||||||
</main>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,39 +1,24 @@
|
|||||||
import { getClient } from "@/app/client";
|
|
||||||
import { NewsList } from "@/components/news/NewsList";
|
|
||||||
import { Metadata, ResolvingMetadata } from "next";
|
import { Metadata, ResolvingMetadata } from "next";
|
||||||
import { PageHeader } from "@/components/general/PageHeader";
|
import { getClient } from "@/app/client";
|
||||||
import { newsQuery, NewsFragment, NewsIndexFragment } from "@/lib/news";
|
import {
|
||||||
|
NewsIndexView,
|
||||||
|
loadNewsIndexProps,
|
||||||
|
} from "@/components/news/NewsIndexView";
|
||||||
|
import { NewsIndexFragment } from "@/gql/graphql";
|
||||||
|
import { newsIndexMetadataQuery } from "@/lib/news";
|
||||||
import { getSeoMetadata } from "@/lib/seo";
|
import { getSeoMetadata } from "@/lib/seo";
|
||||||
|
|
||||||
export async function generateMetadata(
|
export async function generateMetadata(
|
||||||
{ params }: { params: Promise<{}> },
|
_: unknown,
|
||||||
parent: ResolvingMetadata
|
parent: ResolvingMetadata
|
||||||
): Promise<Metadata | null> {
|
): Promise<Metadata | null> {
|
||||||
const { data, error } = await getClient().query(newsQuery, {});
|
const { data, error } = await getClient().query(newsIndexMetadataQuery, {});
|
||||||
if (error) {
|
if (error) throw new Error(error.message);
|
||||||
throw new Error(error.message);
|
if (!data?.index) return null;
|
||||||
}
|
return getSeoMetadata(data.index as NewsIndexFragment, parent);
|
||||||
if (!data?.index) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const index = data.index as NewsIndexFragment;
|
|
||||||
const metadata = await getSeoMetadata(index, parent);
|
|
||||||
return metadata;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default async function Page() {
|
export default async function Page() {
|
||||||
const { data, error } = await getClient().query(newsQuery, {});
|
const props = await loadNewsIndexProps();
|
||||||
if (error) {
|
return <NewsIndexView {...props} />;
|
||||||
throw new Error(error.message);
|
|
||||||
}
|
|
||||||
const news = (data?.news ?? []) as NewsFragment[];
|
|
||||||
const index = data?.index as NewsIndexFragment;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<main className="site-main" id="main">
|
|
||||||
<PageHeader heading={index.title} lead={index.lead} align="left" />
|
|
||||||
<NewsList news={news} />
|
|
||||||
</main>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import { cookies, draftMode } from "next/headers";
|
||||||
|
|
||||||
|
export async function POST() {
|
||||||
|
(await draftMode()).disable();
|
||||||
|
(await cookies()).delete("preview-token");
|
||||||
|
return new Response(null, { status: 204 });
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import { cookies, draftMode } from "next/headers";
|
||||||
|
import { redirect } from "next/navigation";
|
||||||
|
import { NextRequest } from "next/server";
|
||||||
|
|
||||||
|
// Wagtail-headless-preview directs the editor's preview iframe here with
|
||||||
|
// ?content_type=app.Model&token=<signed>. We stash the token in a cookie,
|
||||||
|
// enable Next.js draft mode, and redirect to the type-dispatching renderer.
|
||||||
|
export async function GET(req: NextRequest) {
|
||||||
|
const token = req.nextUrl.searchParams.get("token");
|
||||||
|
const contentType = req.nextUrl.searchParams.get("content_type");
|
||||||
|
if (!token || !contentType) {
|
||||||
|
return new Response("missing token/content_type", { status: 400 });
|
||||||
|
}
|
||||||
|
|
||||||
|
(await draftMode()).enable();
|
||||||
|
(await cookies()).set("preview-token", token, {
|
||||||
|
httpOnly: true,
|
||||||
|
sameSite: "lax",
|
||||||
|
secure: process.env.NODE_ENV === "production",
|
||||||
|
maxAge: 60 * 60 * 24,
|
||||||
|
path: "/",
|
||||||
|
});
|
||||||
|
|
||||||
|
redirect("/preview/render");
|
||||||
|
}
|
||||||
@@ -4,7 +4,7 @@ import {
|
|||||||
eventsOverviewQuery,
|
eventsOverviewQuery,
|
||||||
getSingularEvents,
|
getSingularEvents,
|
||||||
getFutureOccurrences,
|
getFutureOccurrences,
|
||||||
EventFragment,
|
EventOverviewItemFragment,
|
||||||
EventCategory,
|
EventCategory,
|
||||||
EventOrganizer,
|
EventOrganizer,
|
||||||
} from "@/lib/event";
|
} from "@/lib/event";
|
||||||
@@ -59,7 +59,7 @@ export async function GET(req: NextRequest) {
|
|||||||
throw new Error("Failed to fetch events");
|
throw new Error("Failed to fetch events");
|
||||||
}
|
}
|
||||||
|
|
||||||
const futureEvents = (data?.events?.futureEvents ?? []) as EventFragment[];
|
const futureEvents = (data?.events?.futureEvents ?? []) as EventOverviewItemFragment[];
|
||||||
const eventCategories = (data?.eventCategories ?? []) as EventCategory[];
|
const eventCategories = (data?.eventCategories ?? []) as EventCategory[];
|
||||||
const eventOrganizers = (data?.eventOrganizers ?? []) as EventOrganizer[];
|
const eventOrganizers = (data?.eventOrganizers ?? []) as EventOrganizer[];
|
||||||
const venues = (data?.venues ?? []) as VenueFragment[];
|
const venues = (data?.venues ?? []) as VenueFragment[];
|
||||||
|
|||||||
@@ -1,25 +1,13 @@
|
|||||||
import { Metadata, ResolvingMetadata } from "next";
|
import { Metadata, ResolvingMetadata } from "next";
|
||||||
import { notFound } from "next/navigation";
|
import { notFound } from "next/navigation";
|
||||||
import { getClient } from "@/app/client";
|
import { getClient } from "@/app/client";
|
||||||
import { EventDetails } from "@/components/events/EventDetails";
|
import {
|
||||||
import { EventHeader } from "@/components/events/EventHeader";
|
EventPageView,
|
||||||
import { BgPig } from "@/components/general/BgPig";
|
loadEventPageProps,
|
||||||
import { PageContent } from "@/components/general/PageContent";
|
} from "@/components/events/EventPageView";
|
||||||
import { graphql } from "@/gql";
|
import { graphql } from "@/gql";
|
||||||
import { EventFragment } from "@/gql/graphql";
|
|
||||||
import { getEventPig } from "@/lib/event";
|
|
||||||
import { getSeoMetadata } from "@/lib/seo";
|
import { getSeoMetadata } from "@/lib/seo";
|
||||||
|
|
||||||
const eventBySlugQuery = graphql(`
|
|
||||||
query eventBySlug($slug: String!) {
|
|
||||||
event: page(contentType: "events.EventPage", slug: $slug) {
|
|
||||||
... on EventPage {
|
|
||||||
...Event
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`);
|
|
||||||
|
|
||||||
export async function generateStaticParams() {
|
export async function generateStaticParams() {
|
||||||
const allEventSlugsQuery = graphql(`
|
const allEventSlugsQuery = graphql(`
|
||||||
query allEventSlugs {
|
query allEventSlugs {
|
||||||
@@ -52,52 +40,14 @@ export async function generateMetadata(
|
|||||||
parent: ResolvingMetadata
|
parent: ResolvingMetadata
|
||||||
): Promise<Metadata | null> {
|
): Promise<Metadata | null> {
|
||||||
const { slug } = await params;
|
const { slug } = await params;
|
||||||
const { data, error } = await getClient().query(eventBySlugQuery, {
|
const props = await loadEventPageProps({ slug });
|
||||||
slug,
|
if (!props) return null;
|
||||||
});
|
return getSeoMetadata(props.event, parent);
|
||||||
|
|
||||||
if (error) {
|
|
||||||
throw new Error(error.message);
|
|
||||||
}
|
|
||||||
if (!data?.event) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const event = data.event as EventFragment;
|
|
||||||
const metadata = await getSeoMetadata(event, parent);
|
|
||||||
return metadata;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default async function Page({ params }: { params: Params }) {
|
export default async function Page({ params }: { params: Params }) {
|
||||||
const { slug } = await params;
|
const { slug } = await params;
|
||||||
const { data, error } = await getClient().query(eventBySlugQuery, {
|
const props = await loadEventPageProps({ slug });
|
||||||
slug,
|
if (!props) return notFound();
|
||||||
});
|
return <EventPageView {...props} />;
|
||||||
|
|
||||||
if (error) {
|
|
||||||
throw new Error(error.message);
|
|
||||||
}
|
|
||||||
if (!data?.event) {
|
|
||||||
return notFound();
|
|
||||||
}
|
|
||||||
|
|
||||||
const event = data.event as EventFragment;
|
|
||||||
const eventPig = getEventPig(event);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<main className="site-main" id="main">
|
|
||||||
<EventHeader event={event} />
|
|
||||||
<EventDetails event={event} />
|
|
||||||
{event.lead && (
|
|
||||||
<div
|
|
||||||
className="lead event-lead"
|
|
||||||
dangerouslySetInnerHTML={{ __html: event.lead }}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<PageContent blocks={event.body} />
|
|
||||||
</main>
|
|
||||||
{eventPig && <BgPig type={eventPig} color="white" />}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,68 +1,24 @@
|
|||||||
import { Suspense } from "react";
|
|
||||||
import { Metadata, ResolvingMetadata } from "next";
|
import { Metadata, ResolvingMetadata } from "next";
|
||||||
import { getClient } from "@/app/client";
|
import { getClient } from "@/app/client";
|
||||||
import { EventContainer } from "@/components/events/EventContainer";
|
|
||||||
import {
|
import {
|
||||||
eventsOverviewQuery,
|
EventIndexView,
|
||||||
eventIndexMetadataQuery,
|
loadEventIndexProps,
|
||||||
EventFragment,
|
} from "@/components/events/EventIndexView";
|
||||||
EventCategory,
|
import { EventIndexFragment } from "@/gql/graphql";
|
||||||
EventOrganizer,
|
import { eventIndexMetadataQuery } from "@/lib/event";
|
||||||
} from "@/lib/event";
|
|
||||||
import { PageHeader } from "@/components/general/PageHeader";
|
|
||||||
import { EventIndexFragment, VenueFragment } from "@/gql/graphql";
|
|
||||||
import { getSeoMetadata } from "@/lib/seo";
|
import { getSeoMetadata } from "@/lib/seo";
|
||||||
|
|
||||||
export async function generateMetadata(
|
export async function generateMetadata(
|
||||||
{ params }: { params: Promise<{}> },
|
_: unknown,
|
||||||
parent: ResolvingMetadata
|
parent: ResolvingMetadata
|
||||||
): Promise<Metadata | null> {
|
): Promise<Metadata | null> {
|
||||||
const { data, error } = await getClient().query(eventIndexMetadataQuery, {});
|
const { data, error } = await getClient().query(eventIndexMetadataQuery, {});
|
||||||
|
if (error) throw new Error(error.message);
|
||||||
if (error) {
|
if (!data?.index) return null;
|
||||||
throw new Error(error.message);
|
return getSeoMetadata(data.index as EventIndexFragment, parent);
|
||||||
}
|
|
||||||
if (!data?.index) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const index = data.index as EventIndexFragment;
|
|
||||||
const metadata = await getSeoMetadata(index, parent);
|
|
||||||
return metadata;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default async function Page() {
|
export default async function Page() {
|
||||||
const { data, error } = await getClient().query(eventsOverviewQuery, {});
|
const props = await loadEventIndexProps();
|
||||||
if (error) {
|
return <EventIndexView {...props} />;
|
||||||
throw new Error(error.message);
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
!data?.index ||
|
|
||||||
!data?.events?.futureEvents ||
|
|
||||||
!data?.eventCategories ||
|
|
||||||
!data?.eventOrganizers ||
|
|
||||||
!data?.venues
|
|
||||||
) {
|
|
||||||
throw new Error("Failed to render /arrangementer");
|
|
||||||
}
|
|
||||||
|
|
||||||
const index = data?.index as EventIndexFragment;
|
|
||||||
const events = (data?.events?.futureEvents ?? []) as EventFragment[];
|
|
||||||
const eventCategories = (data?.eventCategories ?? []) as EventCategory[];
|
|
||||||
const eventOrganizers = (data?.eventOrganizers ?? []) as EventOrganizer[];
|
|
||||||
const venues = (data?.venues ?? []) as VenueFragment[];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<main className="site-main" id="main">
|
|
||||||
<PageHeader heading="Dette skjer på Chateau Neuf" align="left" />
|
|
||||||
<Suspense>
|
|
||||||
<EventContainer
|
|
||||||
events={events}
|
|
||||||
eventCategories={eventCategories}
|
|
||||||
eventOrganizers={eventOrganizers}
|
|
||||||
venues={venues}
|
|
||||||
/>
|
|
||||||
</Suspense>
|
|
||||||
</main>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,9 +3,15 @@ import "server-only";
|
|||||||
import { cacheExchange, createClient, fetchExchange } from "@urql/core";
|
import { cacheExchange, createClient, fetchExchange } from "@urql/core";
|
||||||
import { registerUrql } from "@urql/next/rsc";
|
import { registerUrql } from "@urql/next/rsc";
|
||||||
|
|
||||||
|
const wagtailBaseUrl = process.env.WAGTAIL_BASE_URL;
|
||||||
|
if (!wagtailBaseUrl) {
|
||||||
|
throw new Error("WAGTAIL_BASE_URL is not set");
|
||||||
|
}
|
||||||
|
const graphqlEndpoint = `${wagtailBaseUrl.replace(/\/$/, "")}/api/graphql/`;
|
||||||
|
|
||||||
const makeClient = () => {
|
const makeClient = () => {
|
||||||
return createClient({
|
return createClient({
|
||||||
url: process.env.GRAPHQL_ENDPOINT ?? "",
|
url: graphqlEndpoint,
|
||||||
exchanges: [cacheExchange, fetchExchange],
|
exchanges: [cacheExchange, fetchExchange],
|
||||||
// requestPolicy: "network-only",
|
// requestPolicy: "network-only",
|
||||||
fetchOptions: { next: { revalidate: 0 } },
|
fetchOptions: { next: { revalidate: 0 } },
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 314 KiB After Width: | Height: | Size: 13 KiB |
@@ -1,25 +1,13 @@
|
|||||||
import { Metadata, ResolvingMetadata } from "next";
|
import { Metadata, ResolvingMetadata } from "next";
|
||||||
import { notFound } from "next/navigation";
|
import { notFound } from "next/navigation";
|
||||||
import { getClient } from "@/app/client";
|
import { getClient } from "@/app/client";
|
||||||
import { AssociationHeader } from "@/components/associations/AssociationHeader";
|
import {
|
||||||
import { PageContent } from "@/components/general/PageContent";
|
AssociationPageView,
|
||||||
|
loadAssociationPageProps,
|
||||||
|
} from "@/components/associations/AssociationPageView";
|
||||||
import { graphql } from "@/gql";
|
import { graphql } from "@/gql";
|
||||||
import { AssociationFragment } from "@/gql/graphql";
|
|
||||||
import { getSeoMetadata } from "@/lib/seo";
|
import { getSeoMetadata } from "@/lib/seo";
|
||||||
|
|
||||||
const associationBySlugQuery = graphql(`
|
|
||||||
query associationBySlug($slug: String!) {
|
|
||||||
association: page(
|
|
||||||
contentType: "associations.AssociationPage"
|
|
||||||
slug: $slug
|
|
||||||
) {
|
|
||||||
... on AssociationPage {
|
|
||||||
...Association
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`);
|
|
||||||
|
|
||||||
type Params = Promise<{ slug: string }>;
|
type Params = Promise<{ slug: string }>;
|
||||||
|
|
||||||
export async function generateMetadata(
|
export async function generateMetadata(
|
||||||
@@ -27,20 +15,9 @@ export async function generateMetadata(
|
|||||||
parent: ResolvingMetadata
|
parent: ResolvingMetadata
|
||||||
): Promise<Metadata | null> {
|
): Promise<Metadata | null> {
|
||||||
const { slug } = await params;
|
const { slug } = await params;
|
||||||
const { data, error } = await getClient().query(associationBySlugQuery, {
|
const props = await loadAssociationPageProps({ slug });
|
||||||
slug,
|
if (!props) return null;
|
||||||
});
|
return getSeoMetadata(props.association, parent);
|
||||||
|
|
||||||
if (error) {
|
|
||||||
throw new Error(error.message);
|
|
||||||
}
|
|
||||||
if (!data?.association) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const association = data.association as AssociationFragment;
|
|
||||||
const metadata = await getSeoMetadata(association, parent);
|
|
||||||
return metadata;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function generateStaticParams() {
|
export async function generateStaticParams() {
|
||||||
@@ -70,23 +47,7 @@ export async function generateStaticParams() {
|
|||||||
|
|
||||||
export default async function Page({ params }: { params: Params }) {
|
export default async function Page({ params }: { params: Params }) {
|
||||||
const { slug } = await params;
|
const { slug } = await params;
|
||||||
const { data, error } = await getClient().query(associationBySlugQuery, {
|
const props = await loadAssociationPageProps({ slug });
|
||||||
slug,
|
if (!props) return notFound();
|
||||||
});
|
return <AssociationPageView {...props} />;
|
||||||
|
|
||||||
if (error) {
|
|
||||||
throw new Error(error.message);
|
|
||||||
}
|
|
||||||
if (!data?.association) {
|
|
||||||
return notFound();
|
|
||||||
}
|
|
||||||
|
|
||||||
const association = data.association as AssociationFragment;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<main className="site-main" id="main">
|
|
||||||
<AssociationHeader association={association} />
|
|
||||||
<PageContent blocks={association.body} />
|
|
||||||
</main>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,106 +1,19 @@
|
|||||||
import { Metadata, ResolvingMetadata } from "next";
|
import { Metadata, ResolvingMetadata } from "next";
|
||||||
import { graphql } from "@/gql";
|
import {
|
||||||
import { AssociationFragment, AssociationIndexFragment } from "@/gql/graphql";
|
AssociationIndexView,
|
||||||
import { getClient } from "@/app/client";
|
loadAssociationIndexProps,
|
||||||
import { AssociationList } from "@/components/associations/AssociationList";
|
} from "@/components/associations/AssociationIndexView";
|
||||||
import { PageHeader } from "@/components/general/PageHeader";
|
|
||||||
import { PageContent } from "@/components/general/PageContent";
|
|
||||||
import { getSeoMetadata } from "@/lib/seo";
|
import { getSeoMetadata } from "@/lib/seo";
|
||||||
|
|
||||||
const allAssociationsQuery = graphql(`
|
|
||||||
query allAssociations {
|
|
||||||
index: associationIndex {
|
|
||||||
... on AssociationIndex {
|
|
||||||
...AssociationIndex
|
|
||||||
}
|
|
||||||
}
|
|
||||||
associations: pages(
|
|
||||||
contentType: "associations.AssociationPage"
|
|
||||||
limit: 1000
|
|
||||||
) {
|
|
||||||
... on AssociationPage {
|
|
||||||
...Association
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`);
|
|
||||||
|
|
||||||
export async function generateMetadata(
|
export async function generateMetadata(
|
||||||
{ params }: { params: Promise<{}> },
|
_: unknown,
|
||||||
parent: ResolvingMetadata
|
parent: ResolvingMetadata
|
||||||
): Promise<Metadata | null> {
|
): Promise<Metadata | null> {
|
||||||
const { data, error } = await getClient().query(allAssociationsQuery, {});
|
const { index } = await loadAssociationIndexProps();
|
||||||
|
return getSeoMetadata(index, parent);
|
||||||
if (error) {
|
|
||||||
throw new Error(error.message);
|
|
||||||
}
|
|
||||||
if (!data?.index) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const index = data.index as AssociationIndexFragment;
|
|
||||||
const metadata = await getSeoMetadata(index, parent);
|
|
||||||
return metadata;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const AssociationIndexDefinition = graphql(`
|
|
||||||
fragment AssociationIndex on AssociationIndex {
|
|
||||||
... on AssociationIndex {
|
|
||||||
title
|
|
||||||
seoTitle
|
|
||||||
searchDescription
|
|
||||||
lead
|
|
||||||
body {
|
|
||||||
...Blocks
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`);
|
|
||||||
|
|
||||||
const AssociationFragmentDefinition = graphql(`
|
|
||||||
fragment Association on AssociationPage {
|
|
||||||
__typename
|
|
||||||
id
|
|
||||||
slug
|
|
||||||
title
|
|
||||||
seoTitle
|
|
||||||
searchDescription
|
|
||||||
excerpt
|
|
||||||
lead
|
|
||||||
body {
|
|
||||||
...Blocks
|
|
||||||
}
|
|
||||||
logo {
|
|
||||||
url
|
|
||||||
width
|
|
||||||
height
|
|
||||||
}
|
|
||||||
associationType
|
|
||||||
websiteUrl
|
|
||||||
}
|
|
||||||
`);
|
|
||||||
|
|
||||||
export default async function Page() {
|
export default async function Page() {
|
||||||
const { data, error } = await getClient().query(allAssociationsQuery, {});
|
const props = await loadAssociationIndexProps();
|
||||||
|
return <AssociationIndexView {...props} />;
|
||||||
if (error) {
|
|
||||||
throw new Error(error.message);
|
|
||||||
}
|
|
||||||
if (!data?.associations || !data.index) {
|
|
||||||
throw new Error("Failed to render /foreninger");
|
|
||||||
}
|
|
||||||
|
|
||||||
const associations = data.associations as AssociationFragment[];
|
|
||||||
const index = data.index as AssociationIndexFragment;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<main className="site-main" id="main">
|
|
||||||
<PageHeader heading={index.title} lead={index.lead} />
|
|
||||||
{index.body && <PageContent blocks={index.body} />}
|
|
||||||
<AssociationList
|
|
||||||
associations={associations}
|
|
||||||
heading="Foreninger og utvalg"
|
|
||||||
/>
|
|
||||||
</main>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
|
After Width: | Height: | Size: 24 KiB |