Scaffold containerized Django/Wagtail app with core features

This commit is contained in:
Codex_B
2026-02-28 11:52:59 +00:00
parent 62323abd62
commit b5f0f40c4c
84 changed files with 1647 additions and 0 deletions

0
apps/core/__init__.py Normal file
View File

6
apps/core/apps.py Normal file
View File

@@ -0,0 +1,6 @@
from django.apps import AppConfig
class CoreConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField"
name = "apps.core"

59
apps/core/consent.py Normal file
View File

@@ -0,0 +1,59 @@
from __future__ import annotations
import time
from dataclasses import dataclass
from urllib.parse import parse_qs, urlencode
from django.conf import settings
CONSENT_COOKIE_NAME = "nhAiConsent"
@dataclass
class ConsentState:
analytics: bool = False
advertising: bool = False
policy_version: int = 0
timestamp: int = 0
@property
def requires_prompt(self) -> bool:
return self.policy_version != settings.CONSENT_POLICY_VERSION
class ConsentService:
@staticmethod
def get_consent(request) -> ConsentState:
raw = request.COOKIES.get(CONSENT_COOKIE_NAME, "")
if not raw:
return ConsentState()
try:
data = {k: v[0] for k, v in parse_qs(raw).items()}
return ConsentState(
analytics=data.get("a", "0") == "1",
advertising=data.get("d", "0") == "1",
policy_version=int(data.get("v", "0")),
timestamp=int(data.get("ts", "0")),
)
except (ValueError, AttributeError):
return ConsentState()
@staticmethod
def set_consent(response, *, analytics: bool, advertising: bool) -> None:
payload = urlencode(
{
"a": int(analytics),
"d": int(advertising),
"v": settings.CONSENT_POLICY_VERSION,
"ts": int(time.time()),
}
)
response.set_cookie(
CONSENT_COOKIE_NAME,
payload,
max_age=60 * 60 * 24 * 365,
httponly=False,
samesite="Lax",
secure=not settings.DEBUG,
)

View File

@@ -0,0 +1,9 @@
from wagtail.models import Site
from apps.core.models import SiteSettings
def site_settings(request):
site = Site.find_for_request(request)
settings_obj = SiteSettings.for_site(site) if site else None
return {"site_settings": settings_obj}

12
apps/core/middleware.py Normal file
View File

@@ -0,0 +1,12 @@
from __future__ import annotations
from .consent import ConsentService
class ConsentMiddleware:
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
request.consent = ConsentService.get_consent(request)
return self.get_response(request)

View File

@@ -0,0 +1,29 @@
# Generated by Django 5.2.11 on 2026-02-28 11:42
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
('wagtailcore', '0094_alter_page_locale'),
('wagtailimages', '0027_image_description'),
]
operations = [
migrations.CreateModel(
name='SiteSettings',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('default_og_image', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='wagtailimages.image')),
('privacy_policy_page', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='wagtailcore.page')),
('site', models.OneToOneField(editable=False, on_delete=django.db.models.deletion.CASCADE, to='wagtailcore.site')),
],
options={
'abstract': False,
},
),
]

View File

21
apps/core/models.py Normal file
View File

@@ -0,0 +1,21 @@
from django.db import models
from django.db.models import SET_NULL
from wagtail.contrib.settings.models import BaseSiteSetting, register_setting
@register_setting
class SiteSettings(BaseSiteSetting):
default_og_image = models.ForeignKey(
"wagtailimages.Image",
null=True,
blank=True,
on_delete=SET_NULL,
related_name="+",
)
privacy_policy_page = models.ForeignKey(
"wagtailcore.Page",
null=True,
blank=True,
on_delete=SET_NULL,
related_name="+",
)

View File

View File

@@ -0,0 +1,30 @@
from __future__ import annotations
from django import template
from django.utils.safestring import mark_safe
from wagtail.models import Site
from apps.blog.models import TagMetadata
from apps.legal.models import LegalPage
register = template.Library()
@register.simple_tag(takes_context=True)
def get_legal_pages(context):
request = context.get("request")
site = Site.find_for_request(request) if request else None
pages = LegalPage.objects.live().filter(show_in_footer=True)
if site:
pages = pages.in_site(site)
return pages
@register.simple_tag
@register.filter
def get_tag_css(tag):
meta = getattr(tag, "metadata", None)
if meta is None:
meta = TagMetadata.objects.filter(tag=tag).first()
classes = meta.get_css_classes() if meta else TagMetadata.get_fallback_css()
return mark_safe(f"{classes['bg']} {classes['text']}")

View File

@@ -0,0 +1,37 @@
from __future__ import annotations
import json
from django import template
from django.utils.safestring import mark_safe
from wagtail.images.models import Image
from apps.core.models import SiteSettings
register = template.Library()
@register.simple_tag(takes_context=True)
def article_json_ld(context, article):
request = context["request"]
site_settings = SiteSettings.for_request(request)
image = article.hero_image or site_settings.default_og_image
image_url = ""
if isinstance(image, Image):
rendition = image.get_rendition("fill-1200x630")
image_url = request.build_absolute_uri(rendition.url)
data = {
"@context": "https://schema.org",
"@type": "Article",
"headline": article.title,
"author": {"@type": "Person", "name": article.author.name},
"datePublished": article.first_published_at.isoformat() if article.first_published_at else "",
"dateModified": article.last_published_at.isoformat() if article.last_published_at else "",
"description": article.search_description or article.summary,
"url": article.get_full_url(request),
"image": image_url,
}
return mark_safe(
'<script type="application/ld+json">' + json.dumps(data, ensure_ascii=True) + "</script>"
)

33
apps/core/views.py Normal file
View File

@@ -0,0 +1,33 @@
from __future__ import annotations
from django.http import HttpRequest, HttpResponse, HttpResponseNotAllowed
from django.shortcuts import redirect, render
from apps.core.consent import ConsentService
def consent_view(request: HttpRequest) -> HttpResponse:
if request.method != "POST":
return HttpResponseNotAllowed(["POST"])
analytics = False
advertising = False
if request.POST.get("accept_all"):
analytics = True
advertising = True
elif request.POST.get("reject_all"):
analytics = False
advertising = False
else:
analytics = request.POST.get("analytics") in {"true", "1", "on"}
advertising = request.POST.get("advertising") in {"true", "1", "on"}
target = request.META.get("HTTP_REFERER", "/")
response = redirect(target)
ConsentService.set_consent(response, analytics=analytics, advertising=advertising)
return response
def robots_txt(request: HttpRequest) -> HttpResponse:
return render(request, "core/robots.txt", content_type="text/plain")