Scaffold containerized Django/Wagtail app with core features
This commit is contained in:
0
apps/core/__init__.py
Normal file
0
apps/core/__init__.py
Normal file
6
apps/core/apps.py
Normal file
6
apps/core/apps.py
Normal 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
59
apps/core/consent.py
Normal 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,
|
||||
)
|
||||
9
apps/core/context_processors.py
Normal file
9
apps/core/context_processors.py
Normal 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
12
apps/core/middleware.py
Normal 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)
|
||||
29
apps/core/migrations/0001_initial.py
Normal file
29
apps/core/migrations/0001_initial.py
Normal 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,
|
||||
},
|
||||
),
|
||||
]
|
||||
0
apps/core/migrations/__init__.py
Normal file
0
apps/core/migrations/__init__.py
Normal file
21
apps/core/models.py
Normal file
21
apps/core/models.py
Normal 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="+",
|
||||
)
|
||||
0
apps/core/templatetags/__init__.py
Normal file
0
apps/core/templatetags/__init__.py
Normal file
30
apps/core/templatetags/core_tags.py
Normal file
30
apps/core/templatetags/core_tags.py
Normal 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']}")
|
||||
37
apps/core/templatetags/seo_tags.py
Normal file
37
apps/core/templatetags/seo_tags.py
Normal 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
33
apps/core/views.py
Normal 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")
|
||||
Reference in New Issue
Block a user