""" Events service s-expression page components. Renders all events, page summary, calendars, calendar month, day, day admin, calendar admin, tickets, ticket admin, markets, and payments pages. Called from route handlers in place of ``render_template()``. """ from __future__ import annotations from typing import Any from markupsafe import escape from shared.sexp.jinja_bridge import sexp from shared.sexp.helpers import ( call_url, get_asset_url, root_header_html, search_mobile_html, search_desktop_html, full_page, oob_page, ) # --------------------------------------------------------------------------- # OOB header helper (same pattern as market) # --------------------------------------------------------------------------- def _oob_header_html(parent_id: str, child_id: str, row_html: str) -> str: """Wrap a header row in OOB div with child placeholder.""" return ( f'
' f'
{row_html}' f'
' ) # --------------------------------------------------------------------------- # Post header helpers (mirrors events/templates/_types/post/header/_header.html) # --------------------------------------------------------------------------- def _post_header_html(ctx: dict, *, oob: bool = False) -> str: """Build the post-level header row.""" post = ctx.get("post") or {} slug = post.get("slug", "") title = (post.get("title") or "")[:160] feature_image = post.get("feature_image") label_parts = [] if feature_image: label_parts.append( f'' ) label_parts.append(f"{escape(title)}") label_html = "".join(label_parts) nav_parts = [] page_cart_count = ctx.get("page_cart_count", 0) if page_cart_count and page_cart_count > 0: cart_href = call_url(ctx, "cart_url", f"/{slug}/") nav_parts.append( f'' f'' f'{page_cart_count}' ) # Post nav: calendar links + admin nav_parts.append(_post_nav_html(ctx)) nav_html = "".join(nav_parts) link_href = call_url(ctx, "blog_url", f"/{slug}/") return sexp( '(~menu-row :id "post-row" :level 1' ' :link-href lh :link-label-html llh' ' :nav-html nh :child-id "post-header-child" :oob oob)', lh=link_href, llh=label_html, nh=nav_html, oob=oob, ) def _post_nav_html(ctx: dict) -> str: """Post desktop nav: calendar links + admin gear.""" from quart import url_for calendars = ctx.get("calendars") or [] rights = ctx.get("rights") or {} is_admin = getattr(rights, "admin", False) if hasattr(rights, "admin") else rights.get("admin", False) hx_select = ctx.get("hx_select_search", "#main-panel") select_colours = ctx.get("select_colours", "") post = ctx.get("post") or {} slug = post.get("slug", "") 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("calendars.calendar.get", calendar_slug=cal_slug) parts.append(sexp( '(~nav-link :href h :icon "fa fa-calendar" :label l :select-colours sc)', h=href, l=cal_name, sc=select_colours, )) if is_admin: admin_href = call_url(ctx, "blog_url", f"/{slug}/admin/") parts.append( f'' f'' ) return "".join(parts) # --------------------------------------------------------------------------- # Post admin header # --------------------------------------------------------------------------- def _post_admin_header_html(ctx: dict, *, oob: bool = False) -> str: """Build the post-admin-level header row.""" post = ctx.get("post") or {} slug = post.get("slug", "") link_href = call_url(ctx, "blog_url", f"/{slug}/admin/") return sexp( '(~menu-row :id "post-admin-row" :level 2' ' :link-href lh :link-label "admin" :icon "fa fa-cog"' ' :child-id "post-admin-header-child" :oob oob)', lh=link_href, oob=oob, ) # --------------------------------------------------------------------------- # Calendars header # --------------------------------------------------------------------------- def _calendars_header_html(ctx: dict, *, oob: bool = False) -> str: """Build the calendars section header row.""" from quart import url_for link_href = url_for("calendars.home") return sexp( '(~menu-row :id "calendars-row" :level 3' ' :link-href lh :link-label-html llh' ' :child-id "calendars-header-child" :oob oob)', lh=link_href, llh='
Calendars
', oob=oob, ) # --------------------------------------------------------------------------- # Calendar header # --------------------------------------------------------------------------- def _calendar_header_html(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("calendars.calendar.get", calendar_slug=cal_slug) label_html = ( '
' '
' f'' f'
{escape(cal_name)}
' '
' f'
{escape(cal_desc)}
' '
' ) # Desktop nav: slots + admin nav_html = _calendar_nav_html(ctx) return sexp( '(~menu-row :id "calendar-row" :level 3' ' :link-href lh :link-label-html llh' ' :nav-html nh :child-id "calendar-header-child" :oob oob)', lh=link_href, llh=label_html, nh=nav_html, oob=oob, ) def _calendar_nav_html(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("calendars.calendar.slots.get", calendar_slug=cal_slug) parts.append(sexp( '(~nav-link :href h :icon "fa fa-clock" :label "Slots" :select-colours sc)', h=slots_href, sc=select_colours, )) if is_admin: admin_href = url_for("calendars.calendar.admin.admin", calendar_slug=cal_slug) parts.append( f'' f'' ) return "".join(parts) # --------------------------------------------------------------------------- # Day header # --------------------------------------------------------------------------- def _day_header_html(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( "calendars.calendar.day.show_day", calendar_slug=cal_slug, year=day_date.year, month=day_date.month, day=day_date.day, ) label_html = ( '
' f'' f' {escape(day_date.strftime("%A %d %B %Y"))}' '
' ) nav_html = _day_nav_html(ctx) return sexp( '(~menu-row :id "day-row" :level 4' ' :link-href lh :link-label-html llh' ' :nav-html nh :child-id "day-header-child" :oob oob)', lh=link_href, llh=label_html, nh=nav_html, oob=oob, ) def _day_nav_html(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: parts.append( '
' '
' ) for entry in confirmed_entries: href = url_for( "calendars.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, ) name = escape(entry.name) start = entry.start_at.strftime("%H:%M") if entry.start_at else "" end = f" – {entry.end_at.strftime('%H:%M')}" if entry.end_at else "" parts.append( f'' f'
' f'
{name}
' f'
{start}{end}
' f'
' ) parts.append('
') if is_admin and day_date: admin_href = url_for( "calendars.calendar.day.admin.admin", calendar_slug=cal_slug, year=day_date.year, month=day_date.month, day=day_date.day, ) parts.append( f'' f'' ) return "".join(parts) # --------------------------------------------------------------------------- # Day admin header # --------------------------------------------------------------------------- def _day_admin_header_html(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( "calendars.calendar.day.admin.admin", calendar_slug=cal_slug, year=day_date.year, month=day_date.month, day=day_date.day, ) return sexp( '(~menu-row :id "day-admin-row" :level 5' ' :link-href lh :link-label "admin" :icon "fa fa-cog"' ' :child-id "day-admin-header-child" :oob oob)', lh=link_href, oob=oob, ) # --------------------------------------------------------------------------- # Calendar admin header # --------------------------------------------------------------------------- def _calendar_admin_header_html(ctx: dict, *, oob: bool = False) -> str: """Build calendar admin header row.""" return sexp( '(~menu-row :id "calendar-admin-row" :level 4' ' :link-label "admin" :icon "fa fa-cog"' ' :child-id "calendar-admin-header-child" :oob oob)', oob=oob, ) # --------------------------------------------------------------------------- # Markets header # --------------------------------------------------------------------------- def _markets_header_html(ctx: dict, *, oob: bool = False) -> str: """Build the markets section header row.""" from quart import url_for link_href = url_for("markets.home") return sexp( '(~menu-row :id "markets-row" :level 3' ' :link-href lh :link-label-html llh' ' :child-id "markets-header-child" :oob oob)', lh=link_href, llh='
Markets
', oob=oob, ) # --------------------------------------------------------------------------- # Payments header # --------------------------------------------------------------------------- def _payments_header_html(ctx: dict, *, oob: bool = False) -> str: """Build the payments section header row.""" from quart import url_for link_href = url_for("payments.home") return sexp( '(~menu-row :id "payments-row" :level 3' ' :link-href lh :link-label-html llh' ' :child-id "payments-header-child" :oob oob)', lh=link_href, llh='
Payments
', oob=oob, ) # --------------------------------------------------------------------------- # Calendars main panel # --------------------------------------------------------------------------- def _calendars_main_panel_html(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 [] hx_select = ctx.get("hx_select_search", "#main-panel") parts = ['
'] if can_create: create_url = url_for("calendars.create_calendar") parts.append( '
' f'
""" f'' '
' '
' '
' ) parts.append('
') parts.append(_calendars_list_html(ctx, calendars)) parts.append('
') return "".join(parts) def _calendars_list_html(ctx: dict, calendars: list) -> str: """Render the calendars list items.""" from quart import url_for from shared.utils import route_prefix hx_select = ctx.get("hx_select_search", "#main-panel") csrf_token = ctx.get("csrf_token") csrf = csrf_token() if callable(csrf_token) else (csrf_token or "") prefix = route_prefix() if not calendars: return '

No calendars yet. Create one above.

' parts = [] for cal in calendars: cal_slug = getattr(cal, "slug", "") cal_name = getattr(cal, "name", "") href = prefix + url_for("calendars.calendar.get", calendar_slug=cal_slug) del_url = url_for("calendars.calendar.delete", calendar_slug=cal_slug) parts.append( f'
' f'' f'

{escape(cal_name)}

' f'

/{escape(cal_slug)}/

' f'
' ) 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): href = url_for("calendars.calendar.get", calendar_slug=cal_slug, year=y, month=m) return href # Month navigation header parts = ['
'] parts.append('
') parts.append('
') # Calendar grid parts.append('
') # Weekday headers parts.append('') # Weeks grid parts.append('
') for week in weeks: for day_cell in week: 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" parts.append(f'
') parts.append('
') parts.append(f'{day_date.strftime("%a")}') # Clickable day number if day_date: day_href = url_for( "calendars.calendar.day.show_day", calendar_slug=cal_slug, year=day_date.year, month=day_date.month, day=day_date.day, ) parts.append( f'{day_date.day}' ) parts.append('
') # Entries for this day parts.append('
') if day_date: for e in month_entries: if 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("_", " ") parts.append( f'
' f'{escape(e.name)}' f'{state_label}' f'
' ) parts.append('
') parts.append('
') return "".join(parts) # --------------------------------------------------------------------------- # 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", "") parts = [f'
'] parts.append( '' '' '' '' '' '' '' '' ) if day_entries: for entry in day_entries: parts.append(_day_row_html(ctx, entry)) else: parts.append('') parts.append('
NameSlot/TimeStateCostTicketsActions
No entries yet.
') # Add entry button add_url = url_for( "calendars.calendar.day.calendar_entries.add_form", calendar_slug=cal_slug, day=day, month=month, year=year, ) parts.append( f'
' f'
' ) parts.append('
') return "".join(parts) 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( "calendars.calendar.day.calendar_entries.calendar_entry.get", calendar_slug=cal_slug, day=day, month=month, year=year, entry_id=entry.id, ) # Name name_html = ( f'
' f'' f'{escape(entry.name)}
' ) # Slot/Time slot = getattr(entry, "slot", None) if slot: slot_href = url_for("calendars.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" → {slot.time_end.strftime('%H:%M')}" if slot.time_end else "" slot_html = ( f'
' f'' f'{escape(slot.name)}' f'({time_start}{time_end})' f'
' ) else: start = entry.start_at.strftime("%H:%M") if entry.start_at else "" end = f" → {entry.end_at.strftime('%H:%M')}" if entry.end_at else "" slot_html = f'
{start}{end}
' # State state = getattr(entry, "state", "pending") or "pending" state_html = _entry_state_badge_html(state) state_td = f'
{state_html}
' # Cost cost = getattr(entry, "cost", None) cost_str = f"£{cost:.2f}" if cost is not None else "£0.00" cost_td = f'{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 = ( f'
' f'
£{tp:.2f}
' f'
{tc_str}
' ) else: tickets_td = 'No tickets' # Actions (entry options) - keep simple, just link to entry actions_td = f'' return f'{name_html}{slot_html}{state_td}{cost_td}{tickets_td}{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 f'{label}' # --------------------------------------------------------------------------- # Day admin main panel # --------------------------------------------------------------------------- def _day_admin_main_panel_html(ctx: dict) -> str: """Render day admin panel (placeholder nav).""" return '
Admin options
' # --------------------------------------------------------------------------- # 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("calendars.calendar.admin.calendar_description_edit", calendar_slug=cal_slug) description_html = _calendar_description_display_html(calendar, desc_edit_url) parts = ['
'] parts.append('

Calendar configuration

') parts.append('
') parts.append(f'
') parts.append(description_html) parts.append('
') # Hidden form for direct PUT parts.append( f'
' f'' '
' f'
{escape(desc)}
' f'' '
' ) parts.append('

') return "".join(parts) def _calendar_description_display_html(calendar, edit_url: str) -> str: """Render calendar description display with edit button.""" desc = getattr(calendar, "description", "") or "" if desc: desc_html = f'

{escape(desc)}

' else: desc_html = '

No description yet.

' return ( f'
{desc_html}' f'
' ) # --------------------------------------------------------------------------- # 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 [] post = ctx.get("post") or {} parts = ['
'] if can_create: create_url = url_for("markets.create_market") parts.append( '
' f'
""" f'' '
' '
' '
' ) parts.append('
') parts.append(_markets_list_html(ctx, markets)) parts.append('
') return "".join(parts) 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 '

