""" Events service s-expression page components. Renders all events, page summary, calendars, calendar month, day, day admin, calendar admin, tickets, ticket admin, and markets pages. Called from route handlers in place of ``render_template()``. """ from __future__ import annotations import os from typing import Any from markupsafe import escape from shared.sx.jinja_bridge import load_service_components from shared.sx.helpers import ( call_url, get_asset_url, sx_call, root_header_sx, post_header_sx, post_admin_header_sx, oob_header_sx, header_child_sx, full_page_sx, oob_page_sx, search_mobile_sx, search_desktop_sx, ) from shared.sx.parser import SxExpr # Load events-specific .sx components at import time load_service_components(os.path.dirname(os.path.dirname(__file__))) # --------------------------------------------------------------------------- # OOB header helper — delegates to shared # --------------------------------------------------------------------------- # --------------------------------------------------------------------------- # Post header helpers — thin wrapper over shared post_header_sx # --------------------------------------------------------------------------- def _clear_oob(*ids: str) -> str: """Generate OOB swaps to remove orphaned header rows/children.""" return "".join(f'
' for i in ids) # All possible header row/child IDs at each depth (deepest first) _EVENTS_DEEP_IDS = [ "entry-admin-row", "entry-admin-header-child", "entry-row", "entry-header-child", "day-admin-row", "day-admin-header-child", "day-row", "day-header-child", "calendar-admin-row", "calendar-admin-header-child", "calendar-row", "calendar-header-child", "calendars-row", "calendars-header-child", "post-admin-row", "post-admin-header-child", ] def _clear_deeper_oob(*keep_ids: str) -> str: """Clear all events header rows/children NOT in keep_ids.""" to_clear = [i for i in _EVENTS_DEEP_IDS if i not in keep_ids] return _clear_oob(*to_clear) async def _ensure_container_nav(ctx: dict) -> dict: """Fetch container_nav if not already present (for post header row).""" if ctx.get("container_nav"): return ctx post = ctx.get("post") or {} post_id = post.get("id") slug = post.get("slug", "") if not post_id: return ctx from shared.infrastructure.fragments import fetch_fragments nav_params = { "container_type": "page", "container_id": str(post_id), "post_slug": slug, } events_nav, market_nav = await fetch_fragments([ ("events", "container-nav", nav_params), ("market", "container-nav", nav_params), ], required=False) return {**ctx, "container_nav": events_nav + market_nav} def _post_header_sx(ctx: dict, *, oob: bool = False) -> str: """Build the post-level header row — delegates to shared sx helper.""" return post_header_sx(ctx, oob=oob) def _post_nav_sx(ctx: dict) -> str: """Post desktop nav: calendar links + container nav (markets, etc.).""" from quart import url_for, g calendars = ctx.get("calendars") or [] select_colours = ctx.get("select_colours", "") current_cal_slug = getattr(g, "calendar_slug", None) parts = [] for cal in calendars: cal_slug = getattr(cal, "slug", "") if hasattr(cal, "slug") else cal.get("slug", "") cal_name = getattr(cal, "name", "") if hasattr(cal, "name") else cal.get("name", "") href = url_for("calendar.get", calendar_slug=cal_slug) is_sel = (cal_slug == current_cal_slug) parts.append(sx_call("nav-link", href=href, icon="fa fa-calendar", label=cal_name, select_colours=select_colours, is_selected=is_sel)) # Container nav fragments (markets, etc.) container_nav = ctx.get("container_nav", "") if container_nav: parts.append(container_nav) # Admin cog → blog admin for this post (cross-domain, no HTMX) rights = ctx.get("rights") or {} has_admin = rights.get("admin") if isinstance(rights, dict) else getattr(rights, "admin", False) if has_admin: post = ctx.get("post") or {} slug = post.get("slug", "") styles = ctx.get("styles") or {} nav_btn = styles.get("nav_button", "") if isinstance(styles, dict) else getattr(styles, "nav_button", "") select_colours = ctx.get("select_colours", "") admin_href = call_url(ctx, "blog_url", f"/{slug}/admin/") aclass = f"{nav_btn} {select_colours}".strip() or ( "justify-center cursor-pointer flex flex-row items-center gap-2 " "rounded bg-stone-200 text-black p-3" ) parts.append( f'' ) return "".join(parts) # --------------------------------------------------------------------------- # Post admin header # --------------------------------------------------------------------------- # --------------------------------------------------------------------------- # Calendars header # --------------------------------------------------------------------------- def _calendars_header_sx(ctx: dict, *, oob: bool = False) -> str: """Build the calendars section header row.""" from quart import url_for link_href = url_for("calendars.home") return sx_call("menu-row-sx", id="calendars-row", level=3, link_href=link_href, link_label_content=SxExpr(sx_call("events-calendars-label")), child_id="calendars-header-child", oob=oob) # --------------------------------------------------------------------------- # Calendar header # --------------------------------------------------------------------------- def _calendar_header_sx(ctx: dict, *, oob: bool = False) -> str: """Build a single calendar's header row.""" from quart import url_for calendar = ctx.get("calendar") if not calendar: return "" cal_slug = getattr(calendar, "slug", "") cal_name = getattr(calendar, "name", "") cal_desc = getattr(calendar, "description", "") or "" link_href = url_for("calendar.get", calendar_slug=cal_slug) label_html = sx_call("events-calendar-label", name=cal_name, description=cal_desc) # Desktop nav: slots + admin nav_html = _calendar_nav_sx(ctx) return sx_call("menu-row-sx", id="calendar-row", level=3, link_href=link_href, link_label_content=SxExpr(label_html), nav=SxExpr(nav_html) if nav_html else None, child_id="calendar-header-child", oob=oob) def _calendar_nav_sx(ctx: dict) -> str: """Calendar desktop nav: Slots + admin link.""" from quart import url_for calendar = ctx.get("calendar") if not calendar: return "" cal_slug = getattr(calendar, "slug", "") rights = ctx.get("rights") or {} is_admin = getattr(rights, "admin", False) if hasattr(rights, "admin") else rights.get("admin", False) select_colours = ctx.get("select_colours", "") parts = [] slots_href = url_for("calendar.slots.get", calendar_slug=cal_slug) parts.append(sx_call("nav-link", href=slots_href, icon="fa fa-clock", label="Slots", select_colours=select_colours)) if is_admin: admin_href = url_for("calendar.admin.admin", calendar_slug=cal_slug) parts.append(sx_call("nav-link", href=admin_href, icon="fa fa-cog", select_colours=select_colours)) return "".join(parts) # --------------------------------------------------------------------------- # Day header # --------------------------------------------------------------------------- def _day_header_sx(ctx: dict, *, oob: bool = False) -> str: """Build day detail header row.""" from quart import url_for calendar = ctx.get("calendar") if not calendar: return "" cal_slug = getattr(calendar, "slug", "") day_date = ctx.get("day_date") if not day_date: return "" link_href = url_for( "calendar.day.show_day", calendar_slug=cal_slug, year=day_date.year, month=day_date.month, day=day_date.day, ) label_html = sx_call("events-day-label", date_str=day_date.strftime("%A %d %B %Y")) nav_html = _day_nav_sx(ctx) return sx_call("menu-row-sx", id="day-row", level=4, link_href=link_href, link_label_content=SxExpr(label_html), nav=SxExpr(nav_html) if nav_html else None, child_id="day-header-child", oob=oob) def _day_nav_sx(ctx: dict) -> str: """Day desktop nav: confirmed entries scrolling menu + admin link.""" from quart import url_for calendar = ctx.get("calendar") if not calendar: return "" cal_slug = getattr(calendar, "slug", "") day_date = ctx.get("day_date") confirmed_entries = ctx.get("confirmed_entries") or [] rights = ctx.get("rights") or {} is_admin = getattr(rights, "admin", False) if hasattr(rights, "admin") else rights.get("admin", False) parts = [] # Confirmed entries nav (scrolling menu) if confirmed_entries: entry_links = [] for entry in confirmed_entries: href = url_for( "calendar.day.calendar_entries.calendar_entry.get", calendar_slug=cal_slug, year=day_date.year, month=day_date.month, day=day_date.day, entry_id=entry.id, ) start = entry.start_at.strftime("%H:%M") if entry.start_at else "" end = f" \u2013 {entry.end_at.strftime('%H:%M')}" if entry.end_at else "" entry_links.append(sx_call("events-day-entry-link", href=href, name=entry.name, time_str=f"{start}{end}")) inner = "".join(entry_links) parts.append(sx_call("events-day-entries-nav", inner=SxExpr(inner))) if is_admin and day_date: admin_href = url_for( "calendar.day.admin.admin", calendar_slug=cal_slug, year=day_date.year, month=day_date.month, day=day_date.day, ) parts.append(sx_call("nav-link", href=admin_href, icon="fa fa-cog")) return "".join(parts) # --------------------------------------------------------------------------- # Day admin header # --------------------------------------------------------------------------- def _day_admin_header_sx(ctx: dict, *, oob: bool = False) -> str: """Build day admin header row.""" from quart import url_for calendar = ctx.get("calendar") if not calendar: return "" cal_slug = getattr(calendar, "slug", "") day_date = ctx.get("day_date") if not day_date: return "" link_href = url_for( "calendar.day.admin.admin", calendar_slug=cal_slug, year=day_date.year, month=day_date.month, day=day_date.day, ) return sx_call("menu-row-sx", id="day-admin-row", level=5, link_href=link_href, link_label="admin", icon="fa fa-cog", child_id="day-admin-header-child", oob=oob) # --------------------------------------------------------------------------- # Calendar admin header # --------------------------------------------------------------------------- def _calendar_admin_header_sx(ctx: dict, *, oob: bool = False) -> str: """Build calendar admin header row with nav links.""" from quart import url_for calendar = ctx.get("calendar") cal_slug = getattr(calendar, "slug", "") if calendar else "" select_colours = ctx.get("select_colours", "") nav_parts = [] if cal_slug: for endpoint, label in [ ("calendar.slots.get", "slots"), ("calendar.admin.calendar_description_edit", "description"), ]: href = url_for(endpoint, calendar_slug=cal_slug) nav_parts.append(sx_call("nav-link", href=href, label=label, select_colours=select_colours)) nav_html = "".join(nav_parts) return sx_call("menu-row-sx", id="calendar-admin-row", level=4, link_label="admin", icon="fa fa-cog", nav=SxExpr(nav_html) if nav_html else None, child_id="calendar-admin-header-child", oob=oob) # --------------------------------------------------------------------------- # Markets header # --------------------------------------------------------------------------- def _markets_header_sx(ctx: dict, *, oob: bool = False) -> str: """Build the markets section header row.""" from quart import url_for link_href = url_for("markets.home") return sx_call("menu-row-sx", id="markets-row", level=3, link_href=link_href, link_label_content=SxExpr(sx_call("events-markets-label")), child_id="markets-header-child", oob=oob) # --------------------------------------------------------------------------- # Calendars main panel # --------------------------------------------------------------------------- def _calendars_main_panel_sx(ctx: dict) -> str: """Render the calendars list + create form panel.""" from quart import url_for rights = ctx.get("rights") or {} is_admin = getattr(rights, "admin", False) if hasattr(rights, "admin") else rights.get("admin", False) has_access = ctx.get("has_access") can_create = has_access("calendars.create_calendar") if callable(has_access) else is_admin csrf_token = ctx.get("csrf_token") csrf = csrf_token() if callable(csrf_token) else (csrf_token or "") calendars = ctx.get("calendars") or [] form_html = "" if can_create: create_url = url_for("calendars.create_calendar") form_html = sx_call("events-calendars-create-form", create_url=create_url, csrf=csrf) list_html = _calendars_list_sx(ctx, calendars) return sx_call("events-calendars-panel", form=SxExpr(form_html), list=SxExpr(list_html)) def _calendars_list_sx(ctx: dict, calendars: list) -> str: """Render the calendars list items.""" from quart import url_for from shared.utils import route_prefix csrf_token = ctx.get("csrf_token") csrf = csrf_token() if callable(csrf_token) else (csrf_token or "") prefix = route_prefix() if not calendars: return sx_call("events-calendars-empty") parts = [] for cal in calendars: cal_slug = getattr(cal, "slug", "") cal_name = getattr(cal, "name", "") href = prefix + url_for("calendar.get", calendar_slug=cal_slug) del_url = url_for("calendar.delete", calendar_slug=cal_slug) csrf_hdr = f'{{"X-CSRFToken":"{csrf}"}}' parts.append(sx_call("events-calendars-item", href=href, cal_name=cal_name, cal_slug=cal_slug, del_url=del_url, csrf_hdr=csrf_hdr)) return "".join(parts) # --------------------------------------------------------------------------- # Calendar month grid # --------------------------------------------------------------------------- def _calendar_main_panel_html(ctx: dict) -> str: """Render the calendar month grid.""" from quart import url_for from quart import session as qsession calendar = ctx.get("calendar") if not calendar: return "" cal_slug = getattr(calendar, "slug", "") hx_select = ctx.get("hx_select_search", "#main-panel") styles = ctx.get("styles") or {} pill_cls = getattr(styles, "pill", "") if hasattr(styles, "pill") else styles.get("pill", "") year = ctx.get("year", 2024) month = ctx.get("month", 1) month_name = ctx.get("month_name", "") weekday_names = ctx.get("weekday_names", []) weeks = ctx.get("weeks", []) prev_month = ctx.get("prev_month", 1) prev_month_year = ctx.get("prev_month_year", year) next_month = ctx.get("next_month", 1) next_month_year = ctx.get("next_month_year", year) prev_year = ctx.get("prev_year", year - 1) next_year = ctx.get("next_year", year + 1) month_entries = ctx.get("month_entries") or [] user = ctx.get("user") qs = qsession if "qsession" not in ctx else ctx["qsession"] def nav_link(y, m): return url_for("calendar.get", calendar_slug=cal_slug, year=y, month=m) # Month navigation arrows nav_arrows = [] for label, yr, mn in [ ("\u00ab", prev_year, month), ("\u2039", prev_month_year, prev_month), ]: href = nav_link(yr, mn) nav_arrows.append(sx_call("events-calendar-nav-arrow", pill_cls=pill_cls, href=href, label=label)) nav_arrows.append(sx_call("events-calendar-month-label", month_name=month_name, year=str(year))) for label, yr, mn in [ ("\u203a", next_month_year, next_month), ("\u00bb", next_year, month), ]: href = nav_link(yr, mn) nav_arrows.append(sx_call("events-calendar-nav-arrow", pill_cls=pill_cls, href=href, label=label)) # Weekday headers wd_html = "".join(sx_call("events-calendar-weekday", name=wd) for wd in weekday_names) # Day cells cells = [] for week in weeks: for day_cell in week: if isinstance(day_cell, dict): in_month = day_cell.get("in_month", True) is_today = day_cell.get("is_today", False) day_date = day_cell.get("date") else: in_month = getattr(day_cell, "in_month", True) is_today = getattr(day_cell, "is_today", False) day_date = getattr(day_cell, "date", None) cell_cls = "min-h-20 sm:min-h-24 bg-white px-3 py-2 text-xs" if not in_month: cell_cls += " bg-stone-50 text-stone-400" if is_today: cell_cls += " ring-2 ring-blue-500 z-10 relative" # Day number link day_num_html = "" day_short_html = "" if day_date: day_href = url_for( "calendar.day.show_day", calendar_slug=cal_slug, year=day_date.year, month=day_date.month, day=day_date.day, ) day_short_html = sx_call("events-calendar-day-short", day_str=day_date.strftime("%a")) day_num_html = sx_call("events-calendar-day-num", pill_cls=pill_cls, href=day_href, num=str(day_date.day)) # Entry badges for this day entry_badges = [] if day_date: for e in month_entries: if e.start_at and e.start_at.date() == day_date: is_mine = ( (user and e.user_id == user.id) or (not user and e.session_id == qs.get("calendar_sid")) ) if e.state == "confirmed": bg_cls = "bg-emerald-200 text-emerald-900" if is_mine else "bg-emerald-100 text-emerald-800" else: bg_cls = "bg-sky-100 text-sky-800" if is_mine else "bg-stone-100 text-stone-700" state_label = (e.state or "pending").replace("_", " ") entry_badges.append(sx_call("events-calendar-entry-badge", bg_cls=bg_cls, name=e.name, state_label=state_label)) badges_html = "".join(entry_badges) cells.append(sx_call("events-calendar-cell", cell_cls=cell_cls, day_short=SxExpr(day_short_html), day_num=SxExpr(day_num_html), badges=SxExpr(badges_html))) cells_html = "".join(cells) arrows_html = "".join(nav_arrows) return sx_call("events-calendar-grid", arrows=SxExpr(arrows_html), weekdays=SxExpr(wd_html), cells=SxExpr(cells_html)) # --------------------------------------------------------------------------- # Day main panel # --------------------------------------------------------------------------- def _day_main_panel_html(ctx: dict) -> str: """Render the day entries table + add button.""" from quart import url_for calendar = ctx.get("calendar") if not calendar: return "" cal_slug = getattr(calendar, "slug", "") day_entries = ctx.get("day_entries") or [] day = ctx.get("day") month = ctx.get("month") year = ctx.get("year") hx_select = ctx.get("hx_select_search", "#main-panel") styles = ctx.get("styles") or {} list_container = getattr(styles, "list_container", "") if hasattr(styles, "list_container") else styles.get("list_container", "") pill_cls = getattr(styles, "pill", "") if hasattr(styles, "pill") else styles.get("pill", "") tr_cls = getattr(styles, "tr", "") if hasattr(styles, "tr") else styles.get("tr", "") pre_action = getattr(styles, "pre_action_button", "") if hasattr(styles, "pre_action_button") else styles.get("pre_action_button", "") rows_html = "" if day_entries: rows_html = "".join(_day_row_html(ctx, entry) for entry in day_entries) else: rows_html = sx_call("events-day-empty-row") add_url = url_for( "calendar.day.calendar_entries.add_form", calendar_slug=cal_slug, day=day, month=month, year=year, ) return sx_call("events-day-table", list_container=list_container, rows=SxExpr(rows_html), pre_action=pre_action, add_url=add_url) def _day_row_html(ctx: dict, entry) -> str: """Render a single day table row.""" from quart import url_for calendar = ctx.get("calendar") cal_slug = getattr(calendar, "slug", "") day = ctx.get("day") month = ctx.get("month") year = ctx.get("year") hx_select = ctx.get("hx_select_search", "#main-panel") styles = ctx.get("styles") or {} pill_cls = getattr(styles, "pill", "") if hasattr(styles, "pill") else styles.get("pill", "") tr_cls = getattr(styles, "tr", "") if hasattr(styles, "tr") else styles.get("tr", "") entry_href = url_for( "calendar.day.calendar_entries.calendar_entry.get", calendar_slug=cal_slug, day=day, month=month, year=year, entry_id=entry.id, ) # Name name_html = sx_call("events-day-row-name", href=entry_href, pill_cls=pill_cls, name=entry.name) # Slot/Time slot = getattr(entry, "slot", None) if slot: slot_href = url_for("calendar.slots.slot.get", calendar_slug=cal_slug, slot_id=slot.id) time_start = slot.time_start.strftime("%H:%M") if slot.time_start else "" time_end = f" \u2192 {slot.time_end.strftime('%H:%M')}" if slot.time_end else "" slot_html = sx_call("events-day-row-slot", href=slot_href, pill_cls=pill_cls, slot_name=slot.name, time_str=f"({time_start}{time_end})") else: start = entry.start_at.strftime("%H:%M") if entry.start_at else "" end = f" \u2192 {entry.end_at.strftime('%H:%M')}" if entry.end_at else "" slot_html = sx_call("events-day-row-time", start=start, end=end) # State state = getattr(entry, "state", "pending") or "pending" state_badge = _entry_state_badge_html(state) state_td = sx_call("events-day-row-state", state_id=f"entry-state-{entry.id}", badge=SxExpr(state_badge)) # Cost cost = getattr(entry, "cost", None) cost_str = f"\u00a3{cost:.2f}" if cost is not None else "\u00a30.00" cost_td = sx_call("events-day-row-cost", cost_str=cost_str) # Tickets tp = getattr(entry, "ticket_price", None) if tp is not None: tc = getattr(entry, "ticket_count", None) tc_str = f"{tc} tickets" if tc is not None else "Unlimited" tickets_td = sx_call("events-day-row-tickets", price_str=f"\u00a3{tp:.2f}", count_str=tc_str) else: tickets_td = sx_call("events-day-row-no-tickets") actions_td = sx_call("events-day-row-actions") return sx_call("events-day-row", tr_cls=tr_cls, name=SxExpr(name_html), slot=SxExpr(slot_html), state=SxExpr(state_td), cost=SxExpr(cost_td), tickets=SxExpr(tickets_td), actions=SxExpr(actions_td)) def _entry_state_badge_html(state: str) -> str: """Render an entry state badge.""" state_classes = { "confirmed": "bg-emerald-100 text-emerald-800", "provisional": "bg-amber-100 text-amber-800", "ordered": "bg-sky-100 text-sky-800", "pending": "bg-stone-100 text-stone-700", "declined": "bg-red-100 text-red-800", } cls = state_classes.get(state, "bg-stone-100 text-stone-700") label = state.replace("_", " ").capitalize() return sx_call("events-state-badge", cls=cls, label=label) # --------------------------------------------------------------------------- # Day admin main panel # --------------------------------------------------------------------------- def _day_admin_main_panel_html(ctx: dict) -> str: """Render day admin panel (placeholder nav).""" return sx_call("events-day-admin-panel") # --------------------------------------------------------------------------- # Calendar admin main panel # --------------------------------------------------------------------------- def _calendar_admin_main_panel_html(ctx: dict) -> str: """Render calendar admin config panel with description editor.""" from quart import url_for calendar = ctx.get("calendar") if not calendar: return "" csrf_token = ctx.get("csrf_token") csrf = csrf_token() if callable(csrf_token) else (csrf_token or "") cal_slug = getattr(calendar, "slug", "") desc = getattr(calendar, "description", "") or "" hx_select = ctx.get("hx_select_search", "#main-panel") desc_edit_url = url_for("calendar.admin.calendar_description_edit", calendar_slug=cal_slug) description_html = _calendar_description_display_html(calendar, desc_edit_url) return sx_call("events-calendar-admin-panel", description_content=SxExpr(description_html), csrf=csrf, description=desc) def _calendar_description_display_html(calendar, edit_url: str) -> str: """Render calendar description display with edit button.""" desc = getattr(calendar, "description", "") or "" return sx_call("events-calendar-description-display", description=desc, edit_url=edit_url) # --------------------------------------------------------------------------- # Markets main panel # --------------------------------------------------------------------------- def _markets_main_panel_html(ctx: dict) -> str: """Render markets list + create form panel.""" from quart import url_for rights = ctx.get("rights") or {} is_admin = getattr(rights, "admin", False) if hasattr(rights, "admin") else rights.get("admin", False) has_access = ctx.get("has_access") can_create = has_access("markets.create_market") if callable(has_access) else is_admin csrf_token = ctx.get("csrf_token") csrf = csrf_token() if callable(csrf_token) else (csrf_token or "") markets = ctx.get("markets") or [] form_html = "" if can_create: create_url = url_for("markets.create_market") form_html = sx_call("events-markets-create-form", create_url=create_url, csrf=csrf) list_html = _markets_list_html(ctx, markets) return sx_call("events-markets-panel", form=SxExpr(form_html), list=SxExpr(list_html)) def _markets_list_html(ctx: dict, markets: list) -> str: """Render markets list items.""" from quart import url_for csrf_token = ctx.get("csrf_token") csrf = csrf_token() if callable(csrf_token) else (csrf_token or "") post = ctx.get("post") or {} slug = post.get("slug", "") if not markets: return sx_call("events-markets-empty") parts = [] for m in markets: m_slug = getattr(m, "slug", "") if hasattr(m, "slug") else m.get("slug", "") m_name = getattr(m, "name", "") if hasattr(m, "name") else m.get("name", "") market_href = call_url(ctx, "market_url", f"/{slug}/{m_slug}/") del_url = url_for("markets.delete_market", market_slug=m_slug) csrf_hdr = f'{{"X-CSRFToken":"{csrf}"}}' parts.append(sx_call("events-markets-item", href=market_href, market_name=m_name, market_slug=m_slug, del_url=del_url, csrf_hdr=csrf_hdr)) return "".join(parts) # --------------------------------------------------------------------------- # Ticket state badge helper # --------------------------------------------------------------------------- def _ticket_state_badge_html(state: str) -> str: """Render a ticket state badge.""" cls_map = { "confirmed": "bg-emerald-100 text-emerald-800", "checked_in": "bg-blue-100 text-blue-800", "reserved": "bg-amber-100 text-amber-800", "cancelled": "bg-red-100 text-red-800", } cls = cls_map.get(state, "bg-stone-100 text-stone-700") label = (state or "").replace("_", " ").capitalize() return sx_call("events-state-badge", cls=cls, label=label) # --------------------------------------------------------------------------- # Tickets main panel (my tickets) # --------------------------------------------------------------------------- def _tickets_main_panel_html(ctx: dict, tickets: list) -> str: """Render my tickets list.""" from quart import url_for ticket_cards = [] if tickets: for ticket in tickets: href = url_for("tickets.ticket_detail", code=ticket.code) entry = getattr(ticket, "entry", None) entry_name = entry.name if entry else "Unknown event" tt = getattr(ticket, "ticket_type", None) state = getattr(ticket, "state", "") cal = getattr(entry, "calendar", None) if entry else None time_str = "" if entry and entry.start_at: time_str = entry.start_at.strftime("%A, %B %d, %Y at %H:%M") if entry.end_at: time_str += f" \u2013 {entry.end_at.strftime('%H:%M')}" ticket_cards.append(sx_call("events-ticket-card", href=href, entry_name=entry_name, type_name=tt.name if tt else None, time_str=time_str or None, cal_name=cal.name if cal else None, badge=_ticket_state_badge_html(state), code_prefix=ticket.code[:8])) cards_html = "".join(ticket_cards) return sx_call("events-tickets-panel", list_container=_list_container(ctx), has_tickets=bool(tickets), cards=SxExpr(cards_html)) # --------------------------------------------------------------------------- # Ticket detail panel # --------------------------------------------------------------------------- def _ticket_detail_panel_html(ctx: dict, ticket) -> str: """Render a single ticket detail with QR code.""" from quart import url_for entry = getattr(ticket, "entry", None) tt = getattr(ticket, "ticket_type", None) state = getattr(ticket, "state", "") code = ticket.code cal = getattr(entry, "calendar", None) if entry else None checked_in_at = getattr(ticket, "checked_in_at", None) bg_map = {"confirmed": "bg-emerald-50", "checked_in": "bg-blue-50", "reserved": "bg-amber-50"} header_bg = bg_map.get(state, "bg-stone-50") entry_name = entry.name if entry else "Ticket" back_href = url_for("tickets.my_tickets") # Badge with larger sizing badge = _ticket_state_badge_html(state).replace('px-2 py-0.5 text-xs', 'px-3 py-1 text-sm') # Time info time_date = entry.start_at.strftime("%A, %B %d, %Y") if entry and entry.start_at else None time_range = entry.start_at.strftime("%H:%M") if entry and entry.start_at else None if time_range and entry.end_at: time_range += f" \u2013 {entry.end_at.strftime('%H:%M')}" tt_desc = f"{tt.name} \u2014 \u00a3{tt.cost:.2f}" if tt and getattr(tt, "cost", None) else None checkin_str = checked_in_at.strftime("Checked in: %B %d, %Y at %H:%M") if checked_in_at else None qr_script = ( f"(function(){{var c=document.getElementById('ticket-qr-{code}');" "if(c&&typeof QRCode!=='undefined'){" "var cv=document.createElement('canvas');" f"QRCode.toCanvas(cv,'{code}',{{width:200,margin:2,color:{{dark:'#1c1917',light:'#ffffff'}}}},function(e){{if(!e)c.appendChild(cv)}});" "}})()" ) return sx_call("events-ticket-detail", list_container=_list_container(ctx), back_href=back_href, header_bg=header_bg, entry_name=entry_name, badge=SxExpr(badge), type_name=tt.name if tt else None, code=code, time_date=time_date, time_range=time_range, cal_name=cal.name if cal else None, type_desc=tt_desc, checkin_str=checkin_str, qr_script=qr_script) # --------------------------------------------------------------------------- # Ticket admin main panel # --------------------------------------------------------------------------- def _ticket_admin_main_panel_html(ctx: dict, tickets: list, stats: dict) -> str: """Render ticket admin dashboard.""" from quart import url_for csrf_token = ctx.get("csrf_token") csrf = csrf_token() if callable(csrf_token) else (csrf_token or "") lookup_url = url_for("ticket_admin.lookup") # Stats cards stats_html = "" for label, key, border, bg, text_cls in [ ("Total", "total", "border-stone-200", "", "text-stone-900"), ("Confirmed", "confirmed", "border-emerald-200", "bg-emerald-50", "text-emerald-700"), ("Checked In", "checked_in", "border-blue-200", "bg-blue-50", "text-blue-700"), ("Reserved", "reserved", "border-amber-200", "bg-amber-50", "text-amber-700"), ]: val = stats.get(key, 0) lbl_cls = text_cls.replace("700", "600").replace("900", "500") if "stone" not in text_cls else "text-stone-500" stats_html += sx_call("events-ticket-admin-stat", border=border, bg=bg, text_cls=text_cls, label_cls=lbl_cls, value=str(val), label=label) # Ticket rows rows_html = "" for ticket in tickets: entry = getattr(ticket, "entry", None) tt = getattr(ticket, "ticket_type", None) state = getattr(ticket, "state", "") code = ticket.code date_html = "" if entry and entry.start_at: date_html = sx_call("events-ticket-admin-date", date_str=entry.start_at.strftime("%d %b %Y, %H:%M")) action_html = "" if state in ("confirmed", "reserved"): checkin_url = url_for("ticket_admin.do_checkin", code=code) action_html = sx_call("events-ticket-admin-checkin-form", checkin_url=checkin_url, code=code, csrf=csrf) elif state == "checked_in": checked_in_at = getattr(ticket, "checked_in_at", None) t_str = checked_in_at.strftime("%H:%M") if checked_in_at else "" action_html = sx_call("events-ticket-admin-checked-in", time_str=t_str) rows_html += sx_call("events-ticket-admin-row", code=code, code_short=code[:12] + "...", entry_name=entry.name if entry else "\u2014", date=SxExpr(date_html), type_name=tt.name if tt else "\u2014", badge=_ticket_state_badge_html(state), action=SxExpr(action_html)) return sx_call("events-ticket-admin-panel", list_container=_list_container(ctx), stats=SxExpr(stats_html), lookup_url=lookup_url, has_tickets=bool(tickets), rows=SxExpr(rows_html)) # --------------------------------------------------------------------------- # All events / page summary entry cards # --------------------------------------------------------------------------- def _entry_card_html(entry, page_info: dict, pending_tickets: dict, ticket_url: str, events_url_fn, *, is_page_scoped: bool = False, post: dict | None = None) -> str: """Render a list card for one event entry.""" pi = page_info.get(getattr(entry, "calendar_container_id", 0), {}) if is_page_scoped and post: page_slug = pi.get("slug", post.get("slug", "")) else: page_slug = pi.get("slug", "") page_title = pi.get("title") day_href = "" if page_slug and entry.start_at: day_href = events_url_fn(f"/{page_slug}/{entry.calendar_slug}/day/{entry.start_at.strftime('%Y/%-m/%-d')}/") entry_href = f"{day_href}entries/{entry.id}/" if day_href else "" # Title (linked or plain) if entry_href: title_html = sx_call("events-entry-title-linked", href=entry_href, name=entry.name) else: title_html = sx_call("events-entry-title-plain", name=entry.name) # Badges badges_html = "" if page_title and (not is_page_scoped or page_title != (post or {}).get("title")): page_href = events_url_fn(f"/{page_slug}/") badges_html += sx_call("events-entry-page-badge", href=page_href, title=page_title) cal_name = getattr(entry, "calendar_name", "") if cal_name: badges_html += sx_call("events-entry-cal-badge", name=cal_name) # Time line time_parts = "" if day_href and not is_page_scoped: time_parts += sx_call("events-entry-time-linked", href=day_href, date_str=entry.start_at.strftime("%a %-d %b")) elif not is_page_scoped: time_parts += sx_call("events-entry-time-plain", date_str=entry.start_at.strftime("%a %-d %b")) time_parts += entry.start_at.strftime("%H:%M") if entry.end_at: time_parts += f' \u2013 {entry.end_at.strftime("%H:%M")}' cost = getattr(entry, "cost", None) cost_html = sx_call("events-entry-cost", cost=f"£{cost:.2f}") if cost else "" # Ticket widget tp = getattr(entry, "ticket_price", None) widget_html = "" if tp is not None: qty = pending_tickets.get(entry.id, 0) widget_html = sx_call("events-entry-widget-wrapper", widget=_ticket_widget_html(entry, qty, ticket_url, ctx={})) return sx_call("events-entry-card", title=SxExpr(title_html), badges=SxExpr(badges_html), time_parts=SxExpr(time_parts), cost=SxExpr(cost_html), widget=SxExpr(widget_html)) def _entry_card_tile_html(entry, page_info: dict, pending_tickets: dict, ticket_url: str, events_url_fn, *, is_page_scoped: bool = False, post: dict | None = None) -> str: """Render a tile card for one event entry.""" pi = page_info.get(getattr(entry, "calendar_container_id", 0), {}) if is_page_scoped and post: page_slug = pi.get("slug", post.get("slug", "")) else: page_slug = pi.get("slug", "") page_title = pi.get("title") day_href = "" if page_slug and entry.start_at: day_href = events_url_fn(f"/{page_slug}/{entry.calendar_slug}/day/{entry.start_at.strftime('%Y/%-m/%-d')}/") entry_href = f"{day_href}entries/{entry.id}/" if day_href else "" # Title if entry_href: title_html = sx_call("events-entry-title-tile-linked", href=entry_href, name=entry.name) else: title_html = sx_call("events-entry-title-tile-plain", name=entry.name) # Badges badges_html = "" if page_title and (not is_page_scoped or page_title != (post or {}).get("title")): page_href = events_url_fn(f"/{page_slug}/") badges_html += sx_call("events-entry-page-badge", href=page_href, title=page_title) cal_name = getattr(entry, "calendar_name", "") if cal_name: badges_html += sx_call("events-entry-cal-badge", name=cal_name) # Time time_html = "" if day_href: time_html += sx_call("events-entry-time-linked", href=day_href, date_str=entry.start_at.strftime("%a %-d %b")).replace(" · ", "") else: time_html += entry.start_at.strftime("%a %-d %b") time_html += f' \u00b7 {entry.start_at.strftime("%H:%M")}' if entry.end_at: time_html += f' \u2013 {entry.end_at.strftime("%H:%M")}' cost = getattr(entry, "cost", None) cost_html = sx_call("events-entry-cost", cost=f"£{cost:.2f}") if cost else "" # Ticket widget tp = getattr(entry, "ticket_price", None) widget_html = "" if tp is not None: qty = pending_tickets.get(entry.id, 0) widget_html = sx_call("events-entry-tile-widget-wrapper", widget=_ticket_widget_html(entry, qty, ticket_url, ctx={})) return sx_call("events-entry-card-tile", title=SxExpr(title_html), badges=SxExpr(badges_html), time=SxExpr(time_html), cost=SxExpr(cost_html), widget=SxExpr(widget_html)) def _ticket_widget_html(entry, qty: int, ticket_url: str, *, ctx: dict) -> str: """Render the inline +/- ticket widget.""" csrf_token_val = "" if ctx: ct = ctx.get("csrf_token") csrf_token_val = ct() if callable(ct) else (ct or "") else: try: from flask_wtf.csrf import generate_csrf csrf_token_val = generate_csrf() except Exception: pass eid = entry.id tp = getattr(entry, "ticket_price", 0) or 0 tgt = f"#page-ticket-{eid}" def _tw_form(count_val, btn_html): return sx_call("events-tw-form", ticket_url=ticket_url, target=tgt, csrf=csrf_token_val, entry_id=str(eid), count_val=str(count_val), btn=SxExpr(btn_html)) if qty == 0: inner = _tw_form(1, sx_call("events-tw-cart-plus")) else: minus = _tw_form(qty - 1, sx_call("events-tw-minus")) cart_icon = sx_call("events-tw-cart-icon", qty=str(qty)) plus = _tw_form(qty + 1, sx_call("events-tw-plus")) inner = minus + cart_icon + plus return sx_call("events-tw-widget", entry_id=str(eid), price=f"£{tp:.2f}", inner=SxExpr(inner)) def _entry_cards_html(entries, page_info, pending_tickets, ticket_url, events_url_fn, view, page, has_more, next_url, *, is_page_scoped=False, post=None) -> str: """Render entry cards (list or tile) with sentinel.""" parts = [] last_date = None for entry in entries: if view == "tile": parts.append(_entry_card_tile_html( entry, page_info, pending_tickets, ticket_url, events_url_fn, is_page_scoped=is_page_scoped, post=post, )) else: entry_date = entry.start_at.strftime("%A %-d %B %Y") if entry.start_at else "" if entry_date != last_date: parts.append(sx_call("events-date-separator", date_str=entry_date)) last_date = entry_date parts.append(_entry_card_html( entry, page_info, pending_tickets, ticket_url, events_url_fn, is_page_scoped=is_page_scoped, post=post, )) if has_more: parts.append(sx_call("events-sentinel", page=str(page), next_url=next_url)) return "".join(parts) # --------------------------------------------------------------------------- # All events / page summary main panels # --------------------------------------------------------------------------- _LIST_SVG = None _TILE_SVG = None def _get_list_svg(): global _LIST_SVG if _LIST_SVG is None: _LIST_SVG = sx_call("events-list-svg") return _LIST_SVG def _get_tile_svg(): global _TILE_SVG if _TILE_SVG is None: _TILE_SVG = sx_call("events-tile-svg") return _TILE_SVG def _view_toggle_html(ctx: dict, view: str) -> str: """Render the list/tile view toggle bar.""" from shared.utils import route_prefix prefix = route_prefix() clh = ctx.get("current_local_href", "/") hx_select = ctx.get("hx_select_search", "#main-panel") list_href = prefix + str(clh) tile_href = prefix + str(clh) if "?" in list_href: list_href = list_href.split("?")[0] if "?" in tile_href: tile_href = tile_href.split("?")[0] + "?view=tile" else: tile_href = tile_href + "?view=tile" list_active = 'bg-stone-200 text-stone-800' if view != 'tile' else 'text-stone-400 hover:text-stone-600' tile_active = 'bg-stone-200 text-stone-800' if view == 'tile' else 'text-stone-400 hover:text-stone-600' return sx_call("events-view-toggle", list_href=list_href, tile_href=tile_href, hx_select=hx_select, list_active=list_active, tile_active=tile_active, list_svg=_get_list_svg(), tile_svg=_get_tile_svg()) def _events_main_panel_html(ctx: dict, entries, has_more, pending_tickets, page_info, page, view, ticket_url, next_url, events_url_fn, *, is_page_scoped=False, post=None) -> str: """Render the events main panel with view toggle + cards.""" toggle = _view_toggle_html(ctx, view) if entries: cards = _entry_cards_html( entries, page_info, pending_tickets, ticket_url, events_url_fn, view, page, has_more, next_url, is_page_scoped=is_page_scoped, post=post, ) grid_cls = ("max-w-full px-3 py-3 grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-4" if view == "tile" else "max-w-full px-3 py-3 space-y-3") body = sx_call("events-grid", grid_cls=grid_cls, cards=SxExpr(cards)) else: body = sx_call("events-empty") return sx_call("events-main-panel-body", toggle=SxExpr(toggle), body=SxExpr(body)) # --------------------------------------------------------------------------- # Utility # --------------------------------------------------------------------------- def _list_container(ctx: dict) -> str: styles = ctx.get("styles") or {} return getattr(styles, "list_container", "") if hasattr(styles, "list_container") else styles.get("list_container", "") # =========================================================================== # PUBLIC API # =========================================================================== # --------------------------------------------------------------------------- # All events # --------------------------------------------------------------------------- async def render_all_events_page(ctx: dict, entries, has_more, pending_tickets, page_info, page, view) -> str: """Full page: all events listing.""" from quart import url_for from shared.utils import route_prefix from shared.infrastructure.urls import events_url prefix = route_prefix() view_param = f"&view={view}" if view != "list" else "" ticket_url = url_for("all_events.adjust_ticket") next_url = prefix + url_for("all_events.entries_fragment", page=page + 1) + (f"?view={view}" if view != "list" else "") content = _events_main_panel_html( ctx, entries, has_more, pending_tickets, page_info, page, view, ticket_url, next_url, events_url, ) hdr = root_header_sx(ctx) return full_page_sx(ctx, header_rows=hdr, content=content) async def render_all_events_oob(ctx: dict, entries, has_more, pending_tickets, page_info, page, view) -> str: """OOB response: all events listing (htmx nav).""" from quart import url_for from shared.utils import route_prefix from shared.infrastructure.urls import events_url prefix = route_prefix() ticket_url = url_for("all_events.adjust_ticket") next_url = prefix + url_for("all_events.entries_fragment", page=page + 1) + (f"?view={view}" if view != "list" else "") content = _events_main_panel_html( ctx, entries, has_more, pending_tickets, page_info, page, view, ticket_url, next_url, events_url, ) return oob_page_sx(content=content) async def render_all_events_cards(entries, has_more, pending_tickets, page_info, page, view) -> str: """Pagination fragment: all events cards only.""" from quart import url_for from shared.utils import route_prefix from shared.infrastructure.urls import events_url prefix = route_prefix() ticket_url = url_for("all_events.adjust_ticket") next_url = prefix + url_for("all_events.entries_fragment", page=page + 1) + (f"?view={view}" if view != "list" else "") return _entry_cards_html( entries, page_info, pending_tickets, ticket_url, events_url, view, page, has_more, next_url, ) # --------------------------------------------------------------------------- # Page summary # --------------------------------------------------------------------------- async def render_page_summary_page(ctx: dict, entries, has_more, pending_tickets, page_info, page, view) -> str: """Full page: page-scoped events listing.""" from quart import url_for from shared.utils import route_prefix from shared.infrastructure.urls import events_url prefix = route_prefix() post = ctx.get("post") or {} ticket_url = url_for("page_summary.adjust_ticket") next_url = prefix + url_for("page_summary.entries_fragment", page=page + 1) + (f"?view={view}" if view != "list" else "") content = _events_main_panel_html( ctx, entries, has_more, pending_tickets, page_info, page, view, ticket_url, next_url, events_url, is_page_scoped=True, post=post, ) hdr = root_header_sx(ctx) hdr += header_child_sx(_post_header_sx(ctx)) return full_page_sx(ctx, header_rows=hdr, content=content) async def render_page_summary_oob(ctx: dict, entries, has_more, pending_tickets, page_info, page, view) -> str: """OOB response: page-scoped events (htmx nav).""" from quart import url_for from shared.utils import route_prefix from shared.infrastructure.urls import events_url prefix = route_prefix() post = ctx.get("post") or {} ticket_url = url_for("page_summary.adjust_ticket") next_url = prefix + url_for("page_summary.entries_fragment", page=page + 1) + (f"?view={view}" if view != "list" else "") content = _events_main_panel_html( ctx, entries, has_more, pending_tickets, page_info, page, view, ticket_url, next_url, events_url, is_page_scoped=True, post=post, ) oobs = _post_header_sx(ctx, oob=True) oobs += _clear_deeper_oob("post-row", "post-header-child") return oob_page_sx(oobs=oobs, content=content) async def render_page_summary_cards(entries, has_more, pending_tickets, page_info, page, view, post) -> str: """Pagination fragment: page-scoped events cards only.""" from quart import url_for from shared.utils import route_prefix from shared.infrastructure.urls import events_url prefix = route_prefix() ticket_url = url_for("page_summary.adjust_ticket") next_url = prefix + url_for("page_summary.entries_fragment", page=page + 1) + (f"?view={view}" if view != "list" else "") return _entry_cards_html( entries, page_info, pending_tickets, ticket_url, events_url, view, page, has_more, next_url, is_page_scoped=True, post=post, ) # --------------------------------------------------------------------------- # Calendars home # --------------------------------------------------------------------------- async def render_calendars_page(ctx: dict) -> str: """Full page: calendars listing.""" content = _calendars_main_panel_sx(ctx) ctx = await _ensure_container_nav(ctx) slug = (ctx.get("post") or {}).get("slug", "") root_hdr = root_header_sx(ctx) post_hdr = _post_header_sx(ctx) admin_hdr = post_admin_header_sx(ctx, slug, selected="calendars") return full_page_sx(ctx, header_rows=root_hdr + post_hdr + admin_hdr, content=content) async def render_calendars_oob(ctx: dict) -> str: """OOB response: calendars listing.""" content = _calendars_main_panel_sx(ctx) ctx = await _ensure_container_nav(ctx) slug = (ctx.get("post") or {}).get("slug", "") oobs = post_admin_header_sx(ctx, slug, oob=True, selected="calendars") oobs += _clear_deeper_oob("post-row", "post-header-child", "post-admin-row", "post-admin-header-child") return oob_page_sx(oobs=oobs, content=content) # --------------------------------------------------------------------------- # Calendar month view # --------------------------------------------------------------------------- async def render_calendar_page(ctx: dict) -> str: """Full page: calendar month view.""" content = _calendar_main_panel_html(ctx) hdr = root_header_sx(ctx) child = _post_header_sx(ctx) + _calendar_header_sx(ctx) hdr += header_child_sx(child) return full_page_sx(ctx, header_rows=hdr, content=content) async def render_calendar_oob(ctx: dict) -> str: """OOB response: calendar month view.""" content = _calendar_main_panel_html(ctx) oobs = _post_header_sx(ctx, oob=True) oobs += oob_header_sx("post-header-child", "calendar-header-child", _calendar_header_sx(ctx)) oobs += _clear_deeper_oob("post-row", "post-header-child", "calendar-row", "calendar-header-child") return oob_page_sx(oobs=oobs, content=content) # --------------------------------------------------------------------------- # Day detail # --------------------------------------------------------------------------- async def render_day_page(ctx: dict) -> str: """Full page: day detail.""" content = _day_main_panel_html(ctx) hdr = root_header_sx(ctx) child = (_post_header_sx(ctx) + _calendar_header_sx(ctx) + _day_header_sx(ctx)) hdr += header_child_sx(child) return full_page_sx(ctx, header_rows=hdr, content=content) async def render_day_oob(ctx: dict) -> str: """OOB response: day detail.""" content = _day_main_panel_html(ctx) oobs = _calendar_header_sx(ctx, oob=True) oobs += oob_header_sx("calendar-header-child", "day-header-child", _day_header_sx(ctx)) oobs += _clear_deeper_oob("post-row", "post-header-child", "calendar-row", "calendar-header-child", "day-row", "day-header-child") return oob_page_sx(oobs=oobs, content=content) # --------------------------------------------------------------------------- # Day admin # --------------------------------------------------------------------------- async def render_day_admin_page(ctx: dict) -> str: """Full page: day admin.""" content = _day_admin_main_panel_html(ctx) ctx = await _ensure_container_nav(ctx) slug = (ctx.get("post") or {}).get("slug", "") root_hdr = root_header_sx(ctx) post_hdr = _post_header_sx(ctx) admin_hdr = post_admin_header_sx(ctx, slug, selected="calendars") child = (admin_hdr + _calendar_header_sx(ctx) + _day_header_sx(ctx) + _day_admin_header_sx(ctx)) hdr = root_hdr + post_hdr + header_child_sx(child) return full_page_sx(ctx, header_rows=hdr, content=content) async def render_day_admin_oob(ctx: dict) -> str: """OOB response: day admin.""" content = _day_admin_main_panel_html(ctx) ctx = await _ensure_container_nav(ctx) slug = (ctx.get("post") or {}).get("slug", "") oobs = (post_admin_header_sx(ctx, slug, oob=True, selected="calendars") + _calendar_header_sx(ctx, oob=True)) oobs += oob_header_sx("day-header-child", "day-admin-header-child", _day_admin_header_sx(ctx)) oobs += _clear_deeper_oob("post-row", "post-header-child", "post-admin-row", "post-admin-header-child", "calendar-row", "calendar-header-child", "day-row", "day-header-child", "day-admin-row", "day-admin-header-child") return oob_page_sx(oobs=oobs, content=content) # --------------------------------------------------------------------------- # Calendar admin # --------------------------------------------------------------------------- def _events_post_admin_header_sx(ctx: dict, *, oob: bool = False, selected: str = "") -> str: """Post-level admin row for events — delegates to shared helper.""" slug = (ctx.get("post") or {}).get("slug", "") return post_admin_header_sx(ctx, slug, oob=oob, selected=selected) async def render_calendar_admin_page(ctx: dict) -> str: """Full page: calendar admin.""" content = _calendar_admin_main_panel_html(ctx) ctx = await _ensure_container_nav(ctx) slug = (ctx.get("post") or {}).get("slug", "") root_hdr = root_header_sx(ctx) post_hdr = _post_header_sx(ctx) admin_hdr = post_admin_header_sx(ctx, slug, selected="calendars") child = admin_hdr + _calendar_header_sx(ctx) + _calendar_admin_header_sx(ctx) hdr = root_hdr + post_hdr + header_child_sx(child) return full_page_sx(ctx, header_rows=hdr, content=content) async def render_calendar_admin_oob(ctx: dict) -> str: """OOB response: calendar admin.""" content = _calendar_admin_main_panel_html(ctx) ctx = await _ensure_container_nav(ctx) slug = (ctx.get("post") or {}).get("slug", "") oobs = (post_admin_header_sx(ctx, slug, oob=True, selected="calendars") + _calendar_header_sx(ctx, oob=True)) oobs += oob_header_sx("calendar-header-child", "calendar-admin-header-child", _calendar_admin_header_sx(ctx)) oobs += _clear_deeper_oob("post-row", "post-header-child", "post-admin-row", "post-admin-header-child", "calendar-row", "calendar-header-child", "calendar-admin-row", "calendar-admin-header-child") return oob_page_sx(oobs=oobs, content=content) # --------------------------------------------------------------------------- # Slots # --------------------------------------------------------------------------- async def render_slots_page(ctx: dict) -> str: """Full page: slots listing.""" from quart import g slots = ctx.get("slots") or [] calendar = ctx.get("calendar") content = render_slots_table(slots, calendar) ctx = await _ensure_container_nav(ctx) slug = (ctx.get("post") or {}).get("slug", "") root_hdr = root_header_sx(ctx) post_hdr = _post_header_sx(ctx) admin_hdr = post_admin_header_sx(ctx, slug, selected="calendars") child = admin_hdr + _calendar_header_sx(ctx) + _calendar_admin_header_sx(ctx) hdr = root_hdr + post_hdr + header_child_sx(child) return full_page_sx(ctx, header_rows=hdr, content=content) async def render_slots_oob(ctx: dict) -> str: """OOB response: slots listing.""" slots = ctx.get("slots") or [] calendar = ctx.get("calendar") content = render_slots_table(slots, calendar) ctx = await _ensure_container_nav(ctx) slug = (ctx.get("post") or {}).get("slug", "") oobs = (post_admin_header_sx(ctx, slug, oob=True, selected="calendars") + _calendar_admin_header_sx(ctx, oob=True)) oobs += _clear_deeper_oob("post-row", "post-header-child", "post-admin-row", "post-admin-header-child", "calendar-row", "calendar-header-child", "calendar-admin-row", "calendar-admin-header-child") return oob_page_sx(oobs=oobs, content=content) # --------------------------------------------------------------------------- # Tickets # --------------------------------------------------------------------------- async def render_tickets_page(ctx: dict, tickets: list) -> str: """Full page: my tickets.""" content = _tickets_main_panel_html(ctx, tickets) hdr = root_header_sx(ctx) return full_page_sx(ctx, header_rows=hdr, content=content) async def render_tickets_oob(ctx: dict, tickets: list) -> str: """OOB response: my tickets.""" content = _tickets_main_panel_html(ctx, tickets) return oob_page_sx(content=content) async def render_ticket_detail_page(ctx: dict, ticket) -> str: """Full page: ticket detail with QR.""" content = _ticket_detail_panel_html(ctx, ticket) hdr = root_header_sx(ctx) return full_page_sx(ctx, header_rows=hdr, content=content) async def render_ticket_detail_oob(ctx: dict, ticket) -> str: """OOB response: ticket detail.""" content = _ticket_detail_panel_html(ctx, ticket) return oob_page_sx(content=content) # --------------------------------------------------------------------------- # Ticket admin # --------------------------------------------------------------------------- async def render_ticket_admin_page(ctx: dict, tickets: list, stats: dict) -> str: """Full page: ticket admin dashboard.""" content = _ticket_admin_main_panel_html(ctx, tickets, stats) hdr = root_header_sx(ctx) return full_page_sx(ctx, header_rows=hdr, content=content) async def render_ticket_admin_oob(ctx: dict, tickets: list, stats: dict) -> str: """OOB response: ticket admin dashboard.""" content = _ticket_admin_main_panel_html(ctx, tickets, stats) return oob_page_sx(content=content) # --------------------------------------------------------------------------- # Markets # --------------------------------------------------------------------------- async def render_markets_page(ctx: dict) -> str: """Full page: markets listing.""" content = _markets_main_panel_html(ctx) hdr = root_header_sx(ctx) child = _post_header_sx(ctx) + _markets_header_sx(ctx) hdr += header_child_sx(child) return full_page_sx(ctx, header_rows=hdr, content=content) async def render_markets_oob(ctx: dict) -> str: """OOB response: markets listing.""" content = _markets_main_panel_html(ctx) oobs = _post_header_sx(ctx, oob=True) oobs += oob_header_sx("post-header-child", "markets-header-child", _markets_header_sx(ctx)) return oob_page_sx(oobs=oobs, content=content) # =========================================================================== # POST / PUT / DELETE response components # =========================================================================== # --------------------------------------------------------------------------- # Ticket widget (public wrapper for _ticket_widget_html) # --------------------------------------------------------------------------- def render_ticket_widget(entry, qty: int, ticket_url: str) -> str: """Render the +/- ticket widget for page_summary / all_events adjust_ticket.""" return _ticket_widget_html(entry, qty, ticket_url, ctx={}) # --------------------------------------------------------------------------- # Ticket admin: checkin result # --------------------------------------------------------------------------- def render_checkin_result(success: bool, error: str | None, ticket) -> str: """Render checkin result: table row on success, error div on failure.""" if not success: return sx_call("events-checkin-error", message=error or "Check-in failed") if not ticket: return "" code = ticket.code entry = getattr(ticket, "entry", None) tt = getattr(ticket, "ticket_type", None) checked_in_at = getattr(ticket, "checked_in_at", None) time_str = checked_in_at.strftime("%H:%M") if checked_in_at else "Just now" date_html = "" if entry and entry.start_at: date_html = sx_call("events-ticket-admin-date", date_str=entry.start_at.strftime("%d %b %Y, %H:%M")) return sx_call("events-checkin-success-row", code=code, code_short=code[:12] + "...", entry_name=entry.name if entry else "\u2014", date=SxExpr(date_html), type_name=tt.name if tt else "\u2014", badge=_ticket_state_badge_html("checked_in"), time_str=time_str) # --------------------------------------------------------------------------- # Ticket admin: lookup result # --------------------------------------------------------------------------- def render_lookup_result(ticket, error: str | None) -> str: """Render ticket lookup result: error div or ticket info card.""" from quart import url_for from shared.browser.app.csrf import generate_csrf_token if error: return sx_call("events-lookup-error", message=error) if not ticket: return "" entry = getattr(ticket, "entry", None) tt = getattr(ticket, "ticket_type", None) state = getattr(ticket, "state", "") code = ticket.code checked_in_at = getattr(ticket, "checked_in_at", None) csrf = generate_csrf_token() # Info section info_html = sx_call("events-lookup-info", entry_name=entry.name if entry else "Unknown event") if tt: info_html += sx_call("events-lookup-type", type_name=tt.name) if entry and entry.start_at: info_html += sx_call("events-lookup-date", date_str=entry.start_at.strftime("%A, %B %d, %Y at %H:%M")) cal = getattr(entry, "calendar", None) if entry else None if cal: info_html += sx_call("events-lookup-cal", cal_name=cal.name) info_html += sx_call("events-lookup-status", badge=_ticket_state_badge_html(state), code=code) if checked_in_at: info_html += sx_call("events-lookup-checkin-time", date_str=checked_in_at.strftime("%B %d, %Y at %H:%M")) # Action area action_html = "" if state in ("confirmed", "reserved"): checkin_url = url_for("ticket_admin.do_checkin", code=code) action_html = sx_call("events-lookup-checkin-btn", checkin_url=checkin_url, code=code, csrf=csrf) elif state == "checked_in": action_html = sx_call("events-lookup-checked-in") elif state == "cancelled": action_html = sx_call("events-lookup-cancelled") return sx_call("events-lookup-card", info=SxExpr(info_html), code=code, action=SxExpr(action_html)) # --------------------------------------------------------------------------- # Ticket admin: entry tickets table # --------------------------------------------------------------------------- def render_entry_tickets_admin(entry, tickets: list) -> str: """Render admin ticket table for a specific entry.""" from quart import url_for from shared.browser.app.csrf import generate_csrf_token csrf = generate_csrf_token() count = len(tickets) suffix = "s" if count != 1 else "" rows_html = "" for ticket in tickets: tt = getattr(ticket, "ticket_type", None) state = getattr(ticket, "state", "") code = ticket.code checked_in_at = getattr(ticket, "checked_in_at", None) action_html = "" if state in ("confirmed", "reserved"): checkin_url = url_for("ticket_admin.do_checkin", code=code) action_html = sx_call("events-entry-tickets-admin-checkin", checkin_url=checkin_url, code=code, csrf=csrf) elif state == "checked_in": t_str = checked_in_at.strftime("%H:%M") if checked_in_at else "" action_html = sx_call("events-ticket-admin-checked-in", time_str=t_str) rows_html += sx_call("events-entry-tickets-admin-row", code=code, code_short=code[:12] + "...", type_name=tt.name if tt else "\u2014", badge=_ticket_state_badge_html(state), action=SxExpr(action_html)) if tickets: body_html = sx_call("events-entry-tickets-admin-table", rows=SxExpr(rows_html)) else: body_html = sx_call("events-entry-tickets-admin-empty") return sx_call("events-entry-tickets-admin-panel", entry_name=entry.name, count_label=f"{count} ticket{suffix}", body=SxExpr(body_html)) # --------------------------------------------------------------------------- # Day main panel -- public API # --------------------------------------------------------------------------- def render_day_main_panel(ctx: dict) -> str: """Public wrapper for day main panel rendering.""" return _day_main_panel_html(ctx) # --------------------------------------------------------------------------- # Entry main panel # --------------------------------------------------------------------------- def _entry_main_panel_html(ctx: dict) -> str: """Render the entry detail panel (name, slot, time, state, cost, tickets, buy form, date, posts, options + edit button).""" from quart import url_for entry = ctx.get("entry") if not entry: return "" calendar = ctx.get("calendar") cal_slug = getattr(calendar, "slug", "") if calendar else "" day = ctx.get("day") month = ctx.get("month") year = ctx.get("year") styles = ctx.get("styles") or {} list_container = getattr(styles, "list_container", "") if hasattr(styles, "list_container") else styles.get("list_container", "") pre_action = getattr(styles, "pre_action_button", "") if hasattr(styles, "pre_action_button") else styles.get("pre_action_button", "") eid = entry.id state = getattr(entry, "state", "pending") or "pending" def _field(label, content_html): return sx_call("events-entry-field", label=label, content=SxExpr(content_html)) # Name name_html = _field("Name", sx_call("events-entry-name-field", name=entry.name)) # Slot slot = getattr(entry, "slot", None) if slot: flex_label = "(flexible)" if getattr(slot, "flexible", False) else "(fixed)" slot_inner = sx_call("events-entry-slot-assigned", slot_name=slot.name, flex_label=flex_label) else: slot_inner = sx_call("events-entry-slot-none") slot_html = _field("Slot", slot_inner) # Time Period start_str = entry.start_at.strftime("%H:%M") if entry.start_at else "" end_str = f" \u2013 {entry.end_at.strftime('%H:%M')}" if entry.end_at else " \u2013 open-ended" time_html = _field("Time Period", sx_call("events-entry-time-field", time_str=start_str + end_str)) # State state_html = _field("State", sx_call("events-entry-state-field", entry_id=str(eid), badge=_entry_state_badge_html(state))) # Cost cost = getattr(entry, "cost", None) cost_str = f"{cost:.2f}" if cost is not None else "0.00" cost_html = _field("Cost", sx_call("events-entry-cost-field", cost=f"£{cost_str}")) # Ticket Configuration (admin) tickets_html = _field("Tickets", sx_call("events-entry-tickets-field", entry_id=str(eid), tickets_config=render_entry_tickets_config(entry, calendar, day, month, year))) # Buy Tickets (public-facing) ticket_remaining = ctx.get("ticket_remaining") ticket_sold_count = ctx.get("ticket_sold_count", 0) user_ticket_count = ctx.get("user_ticket_count", 0) user_ticket_counts_by_type = ctx.get("user_ticket_counts_by_type") or {} buy_html = render_buy_form( entry, ticket_remaining, ticket_sold_count, user_ticket_count, user_ticket_counts_by_type, ) # Date date_str = entry.start_at.strftime("%A, %B %d, %Y") if entry.start_at else "" date_html = _field("Date", sx_call("events-entry-date-field", date_str=date_str)) # Associated Posts entry_posts = ctx.get("entry_posts") or [] posts_html = _field("Associated Posts", sx_call("events-entry-posts-field", entry_id=str(eid), posts_panel=render_entry_posts_panel(entry_posts, entry, calendar, day, month, year))) # Options and Edit Button edit_url = url_for( "calendar.day.calendar_entries.calendar_entry.get_edit", entry_id=eid, calendar_slug=cal_slug, day=day, month=month, year=year, ) return sx_call("events-entry-panel", entry_id=str(eid), list_container=list_container, name=SxExpr(name_html), slot=SxExpr(slot_html), time=SxExpr(time_html), state=SxExpr(state_html), cost=SxExpr(cost_html), tickets=SxExpr(tickets_html), buy=SxExpr(buy_html), date=SxExpr(date_html), posts=SxExpr(posts_html), options=_entry_options_html(entry, calendar, day, month, year), pre_action=pre_action, edit_url=edit_url) # --------------------------------------------------------------------------- # Entry header row # --------------------------------------------------------------------------- def _entry_header_html(ctx: dict, *, oob: bool = False) -> str: """Build entry detail header row.""" from quart import url_for calendar = ctx.get("calendar") if not calendar: return "" cal_slug = getattr(calendar, "slug", "") entry = ctx.get("entry") if not entry: return "" day = ctx.get("day") month = ctx.get("month") year = ctx.get("year") link_href = url_for( "calendar.day.calendar_entries.calendar_entry.get", calendar_slug=cal_slug, year=year, month=month, day=day, entry_id=entry.id, ) label_html = sx_call("events-entry-label", entry_id=str(entry.id), title=_entry_title_html(entry), times=_entry_times_html(entry)) nav_html = _entry_nav_html(ctx) return sx_call("menu-row-sx", id="entry-row", level=5, link_href=link_href, link_label_content=SxExpr(label_html), nav=SxExpr(nav_html) if nav_html else None, child_id="entry-header-child", oob=oob) def _entry_times_html(entry) -> str: """Render entry times label.""" start = entry.start_at end = entry.end_at if not start: return "" start_str = start.strftime("%H:%M") end_str = f" \u2192 {end.strftime('%H:%M')}" if end else "" return sx_call("events-entry-times", time_str=start_str + end_str) # --------------------------------------------------------------------------- # Entry nav (desktop + admin link) # --------------------------------------------------------------------------- def _entry_nav_html(ctx: dict) -> str: """Entry desktop nav: associated posts scrolling menu + admin link.""" from quart import url_for calendar = ctx.get("calendar") if not calendar: return "" cal_slug = getattr(calendar, "slug", "") entry = ctx.get("entry") if not entry: return "" day = ctx.get("day") month = ctx.get("month") year = ctx.get("year") entry_posts = ctx.get("entry_posts") or [] rights = ctx.get("rights") or {} is_admin = getattr(rights, "admin", False) if hasattr(rights, "admin") else rights.get("admin", False) blog_url_fn = ctx.get("blog_url") parts = [] # Associated Posts scrolling menu if entry_posts: post_links = "" for ep in entry_posts: slug = getattr(ep, "slug", "") title = getattr(ep, "title", "") feat = getattr(ep, "feature_image", None) href = blog_url_fn(f"/{slug}/") if blog_url_fn else f"/{slug}/" if feat: img_html = sx_call("events-post-img", src=feat, alt=title) else: img_html = sx_call("events-post-img-placeholder") post_links += sx_call("events-entry-nav-post-link", href=href, img=SxExpr(img_html), title=title) parts.append(sx_call("events-entry-posts-nav-oob", items=SxExpr(post_links)).replace(' :hx-swap-oob "true"', '')) # Admin link if is_admin: admin_url = url_for( "calendar.day.calendar_entries.calendar_entry.admin.admin", calendar_slug=cal_slug, day=day, month=month, year=year, entry_id=entry.id, ) parts.append(sx_call("events-entry-admin-link", href=admin_url)) return "".join(parts) # --------------------------------------------------------------------------- # Entry page / OOB rendering # --------------------------------------------------------------------------- async def render_entry_page(ctx: dict) -> str: """Full page: entry detail.""" content = _entry_main_panel_html(ctx) hdr = root_header_sx(ctx) child = (_post_header_sx(ctx) + _calendar_header_sx(ctx) + _day_header_sx(ctx) + _entry_header_html(ctx)) hdr += header_child_sx(child) nav_html = _entry_nav_html(ctx) return full_page_sx(ctx, header_rows=hdr, content=content, menu=nav_html) async def render_entry_oob(ctx: dict) -> str: """OOB response: entry detail.""" content = _entry_main_panel_html(ctx) oobs = _day_header_sx(ctx, oob=True) oobs += oob_header_sx("day-header-child", "entry-header-child", _entry_header_html(ctx)) oobs += _clear_deeper_oob("post-row", "post-header-child", "calendar-row", "calendar-header-child", "day-row", "day-header-child", "entry-row", "entry-header-child") nav_html = _entry_nav_html(ctx) return oob_page_sx(oobs=oobs, content=content, menu=nav_html) # --------------------------------------------------------------------------- # Entry optioned (confirm/decline/provisional response) # --------------------------------------------------------------------------- def render_entry_optioned(entry, calendar, day, month, year) -> str: """Render entry options buttons + OOB title & state swaps.""" options = _entry_options_html(entry, calendar, day, month, year) title = _entry_title_html(entry) state = _entry_state_badge_html(getattr(entry, "state", "pending") or "pending") return options + sx_call("events-entry-optioned-oob", entry_id=str(entry.id), title=SxExpr(title), state=SxExpr(state)) def _entry_title_html(entry) -> str: """Render entry title (icon + name + state badge).""" state = getattr(entry, "state", "pending") or "pending" return sx_call("events-entry-title", name=entry.name, badge=_entry_state_badge_html(state)) def _entry_options_html(entry, calendar, day, month, year) -> str: """Render confirm/decline/provisional buttons based on entry state.""" from quart import url_for, g from shared.browser.app.csrf import generate_csrf_token csrf = generate_csrf_token() styles = getattr(g, "styles", None) or {} action_btn = getattr(styles, "action_button", "") if hasattr(styles, "action_button") else styles.get("action_button", "") cal_slug = getattr(calendar, "slug", "") eid = entry.id state = getattr(entry, "state", "pending") or "pending" target = f"#calendar_entry_options_{eid}" def _make_button(action_name, label, confirm_title, confirm_text, *, trigger_type="submit"): url = url_for( f"calendar.day.calendar_entries.calendar_entry.{action_name}", calendar_slug=cal_slug, day=day, month=month, year=year, entry_id=eid, ) btn_type = "button" if trigger_type == "button" else "submit" return sx_call("events-entry-option-button", url=url, target=target, csrf=csrf, btn_type=btn_type, action_btn=action_btn, confirm_title=confirm_title, confirm_text=confirm_text, label=label, is_btn=trigger_type == "button") buttons_html = "" if state == "provisional": buttons_html += _make_button( "confirm_entry", "confirm", "Confirm entry?", "Are you sure you want to confirm this entry?", ) buttons_html += _make_button( "decline_entry", "decline", "Decline entry?", "Are you sure you want to decline this entry?", ) elif state == "confirmed": buttons_html += _make_button( "provisional_entry", "provisional", "Provisional entry?", "Are you sure you want to provisional this entry?", trigger_type="button", ) return sx_call("events-entry-options", entry_id=str(eid), buttons=SxExpr(buttons_html)) # --------------------------------------------------------------------------- # Entry tickets config (display + form) # --------------------------------------------------------------------------- def render_entry_tickets_config(entry, calendar, day, month, year) -> str: """Render ticket config display + edit form for admin entry view.""" from quart import url_for from shared.browser.app.csrf import generate_csrf_token csrf = generate_csrf_token() cal_slug = getattr(calendar, "slug", "") eid = entry.id tp = getattr(entry, "ticket_price", None) tc = getattr(entry, "ticket_count", None) eid_s = str(eid) show_js = f"document.getElementById('ticket-form-{eid}').classList.remove('hidden'); this.classList.add('hidden');" hide_js = (f"document.getElementById('ticket-form-{eid}').classList.add('hidden'); " f"document.getElementById('entry-tickets-{eid}').querySelectorAll('button:not([type=submit])').forEach(btn => btn.classList.remove('hidden'));") if tp is not None: tc_str = f"{tc} tickets" if tc is not None else "Unlimited" display_html = sx_call("events-ticket-config-display", price_str=f"£{tp:.2f}", count_str=tc_str, show_js=show_js) else: display_html = sx_call("events-ticket-config-none", show_js=show_js) update_url = url_for( "calendar.day.calendar_entries.calendar_entry.update_tickets", entry_id=eid, calendar_slug=cal_slug, day=day, month=month, year=year, ) hidden_cls = "" if tp is None else "hidden" tp_val = f"{tp:.2f}" if tp is not None else "" tc_val = str(tc) if tc is not None else "" form_html = sx_call("events-ticket-config-form", entry_id=eid_s, hidden_cls=hidden_cls, update_url=update_url, csrf=csrf, price_val=tp_val, count_val=tc_val, hide_js=hide_js) return display_html + form_html # --------------------------------------------------------------------------- # Entry posts panel # --------------------------------------------------------------------------- def render_entry_posts_panel(entry_posts, entry, calendar, day, month, year) -> str: """Render associated posts list with remove buttons and search input.""" from quart import url_for from shared.browser.app.csrf import generate_csrf_token csrf = generate_csrf_token() cal_slug = getattr(calendar, "slug", "") eid = entry.id eid_s = str(eid) posts_html = "" if entry_posts: items = "" for ep in entry_posts: ep_title = getattr(ep, "title", "") ep_id = getattr(ep, "id", 0) feat = getattr(ep, "feature_image", None) img_html = (sx_call("events-post-img", src=feat, alt=ep_title) if feat else sx_call("events-post-img-placeholder")) del_url = url_for( "calendar.day.calendar_entries.calendar_entry.remove_post", calendar_slug=cal_slug, day=day, month=month, year=year, entry_id=eid, post_id=ep_id, ) items += sx_call("events-entry-post-item", img=SxExpr(img_html), title=ep_title, del_url=del_url, entry_id=eid_s, csrf_hdr=f'{{"X-CSRFToken": "{csrf}"}}') posts_html = sx_call("events-entry-posts-list", items=SxExpr(items)) else: posts_html = sx_call("events-entry-posts-none") search_url = url_for( "calendar.day.calendar_entries.calendar_entry.search_posts", calendar_slug=cal_slug, day=day, month=month, year=year, entry_id=eid, ) return sx_call("events-entry-posts-panel", posts=SxExpr(posts_html), search_url=search_url, entry_id=eid_s) # --------------------------------------------------------------------------- # Entry posts nav OOB # --------------------------------------------------------------------------- def render_entry_posts_nav_oob(entry_posts) -> str: """Render OOB nav for entry posts (scrolling menu).""" from quart import g styles = getattr(g, "styles", None) or {} nav_btn = getattr(styles, "nav_button", "") if hasattr(styles, "nav_button") else styles.get("nav_button", "") blog_url_fn = getattr(g, "blog_url", None) if not entry_posts: return sx_call("events-entry-posts-nav-oob-empty") items = "" for ep in entry_posts: slug = getattr(ep, "slug", "") title = getattr(ep, "title", "") feat = getattr(ep, "feature_image", None) href = blog_url_fn(f"/{slug}/") if blog_url_fn else f"/{slug}/" img_html = (sx_call("events-post-img", src=feat, alt=title) if feat else sx_call("events-post-img-placeholder")) items += sx_call("events-entry-nav-post", href=href, nav_btn=nav_btn, img=SxExpr(img_html), title=title) return sx_call("events-entry-posts-nav-oob", items=SxExpr(items)) # --------------------------------------------------------------------------- # Day entries nav OOB # --------------------------------------------------------------------------- def render_day_entries_nav_oob(confirmed_entries, calendar, day_date) -> str: """Render OOB nav for confirmed entries in a day.""" from quart import url_for, g styles = getattr(g, "styles", None) or {} nav_btn = getattr(styles, "nav_button", "") if hasattr(styles, "nav_button") else styles.get("nav_button", "") cal_slug = getattr(calendar, "slug", "") if not confirmed_entries: return sx_call("events-day-entries-nav-oob-empty") items = "" for entry in confirmed_entries: href = url_for( "calendar.day.calendar_entries.calendar_entry.get", calendar_slug=cal_slug, year=day_date.year, month=day_date.month, day=day_date.day, entry_id=entry.id, ) start = entry.start_at.strftime("%H:%M") if entry.start_at else "" end = f" \u2013 {entry.end_at.strftime('%H:%M')}" if entry.end_at else "" items += sx_call("events-day-nav-entry", href=href, nav_btn=nav_btn, name=entry.name, time_str=start + end) return sx_call("events-day-entries-nav-oob", items=SxExpr(items)) # --------------------------------------------------------------------------- # Post nav entries OOB # --------------------------------------------------------------------------- def render_post_nav_entries_oob(associated_entries, calendars, post) -> str: """Render OOB nav for associated entries and calendars of a post.""" from quart import g from shared.infrastructure.urls import events_url styles = getattr(g, "styles", None) or {} nav_btn = getattr(styles, "nav_button_less_pad", "") if hasattr(styles, "nav_button_less_pad") else styles.get("nav_button_less_pad", "") has_entries = associated_entries and getattr(associated_entries, "entries", None) has_items = has_entries or calendars if not has_items: return sx_call("events-post-nav-oob-empty") slug = post.get("slug", "") if isinstance(post, dict) else getattr(post, "slug", "") items = "" if has_entries: for entry in associated_entries.entries: entry_path = ( f"/{slug}/{entry.calendar_slug}/" f"{entry.start_at.year}/{entry.start_at.month}/{entry.start_at.day}/" f"entries/{entry.id}/" ) href = events_url(entry_path) time_str = entry.start_at.strftime("%b %d, %Y at %H:%M") end_str = f" \u2013 {entry.end_at.strftime('%H:%M')}" if entry.end_at else "" items += sx_call("events-post-nav-entry", href=href, nav_btn=nav_btn, name=entry.name, time_str=time_str + end_str) if calendars: for cal in calendars: cs = getattr(cal, "slug", "") local_href = events_url(f"/{slug}/{cs}/") items += sx_call("events-post-nav-calendar", href=local_href, nav_btn=nav_btn, name=cal.name) hs = ("on load or scroll " "if window.innerWidth >= 640 and my.scrollWidth > my.clientWidth " "remove .hidden from .entries-nav-arrow add .flex to .entries-nav-arrow " "else add .hidden to .entries-nav-arrow remove .flex from .entries-nav-arrow end") return sx_call("events-post-nav-wrapper", items=SxExpr(items), hyperscript=hs) # --------------------------------------------------------------------------- # Calendar description display + edit form # --------------------------------------------------------------------------- def render_calendar_description(calendar, *, oob: bool = False) -> str: """Render calendar description display with edit button, optionally with OOB title.""" from quart import url_for cal_slug = getattr(calendar, "slug", "") edit_url = url_for("calendar.admin.calendar_description_edit", calendar_slug=cal_slug) html = _calendar_description_display_html(calendar, edit_url) if oob: desc = getattr(calendar, "description", "") or "" html += sx_call("events-calendar-description-title-oob", description=desc) return html def render_calendar_description_edit(calendar) -> str: """Render calendar description edit form.""" from quart import url_for from shared.browser.app.csrf import generate_csrf_token csrf = generate_csrf_token() cal_slug = getattr(calendar, "slug", "") desc = getattr(calendar, "description", "") or "" save_url = url_for("calendar.admin.calendar_description_save", calendar_slug=cal_slug) cancel_url = url_for("calendar.admin.calendar_description_view", calendar_slug=cal_slug) return sx_call("events-calendar-description-edit-form", save_url=save_url, cancel_url=cancel_url, csrf=csrf, description=desc) # --------------------------------------------------------------------------- # Calendars list panel (for POST create / DELETE) # --------------------------------------------------------------------------- def render_calendars_list_panel(ctx: dict) -> str: """Render the calendars main panel HTML for POST/DELETE response.""" return _calendars_main_panel_sx(ctx) # --------------------------------------------------------------------------- # Markets list panel (for POST create / DELETE) # --------------------------------------------------------------------------- def render_markets_list_panel(ctx: dict) -> str: """Render the markets main panel HTML for POST/DELETE response.""" return _markets_main_panel_html(ctx) # --------------------------------------------------------------------------- # Slot main panel # --------------------------------------------------------------------------- def render_slot_main_panel(slot, calendar, *, oob: bool = False) -> str: """Render slot detail view.""" from quart import url_for, g styles = getattr(g, "styles", None) or {} list_container = getattr(styles, "list_container", "") if hasattr(styles, "list_container") else styles.get("list_container", "") pre_action = getattr(styles, "pre_action_button", "") if hasattr(styles, "pre_action_button") else styles.get("pre_action_button", "") cal_slug = getattr(calendar, "slug", "") days_display = getattr(slot, "days_display", "\u2014") days = days_display.split(", ") flexible = getattr(slot, "flexible", False) time_start = slot.time_start.strftime("%H:%M") if slot.time_start else "" time_end = slot.time_end.strftime("%H:%M") if slot.time_end else "" cost = getattr(slot, "cost", None) cost_str = f"{cost:.2f}" if cost is not None else "" desc = getattr(slot, "description", "") or "" edit_url = url_for("calendar.slots.slot.get_edit", slot_id=slot.id, calendar_slug=cal_slug) # Days pills if days and days[0] != "\u2014": days_inner = "".join( sx_call("events-slot-day-pill", day=d) for d in days ) days_html = sx_call("events-slot-days-pills", days_inner=SxExpr(days_inner)) else: days_html = sx_call("events-slot-no-days") sid = str(slot.id) result = sx_call("events-slot-panel", slot_id=sid, list_container=list_container, days=SxExpr(days_html), flexible="yes" if flexible else "no", time_str=f"{time_start} \u2014 {time_end}", cost_str=cost_str, pre_action=pre_action, edit_url=edit_url) if oob: result += sx_call("events-slot-description-oob", description=desc) return result # --------------------------------------------------------------------------- # Slots table # --------------------------------------------------------------------------- def render_slots_table(slots, calendar) -> str: """Render slots table with rows and add button.""" from quart import url_for, g from shared.browser.app.csrf import generate_csrf_token csrf = generate_csrf_token() styles = getattr(g, "styles", None) or {} list_container = getattr(styles, "list_container", "") if hasattr(styles, "list_container") else styles.get("list_container", "") tr_cls = getattr(styles, "tr", "") if hasattr(styles, "tr") else styles.get("tr", "") pill_cls = getattr(styles, "pill", "") if hasattr(styles, "pill") else styles.get("pill", "") action_btn = getattr(styles, "action_button", "") if hasattr(styles, "action_button") else styles.get("action_button", "") pre_action = getattr(styles, "pre_action_button", "") if hasattr(styles, "pre_action_button") else styles.get("pre_action_button", "") hx_select = getattr(g, "hx_select_search", "#main-panel") cal_slug = getattr(calendar, "slug", "") rows_html = "" if slots: for s in slots: slot_href = url_for("calendar.slots.slot.get", calendar_slug=cal_slug, slot_id=s.id) del_url = url_for("calendar.slots.slot.slot_delete", calendar_slug=cal_slug, slot_id=s.id) desc = getattr(s, "description", "") or "" days_display = getattr(s, "days_display", "\u2014") day_list = days_display.split(", ") if day_list and day_list[0] != "\u2014": days_inner = "".join( sx_call("events-slot-day-pill", day=d) for d in day_list ) days_html = sx_call("events-slot-days-pills", days_inner=SxExpr(days_inner)) else: days_html = sx_call("events-slot-no-days") time_start = s.time_start.strftime("%H:%M") if s.time_start else "" time_end = s.time_end.strftime("%H:%M") if s.time_end else "" cost = getattr(s, "cost", None) cost_str = f"{cost:.2f}" if cost is not None else "" rows_html += sx_call("events-slots-row", tr_cls=tr_cls, slot_href=slot_href, pill_cls=pill_cls, hx_select=hx_select, slot_name=s.name, description=desc, flexible="yes" if s.flexible else "no", days=SxExpr(days_html), time_str=f"{time_start} - {time_end}", cost_str=cost_str, action_btn=action_btn, del_url=del_url, csrf_hdr=f'{{"X-CSRFToken": "{csrf}"}}') else: rows_html = sx_call("events-slots-empty-row") add_url = url_for("calendar.slots.add_form", calendar_slug=cal_slug) return sx_call("events-slots-table", list_container=list_container, rows=SxExpr(rows_html), pre_action=pre_action, add_url=add_url) # --------------------------------------------------------------------------- # Ticket type main panel # --------------------------------------------------------------------------- def render_ticket_type_main_panel(ticket_type, entry, calendar, day, month, year, *, oob: bool = False) -> str: """Render ticket type detail view.""" from quart import url_for, g styles = getattr(g, "styles", None) or {} list_container = getattr(styles, "list_container", "") if hasattr(styles, "list_container") else styles.get("list_container", "") pre_action = getattr(styles, "pre_action_button", "") if hasattr(styles, "pre_action_button") else styles.get("pre_action_button", "") cal_slug = getattr(calendar, "slug", "") cost = getattr(ticket_type, "cost", None) cost_str = f"\u00a3{cost:.2f}" if cost is not None else "\u00a30.00" count = getattr(ticket_type, "count", 0) tid = str(ticket_type.id) edit_url = url_for( "calendar.day.calendar_entries.calendar_entry.ticket_types.ticket_type.get_edit", ticket_type_id=ticket_type.id, calendar_slug=cal_slug, year=year, month=month, day=day, entry_id=entry.id, ) def _col(label, val): return sx_call("events-ticket-type-col", label=label, value=val) return sx_call("events-ticket-type-panel", ticket_id=tid, list_container=list_container, c1=_col("Name", ticket_type.name), c2=_col("Cost", cost_str), c3=_col("Count", str(count)), pre_action=pre_action, edit_url=edit_url) # --------------------------------------------------------------------------- # Ticket types table # --------------------------------------------------------------------------- def render_ticket_types_table(ticket_types, entry, calendar, day, month, year) -> str: """Render ticket types table with rows and add button.""" from quart import url_for, g from shared.browser.app.csrf import generate_csrf_token csrf = generate_csrf_token() styles = getattr(g, "styles", None) or {} list_container = getattr(styles, "list_container", "") if hasattr(styles, "list_container") else styles.get("list_container", "") tr_cls = getattr(styles, "tr", "") if hasattr(styles, "tr") else styles.get("tr", "") pill_cls = getattr(styles, "pill", "") if hasattr(styles, "pill") else styles.get("pill", "") action_btn = getattr(styles, "action_button", "") if hasattr(styles, "action_button") else styles.get("action_button", "") hx_select = getattr(g, "hx_select_search", "#main-panel") cal_slug = getattr(calendar, "slug", "") eid = entry.id rows_html = "" if ticket_types: for tt in ticket_types: tt_href = url_for( "calendar.day.calendar_entries.calendar_entry.ticket_types.ticket_type.get", calendar_slug=cal_slug, year=year, month=month, day=day, entry_id=eid, ticket_type_id=tt.id, ) del_url = url_for( "calendar.day.calendar_entries.calendar_entry.ticket_types.ticket_type.delete", calendar_slug=cal_slug, year=year, month=month, day=day, entry_id=eid, ticket_type_id=tt.id, ) cost = getattr(tt, "cost", None) cost_str = f"\u00a3{cost:.2f}" if cost is not None else "\u00a30.00" rows_html += sx_call("events-ticket-types-row", tr_cls=tr_cls, tt_href=tt_href, pill_cls=pill_cls, hx_select=hx_select, tt_name=tt.name, cost_str=cost_str, count=str(tt.count), action_btn=action_btn, del_url=del_url, csrf_hdr=f'{{"X-CSRFToken": "{csrf}"}}') else: rows_html = sx_call("events-ticket-types-empty-row") add_url = url_for( "calendar.day.calendar_entries.calendar_entry.ticket_types.add_form", calendar_slug=cal_slug, entry_id=eid, year=year, month=month, day=day, ) return sx_call("events-ticket-types-table", list_container=list_container, rows=SxExpr(rows_html), action_btn=action_btn, add_url=add_url) # --------------------------------------------------------------------------- # Buy result (ticket purchase confirmation) # --------------------------------------------------------------------------- def render_buy_result(entry, created_tickets, remaining, cart_count) -> str: """Render buy result card with created tickets + OOB cart icon.""" from quart import url_for cart_html = _cart_icon_oob(cart_count) count = len(created_tickets) suffix = "s" if count != 1 else "" tickets_html = "" for ticket in created_tickets: href = url_for("tickets.ticket_detail", code=ticket.code) tickets_html += sx_call("events-buy-result-ticket", href=href, code_short=ticket.code[:12] + "...") remaining_html = "" if remaining is not None: r_suffix = "s" if remaining != 1 else "" remaining_html = sx_call("events-buy-result-remaining", text=f"{remaining} ticket{r_suffix} remaining") my_href = url_for("tickets.my_tickets") return cart_html + sx_call("events-buy-result", entry_id=str(entry.id), count_label=f"{count} ticket{suffix} reserved", tickets=SxExpr(tickets_html), remaining=SxExpr(remaining_html), my_tickets_href=my_href) # --------------------------------------------------------------------------- # Buy form (ticket +/- controls) # --------------------------------------------------------------------------- def render_buy_form(entry, ticket_remaining, ticket_sold_count, user_ticket_count, user_ticket_counts_by_type) -> str: """Render the ticket buy/adjust form with +/- controls.""" from quart import url_for from shared.browser.app.csrf import generate_csrf_token csrf = generate_csrf_token() eid = entry.id eid_s = str(eid) tp = getattr(entry, "ticket_price", None) state = getattr(entry, "state", "") ticket_types = getattr(entry, "ticket_types", None) or [] if tp is None: return "" if state != "confirmed": return sx_call("events-buy-not-confirmed", entry_id=eid_s) adjust_url = url_for("tickets.adjust_quantity") target = f"#ticket-buy-{eid}" # Info line info_html = "" info_items = "" if ticket_sold_count: info_items += sx_call("events-buy-info-sold", count=str(ticket_sold_count)) if ticket_remaining is not None: info_items += sx_call("events-buy-info-remaining", count=str(ticket_remaining)) if user_ticket_count: info_items += sx_call("events-buy-info-basket", count=str(user_ticket_count)) if info_items: info_html = sx_call("events-buy-info-bar", items=SxExpr(info_items)) active_types = [tt for tt in ticket_types if getattr(tt, "deleted_at", None) is None] body_html = "" if active_types: type_items = "" for tt in active_types: type_count = user_ticket_counts_by_type.get(tt.id, 0) if user_ticket_counts_by_type else 0 cost_str = f"\u00a3{tt.cost:.2f}" if tt.cost is not None else "\u00a30.00" type_items += sx_call("events-buy-type-item", type_name=tt.name, cost_str=cost_str, adjust_controls=_ticket_adjust_controls(csrf, adjust_url, target, eid, type_count, ticket_type_id=tt.id)) body_html = sx_call("events-buy-types-wrapper", items=SxExpr(type_items)) else: qty = user_ticket_count or 0 body_html = sx_call("events-buy-default", price_str=f"\u00a3{tp:.2f}", adjust_controls=_ticket_adjust_controls(csrf, adjust_url, target, eid, qty)) return sx_call("events-buy-panel", entry_id=eid_s, info=SxExpr(info_html), body=SxExpr(body_html)) def _ticket_adjust_controls(csrf, adjust_url, target, entry_id, count, *, ticket_type_id=None): """Render +/- ticket controls for buy form.""" from quart import url_for tt_html = sx_call("events-adjust-tt-hidden", ticket_type_id=str(ticket_type_id)) if ticket_type_id else "" eid_s = str(entry_id) def _adj_form(count_val, btn_html, *, extra_cls=""): return sx_call("events-adjust-form", adjust_url=adjust_url, target=target, extra_cls=extra_cls, csrf=csrf, entry_id=eid_s, tt=SxExpr(tt_html) if tt_html else None, count_val=str(count_val), btn=SxExpr(btn_html)) if count == 0: return _adj_form(1, sx_call("events-adjust-cart-plus"), extra_cls="flex items-center") my_tickets_href = url_for("tickets.my_tickets") minus = _adj_form(count - 1, sx_call("events-adjust-minus")) cart_icon = sx_call("events-adjust-cart-icon", href=my_tickets_href, count=str(count)) plus = _adj_form(count + 1, sx_call("events-adjust-plus")) return sx_call("events-adjust-controls", minus=SxExpr(minus), cart_icon=SxExpr(cart_icon), plus=SxExpr(plus)) # --------------------------------------------------------------------------- # Adjust response (OOB cart icon + buy form) # --------------------------------------------------------------------------- def render_adjust_response(entry, ticket_remaining, ticket_sold_count, user_ticket_count, user_ticket_counts_by_type, cart_count) -> str: """Render ticket adjust response: OOB cart icon + buy form.""" cart_html = _cart_icon_oob(cart_count) form_html = render_buy_form( entry, ticket_remaining, ticket_sold_count, user_ticket_count, user_ticket_counts_by_type, ) return cart_html + form_html def _cart_icon_oob(count: int) -> str: """Render the OOB cart icon/badge swap.""" from quart import g blog_url_fn = getattr(g, "blog_url", None) cart_url_fn = getattr(g, "cart_url", None) site_fn = getattr(g, "site", None) logo = "" if site_fn: site_obj = site_fn() if callable(site_fn) else site_fn logo = getattr(site_obj, "logo", "") if site_obj else "" if count == 0: blog_href = blog_url_fn("/") if blog_url_fn else "/" return sx_call("events-cart-icon-logo", blog_href=blog_href, logo=logo) cart_href = cart_url_fn("/") if cart_url_fn else "/" return sx_call("events-cart-icon-badge", cart_href=cart_href, count=str(count)) # =========================================================================== # SLOT PICKER JS — shared by entry edit + entry add forms # =========================================================================== _SLOT_PICKER_JS = """\ """ # =========================================================================== # Entry edit form # =========================================================================== def _slot_options_html(day_slots, selected_slot_id=None) -> str: """Build slot