"""
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'
'
)
# ---------------------------------------------------------------------------
# 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(
'')
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''
)
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''
)
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('')
# Calendar grid
parts.append('')
# Weekday headers
parts.append('
')
for wd in weekday_names:
parts.append(f'
{wd}
')
parts.append('
')
# Weeks grid
parts.append('
')
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"
parts.append(f'
')
parts.append('
')
if day_date:
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 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("_", " ")
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(
''
'Name '
'Slot/Time '
'State '
'Cost '
'Tickets '
'Actions '
' '
)
if day_entries:
for entry in day_entries:
parts.append(_day_row_html(ctx, entry))
else:
parts.append('No entries yet. ')
parts.append('
')
# 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''
f'+ Add entry
'
)
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' '
)
# 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'
Description ')
parts.append(description_html)
parts.append('
')
# Hidden form for direct PUT
parts.append(
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''
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' '
'Name '
'
'
'Add market '
)
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''
)
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 (
''
)
# ---------------------------------------------------------------------------
# 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 and entry.start_at:
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'')
# QR code
parts.append(
f'
'
)
# Event details
parts.append('
')
if entry and entry.start_at:
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(
'
'
)
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'
'
)
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'{col} ')
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'{code[:12]}... ')
parts.append(f'{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(' ')
parts.append(f'{escape(tt.name) if tt else "—"} ')
parts.append(f'{_ticket_state_badge_html(state)} ')
# Actions
parts.append('')
if state in ("confirmed", "reserved"):
checkin_url = url_for("ticket_admin.do_checkin", code=code)
parts.append(
f''
f' '
''
' Check in '
)
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(' ')
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 and entry.start_at:
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 and entry.start_at:
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.start_at else ""
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(
''
)
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
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_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
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(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
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_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
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_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
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_html(ctx)
hdr = root_header_html(ctx)
child = _post_header_html(ctx) + _post_admin_header_html(ctx) + _calendars_header_html(ctx)
hdr += sexp('(div :id "root-header-child" :class "w-full" (raw! h))', h=child)
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)
child = _post_header_html(ctx) + _post_admin_header_html(ctx) + _calendar_header_html(ctx)
hdr += sexp('(div :id "root-header-child" :class "w-full" (raw! h))', h=child)
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)
child = (_post_header_html(ctx) + _post_admin_header_html(ctx)
+ _calendar_header_html(ctx) + _day_header_html(ctx))
hdr += sexp('(div :id "root-header-child" :class "w-full" (raw! h))', h=child)
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)
child = (_post_header_html(ctx) + _post_admin_header_html(ctx)
+ _calendar_header_html(ctx) + _day_header_html(ctx)
+ _day_admin_header_html(ctx))
hdr += sexp('(div :id "root-header-child" :class "w-full" (raw! h))', h=child)
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)
child = (_post_header_html(ctx) + _post_admin_header_html(ctx)
+ _calendar_header_html(ctx) + _calendar_admin_header_html(ctx))
hdr += sexp('(div :id "root-header-child" :class "w-full" (raw! h))', h=child)
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)
child = _post_header_html(ctx) + _post_admin_header_html(ctx) + _markets_header_html(ctx)
hdr += sexp('(div :id "root-header-child" :class "w-full" (raw! h))', h=child)
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)
child = _post_header_html(ctx) + _post_admin_header_html(ctx) + _payments_header_html(ctx)
hdr += sexp('(div :id "root-header-child" :class "w-full" (raw! h))', h=child)
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)
# ===========================================================================
# 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:
err_msg = escape(error or "Check-in failed")
return (
''
f' {err_msg}'
'
'
)
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"
entry_name = escape(entry.name) if entry else "—"
date_html = ""
if entry and entry.start_at:
date_html = f'{entry.start_at.strftime("%d %b %Y, %H:%M")}
'
tt_name = escape(tt.name) if tt else "—"
return (
f''
f'{code[:12]}... '
f'{entry_name}
{date_html} '
f'{tt_name} '
f'{_ticket_state_badge_html("checked_in")} '
f''
f' {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 (
''
f' {escape(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()
entry_name = escape(entry.name) if entry else "Unknown event"
parts = ['']
parts.append('
')
parts.append(f'
{entry_name}
')
if tt:
parts.append(f'
{escape(tt.name)}
')
if entry and entry.start_at:
parts.append(f'
{entry.start_at.strftime("%A, %B %d, %Y at %H:%M")}
')
cal = getattr(entry, "calendar", None) if entry else None
if cal:
parts.append(f'
{escape(cal.name)}
')
parts.append('
')
parts.append(_ticket_state_badge_html(state))
parts.append(f'{code} ')
parts.append('
')
if checked_in_at:
parts.append(f'
Checked in: {checked_in_at.strftime("%B %d, %Y at %H:%M")}
')
parts.append('
')
# Action area
parts.append(f'
')
if state in ("confirmed", "reserved"):
checkin_url = url_for("ticket_admin.do_checkin", code=code)
parts.append(
f'
'
f' '
''
' Check In '
)
elif state == "checked_in":
parts.append(
'
'
)
elif state == "cancelled":
parts.append(
'
'
)
parts.append('
')
return "".join(parts)
# ---------------------------------------------------------------------------
# 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 ""
parts = ['']
parts.append(
'
'
f'
Tickets for: {escape(entry.name)} '
f'{count} ticket{suffix} '
''
)
if tickets:
parts.append('
')
parts.append(
'
'
'Code '
'Type '
'State '
'Actions '
' '
)
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)
parts.append(f'')
parts.append(f'{code[:12]}... ')
parts.append(f'{escape(tt.name) if tt else "—"} ')
parts.append(f'{_ticket_state_badge_html(state)} ')
parts.append('')
if state in ("confirmed", "reserved"):
checkin_url = url_for("ticket_admin.do_checkin", code=code)
parts.append(
f''
f' '
''
'Check in '
)
elif state == "checked_in":
t_str = checked_in_at.strftime("%H:%M") if checked_in_at else ""
parts.append(f' {t_str} ')
parts.append(' ')
parts.append('
')
else:
parts.append('
No tickets for this entry
')
parts.append('
')
return "".join(parts)
# ---------------------------------------------------------------------------
# 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
from shared.browser.app.csrf import generate_csrf_token
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"
parts = [f'']
# Name
parts.append(
''
'
Name
'
f'
{escape(entry.name)}
'
'
'
)
# Slot
slot = getattr(entry, "slot", None)
parts.append(
''
'
Slot
'
'
'
)
if slot:
flex_label = "(flexible)" if getattr(slot, "flexible", False) else "(fixed)"
parts.append(
f'{escape(slot.name)} '
f'{flex_label} '
)
else:
parts.append('No slot assigned ')
parts.append('
')
# Time Period
start_str = entry.start_at.strftime("%H:%M") if entry.start_at else ""
end_str = f" – {entry.end_at.strftime('%H:%M')}" if entry.end_at else " – open-ended"
parts.append(
''
'
Time Period
'
f'
{start_str}{end_str}
'
'
'
)
# State
state_badge = _entry_state_badge_html(state)
parts.append(
''
)
# Cost
cost = getattr(entry, "cost", None)
cost_str = f"{cost:.2f}" if cost is not None else "0.00"
parts.append(
''
'
Cost
'
f'
£{cost_str}
'
'
'
)
# Ticket Configuration (admin)
parts.append(
''
'
Tickets
'
f'
'
)
parts.append(render_entry_tickets_config(entry, calendar, day, month, year))
parts.append('
')
# 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 {}
parts.append(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 ""
parts.append(
''
)
# Associated Posts
entry_posts = ctx.get("entry_posts") or []
parts.append(
''
'
Associated Posts
'
f'
'
)
parts.append(render_entry_posts_panel(entry_posts, entry, calendar, day, month, year))
parts.append('
')
# Options and Edit Button
parts.append('')
parts.append(_entry_options_html(entry, calendar, day, month, year))
edit_url = url_for(
"calendars.calendar.day.calendar_entries.calendar_entry.get_edit",
entry_id=eid, calendar_slug=cal_slug,
day=day, month=month, year=year,
)
parts.append(
f''
'Edit '
)
parts.append('
')
return "".join(parts)
# ---------------------------------------------------------------------------
# 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(
"calendars.calendar.day.calendar_entries.calendar_entry.get",
calendar_slug=cal_slug,
year=year, month=month, day=day,
entry_id=entry.id,
)
label_html = (
f''
+ _entry_title_html(entry)
+ _entry_times_html(entry)
+ '
'
)
nav_html = _entry_nav_html(ctx)
return sexp(
'(~menu-row :id "entry-row" :level 5'
' :link-href lh :link-label-html llh'
' :nav-html nh :child-id "entry-header-child" :oob oob)',
lh=link_href,
llh=label_html,
nh=nav_html,
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" → {end.strftime('%H:%M')}" if end else ""
return f'{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:
parts.append(
'')
# Admin link
if is_admin:
admin_url = url_for(
"calendars.calendar.day.calendar_entries.calendar_entry.admin.admin",
calendar_slug=cal_slug,
day=day, month=month, year=year,
entry_id=entry.id,
)
parts.append(
f''
' Admin '
)
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_html(ctx)
child = (_post_header_html(ctx) + _post_admin_header_html(ctx)
+ _calendar_header_html(ctx) + _day_header_html(ctx)
+ _entry_header_html(ctx))
hdr += sexp('(div :id "root-header-child" :class "w-full" (raw! h))', h=child)
nav_html = _entry_nav_html(ctx)
return full_page(ctx, header_rows_html=hdr, content_html=content, menu_html=nav_html)
async def render_entry_oob(ctx: dict) -> str:
"""OOB response: entry detail."""
content = _entry_main_panel_html(ctx)
oobs = _day_header_html(ctx, oob=True)
oobs += _oob_header_html("day-header-child", "entry-header-child",
_entry_header_html(ctx))
nav_html = _entry_nav_html(ctx)
return oob_page(ctx, oobs_html=oobs, content_html=content, menu_html=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
+ f'{title}
'
+ f'{state}
'
)
def _entry_title_html(entry) -> str:
"""Render entry title (icon + name + state badge)."""
state = getattr(entry, "state", "pending") or "pending"
return (
f' {escape(entry.name)} '
+ _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}"
parts = [f'']
def _make_button(action_name, label, confirm_title, confirm_text, *, trigger_type="submit"):
url = url_for(
f"calendars.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"
trigger_attr = ' hx-trigger="confirmed"' if trigger_type == "button" else ""
return (
f'
'
f' '
f' {label} '
)
if state == "provisional":
parts.append(_make_button(
"confirm_entry", "confirm",
"Confirm entry?", "Are you sure you want to confirm this entry?",
))
parts.append(_make_button(
"decline_entry", "decline",
"Decline entry?", "Are you sure you want to decline this entry?",
))
elif state == "confirmed":
parts.append(_make_button(
"provisional_entry", "provisional",
"Provisional entry?", "Are you sure you want to provisional this entry?",
trigger_type="button",
))
parts.append("")
return "".join(parts)
# ---------------------------------------------------------------------------
# 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)
parts = []
if tp is not None:
parts.append('')
parts.append(f'
Price: ')
parts.append(f'£{tp:.2f}
')
parts.append(f'
Available: ')
tc_str = f"{tc} tickets" if tc is not None else "Unlimited"
parts.append(f'{tc_str}
')
parts.append(
f'
"""
'Edit ticket config '
)
else:
parts.append('')
parts.append('No tickets configured ')
parts.append(
f'"""
'Configure tickets
'
)
# Form
update_url = url_for(
"calendars.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 ""
parts.append(
f''
f' '
f'Ticket Price (£) '
f'
'
f'Total Tickets '
f'
'
''
'Save '
f' btn.classList.remove('hidden'));">"""
'Cancel
'
)
return "".join(parts)
# ---------------------------------------------------------------------------
# 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
parts = ['']
if entry_posts:
parts.append('
')
for ep in entry_posts:
ep_title = escape(getattr(ep, "title", ""))
ep_id = getattr(ep, "id", 0)
feat = getattr(ep, "feature_image", None)
if feat:
img = f'
'
else:
img = '
'
del_url = url_for(
"calendars.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,
)
parts.append(
f'
'
f'{img}{ep_title} '
f'"""
' Remove
'
)
parts.append('
')
else:
parts.append('
No posts associated
')
# Search to add
search_url = url_for(
"calendars.calendar.day.calendar_entries.calendar_entry.search_posts",
calendar_slug=cal_slug, day=day, month=month, year=year, entry_id=eid,
)
parts.append(
'
'
)
parts.append('
')
return "".join(parts)
# ---------------------------------------------------------------------------
# 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 '
'
parts = [
'')
return "".join(parts)
# ---------------------------------------------------------------------------
# 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 '
'
parts = [
'')
return "".join(parts)
# ---------------------------------------------------------------------------
# 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 '
'
slug = post.get("slug", "") if isinstance(post, dict) else getattr(post, "slug", "")
parts = [
''
'
'
' '
'
')
parts.append(
''
)
parts.append(
'
'
' '
)
parts.append('
')
return "".join(parts)
# ---------------------------------------------------------------------------
# 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("calendars.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 += (
''
f'{escape(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("calendars.calendar.admin.calendar_description_save", calendar_slug=cal_slug)
cancel_url = url_for("calendars.calendar.admin.calendar_description_view", calendar_slug=cal_slug)
return (
''
f'
'
f' '
f'{escape(desc)} '
''
'Save '
f'Cancel '
'
'
)
# ---------------------------------------------------------------------------
# Payments panel (public wrapper)
# ---------------------------------------------------------------------------
def render_payments_panel(ctx: dict) -> str:
"""Render the payments config panel for PUT response."""
return _payments_main_panel_html(ctx)
# ---------------------------------------------------------------------------
# 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_html(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", "—")
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("calendars.calendar.slots.slot.get_edit", slot_id=slot.id, calendar_slug=cal_slug)
parts = [f'']
# Days
parts.append(
''
'
Days
'
'
'
)
if days and days[0] != "—":
parts.append('
')
for d in days:
parts.append(f'{escape(d)} ')
parts.append('
')
else:
parts.append('
No days ')
parts.append('
')
# Flexible
parts.append(
''
'
Flexible
'
f'
{"yes" if flexible else "no"}
'
)
# Time & Cost
parts.append(
''
'
'
'
Time
'
f'
{time_start} — {time_end}
'
'
'
)
# Edit button
parts.append(
f'Edit '
)
parts.append(' ')
if oob:
parts.append(
f''
f'{escape(desc)}
'
)
return "".join(parts)
# ---------------------------------------------------------------------------
# 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", "")
parts = [f'']
parts.append(
''
'Name '
'Flexible '
'Days '
'Time '
'Cost '
'Actions '
' '
)
if slots:
for s in slots:
slot_href = url_for("calendars.calendar.slots.slot.get", calendar_slug=cal_slug, slot_id=s.id)
del_url = url_for("calendars.calendar.slots.slot.slot_delete", calendar_slug=cal_slug, slot_id=s.id)
desc = getattr(s, "description", "") or ""
desc_html = f'{escape(desc)}
' if desc else '
'
days_display = getattr(s, "days_display", "—")
day_list = days_display.split(", ")
if day_list and day_list[0] != "—":
days_html = '' + "".join(
f'{escape(d)} ' for d in day_list
) + '
'
else:
days_html = '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 ""
parts.append(
f''
f'{desc_html} '
f'{"yes" if s.flexible else "no"} '
f'{days_html} '
f'{time_start} - {time_end} '
f'{cost_str} '
f''
f' '
)
else:
parts.append('No slots yet. ')
parts.append('
')
# Add button
add_url = url_for("calendars.calendar.slots.add_form", calendar_slug=cal_slug)
parts.append(
f''
f''
'+ Add slot
'
)
parts.append(' ')
return "".join(parts)
# ---------------------------------------------------------------------------
# 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", "")
name = escape(getattr(ticket_type, "name", ""))
cost = getattr(ticket_type, "cost", None)
cost_str = f"£{cost:.2f}" if cost is not None else "£0.00"
count = getattr(ticket_type, "count", 0)
edit_url = url_for(
"calendars.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,
)
return (
f''
)
# ---------------------------------------------------------------------------
# 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
parts = [f'']
parts.append(
''
'Name '
'Cost '
'Count '
'Actions '
' '
)
if ticket_types:
for tt in ticket_types:
tt_href = url_for(
"calendars.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(
"calendars.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"£{cost:.2f}" if cost is not None else "£0.00"
parts.append(
f''
f' '
f'{cost_str} '
f'{tt.count} '
f''
f' '
)
else:
parts.append('No ticket types yet. ')
parts.append('
')
# Add button
add_url = url_for(
"calendars.calendar.day.calendar_entries.calendar_entry.ticket_types.add_form",
calendar_slug=cal_slug, entry_id=eid, year=year, month=month, day=day,
)
parts.append(
f''
f''
' Add ticket type
'
)
parts.append(' ')
return "".join(parts)
# ---------------------------------------------------------------------------
# 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
# OOB cart icon
html = _cart_icon_oob(cart_count)
count = len(created_tickets)
suffix = "s" if count != 1 else ""
parts = [f'']
parts.append(
'
'
' '
f'{count} ticket{suffix} reserved
'
)
parts.append('
')
if remaining is not None:
r_suffix = "s" if remaining != 1 else ""
parts.append(f'
{remaining} ticket{r_suffix} remaining
')
my_href = url_for("tickets.my_tickets")
parts.append(
'
'
)
parts.append('
')
return html + "".join(parts)
# ---------------------------------------------------------------------------
# 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
tp = getattr(entry, "ticket_price", None)
state = getattr(entry, "state", "")
ticket_types = getattr(entry, "ticket_types", None) or []
if tp is None:
return ""
if tp is not None and state != "confirmed":
return (
f''
' '
'Tickets available once this event is confirmed.
'
)
adjust_url = url_for("tickets.adjust_quantity")
target = f"#ticket-buy-{eid}"
parts = [f'']
parts.append(
'
'
' Tickets '
)
# Info line
info_parts = []
if ticket_sold_count:
info_parts.append(f'
{ticket_sold_count} sold ')
if ticket_remaining is not None:
info_parts.append(f'
{ticket_remaining} remaining ')
if user_ticket_count:
info_parts.append(
'
'
f' {user_ticket_count} in basket '
)
if info_parts:
parts.append(f'
{"".join(info_parts)}
')
active_types = [tt for tt in ticket_types if getattr(tt, "deleted_at", None) is None]
if active_types:
# Multiple ticket types
parts.append('
')
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"£{tt.cost:.2f}" if tt.cost is not None else "£0.00"
parts.append(
'
'
f'
{escape(tt.name)}
'
f'
{cost_str}
'
)
parts.append(_ticket_adjust_controls(csrf, adjust_url, target, eid, type_count, ticket_type_id=tt.id))
parts.append('
')
parts.append('
')
else:
# Simple ticket
parts.append(
'
'
f'£{tp:.2f} '
'per ticket
'
)
qty = user_ticket_count or 0
parts.append(_ticket_adjust_controls(csrf, adjust_url, target, eid, qty))
parts.append('
')
return "".join(parts)
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_hidden = f' ' if ticket_type_id else ""
my_tickets_href = url_for("tickets.my_tickets")
if count == 0:
return (
f''
f' '
f' '
f'{tt_hidden}'
' '
''
' '
)
return (
''
)
# ---------------------------------------------------------------------------
# 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 (
''
)
cart_href = cart_url_fn("/") if cart_url_fn else "/"
return (
''
)