"""Layout registrations, page helpers, and shared hydration helpers. All helpers return data dicts — no sx_call(). Markup composition lives entirely in .sx defpage and .sx defcomp files. """ from __future__ import annotations from typing import Any from shared.sx.parser import SxExpr # --------------------------------------------------------------------------- # Shared hydration helpers # --------------------------------------------------------------------------- def _add_to_defpage_ctx(**kwargs: Any) -> None: """Add data to g._defpage_ctx for the app-level context_processor.""" from quart import g if not hasattr(g, '_defpage_ctx'): g._defpage_ctx = {} g._defpage_ctx.update(kwargs) def _ensure_post_defpage_ctx() -> None: """Copy g.post_data["post"] into g._defpage_ctx for layout IO primitives.""" from quart import g post_data = getattr(g, "post_data", None) if post_data and post_data.get("post"): _add_to_defpage_ctx(post=post_data["post"]) async def _ensure_container_nav_defpage_ctx() -> None: """Fetch container_nav and add to g._defpage_ctx for layout IO primitives.""" from quart import g dctx = getattr(g, "_defpage_ctx", None) or {} if dctx.get("container_nav"): return post = dctx.get("post") or {} post_id = post.get("id") slug = post.get("slug", "") if not post_id: return from shared.infrastructure.fragments import fetch_fragments current_cal = getattr(g, "calendar_slug", "") or "" nav_params = { "container_type": "page", "container_id": str(post_id), "post_slug": slug, "current_calendar": current_cal, } events_nav, market_nav = await fetch_fragments([ ("events", "container-nav", nav_params), ("market", "container-nav", nav_params), ], required=False) _add_to_defpage_ctx(container_nav=events_nav + market_nav) async def _ensure_calendar(calendar_slug: str | None) -> None: """Load calendar into g.calendar if not already present.""" from quart import g, abort if hasattr(g, 'calendar'): _add_to_defpage_ctx(calendar=g.calendar) return from bp.calendar.services.calendar_view import ( get_calendar_by_post_and_slug, get_calendar_by_slug, ) post_data = getattr(g, "post_data", None) if post_data: post_id = (post_data.get("post") or {}).get("id") cal = await get_calendar_by_post_and_slug(g.s, post_id, calendar_slug) else: cal = await get_calendar_by_slug(g.s, calendar_slug) if not cal: abort(404) g.calendar = cal g.calendar_slug = calendar_slug _add_to_defpage_ctx(calendar=cal) _ensure_post_defpage_ctx() async def _ensure_entry(entry_id: int | None) -> None: """Load calendar entry into g.entry if not already present.""" from quart import g, abort if hasattr(g, 'entry'): _add_to_defpage_ctx(entry=g.entry) return from sqlalchemy import select from models.calendars import CalendarEntry result = await g.s.execute( select(CalendarEntry).where( CalendarEntry.id == entry_id, CalendarEntry.deleted_at.is_(None), ) ) entry = result.scalar_one_or_none() if entry is None: abort(404) g.entry = entry _add_to_defpage_ctx(entry=entry) async def _ensure_entry_context(entry_id: int | None) -> None: """Load full entry context (ticket data, posts) into g.* and _defpage_ctx.""" from quart import g from sqlalchemy import select from sqlalchemy.orm import selectinload from models.calendars import CalendarEntry from bp.tickets.services.tickets import ( get_available_ticket_count, get_sold_ticket_count, get_user_reserved_count, ) from shared.infrastructure.cart_identity import current_cart_identity from bp.calendar_entry.services.post_associations import get_entry_posts await _ensure_entry(entry_id) # Reload with ticket_types eagerly loaded stmt = ( select(CalendarEntry) .where(CalendarEntry.id == entry_id, CalendarEntry.deleted_at.is_(None)) .options(selectinload(CalendarEntry.ticket_types)) ) result = await g.s.execute(stmt) calendar_entry = result.scalar_one_or_none() if calendar_entry and getattr(g, "calendar", None): if calendar_entry.calendar_id != g.calendar.id: calendar_entry = None if calendar_entry: await g.s.refresh(calendar_entry, ['slot']) g.entry = calendar_entry entry_posts = await get_entry_posts(g.s, calendar_entry.id) ticket_remaining = await get_available_ticket_count(g.s, calendar_entry.id) ticket_sold_count = await get_sold_ticket_count(g.s, calendar_entry.id) ident = current_cart_identity() user_ticket_count = await get_user_reserved_count( g.s, calendar_entry.id, user_id=ident["user_id"], session_id=ident["session_id"], ) user_ticket_counts_by_type = {} if calendar_entry.ticket_types: for tt in calendar_entry.ticket_types: if tt.deleted_at is None: user_ticket_counts_by_type[tt.id] = await get_user_reserved_count( g.s, calendar_entry.id, user_id=ident["user_id"], session_id=ident["session_id"], ticket_type_id=tt.id, ) _add_to_defpage_ctx( entry=calendar_entry, entry_posts=entry_posts, ticket_remaining=ticket_remaining, ticket_sold_count=ticket_sold_count, user_ticket_count=user_ticket_count, user_ticket_counts_by_type=user_ticket_counts_by_type, ) async def _ensure_day_data(year: int, month: int, day: int) -> None: """Load day-specific data for layout header functions.""" from quart import g, session as qsession if hasattr(g, 'day_date'): return from datetime import date as date_cls, datetime, timezone, timedelta from sqlalchemy import select from bp.calendar.services import get_visible_entries_for_period from models.calendars import CalendarSlot calendar = getattr(g, "calendar", None) if not calendar: return try: day_date = date_cls(year, month, day) except (ValueError, TypeError): return period_start = datetime(year, month, day, tzinfo=timezone.utc) period_end = period_start + timedelta(days=1) user = getattr(g, "user", None) session_id = qsession.get("calendar_sid") visible = await get_visible_entries_for_period( sess=g.s, calendar_id=calendar.id, period_start=period_start, period_end=period_end, user=user, session_id=session_id, ) weekday_attr = ["mon", "tue", "wed", "thu", "fri", "sat", "sun"][day_date.weekday()] stmt = ( select(CalendarSlot) .where( CalendarSlot.calendar_id == calendar.id, getattr(CalendarSlot, weekday_attr) == True, # noqa: E712 CalendarSlot.deleted_at.is_(None), ) .order_by(CalendarSlot.time_start.asc(), CalendarSlot.id.asc()) ) result = await g.s.execute(stmt) day_slots = list(result.scalars()) g.day_date = day_date _add_to_defpage_ctx( qsession=qsession, day_date=day_date, day=day, year=year, month=month, day_entries=visible.merged_entries, user_entries=visible.user_entries, confirmed_entries=visible.confirmed_entries, day_slots=day_slots, ) # --------------------------------------------------------------------------- # Layouts — all layouts delegate to .sx defcomps via register_sx_layout # --------------------------------------------------------------------------- def _register_events_layouts() -> None: from shared.sx.layouts import register_sx_layout register_sx_layout("events-calendar-admin", "events-cal-admin-layout-full", "events-cal-admin-layout-oob") register_sx_layout("events-slots", "events-slots-layout-full", "events-slots-layout-oob") register_sx_layout("events-slot", "events-slot-layout-full", "events-slot-layout-oob") register_sx_layout("events-day-admin", "events-day-admin-layout-full", "events-day-admin-layout-oob") register_sx_layout("events-entry", "events-entry-layout-full", "events-entry-layout-oob") register_sx_layout("events-entry-admin", "events-entry-admin-layout-full", "events-entry-admin-layout-oob") register_sx_layout("events-ticket-types", "events-ticket-types-layout-full", "events-ticket-types-layout-oob") register_sx_layout("events-ticket-type", "events-ticket-type-layout-full", "events-ticket-type-layout-oob") register_sx_layout("events-markets", "events-markets-layout-full", "events-markets-layout-oob") # --------------------------------------------------------------------------- # Badge data helpers # --------------------------------------------------------------------------- _ENTRY_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", } _TICKET_STATE_CLASSES = { "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", } def _entry_badge_data(state: str) -> dict: cls = _ENTRY_STATE_CLASSES.get(state, "bg-stone-100 text-stone-700") label = state.replace("_", " ").capitalize() return {"cls": cls, "label": label} def _ticket_badge_data(state: str) -> dict: cls = _TICKET_STATE_CLASSES.get(state, "bg-stone-100 text-stone-700") label = (state or "").replace("_", " ").capitalize() return {"cls": cls, "label": label} # --------------------------------------------------------------------------- # Styles helper # --------------------------------------------------------------------------- def _styles_data() -> dict: """Extract common style classes from g.styles.""" from quart import g styles = getattr(g, "styles", None) or {} def _gs(attr): return getattr(styles, attr, "") if hasattr(styles, attr) else styles.get(attr, "") return { "list-container": _gs("list_container"), "pre-action": _gs("pre_action_button"), "action-btn": _gs("action_button"), "tr-cls": _gs("tr"), "pill-cls": _gs("pill"), "nav-btn": _gs("nav_button"), } # --------------------------------------------------------------------------- # Page helpers # --------------------------------------------------------------------------- def _register_events_helpers() -> None: from shared.sx.pages import register_page_helpers register_page_helpers("events", { "calendar-admin-data": _h_calendar_admin_data, "day-admin-data": _h_day_admin_data, "slots-data": _h_slots_data, "slot-data": _h_slot_data, "entry-data": _h_entry_data, "entry-admin-data": _h_entry_admin_data, "ticket-types-data": _h_ticket_types_data, "ticket-type-data": _h_ticket_type_data, "tickets-data": _h_tickets_data, "ticket-detail-data": _h_ticket_detail_data, "ticket-admin-data": _h_ticket_admin_data, "markets-data": _h_markets_data, }) # --------------------------------------------------------------------------- # Calendar admin # --------------------------------------------------------------------------- async def _h_calendar_admin_data(calendar_slug=None, **kw) -> dict: from quart import url_for from shared.browser.app.csrf import generate_csrf_token await _ensure_calendar(calendar_slug) await _ensure_container_nav_defpage_ctx() from quart import g calendar = getattr(g, "calendar", None) if not calendar: return {} csrf = generate_csrf_token() cal_slug = getattr(calendar, "slug", "") desc = getattr(calendar, "description", "") or "" desc_edit_url = url_for("calendar.admin.calendar_description_edit", calendar_slug=cal_slug) return { "cal-description": desc, "csrf": csrf, "desc-edit-url": desc_edit_url, } # --------------------------------------------------------------------------- # Day admin # --------------------------------------------------------------------------- async def _h_day_admin_data(calendar_slug=None, year=None, month=None, day=None, **kw) -> dict: await _ensure_calendar(calendar_slug) await _ensure_container_nav_defpage_ctx() if year is not None: await _ensure_day_data(int(year), int(month), int(day)) return {} # --------------------------------------------------------------------------- # Slots listing # --------------------------------------------------------------------------- async def _h_slots_data(calendar_slug=None, **kw) -> dict: from quart import g, url_for from shared.browser.app.csrf import generate_csrf_token from bp.slots.services.slots import list_slots as svc_list_slots await _ensure_calendar(calendar_slug) await _ensure_container_nav_defpage_ctx() calendar = getattr(g, "calendar", None) slots = await svc_list_slots(g.s, calendar.id) if calendar else [] _add_to_defpage_ctx(slots=slots) styles = _styles_data() csrf = generate_csrf_token() cal_slug = getattr(calendar, "slug", "") hx_select = getattr(g, "hx_select_search", "#main-panel") csrf_hdr = {"X-CSRFToken": csrf} add_url = url_for("calendar.slots.add_form", calendar_slug=cal_slug) slots_list = [] for s in slots: slot_href = url_for("defpage_slot_detail", calendar_slug=cal_slug, slot_id=s.id) del_url = url_for("calendar.slots.slot.slot_delete", calendar_slug=cal_slug, slot_id=s.id) desc = getattr(s, "description", "") or "" days_display = getattr(s, "days_display", "\u2014") day_list = days_display.split(", ") has_days = bool(day_list and day_list[0] != "\u2014") 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 "" slots_list.append({ "name": s.name, "description": desc, "day-list": day_list if has_days else [], "has-days": has_days, "flexible": "yes" if s.flexible else "no", "time-str": f"{time_start} - {time_end}", "cost-str": cost_str, "slot-href": slot_href, "del-url": del_url, }) return { "has-slots": bool(slots), "slots-list": slots_list, "add-url": add_url, "csrf-hdr": csrf_hdr, "hx-select": hx_select, **styles, } # --------------------------------------------------------------------------- # Slot detail # --------------------------------------------------------------------------- async def _h_slot_data(calendar_slug=None, slot_id=None, **kw) -> dict: from quart import g, abort, url_for await _ensure_calendar(calendar_slug) await _ensure_container_nav_defpage_ctx() from bp.slot.services.slot import get_slot as svc_get_slot slot = await svc_get_slot(g.s, slot_id) if slot_id else None if not slot: abort(404) g.slot = slot _add_to_defpage_ctx(slot=slot) calendar = getattr(g, "calendar", None) styles = _styles_data() cal_slug = getattr(calendar, "slug", "") days_display = getattr(slot, "days_display", "\u2014") day_list = days_display.split(", ") has_days = bool(day_list and day_list[0] != "\u2014") 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 "" edit_url = url_for("calendar.slots.slot.get_edit", slot_id=slot.id, calendar_slug=cal_slug) return { "slot-id-str": str(slot.id), "day-list": day_list if has_days else [], "has-days": has_days, "flexible": "yes" if getattr(slot, "flexible", False) else "no", "time-str": f"{time_start} \u2014 {time_end}", "cost-str": cost_str, "edit-url": edit_url, **styles, } # --------------------------------------------------------------------------- # Entry detail (complex — sub-panels returned as SxExpr) # --------------------------------------------------------------------------- async def _h_entry_data(calendar_slug=None, entry_id=None, **kw) -> dict: from quart import url_for, g from .entries import ( _entry_nav_html, _entry_options_html, render_entry_tickets_config, render_entry_posts_panel, ) from .tickets import render_buy_form await _ensure_calendar(calendar_slug) await _ensure_entry_context(entry_id) from shared.sx.page import get_template_context ctx = await get_template_context() 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 = _styles_data() eid = entry.id state = getattr(entry, "state", "pending") or "pending" # Simple field data slot = getattr(entry, "slot", None) has_slot = slot is not None slot_name = slot.name if slot else "" flex_label = "(flexible)" if slot and getattr(slot, "flexible", False) else "(fixed)" start_str = entry.start_at.strftime("%H:%M") if entry.start_at else "" end_str = f" \u2013 {entry.end_at.strftime('%H:%M')}" if entry.end_at else " \u2013 open-ended" cost = getattr(entry, "cost", None) cost_str = f"\u00a3{cost:.2f}" if cost is not None else "\u00a30.00" date_str = entry.start_at.strftime("%A, %B %d, %Y") if entry.start_at else "" badge = _entry_badge_data(state) edit_url = url_for( "calendar.day.calendar_entries.calendar_entry.get_edit", entry_id=eid, calendar_slug=cal_slug, day=day, month=month, year=year, ) # Complex sub-panels (pre-composed as SxExpr) 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 {} entry_posts = ctx.get("entry_posts") or [] tickets_config = render_entry_tickets_config(entry, calendar, day, month, year) buy_form = render_buy_form( entry, ticket_remaining, ticket_sold_count, user_ticket_count, user_ticket_counts_by_type, ) posts_panel = render_entry_posts_panel( entry_posts, entry, calendar, day, month, year, ) options_html = _entry_options_html(entry, calendar, day, month, year) # Entry menu (pre-composed for :menu slot) entry_menu = _entry_nav_html(ctx) return { "entry-id-str": str(eid), "entry-name": entry.name, "has-slot": has_slot, "slot-name": slot_name, "flex-label": flex_label, "time-str": start_str + end_str, "state-badge-cls": badge["cls"], "state-badge-label": badge["label"], "cost-str": cost_str, "date-str": date_str, "edit-url": edit_url, "tickets-config": SxExpr(tickets_config), "buy-form": SxExpr(buy_form) if buy_form else None, "posts-panel": SxExpr(posts_panel), "options-html": SxExpr(options_html), "entry-menu": SxExpr(entry_menu) if entry_menu else None, **styles, } # --------------------------------------------------------------------------- # Entry admin # --------------------------------------------------------------------------- async def _h_entry_admin_data(calendar_slug=None, entry_id=None, year=None, month=None, day=None, **kw) -> dict: from quart import url_for, g await _ensure_calendar(calendar_slug) await _ensure_container_nav_defpage_ctx() await _ensure_entry_context(entry_id) calendar = getattr(g, "calendar", None) entry = getattr(g, "entry", None) if not calendar or not entry: return {} cal_slug = getattr(calendar, "slug", "") styles = _styles_data() from shared.sx.page import get_template_context ctx = await get_template_context() select_colours = ctx.get("select_colours", "") ticket_types_href = url_for( "calendar.day.calendar_entries.calendar_entry.ticket_types.get", calendar_slug=cal_slug, entry_id=entry.id, year=year, month=month, day=day, ) return { "ticket-types-href": ticket_types_href, "select-colours": select_colours, **styles, } # --------------------------------------------------------------------------- # Ticket types listing # --------------------------------------------------------------------------- async def _h_ticket_types_data(calendar_slug=None, entry_id=None, year=None, month=None, day=None, **kw) -> dict: from quart import g, url_for from shared.browser.app.csrf import generate_csrf_token await _ensure_calendar(calendar_slug) await _ensure_entry(entry_id) entry = getattr(g, "entry", None) calendar = getattr(g, "calendar", None) from bp.ticket_types.services.tickets import list_ticket_types as svc_list_ticket_types ticket_types = await svc_list_ticket_types(g.s, entry.id) if entry else [] _add_to_defpage_ctx(ticket_types=ticket_types) styles = _styles_data() csrf = generate_csrf_token() cal_slug = getattr(calendar, "slug", "") hx_select = getattr(g, "hx_select_search", "#main-panel") eid = entry.id if entry else 0 csrf_hdr = {"X-CSRFToken": csrf} types_list = [] for tt in (ticket_types or []): tt_href = url_for( "calendar.day.calendar_entries.calendar_entry.ticket_types.ticket_type.get", calendar_slug=cal_slug, year=year, month=month, day=day, entry_id=eid, ticket_type_id=tt.id, ) del_url = url_for( "calendar.day.calendar_entries.calendar_entry.ticket_types.ticket_type.delete", calendar_slug=cal_slug, year=year, month=month, day=day, entry_id=eid, ticket_type_id=tt.id, ) cost = getattr(tt, "cost", None) cost_str = f"\u00a3{cost:.2f}" if cost is not None else "\u00a30.00" types_list.append({ "tt-href": tt_href, "tt-name": tt.name, "cost-str": cost_str, "count": str(tt.count), "del-url": del_url, }) add_url = url_for( "calendar.day.calendar_entries.calendar_entry.ticket_types.add_form", calendar_slug=cal_slug, entry_id=eid, year=year, month=month, day=day, ) return { "has-types": bool(ticket_types), "types-list": types_list, "add-url": add_url, "csrf-hdr": csrf_hdr, "hx-select": hx_select, **styles, } # --------------------------------------------------------------------------- # Ticket type detail # --------------------------------------------------------------------------- async def _h_ticket_type_data(calendar_slug=None, entry_id=None, ticket_type_id=None, year=None, month=None, day=None, **kw) -> dict: from quart import g, abort, url_for await _ensure_calendar(calendar_slug) await _ensure_entry(entry_id) from bp.ticket_type.services.ticket import get_ticket_type as svc_get_ticket_type ticket_type = await svc_get_ticket_type(g.s, ticket_type_id) if ticket_type_id else None if not ticket_type: abort(404) g.ticket_type = ticket_type _add_to_defpage_ctx(ticket_type=ticket_type) entry = getattr(g, "entry", None) calendar = getattr(g, "calendar", None) styles = _styles_data() cal_slug = getattr(calendar, "slug", "") cost = getattr(ticket_type, "cost", None) cost_str = f"\u00a3{cost:.2f}" if cost is not None else "\u00a30.00" count = getattr(ticket_type, "count", 0) edit_url = url_for( "calendar.day.calendar_entries.calendar_entry.ticket_types.ticket_type.get_edit", ticket_type_id=ticket_type.id, calendar_slug=cal_slug, year=year, month=month, day=day, entry_id=entry.id if entry else 0, ) return { "ticket-id": str(ticket_type.id), "tt-name": ticket_type.name, "cost-str": cost_str, "count-str": str(count), "edit-url": edit_url, **styles, } # --------------------------------------------------------------------------- # My tickets # --------------------------------------------------------------------------- async def _h_tickets_data(**kw) -> dict: from quart import g, url_for from shared.infrastructure.cart_identity import current_cart_identity from bp.tickets.services.tickets import get_user_tickets ident = current_cart_identity() tickets = await get_user_tickets( g.s, user_id=ident["user_id"], session_id=ident["session_id"], ) from shared.sx.page import get_template_context ctx = await get_template_context() styles = ctx.get("styles") or {} list_container = getattr(styles, "list_container", "") if hasattr(styles, "list_container") else styles.get("list_container", "") tickets_list = [] for ticket in (tickets or []): href = url_for("defpage_ticket_detail", code=ticket.code) entry = getattr(ticket, "entry", None) entry_name = entry.name if entry else "Unknown event" tt = getattr(ticket, "ticket_type", None) state = getattr(ticket, "state", "") cal = getattr(entry, "calendar", None) if entry else None time_str = "" if entry and entry.start_at: time_str = entry.start_at.strftime("%A, %B %d, %Y at %H:%M") if entry.end_at: time_str += f" \u2013 {entry.end_at.strftime('%H:%M')}" badge = _ticket_badge_data(state) tickets_list.append({ "href": href, "entry-name": entry_name, "type-name": tt.name if tt else None, "time-str": time_str or None, "cal-name": cal.name if cal else None, "badge-cls": badge["cls"], "badge-label": badge["label"], "code-prefix": ticket.code[:8], }) return { "has-tickets": bool(tickets), "tickets-list": tickets_list, "list-container": list_container, } # --------------------------------------------------------------------------- # Ticket detail # --------------------------------------------------------------------------- async def _h_ticket_detail_data(code=None, **kw) -> dict: from quart import g, abort, url_for from shared.infrastructure.cart_identity import current_cart_identity from bp.tickets.services.tickets import get_ticket_by_code ticket = await get_ticket_by_code(g.s, code) if code else None if not ticket: abort(404) # Verify ownership ident = current_cart_identity() if ident["user_id"] is not None: if ticket.user_id != ident["user_id"]: abort(404) elif ident["session_id"] is not None: if ticket.session_id != ident["session_id"]: abort(404) else: abort(404) from shared.sx.page import get_template_context ctx = await get_template_context() styles = ctx.get("styles") or {} list_container = getattr(styles, "list_container", "") if hasattr(styles, "list_container") else styles.get("list_container", "") entry = getattr(ticket, "entry", None) tt = getattr(ticket, "ticket_type", None) state = getattr(ticket, "state", "") ticket_code = ticket.code cal = getattr(entry, "calendar", None) if entry else None checked_in_at = getattr(ticket, "checked_in_at", None) bg_map = {"confirmed": "bg-emerald-50", "checked_in": "bg-blue-50", "reserved": "bg-amber-50"} header_bg = bg_map.get(state, "bg-stone-50") entry_name = entry.name if entry else "Ticket" back_href = url_for("defpage_my_tickets") badge = _ticket_badge_data(state) time_date = entry.start_at.strftime("%A, %B %d, %Y") if entry and entry.start_at else None time_range = entry.start_at.strftime("%H:%M") if entry and entry.start_at else None if time_range and entry.end_at: time_range += f" \u2013 {entry.end_at.strftime('%H:%M')}" tt_desc = f"{tt.name} \u2014 \u00a3{tt.cost:.2f}" if tt and getattr(tt, "cost", None) else None checkin_str = checked_in_at.strftime("Checked in: %B %d, %Y at %H:%M") if checked_in_at else None qr_script = ( f"(function(){{var c=document.getElementById('ticket-qr-{ticket_code}');" "if(c&&typeof QRCode!=='undefined'){" "var cv=document.createElement('canvas');" f"QRCode.toCanvas(cv,'{ticket_code}',{{width:200,margin:2,color:{{dark:'#1c1917',light:'#ffffff'}}}},function(e){{if(!e)c.appendChild(cv)}});" "}})()" ) return { "list-container": list_container, "back-href": back_href, "header-bg": header_bg, "entry-name": entry_name, "badge-cls": badge["cls"], "badge-label": badge["label"], "type-name": tt.name if tt else None, "ticket-code": ticket_code, "time-date": time_date, "time-range": time_range, "cal-name": cal.name if cal else None, "type-desc": tt_desc, "checkin-str": checkin_str, "qr-script": qr_script, } # --------------------------------------------------------------------------- # Ticket admin dashboard # --------------------------------------------------------------------------- async def _h_ticket_admin_data(**kw) -> dict: from quart import g, url_for from sqlalchemy import select, func from sqlalchemy.orm import selectinload from models.calendars import CalendarEntry, Ticket from shared.browser.app.csrf import generate_csrf_token result = await g.s.execute( select(Ticket) .options( selectinload(Ticket.entry).selectinload(CalendarEntry.calendar), selectinload(Ticket.ticket_type), ) .order_by(Ticket.created_at.desc()) .limit(50) ) tickets = result.scalars().all() total = await g.s.scalar(select(func.count(Ticket.id))) confirmed = await g.s.scalar( select(func.count(Ticket.id)).where(Ticket.state == "confirmed") ) checked_in = await g.s.scalar( select(func.count(Ticket.id)).where(Ticket.state == "checked_in") ) reserved = await g.s.scalar( select(func.count(Ticket.id)).where(Ticket.state == "reserved") ) csrf = generate_csrf_token() lookup_url = url_for("ticket_admin.lookup") from shared.sx.page import get_template_context ctx = await get_template_context() styles = ctx.get("styles") or {} list_container = getattr(styles, "list_container", "") if hasattr(styles, "list_container") else styles.get("list_container", "") # Stats cards data admin_stats = [] 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_map = {"total": total, "confirmed": confirmed, "checked_in": checked_in, "reserved": reserved} val = val_map.get(key, 0) or 0 lbl_cls = text_cls.replace("700", "600").replace("900", "500") if "stone" not in text_cls else "text-stone-500" admin_stats.append({ "border": border, "bg": bg, "text-cls": text_cls, "label-cls": lbl_cls, "value": str(val), "label": label, }) # Ticket rows data admin_tickets = [] for ticket in tickets: entry = getattr(ticket, "entry", None) tt = getattr(ticket, "ticket_type", None) state = getattr(ticket, "state", "") tcode = ticket.code checked_in_at = getattr(ticket, "checked_in_at", None) date_str = None if entry and entry.start_at: date_str = entry.start_at.strftime("%d %b %Y, %H:%M") badge = _ticket_badge_data(state) can_checkin = state in ("confirmed", "reserved") is_checked_in = state == "checked_in" checkin_url = url_for("ticket_admin.do_checkin", code=tcode) if can_checkin else None checkin_time = checked_in_at.strftime("%H:%M") if checked_in_at else "" admin_tickets.append({ "code": tcode, "code-short": tcode[:12] + "...", "entry-name": entry.name if entry else "\u2014", "date-str": date_str, "type-name": tt.name if tt else "\u2014", "badge-cls": badge["cls"], "badge-label": badge["label"], "can-checkin": can_checkin, "is-checked-in": is_checked_in, "checkin-url": checkin_url, "checkin-time": checkin_time, }) return { "admin-stats": admin_stats, "admin-tickets": admin_tickets, "list-container": list_container, "lookup-url": lookup_url, "csrf": csrf, "has-tickets": bool(tickets), } # --------------------------------------------------------------------------- # Markets # --------------------------------------------------------------------------- async def _h_markets_data(**kw) -> dict: from quart import url_for from shared.browser.app.csrf import generate_csrf_token from shared.sx.helpers import call_url _ensure_post_defpage_ctx() from shared.sx.page import get_template_context ctx = await get_template_context() 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 = generate_csrf_token() markets_raw = ctx.get("markets") or [] post = ctx.get("post") or {} slug = post.get("slug", "") csrf_hdr = {"X-CSRFToken": csrf} markets_list = [] for m in markets_raw: 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) markets_list.append({ "href": market_href, "name": m_name, "slug": m_slug, "del-url": del_url, "csrf-hdr": csrf_hdr, }) return { "can-create": can_create, "create-url": url_for("markets.create_market") if can_create else None, "csrf": csrf, "markets-list": markets_list, }