From 0fb9b1f880d40d7b7f73a50db53a6899ee444491 Mon Sep 17 00:00:00 2001 From: giles Date: Tue, 10 Feb 2026 10:36:41 +0000 Subject: [PATCH] feat: nest calendars under //calendars with auto slug injection Co-Authored-By: Claude Opus 4.6 --- app.py | 65 +++++++++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 62 insertions(+), 3 deletions(-) diff --git a/app.py b/app.py index 3331f9c..8c0b434 100644 --- a/app.py +++ b/app.py @@ -3,8 +3,9 @@ from __future__ import annotations import path_setup # noqa: F401 # adds shared_lib to sys.path from pathlib import Path -from quart import g +from quart import g, abort from jinja2 import FileSystemLoader, ChoiceLoader +from sqlalchemy import select from shared.factory import create_base_app @@ -40,6 +41,9 @@ async def events_context() -> dict: def create_app() -> "Quart": + from models.ghost_content import Post + from models.calendars import Calendar + app = create_base_app("events", context_fn=events_context) # App-specific templates override shared templates @@ -49,12 +53,67 @@ def create_app() -> "Quart": app.jinja_loader, ]) - # Calendars blueprint at root — standalone mode (no post nesting) + # Calendars nested under post slug: //calendars/... app.register_blueprint( register_calendars(), - url_prefix="/calendars", + url_prefix="//calendars", ) + # --- Auto-inject slug into url_for() calls --- + @app.url_value_preprocessor + def pull_slug(endpoint, values): + if values and "slug" in values: + g.post_slug = values.pop("slug") + + @app.url_defaults + def inject_slug(endpoint, values): + slug = g.get("post_slug") + if slug and "slug" not in values: + if app.url_map.is_endpoint_expecting(endpoint, "slug"): + values["slug"] = slug + + # --- Load post data for slug --- + @app.before_request + async def hydrate_post(): + slug = getattr(g, "post_slug", None) + if not slug: + return + post = ( + await g.s.execute( + select(Post).where(Post.slug == slug) + ) + ).scalar_one_or_none() + if not post: + abort(404) + g.post_data = { + "post": { + "id": post.id, + "title": post.title, + "slug": post.slug, + "feature_image": post.feature_image, + "status": post.status, + "visibility": post.visibility, + }, + } + + @app.context_processor + async def inject_post(): + post_data = getattr(g, "post_data", None) + if not post_data: + return {} + post_id = post_data["post"]["id"] + calendars = ( + await g.s.execute( + select(Calendar) + .where(Calendar.post_id == post_id, Calendar.deleted_at.is_(None)) + .order_by(Calendar.name.asc()) + ) + ).scalars().all() + return { + **post_data, + "calendars": calendars, + } + # Tickets blueprint — user-facing ticket views and QR codes from bp.tickets.routes import register as register_tickets app.register_blueprint(register_tickets())