No markets yet. Create one above.

' 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) parts.append( f'
' f'' f'

{escape(m_name)}

' f'

/{escape(m_slug)}/

' f'
' ) return "".join(parts) # --------------------------------------------------------------------------- # Payments main panel # --------------------------------------------------------------------------- def _payments_main_panel_html(ctx: dict) -> str: """Render SumUp payment config form.""" from quart import url_for csrf_token = ctx.get("csrf_token") csrf = csrf_token() if callable(csrf_token) else (csrf_token or "") sumup_configured = ctx.get("sumup_configured", False) merchant_code = ctx.get("sumup_merchant_code", "") checkout_prefix = ctx.get("sumup_checkout_prefix", "") update_url = url_for("payments.update_sumup") placeholder = "--------" if sumup_configured else "sup_sk_..." key_note = '

Key is set. Leave blank to keep current key.

' if sumup_configured else "" connected = ('' ' Connected') if sumup_configured else "" return ( '
' '
' '

' ' SumUp Payment

' '

Configure per-page SumUp credentials. Leave blank to use the global merchant account.

' f'
' f'' '
' f'
' '
' f'' f'{key_note}
' '
' f'
' '{connected}
' ) # --------------------------------------------------------------------------- # 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 f'{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 parts = [f'
'] parts.append('

My Tickets

') if tickets: parts.append('
') 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", "") parts.append( f'' '
' f'
{escape(entry_name)}
' ) if tt: parts.append(f'
{escape(tt.name)}
') if entry: parts.append( '
' f'{entry.start_at.strftime("%A, %B %d, %Y at %H:%M")}' ) if entry.end_at: parts.append(f' – {entry.end_at.strftime("%H:%M")}') parts.append('
') cal = getattr(entry, "calendar", None) if cal: parts.append(f'
{escape(cal.name)}
') parts.append('
') parts.append(_ticket_state_badge_html(state)) parts.append(f'{ticket.code[:8]}...') parts.append('
') parts.append('
') else: parts.append( '
' '' '

No tickets yet

' '

Tickets will appear here after you purchase them.

' ) parts.append('
') return "".join(parts) # --------------------------------------------------------------------------- # 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 # Background color for header 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") parts = [f'
'] parts.append( f'' ' Back to my tickets' ) parts.append('
') # Header parts.append(f'
') parts.append(f'

{escape(entry_name)}

') parts.append(_ticket_state_badge_html(state).replace('px-2 py-0.5 text-xs', 'px-3 py-1 text-sm')) parts.append('
') if tt: parts.append(f'
{escape(tt.name)}
') parts.append('
') # QR code parts.append( f'
' f'
' f'

{code}

' ) # Event details parts.append('
') if entry: parts.append( '
' f'
{entry.start_at.strftime("%A, %B %d, %Y")}
' f'
{entry.start_at.strftime("%H:%M")}' ) if entry.end_at: parts.append(f' – {entry.end_at.strftime("%H:%M")}') parts.append('
') cal = getattr(entry, "calendar", None) if cal: parts.append( '
' f'
{escape(cal.name)}
' ) if tt and getattr(tt, "cost", None): parts.append( '
' f'
{escape(tt.name)} — £{tt.cost:.2f}
' ) checked_in_at = getattr(ticket, "checked_in_at", None) if checked_in_at: parts.append( '
' f'
Checked in: {checked_in_at.strftime("%B %d, %Y at %H:%M")}
' ) parts.append('
') # QR code script parts.append( '' '' ) parts.append('
') return "".join(parts) # --------------------------------------------------------------------------- # 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") parts = [f'
'] parts.append('

Ticket Admin

') # Stats parts.append('
') 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" parts.append( f'
' f'
{val}
' f'
{label}
' ) parts.append('
') # Scanner parts.append( '
' '

Scan / Look Up Ticket

' '
' f'' '
' '
' 'Enter a ticket code to look it up
' ) # Recent tickets table parts.append('
') parts.append('

Recent Tickets

') if tickets: parts.append('
') for col in ["Code", "Event", "Type", "State", "Actions"]: parts.append(f'') parts.append('') for ticket in tickets: entry = getattr(ticket, "entry", None) tt = getattr(ticket, "ticket_type", None) state = getattr(ticket, "state", "") code = ticket.code parts.append(f'') parts.append(f'') parts.append(f'') parts.append(f'') parts.append(f'') # Actions parts.append('') parts.append('
{col}
{code[:12]}...
{escape(entry.name) if entry else "—"}
') if entry and entry.start_at: parts.append(f'
{entry.start_at.strftime("%d %b %Y, %H:%M")}
') parts.append('
{escape(tt.name) if tt else "—"}{_ticket_state_badge_html(state)}') if state in ("confirmed", "reserved"): checkin_url = url_for("ticket_admin.do_checkin", code=code) parts.append( f'
' f'' '
' ) 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 "" parts.append(f' {t_str}') parts.append('
') else: parts.append('
No tickets yet
') parts.append('
') return "".join(parts) # --------------------------------------------------------------------------- # 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: day_href = events_url_fn(f"/{page_slug}/calendars/{entry.calendar_slug}/day/{entry.start_at.strftime('%Y/%-m/%-d')}/") entry_href = f"{day_href}entries/{entry.id}/" if day_href else "" parts = ['
'] parts.append('
') parts.append('
') if entry_href: parts.append(f'') parts.append(f'

{escape(entry.name)}

') if entry_href: parts.append('
') # Badges parts.append('
') if page_title and (not is_page_scoped or page_title != (post or {}).get("title")): page_href = events_url_fn(f"/{page_slug}/") parts.append( f'' f'{escape(page_title)}' ) cal_name = getattr(entry, "calendar_name", "") if cal_name: parts.append(f'{escape(cal_name)}') parts.append('
') # Time parts.append('
') if day_href and not is_page_scoped: parts.append(f'{entry.start_at.strftime("%a %-d %b")} · ') elif not is_page_scoped: parts.append(f'{entry.start_at.strftime("%a %-d %b")} · ') parts.append(entry.start_at.strftime("%H:%M")) if entry.end_at: parts.append(f' – {entry.end_at.strftime("%H:%M")}') parts.append('
') cost = getattr(entry, "cost", None) if cost: parts.append(f'
£{cost:.2f}
') parts.append('
') # Ticket widget tp = getattr(entry, "ticket_price", None) if tp is not None: qty = pending_tickets.get(entry.id, 0) parts.append('
') parts.append(_ticket_widget_html(entry, qty, ticket_url, ctx={})) parts.append('
') parts.append('
') return "".join(parts) 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: day_href = events_url_fn(f"/{page_slug}/calendars/{entry.calendar_slug}/day/{entry.start_at.strftime('%Y/%-m/%-d')}/") entry_href = f"{day_href}entries/{entry.id}/" if day_href else "" parts = ['
'] if entry_href: parts.append(f'') parts.append(f'

{escape(entry.name)}

') if entry_href: parts.append('
') parts.append('
') if page_title and (not is_page_scoped or page_title != (post or {}).get("title")): page_href = events_url_fn(f"/{page_slug}/") parts.append( f'' f'{escape(page_title)}' ) cal_name = getattr(entry, "calendar_name", "") if cal_name: parts.append(f'{escape(cal_name)}') parts.append('
') parts.append('
') if day_href: parts.append(f'{entry.start_at.strftime("%a %-d %b")}') else: parts.append(entry.start_at.strftime("%a %-d %b")) parts.append(f' · {entry.start_at.strftime("%H:%M")}') if entry.end_at: parts.append(f' – {entry.end_at.strftime("%H:%M")}') parts.append('
') cost = getattr(entry, "cost", None) if cost: parts.append(f'
£{cost:.2f}
') parts.append('
') tp = getattr(entry, "ticket_price", None) if tp is not None: qty = pending_tickets.get(entry.id, 0) parts.append('
') parts.append(_ticket_widget_html(entry, qty, ticket_url, ctx={})) parts.append('
') parts.append('
') return "".join(parts) 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: from quart import g as _g ct = getattr(_g, "_csrf_token", None) try: from quart import current_app with current_app.app_context(): pass except Exception: pass # Use a deferred approach - get CSRF from template context csrf_token_val = "" # For the ticket widget, we need to get csrf token from the app try: from flask_wtf.csrf import generate_csrf csrf_token_val = generate_csrf() except Exception: pass if not csrf_token_val: try: from quart import current_app csrf_token_val = current_app.config.get("WTF_CSRF_SECRET_KEY", "") except Exception: pass eid = entry.id tp = getattr(entry, "ticket_price", 0) or 0 cart_url_fn = None parts = [f'
'] parts.append(f'£{tp:.2f}') if qty == 0: parts.append( f'
' f'' f'' '' '
' ) else: # Minus button parts.append( f'
' f'' f'' f'' '
' ) # Cart icon with count parts.append( '' '' '' '' f'{qty}' '' ) # Plus button parts.append( f'
' f'' f'' f'' '
' ) parts.append('
') return "".join(parts) 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_date != last_date: parts.append( f'

' f'{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( f'' ) return "".join(parts) # --------------------------------------------------------------------------- # All events / page summary main panels # --------------------------------------------------------------------------- _LIST_SVG = '' _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", "/") qs_fn = ctx.get("qs") hx_select = ctx.get("hx_select_search", "#main-panel") # Build hrefs - list removes view param, tile sets view=tile list_href = prefix + str(clh) tile_href = prefix + str(clh) # Use simple query parameter manipulation 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 ( '""" ) 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.""" parts = [_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, ) if view == "tile": parts.append(f'
{cards}
') else: parts.append(f'
{cards}
') else: parts.append( '
' '' '

No upcoming events

' ) parts.append('
') return "".join(parts) # --------------------------------------------------------------------------- # 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, 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_html(ctx) return full_page(ctx, header_rows_html=hdr, content_html=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, 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(ctx, content_html=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, 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, 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_html(ctx) hdr += sexp( '(div :id "root-header-child" :class "w-full" (raw! ph))', ph=_post_header_html(ctx), ) return full_page(ctx, header_rows_html=hdr, content_html=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, 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_html(ctx, oob=True) return oob_page(ctx, oobs_html=oobs, content_html=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, 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_html(ctx) hdr = root_header_html(ctx) hdr += sexp( '(div :id "root-header-child" :class "w-full" (raw! ph (raw! pah (raw! ch))))', ph=_post_header_html(ctx), pah=_post_admin_header_html(ctx), ch=_calendars_header_html(ctx), ) return full_page(ctx, header_rows_html=hdr, content_html=content) async def render_calendars_oob(ctx: dict) -> str: """OOB response: calendars listing.""" content = _calendars_main_panel_html(ctx) oobs = _post_admin_header_html(ctx, oob=True) oobs += _oob_header_html("post-admin-header-child", "calendars-header-child", _calendars_header_html(ctx)) return oob_page(ctx, oobs_html=oobs, content_html=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_html(ctx) hdr += sexp( '(div :id "root-header-child" :class "w-full" (raw! ph (raw! pah (raw! ch))))', ph=_post_header_html(ctx), pah=_post_admin_header_html(ctx), ch=_calendar_header_html(ctx), ) return full_page(ctx, header_rows_html=hdr, content_html=content) async def render_calendar_oob(ctx: dict) -> str: """OOB response: calendar month view.""" content = _calendar_main_panel_html(ctx) oobs = _post_header_html(ctx, oob=True) oobs += _oob_header_html("post-header-child", "calendar-header-child", _calendar_header_html(ctx)) return oob_page(ctx, oobs_html=oobs, content_html=content) # --------------------------------------------------------------------------- # Day detail # --------------------------------------------------------------------------- async def render_day_page(ctx: dict) -> str: """Full page: day detail.""" content = _day_main_panel_html(ctx) hdr = root_header_html(ctx) hdr += sexp( '(div :id "root-header-child" :class "w-full" (raw! ph (raw! pah (raw! ch (raw! dh)))))', ph=_post_header_html(ctx), pah=_post_admin_header_html(ctx), ch=_calendar_header_html(ctx), dh=_day_header_html(ctx), ) return full_page(ctx, header_rows_html=hdr, content_html=content) async def render_day_oob(ctx: dict) -> str: """OOB response: day detail.""" content = _day_main_panel_html(ctx) oobs = _calendar_header_html(ctx, oob=True) oobs += _oob_header_html("calendar-header-child", "day-header-child", _day_header_html(ctx)) return oob_page(ctx, oobs_html=oobs, content_html=content) # --------------------------------------------------------------------------- # Day admin # --------------------------------------------------------------------------- async def render_day_admin_page(ctx: dict) -> str: """Full page: day admin.""" content = _day_admin_main_panel_html(ctx) hdr = root_header_html(ctx) hdr += sexp( '(div :id "root-header-child" :class "w-full" (raw! ph (raw! pah (raw! ch (raw! dh (raw! dah))))))', ph=_post_header_html(ctx), pah=_post_admin_header_html(ctx), ch=_calendar_header_html(ctx), dh=_day_header_html(ctx), dah=_day_admin_header_html(ctx), ) return full_page(ctx, header_rows_html=hdr, content_html=content) async def render_day_admin_oob(ctx: dict) -> str: """OOB response: day admin.""" content = _day_admin_main_panel_html(ctx) oobs = _calendar_header_html(ctx, oob=True) oobs += _oob_header_html("day-header-child", "day-admin-header-child", _day_admin_header_html(ctx)) return oob_page(ctx, oobs_html=oobs, content_html=content) # --------------------------------------------------------------------------- # Calendar admin # --------------------------------------------------------------------------- async def render_calendar_admin_page(ctx: dict) -> str: """Full page: calendar admin.""" content = _calendar_admin_main_panel_html(ctx) hdr = root_header_html(ctx) hdr += sexp( '(div :id "root-header-child" :class "w-full" (raw! ph (raw! pah (raw! ch (raw! cah)))))', ph=_post_header_html(ctx), pah=_post_admin_header_html(ctx), ch=_calendar_header_html(ctx), cah=_calendar_admin_header_html(ctx), ) return full_page(ctx, header_rows_html=hdr, content_html=content) async def render_calendar_admin_oob(ctx: dict) -> str: """OOB response: calendar admin.""" content = _calendar_admin_main_panel_html(ctx) oobs = _calendar_header_html(ctx, oob=True) oobs += _oob_header_html("calendar-header-child", "calendar-admin-header-child", _calendar_admin_header_html(ctx)) return oob_page(ctx, oobs_html=oobs, content_html=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_html(ctx) return full_page(ctx, header_rows_html=hdr, content_html=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(ctx, content_html=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_html(ctx) return full_page(ctx, header_rows_html=hdr, content_html=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(ctx, content_html=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_html(ctx) return full_page(ctx, header_rows_html=hdr, content_html=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(ctx, content_html=content) # --------------------------------------------------------------------------- # Markets # --------------------------------------------------------------------------- async def render_markets_page(ctx: dict) -> str: """Full page: markets listing.""" content = _markets_main_panel_html(ctx) hdr = root_header_html(ctx) hdr += sexp( '(div :id "root-header-child" :class "w-full" (raw! ph (raw! pah (raw! mh))))', ph=_post_header_html(ctx), pah=_post_admin_header_html(ctx), mh=_markets_header_html(ctx), ) return full_page(ctx, header_rows_html=hdr, content_html=content) async def render_markets_oob(ctx: dict) -> str: """OOB response: markets listing.""" content = _markets_main_panel_html(ctx) oobs = _post_admin_header_html(ctx, oob=True) oobs += _oob_header_html("post-admin-header-child", "markets-header-child", _markets_header_html(ctx)) return oob_page(ctx, oobs_html=oobs, content_html=content) # --------------------------------------------------------------------------- # Payments # --------------------------------------------------------------------------- async def render_payments_page(ctx: dict) -> str: """Full page: payments admin.""" content = _payments_main_panel_html(ctx) hdr = root_header_html(ctx) hdr += sexp( '(div :id "root-header-child" :class "w-full" (raw! ph (raw! pah (raw! pyh))))', ph=_post_header_html(ctx), pah=_post_admin_header_html(ctx), pyh=_payments_header_html(ctx), ) return full_page(ctx, header_rows_html=hdr, content_html=content) async def render_payments_oob(ctx: dict) -> str: """OOB response: payments admin.""" content = _payments_main_panel_html(ctx) oobs = _post_admin_header_html(ctx, oob=True) oobs += _oob_header_html("post-admin-header-child", "payments-header-child", _payments_header_html(ctx)) return oob_page(ctx, oobs_html=oobs, content_html=content)