Files
rose-ash/events/bp/fragments/routes.py
giles 16da08ff05 Fix market and calendar URL routing
Market: blog links now use market_url('/{slug}/') instead of
events_url('/{slug}/markets/'), matching the market service's
actual route structure /<page_slug>/<market_slug>/.

Calendar: flatten route from /<slug>/calendars/<calendar_slug>/
to /<slug>/<calendar_slug>/ by changing the events app blueprint
prefix and moving listing routes to explicit /calendars/ paths.
Update all hardcoded calendar URL paths across blog and events
services (Python + Jinja templates).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-28 13:58:05 +00:00

226 lines
9.1 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Events app fragment endpoints.
Exposes HTML fragments at ``/internal/fragments/<type>`` for consumption
by other coop apps via the fragment client.
"""
from __future__ import annotations
from quart import Blueprint, Response, g, render_template, request
from shared.infrastructure.fragments import FRAGMENT_HEADER
from shared.infrastructure.data_client import fetch_data
from shared.contracts.dtos import PostDTO, dto_from_dict
from shared.services.registry import services
def register():
bp = Blueprint("fragments", __name__, url_prefix="/internal/fragments")
_handlers: dict[str, object] = {}
@bp.before_request
async def _require_fragment_header():
if not request.headers.get(FRAGMENT_HEADER):
return Response("", status=403)
@bp.get("/<fragment_type>")
async def get_fragment(fragment_type: str):
handler = _handlers.get(fragment_type)
if handler is None:
return Response("", status=200, content_type="text/html")
html = await handler()
return Response(html, status=200, content_type="text/html")
# --- container-nav fragment: calendar entries + calendar links -----------
async def _container_nav_handler():
from quart import current_app
from shared.infrastructure.urls import events_url
from shared.sexp.jinja_bridge import sexp as render_sexp
container_type = request.args.get("container_type", "page")
container_id = int(request.args.get("container_id", 0))
post_slug = request.args.get("post_slug", "")
paginate_url_base = request.args.get("paginate_url", "")
page = int(request.args.get("page", 1))
exclude = request.args.get("exclude", "")
excludes = [e.strip() for e in exclude.split(",") if e.strip()]
styles = current_app.jinja_env.globals.get("styles", {})
nav_class = styles.get("nav_button_less_pad", "")
html_parts = []
# Calendar entries nav
if not any(e.startswith("calendar") for e in excludes):
entries, has_more = await services.calendar.associated_entries(
g.s, container_type, container_id, page,
)
for entry in entries:
entry_path = (
f"/{post_slug}/{entry.calendar_slug}/"
f"{entry.start_at.year}/{entry.start_at.month}/"
f"{entry.start_at.day}/entries/{entry.id}/"
)
date_str = entry.start_at.strftime("%b %d, %Y at %H:%M")
if entry.end_at:
date_str += f" {entry.end_at.strftime('%H:%M')}"
html_parts.append(render_sexp(
'(~calendar-entry-nav :href href :name name :date-str date-str :nav-class nav-class)',
href=events_url(entry_path), name=entry.name,
**{"date-str": date_str, "nav-class": nav_class},
))
# Infinite scroll sentinel (kept as raw HTML — HTMX-specific)
if has_more and paginate_url_base:
html_parts.append(
f'<div id="entries-load-sentinel-{page}"'
f' hx-get="{paginate_url_base}?page={page + 1}"'
f' hx-trigger="intersect once"'
f' hx-swap="beforebegin"'
f' _="on htmx:afterRequest trigger scroll on #associated-entries-container"'
f' class="flex-shrink-0 w-1"></div>'
)
# Calendar links nav
if not any(e.startswith("calendar") for e in excludes):
calendars = await services.calendar.calendars_for_container(
g.s, container_type, container_id,
)
for cal in calendars:
href = events_url(f"/{post_slug}/{cal.slug}/")
html_parts.append(render_sexp(
'(~calendar-link-nav :href href :name name :nav-class nav-class)',
href=href, name=cal.name, **{"nav-class": nav_class},
))
return "\n".join(html_parts)
_handlers["container-nav"] = _container_nav_handler
# --- container-cards fragment: entries for blog listing cards ------------
async def _container_cards_handler():
post_ids_raw = request.args.get("post_ids", "")
post_slugs_raw = request.args.get("post_slugs", "")
post_ids = [int(x) for x in post_ids_raw.split(",") if x.strip()]
post_slugs = [x.strip() for x in post_slugs_raw.split(",") if x.strip()]
if not post_ids:
return ""
# Build post_id -> slug mapping
slug_map = {}
for i, pid in enumerate(post_ids):
slug_map[pid] = post_slugs[i] if i < len(post_slugs) else ""
batch = await services.calendar.confirmed_entries_for_posts(g.s, post_ids)
return await render_template(
"fragments/container_cards_entries.html",
batch=batch, post_ids=post_ids, slug_map=slug_map,
)
_handlers["container-cards"] = _container_cards_handler
# --- account-nav-item fragment: tickets + bookings links for account nav -
async def _account_nav_item_handler():
from quart import current_app
from shared.infrastructure.urls import account_url
styles = current_app.jinja_env.globals.get("styles", {})
nav_class = styles.get("nav_button", "")
hx_select = (
"#main-panel, #search-mobile, #search-count-mobile,"
" #search-desktop, #search-count-desktop, #menu-items-nav-wrapper"
)
tickets_url = account_url("/tickets/")
bookings_url = account_url("/bookings/")
# These two links use HTMX navigation — kept as raw HTML for the
# hx-* attributes that don't map neatly to a reusable component.
parts = []
for href, label in [(tickets_url, "tickets"), (bookings_url, "bookings")]:
parts.append(
f'<div class="relative nav-group">'
f'<a href="{href}" hx-get="{href}" hx-target="#main-panel"'
f' hx-select="{hx_select}" hx-swap="outerHTML"'
f' hx-push-url="true" class="{nav_class}">{label}</a></div>'
)
return "\n".join(parts)
_handlers["account-nav-item"] = _account_nav_item_handler
# --- account-page fragment: tickets or bookings panel --------------------
async def _account_page_handler():
slug = request.args.get("slug", "")
user_id = request.args.get("user_id", type=int)
if not user_id:
return ""
if slug == "tickets":
tickets = await services.calendar.user_tickets(g.s, user_id=user_id)
return await render_template(
"fragments/account_page_tickets.html",
tickets=tickets,
)
elif slug == "bookings":
bookings = await services.calendar.user_bookings(g.s, user_id=user_id)
return await render_template(
"fragments/account_page_bookings.html",
bookings=bookings,
)
return ""
_handlers["account-page"] = _account_page_handler
# --- link-card fragment: event page preview card ----------------------------
async def _link_card_handler():
from shared.infrastructure.urls import events_url
from shared.sexp.jinja_bridge import sexp as render_sexp
slug = request.args.get("slug", "")
keys_raw = request.args.get("keys", "")
# Batch mode
if keys_raw:
slugs = [k.strip() for k in keys_raw.split(",") if k.strip()]
parts = []
for s in slugs:
parts.append(f"<!-- fragment:{s} -->")
raw = await fetch_data("blog", "post-by-slug", params={"slug": s}, required=False)
post = dto_from_dict(PostDTO, raw) if raw else None
if post:
calendars = await services.calendar.calendars_for_container(
g.s, "page", post.id,
)
cal_names = ", ".join(c.name for c in calendars) if calendars else ""
parts.append(render_sexp(
'(~link-card :title title :image image :subtitle subtitle :link link)',
title=post.title, image=post.feature_image,
subtitle=cal_names, link=events_url(f"/{post.slug}"),
))
return "\n".join(parts)
# Single mode
if not slug:
return ""
raw = await fetch_data("blog", "post-by-slug", params={"slug": slug}, required=False)
post = dto_from_dict(PostDTO, raw) if raw else None
if not post:
return ""
calendars = await services.calendar.calendars_for_container(
g.s, "page", post.id,
)
cal_names = ", ".join(c.name for c in calendars) if calendars else ""
return render_sexp(
'(~link-card :title title :image image :subtitle subtitle :link link)',
title=post.title, image=post.feature_image,
subtitle=cal_names, link=events_url(f"/{post.slug}"),
)
_handlers["link-card"] = _link_card_handler
bp._fragment_handlers = _handlers
return bp