"""Events app fragment endpoints. Exposes sx fragments at ``/internal/fragments/`` 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("/") 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"") 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