Files
mono/events/bp/fragments/routes.py
giles e8bc228c7f Rebrand sexp → sx across web platform (173 files)
Rename all sexp directories, files, identifiers, and references to sx.
artdag/ excluded (separate media processing DSL).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-01 11:06:57 +00:00

217 lines
8.5 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 sx 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] = {}
# Fragment types that still return HTML (Jinja templates)
_html_types = {"container-cards", "account-page"}
@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/sx")
result = await handler()
ct = "text/html" if fragment_type in _html_types else "text/sx"
return Response(result, status=200, content_type=ct)
# --- 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.sx.helpers import sx_call
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", "")
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')}"
parts.append(sx_call("calendar-entry-nav",
href=events_url(entry_path), name=entry.name,
date_str=date_str, nav_class=nav_class))
if has_more and paginate_url_base:
parts.append(sx_call("htmx-sentinel",
id=f"entries-load-sentinel-{page}",
hx_get=f"{paginate_url_base}?page={page + 1}",
hx_trigger="intersect once",
hx_swap="beforebegin",
**{"class": "flex-shrink-0 w-1"}))
# 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}/")
parts.append(sx_call("calendar-link-nav",
href=href, name=cal.name, nav_class=nav_class))
if not parts:
return ""
return "(<> " + " ".join(parts) + ")"
_handlers["container-nav"] = _container_nav_handler
# --- container-cards fragment: entries for blog listing cards (still Jinja) --
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 ""
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 -----------------
async def _account_nav_item_handler():
from quart import current_app
from shared.infrastructure.urls import account_url
from shared.sx.helpers import sx_call
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/")
parts = []
for href, label in [(tickets_url, "tickets"), (bookings_url, "bookings")]:
parts.append(sx_call("nav-group-link",
href=href, hx_select=hx_select, nav_class=nav_class, label=label))
return "(<> " + " ".join(parts) + ")"
_handlers["account-nav-item"] = _account_nav_item_handler
# --- account-page fragment: tickets or bookings panel (still Jinja) ------
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.sx.helpers import sx_call
slug = request.args.get("slug", "")
keys_raw = request.args.get("keys", "")
def _event_link_card_sx(post, cal_names: str) -> str:
return sx_call("link-card",
title=post.title, image=post.feature_image,
subtitle=cal_names,
link=events_url(f"/{post.slug}"))
# 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(_event_link_card_sx(post, cal_names))
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 _event_link_card_sx(post, cal_names)
_handlers["link-card"] = _link_card_handler
bp._fragment_handlers = _handlers
return bp