From 5344b382a51d261915aeafafee5e043b9a2e1314 Mon Sep 17 00:00:00 2001 From: giles Date: Wed, 4 Mar 2026 17:07:08 +0000 Subject: [PATCH] =?UTF-8?q?Slim=20events=20+=20sx=20sxc/pages/=5F=5Finit?= =?UTF-8?q?=5F=5F.py=20=E2=86=92=20registration-only?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Events: 3861 → 21 lines, split into 8 sub-modules (renders, helpers, layouts, calendar, entries, slots, tickets, utils). Updated 16 bp routes. SX Docs: 3224 → 27 lines, split into 5 sub-modules (renders, utils, essays, helpers, layouts). Updated 37 import sites in bp/pages/routes.py. Co-Authored-By: Claude Opus 4.6 --- events/bp/all_events/routes.py | 6 +- events/bp/calendar/admin/routes.py | 6 +- events/bp/calendar/routes.py | 8 +- events/bp/calendar_entries/routes.py | 6 +- events/bp/calendar_entry/routes.py | 22 +- events/bp/calendars/routes.py | 6 +- events/bp/day/routes.py | 2 +- events/bp/markets/routes.py | 4 +- events/bp/page/routes.py | 6 +- events/bp/slot/routes.py | 8 +- events/bp/slots/routes.py | 6 +- events/bp/ticket_admin/routes.py | 6 +- events/bp/ticket_type/routes.py | 8 +- events/bp/ticket_types/routes.py | 6 +- events/bp/tickets/routes.py | 4 +- events/sxc/pages/__init__.py | 3848 +------------------------- events/sxc/pages/calendar.py | 678 +++++ events/sxc/pages/entries.py | 1042 +++++++ events/sxc/pages/helpers.py | 396 +++ events/sxc/pages/layouts.py | 288 ++ events/sxc/pages/renders.py | 287 ++ events/sxc/pages/slots.py | 381 +++ events/sxc/pages/tickets.py | 723 +++++ events/sxc/pages/utils.py | 170 ++ sx/bp/pages/routes.py | 74 +- sx/sxc/pages/__init__.py | 3201 +-------------------- sx/sxc/pages/essays.py | 2615 +++++++++++++++++ sx/sxc/pages/helpers.py | 239 ++ sx/sxc/pages/layouts.py | 112 + sx/sxc/pages/renders.py | 92 + sx/sxc/pages/utils.py | 137 + 31 files changed, 7255 insertions(+), 7132 deletions(-) create mode 100644 events/sxc/pages/calendar.py create mode 100644 events/sxc/pages/entries.py create mode 100644 events/sxc/pages/helpers.py create mode 100644 events/sxc/pages/layouts.py create mode 100644 events/sxc/pages/renders.py create mode 100644 events/sxc/pages/slots.py create mode 100644 events/sxc/pages/tickets.py create mode 100644 events/sxc/pages/utils.py create mode 100644 sx/sxc/pages/essays.py create mode 100644 sx/sxc/pages/helpers.py create mode 100644 sx/sxc/pages/layouts.py create mode 100644 sx/sxc/pages/renders.py create mode 100644 sx/sxc/pages/utils.py diff --git a/events/bp/all_events/routes.py b/events/bp/all_events/routes.py index c99cee5..6ca913c 100644 --- a/events/bp/all_events/routes.py +++ b/events/bp/all_events/routes.py @@ -67,7 +67,7 @@ def register() -> Blueprint: entries, has_more, pending_tickets, page_info = await _load_entries(page) from shared.sx.page import get_template_context - from sxc.pages import render_all_events_page, render_all_events_oob + from sxc.pages.renders import render_all_events_page, render_all_events_oob ctx = await get_template_context() if is_htmx_request(): @@ -84,7 +84,7 @@ def register() -> Blueprint: entries, has_more, pending_tickets, page_info = await _load_entries(page) - from sxc.pages import render_all_events_cards + from sxc.pages.renders import render_all_events_cards sx_src = await render_all_events_cards(entries, has_more, pending_tickets, page_info, page, view) return sx_response(sx_src) @@ -125,7 +125,7 @@ def register() -> Blueprint: if ident["session_id"] is not None: frag_params["session_id"] = ident["session_id"] - from sxc.pages import render_ticket_widget + from sxc.pages.renders import render_ticket_widget widget_html = await render_ticket_widget(entry, qty, "/all-tickets/adjust") mini_html = await fetch_fragment("cart", "cart-mini", params=frag_params, required=False) return sx_response(widget_html + (mini_html or "")) diff --git a/events/bp/calendar/admin/routes.py b/events/bp/calendar/admin/routes.py index 606996d..123e3ff 100644 --- a/events/bp/calendar/admin/routes.py +++ b/events/bp/calendar/admin/routes.py @@ -18,7 +18,7 @@ def register(): @bp.get("/description/") @require_admin async def calendar_description_edit(calendar_slug: str, **kwargs): - from sxc.pages import render_calendar_description_edit + from sxc.pages.renders import render_calendar_description_edit html = await render_calendar_description_edit(g.calendar) return sx_response(html) @@ -34,7 +34,7 @@ def register(): g.calendar.description = description await g.s.flush() - from sxc.pages import render_calendar_description + from sxc.pages.renders import render_calendar_description html = await render_calendar_description(g.calendar, oob=True) return sx_response(html) @@ -42,7 +42,7 @@ def register(): @bp.get("/description/view/") @require_admin async def calendar_description_view(calendar_slug: str, **kwargs): - from sxc.pages import render_calendar_description + from sxc.pages.renders import render_calendar_description html = await render_calendar_description(g.calendar) return sx_response(html) diff --git a/events/bp/calendar/routes.py b/events/bp/calendar/routes.py index b7525a1..51e2900 100644 --- a/events/bp/calendar/routes.py +++ b/events/bp/calendar/routes.py @@ -158,7 +158,7 @@ def register(): confirmed_entries = visible.confirmed_entries from shared.sx.page import get_template_context - from sxc.pages import render_calendar_page, render_calendar_oob + from sxc.pages.renders import render_calendar_page, render_calendar_oob tctx = await get_template_context() tctx.update(dict( @@ -199,7 +199,7 @@ def register(): await update_calendar_description(g.calendar, description) from shared.sx.page import get_template_context - from sxc.pages import _calendar_admin_main_panel_html + from sxc.pages.calendar import _calendar_admin_main_panel_html ctx = await get_template_context() html = await _calendar_admin_main_panel_html(ctx) return sx_response(html) @@ -218,13 +218,13 @@ def register(): # If we have post context (blog-embedded mode), update nav post_data = getattr(g, "post_data", None) from shared.sx.page import get_template_context - from sxc.pages import render_calendars_list_panel + from sxc.pages.renders import render_calendars_list_panel ctx = await get_template_context() html = await render_calendars_list_panel(ctx) if post_data: from shared.services.entry_associations import get_associated_entries - from sxc.pages import render_post_nav_entries_oob + from sxc.pages.renders import render_post_nav_entries_oob post_id = (post_data.get("post") or {}).get("id") cals = ( diff --git a/events/bp/calendar_entries/routes.py b/events/bp/calendar_entries/routes.py index 9c5a6a3..ddc3008 100644 --- a/events/bp/calendar_entries/routes.py +++ b/events/bp/calendar_entries/routes.py @@ -258,7 +258,7 @@ def register(): "styles": styles, } - from sxc.pages import render_day_main_panel + from sxc.pages.renders import render_day_main_panel html = await render_day_main_panel(ctx) mini_html = await fetch_fragment("cart", "cart-mini", params=frag_params, required=False) return sx_response(html + (mini_html or "")) @@ -279,12 +279,12 @@ def register(): result = await g.s.execute(stmt) day_slots = list(result.scalars()) - from sxc.pages import render_entry_add_form + from sxc.pages.renders import render_entry_add_form return sx_response(await render_entry_add_form(g.calendar, day, month, year, day_slots)) @bp.get("/add-button/") async def add_button(day: int, month: int, year: int, **kwargs): - from sxc.pages import render_entry_add_button + from sxc.pages.renders import render_entry_add_button return sx_response(await render_entry_add_button(g.calendar, day, month, year)) diff --git a/events/bp/calendar_entry/routes.py b/events/bp/calendar_entry/routes.py index f39c8be..fdbc53a 100644 --- a/events/bp/calendar_entry/routes.py +++ b/events/bp/calendar_entry/routes.py @@ -111,7 +111,7 @@ def register(): ) # Render OOB nav - from sxc.pages import render_day_entries_nav_oob + from sxc.pages.renders import render_day_entries_nav_oob return await render_day_entries_nav_oob(visible.confirmed_entries, calendar, day_date) async def get_post_nav_oob(entry_id: int): @@ -148,7 +148,7 @@ def register(): ).scalars().all() # Render OOB nav for this post - from sxc.pages import render_post_nav_entries_oob + from sxc.pages.renders import render_post_nav_entries_oob nav_oob = await render_post_nav_entries_oob(associated_entries, calendars, post) nav_oobs.append(nav_oob) @@ -256,7 +256,7 @@ def register(): result = await g.s.execute(stmt) day_slots = list(result.scalars()) - from sxc.pages import render_entry_edit_form + from sxc.pages.renders import render_entry_edit_form return sx_response(await render_entry_edit_form(g.entry, g.calendar, day, month, year, day_slots)) @bp.put("/") @@ -420,7 +420,7 @@ def register(): nav_oob = await get_day_nav_oob(year, month, day) from shared.sx.page import get_template_context - from sxc.pages import _entry_main_panel_html + from sxc.pages.entries import _entry_main_panel_html tctx = await get_template_context() html = await _entry_main_panel_html(tctx) @@ -448,7 +448,7 @@ def register(): # Re-read entry to get updated state await g.s.refresh(g.entry) - from sxc.pages import render_entry_optioned + from sxc.pages.renders import render_entry_optioned html = await render_entry_optioned(g.entry, g.calendar, day, month, year) return sx_response(html + day_nav_oob + post_nav_oob) @@ -473,7 +473,7 @@ def register(): # Re-read entry to get updated state await g.s.refresh(g.entry) - from sxc.pages import render_entry_optioned + from sxc.pages.renders import render_entry_optioned html = await render_entry_optioned(g.entry, g.calendar, day, month, year) return sx_response(html + day_nav_oob + post_nav_oob) @@ -498,7 +498,7 @@ def register(): # Re-read entry to get updated state await g.s.refresh(g.entry) - from sxc.pages import render_entry_optioned + from sxc.pages.renders import render_entry_optioned html = await render_entry_optioned(g.entry, g.calendar, day, month, year) return sx_response(html + day_nav_oob + post_nav_oob) @@ -542,7 +542,7 @@ def register(): # Return just the tickets fragment (targeted by hx-target="#entry-tickets-...") await g.s.refresh(g.entry) - from sxc.pages import render_entry_tickets_config + from sxc.pages.renders import render_entry_tickets_config html = await render_entry_tickets_config(g.entry, g.calendar, request.view_args.get("day"), request.view_args.get("month"), request.view_args.get("year")) return sx_response(html) @@ -558,7 +558,7 @@ def register(): total_pages = math.ceil(total / per_page) if total > 0 else 0 va = request.view_args or {} - from sxc.pages import render_post_search_results + from sxc.pages.renders import render_post_search_results return sx_response(await render_post_search_results( search_posts, query, page, total_pages, g.entry, g.calendar, @@ -592,7 +592,7 @@ def register(): entry_posts = await get_entry_posts(g.s, entry_id) # Return updated posts list + OOB nav update - from sxc.pages import render_entry_posts_panel, render_entry_posts_nav_oob + from sxc.pages.renders import render_entry_posts_panel, render_entry_posts_nav_oob va = request.view_args or {} html = await render_entry_posts_panel(entry_posts, g.entry, g.calendar, va.get("day"), va.get("month"), va.get("year")) nav_oob = await render_entry_posts_nav_oob(entry_posts) @@ -614,7 +614,7 @@ def register(): entry_posts = await get_entry_posts(g.s, entry_id) # Return updated posts list + OOB nav update - from sxc.pages import render_entry_posts_panel, render_entry_posts_nav_oob + from sxc.pages.renders import render_entry_posts_panel, render_entry_posts_nav_oob va = request.view_args or {} html = await render_entry_posts_panel(entry_posts, g.entry, g.calendar, va.get("day"), va.get("month"), va.get("year")) nav_oob = await render_entry_posts_nav_oob(entry_posts) diff --git a/events/bp/calendars/routes.py b/events/bp/calendars/routes.py index 336425c..b3cd72e 100644 --- a/events/bp/calendars/routes.py +++ b/events/bp/calendars/routes.py @@ -32,7 +32,7 @@ def register(): @cache_page(tag="calendars") async def home(**kwargs): from shared.sx.page import get_template_context - from sxc.pages import render_calendars_page, render_calendars_oob + from sxc.pages.renders import render_calendars_page, render_calendars_oob ctx = await get_template_context() if not is_htmx_request(): @@ -67,14 +67,14 @@ def register(): return await make_response(render_comp("error-inline", message=str(e)), 422) from shared.sx.page import get_template_context - from sxc.pages import render_calendars_list_panel + from sxc.pages.renders import render_calendars_list_panel ctx = await get_template_context() html = await render_calendars_list_panel(ctx) # Blog-embedded mode: also update post nav if post_data: from shared.services.entry_associations import get_associated_entries - from sxc.pages import render_post_nav_entries_oob + from sxc.pages.renders import render_post_nav_entries_oob cals = ( await g.s.execute( diff --git a/events/bp/day/routes.py b/events/bp/day/routes.py index e1588c8..8c431af 100644 --- a/events/bp/day/routes.py +++ b/events/bp/day/routes.py @@ -123,7 +123,7 @@ def register(): - pending only for current user/session """ from shared.sx.page import get_template_context - from sxc.pages import render_day_page, render_day_oob + from sxc.pages.renders import render_day_page, render_day_oob tctx = await get_template_context() if not is_htmx_request(): diff --git a/events/bp/markets/routes.py b/events/bp/markets/routes.py index ac6a593..415f6c0 100644 --- a/events/bp/markets/routes.py +++ b/events/bp/markets/routes.py @@ -42,7 +42,7 @@ def register(): return await make_response(render_comp("error-inline", message=str(e)), 422) from shared.sx.page import get_template_context - from sxc.pages import render_markets_list_panel + from sxc.pages.renders import render_markets_list_panel ctx = await get_template_context() return sx_response(await render_markets_list_panel(ctx)) @@ -55,7 +55,7 @@ def register(): return await make_response("Market not found", 404) from shared.sx.page import get_template_context - from sxc.pages import render_markets_list_panel + from sxc.pages.renders import render_markets_list_panel ctx = await get_template_context() return sx_response(await render_markets_list_panel(ctx)) diff --git a/events/bp/page/routes.py b/events/bp/page/routes.py index 7366fa1..2f4ac06 100644 --- a/events/bp/page/routes.py +++ b/events/bp/page/routes.py @@ -47,7 +47,7 @@ def register() -> Blueprint: entries, has_more, pending_tickets = await _load_entries(post["id"], page) from shared.sx.page import get_template_context - from sxc.pages import render_page_summary_page, render_page_summary_oob + from sxc.pages.renders import render_page_summary_page, render_page_summary_oob ctx = await get_template_context() if is_htmx_request(): @@ -65,7 +65,7 @@ def register() -> Blueprint: entries, has_more, pending_tickets = await _load_entries(post["id"], page) - from sxc.pages import render_page_summary_cards + from sxc.pages.renders import render_page_summary_cards sx_src = await render_page_summary_cards(entries, has_more, pending_tickets, {}, page, view, post) return sx_response(sx_src) @@ -106,7 +106,7 @@ def register() -> Blueprint: if ident["session_id"] is not None: frag_params["session_id"] = ident["session_id"] - from sxc.pages import render_ticket_widget + from sxc.pages.renders import render_ticket_widget widget_html = await render_ticket_widget(entry, qty, f"/{g.post_slug}/tickets/adjust") mini_html = await fetch_fragment("cart", "cart-mini", params=frag_params, required=False) return sx_response(widget_html + (mini_html or "")) diff --git a/events/bp/slot/routes.py b/events/bp/slot/routes.py index 7b821c0..dea7507 100644 --- a/events/bp/slot/routes.py +++ b/events/bp/slot/routes.py @@ -35,7 +35,7 @@ def register(): slot = await svc_get_slot(g.s, slot_id) if not slot: return await make_response("Not found", 404) - from sxc.pages import render_slot_edit_form + from sxc.pages.renders import render_slot_edit_form return sx_response(await render_slot_edit_form(slot, g.calendar)) @bp.get("/view/") @@ -44,7 +44,7 @@ def register(): slot = await svc_get_slot(g.s, slot_id) if not slot: return await make_response("Not found", 404) - from sxc.pages import render_slot_main_panel + from sxc.pages.renders import render_slot_main_panel return sx_response(await render_slot_main_panel(slot, g.calendar)) @bp.delete("/") @@ -53,7 +53,7 @@ def register(): async def slot_delete(slot_id: int, **kwargs): await svc_delete_slot(g.s, slot_id) slots = await svc_list_slots(g.s, g.calendar.id) - from sxc.pages import render_slots_table + from sxc.pages.renders import render_slots_table return sx_response(await render_slots_table(slots, g.calendar)) @bp.put("/") @@ -135,7 +135,7 @@ def register(): } ), 422 - from sxc.pages import render_slot_main_panel + from sxc.pages.renders import render_slot_main_panel return sx_response(await render_slot_main_panel(slot, g.calendar, oob=True)) diff --git a/events/bp/slots/routes.py b/events/bp/slots/routes.py index 708db6a..41cb73b 100644 --- a/events/bp/slots/routes.py +++ b/events/bp/slots/routes.py @@ -110,20 +110,20 @@ def register(): # Success → re-render the slots table slots = await svc_list_slots(g.s, g.calendar.id) - from sxc.pages import render_slots_table + from sxc.pages.renders import render_slots_table return sx_response(await render_slots_table(slots, g.calendar)) @bp.get("/add") @require_admin async def add_form(**kwargs): - from sxc.pages import render_slot_add_form + from sxc.pages.renders import render_slot_add_form return sx_response(await render_slot_add_form(g.calendar)) @bp.get("/add-button") @require_admin async def add_button(**kwargs): - from sxc.pages import render_slot_add_button + from sxc.pages.renders import render_slot_add_button return sx_response(await render_slot_add_button(g.calendar)) return bp diff --git a/events/bp/ticket_admin/routes.py b/events/bp/ticket_admin/routes.py index 3c8b4b5..d519b70 100644 --- a/events/bp/ticket_admin/routes.py +++ b/events/bp/ticket_admin/routes.py @@ -53,7 +53,7 @@ def register() -> Blueprint: tickets = await get_tickets_for_entry(g.s, entry_id) - from sxc.pages import render_entry_tickets_admin + from sxc.pages.renders import render_entry_tickets_admin html = await render_entry_tickets_admin(entry, tickets) return sx_response(html) @@ -69,7 +69,7 @@ def register() -> Blueprint: ) ticket = await get_ticket_by_code(g.s, code) - from sxc.pages import render_lookup_result + from sxc.pages.renders import render_lookup_result if not ticket: return sx_response(await render_lookup_result(None, "Ticket not found")) @@ -82,7 +82,7 @@ def register() -> Blueprint: """Check in a ticket by its code.""" success, error = await checkin_ticket(g.s, code) - from sxc.pages import render_checkin_result + from sxc.pages.renders import render_checkin_result if not success: return sx_response(await render_checkin_result(False, error, None)) diff --git a/events/bp/ticket_type/routes.py b/events/bp/ticket_type/routes.py index 608c198..64dbfd6 100644 --- a/events/bp/ticket_type/routes.py +++ b/events/bp/ticket_type/routes.py @@ -30,7 +30,7 @@ def register(): if not ticket_type: return await make_response("Not found", 404) - from sxc.pages import render_ticket_type_edit_form + from sxc.pages.renders import render_ticket_type_edit_form va = request.view_args or {} return sx_response(await render_ticket_type_edit_form( ticket_type, g.entry, g.calendar, @@ -45,7 +45,7 @@ def register(): if not ticket_type: return await make_response("Not found", 404) - from sxc.pages import render_ticket_type_main_panel + from sxc.pages.renders import render_ticket_type_main_panel va = request.view_args or {} return sx_response(await render_ticket_type_main_panel( ticket_type, g.entry, g.calendar, @@ -112,7 +112,7 @@ def register(): return await make_response("Not found", 404) # Return updated view with OOB flag - from sxc.pages import render_ticket_type_main_panel + from sxc.pages.renders import render_ticket_type_main_panel va = request.view_args or {} return sx_response(await render_ticket_type_main_panel( ticket_type, g.entry, g.calendar, @@ -131,7 +131,7 @@ def register(): # Re-render the ticket types list ticket_types = await svc_list_ticket_types(g.s, g.entry.id) - from sxc.pages import render_ticket_types_table + from sxc.pages.renders import render_ticket_types_table va = request.view_args or {} return sx_response(await render_ticket_types_table( ticket_types, g.entry, g.calendar, diff --git a/events/bp/ticket_types/routes.py b/events/bp/ticket_types/routes.py index a11eedc..e85e82d 100644 --- a/events/bp/ticket_types/routes.py +++ b/events/bp/ticket_types/routes.py @@ -93,7 +93,7 @@ def register(): # Success → re-render the ticket types table ticket_types = await svc_list_ticket_types(g.s, g.entry.id) - from sxc.pages import render_ticket_types_table + from sxc.pages.renders import render_ticket_types_table va = request.view_args or {} return sx_response(await render_ticket_types_table( ticket_types, g.entry, g.calendar, @@ -104,7 +104,7 @@ def register(): @require_admin async def add_form(**kwargs): """Show the add ticket type form.""" - from sxc.pages import render_ticket_type_add_form + from sxc.pages.renders import render_ticket_type_add_form va = request.view_args or {} return sx_response(await render_ticket_type_add_form( g.entry, g.calendar, @@ -115,7 +115,7 @@ def register(): @require_admin async def add_button(**kwargs): """Show the add ticket type button.""" - from sxc.pages import render_ticket_type_add_button + from sxc.pages.renders import render_ticket_type_add_button va = request.view_args or {} return sx_response(await render_ticket_type_add_button( g.entry, g.calendar, diff --git a/events/bp/tickets/routes.py b/events/bp/tickets/routes.py index 6d43ba4..d884ca5 100644 --- a/events/bp/tickets/routes.py +++ b/events/bp/tickets/routes.py @@ -126,7 +126,7 @@ def register() -> Blueprint: summary = dto_from_dict(CartSummaryDTO, raw_summary) if raw_summary else CartSummaryDTO() cart_count = summary.count + summary.calendar_count + summary.ticket_count - from sxc.pages import render_buy_result + from sxc.pages.renders import render_buy_result return sx_response(await render_buy_result(entry, created, remaining, cart_count)) @bp.post("/adjust/") @@ -249,7 +249,7 @@ def register() -> Blueprint: summary = dto_from_dict(CartSummaryDTO, raw_summary) if raw_summary else CartSummaryDTO() cart_count = summary.count + summary.calendar_count + summary.ticket_count - from sxc.pages import render_adjust_response + from sxc.pages.renders import render_adjust_response return sx_response(await render_adjust_response( entry, ticket_remaining, ticket_sold_count, user_ticket_count, user_ticket_counts_by_type, cart_count, diff --git a/events/sxc/pages/__init__.py b/events/sxc/pages/__init__.py index 5c38499..7052bc5 100644 --- a/events/sxc/pages/__init__.py +++ b/events/sxc/pages/__init__.py @@ -1,22 +1,11 @@ """Events defpage setup — registers layouts, page helpers, and loads .sx pages.""" from __future__ import annotations -from typing import Any -from markupsafe import escape - -from shared.sx.parser import SxExpr -from shared.sx.helpers import ( - call_url, get_asset_url, render_to_sx, - render_to_sx_with_env, _ctx_to_env, - post_header_sx, post_admin_header_sx, - oob_header_sx, header_child_sx, - full_page_sx, oob_page_sx, - search_mobile_sx, search_desktop_sx, -) - def setup_events_pages() -> None: """Register events-specific layouts, page helpers, and load page definitions.""" + from .layouts import _register_events_layouts + from .helpers import _register_events_helpers _register_events_layouts() _register_events_helpers() _load_events_page_files() @@ -26,3836 +15,7 @@ def _load_events_page_files() -> None: import os from shared.sx.pages import load_page_dir from shared.sx.jinja_bridge import load_service_components - sxc_dir = os.path.dirname(os.path.dirname(__file__)) # events/sxc/ - service_root = os.path.dirname(sxc_dir) # events/ + sxc_dir = os.path.dirname(os.path.dirname(__file__)) + service_root = os.path.dirname(sxc_dir) load_service_components(service_root, service_name="events") load_page_dir(os.path.dirname(__file__), "events") - - -# --------------------------------------------------------------------------- -# OOB header helper — delegates to shared -# --------------------------------------------------------------------------- - - - -# --------------------------------------------------------------------------- -# Post header helpers — thin wrapper over shared post_header_sx -# --------------------------------------------------------------------------- - -def _clear_oob(*ids: str) -> str: - """Generate OOB swaps to remove orphaned header rows/children.""" - return "".join(f'(div :id "{i}" :hx-swap-oob "outerHTML")' for i in ids) - - -# All possible header row/child IDs at each depth (deepest first) -_EVENTS_DEEP_IDS = [ - "entry-admin-row", "entry-admin-header-child", - "entry-row", "entry-header-child", - "day-admin-row", "day-admin-header-child", - "day-row", "day-header-child", - "calendar-admin-row", "calendar-admin-header-child", - "calendar-row", "calendar-header-child", - "calendars-row", "calendars-header-child", - "post-admin-row", "post-admin-header-child", -] - - -def _clear_deeper_oob(*keep_ids: str) -> str: - """Clear all events header rows/children NOT in keep_ids.""" - to_clear = [i for i in _EVENTS_DEEP_IDS if i not in keep_ids] - return _clear_oob(*to_clear) - - -async def _ensure_container_nav(ctx: dict) -> dict: - """Fetch container_nav if not already present (for post header row).""" - if ctx.get("container_nav"): - return ctx - post = ctx.get("post") or {} - post_id = post.get("id") - slug = post.get("slug", "") - if not post_id: - return ctx - from quart import g - 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) - return {**ctx, "container_nav": events_nav + market_nav} - - -async def _post_header_sx(ctx: dict, *, oob: bool = False) -> str: - """Build the post-level header row — delegates to shared sx helper.""" - return await post_header_sx(ctx, oob=oob) - - -async def _post_nav_sx(ctx: dict) -> str: - """Post desktop nav: calendar links + container nav (markets, etc.).""" - from quart import url_for, g - - calendars = ctx.get("calendars") or [] - select_colours = ctx.get("select_colours", "") - current_cal_slug = getattr(g, "calendar_slug", None) - - parts = [] - for cal in calendars: - cal_slug = getattr(cal, "slug", "") if hasattr(cal, "slug") else cal.get("slug", "") - cal_name = getattr(cal, "name", "") if hasattr(cal, "name") else cal.get("name", "") - href = url_for("calendar.get", calendar_slug=cal_slug) - is_sel = (cal_slug == current_cal_slug) - parts.append(await render_to_sx("nav-link", href=href, icon="fa fa-calendar", - label=cal_name, select_colours=select_colours, - is_selected=is_sel)) - # Container nav fragments (markets, etc.) - container_nav = ctx.get("container_nav", "") - if container_nav: - parts.append(container_nav) - - # Admin cog → blog admin for this post (cross-domain, no HTMX) - rights = ctx.get("rights") or {} - has_admin = rights.get("admin") if isinstance(rights, dict) else getattr(rights, "admin", False) - if has_admin: - post = ctx.get("post") or {} - slug = post.get("slug", "") - styles = ctx.get("styles") or {} - nav_btn = styles.get("nav_button", "") if isinstance(styles, dict) else getattr(styles, "nav_button", "") - select_colours = ctx.get("select_colours", "") - admin_href = call_url(ctx, "blog_url", f"/{slug}/admin/") - aclass = f"{nav_btn} {select_colours}".strip() or ( - "justify-center cursor-pointer flex flex-row items-center gap-2 " - "rounded bg-stone-200 text-black p-3" - ) - parts.append( - f'' - ) - - return "".join(parts) - - -# --------------------------------------------------------------------------- -# Post admin header -# --------------------------------------------------------------------------- - -# --------------------------------------------------------------------------- -# Calendars header -# --------------------------------------------------------------------------- - -async def _calendars_header_sx(ctx: dict, *, oob: bool = False) -> str: - """Build the calendars section header row.""" - from quart import url_for - link_href = url_for("calendars.home") - return await render_to_sx("menu-row-sx", id="calendars-row", level=3, - link_href=link_href, - link_label_content=SxExpr(await render_to_sx("events-calendars-label")), - child_id="calendars-header-child", oob=oob) - - -# --------------------------------------------------------------------------- -# Calendar header -# --------------------------------------------------------------------------- - -async def _calendar_header_sx(ctx: dict, *, oob: bool = False) -> str: - """Build a single calendar's header row.""" - from quart import url_for - calendar = ctx.get("calendar") - if not calendar: - return "" - cal_slug = getattr(calendar, "slug", "") - cal_name = getattr(calendar, "name", "") - cal_desc = getattr(calendar, "description", "") or "" - - link_href = url_for("calendar.get", calendar_slug=cal_slug) - label_html = await render_to_sx("events-calendar-label", - name=cal_name, description=cal_desc) - - # Desktop nav: slots + admin - nav_html = await _calendar_nav_sx(ctx) - - return await render_to_sx("menu-row-sx", id="calendar-row", level=3, - link_href=link_href, link_label_content=SxExpr(label_html), - nav=SxExpr(nav_html) if nav_html else None, child_id="calendar-header-child", oob=oob) - - -async def _calendar_nav_sx(ctx: dict) -> str: - """Calendar desktop nav: Slots + admin link.""" - from quart import url_for - calendar = ctx.get("calendar") - if not calendar: - return "" - cal_slug = getattr(calendar, "slug", "") - rights = ctx.get("rights") or {} - is_admin = getattr(rights, "admin", False) if hasattr(rights, "admin") else rights.get("admin", False) - select_colours = ctx.get("select_colours", "") - - parts = [] - slots_href = url_for("defpage_slots_listing", calendar_slug=cal_slug) - parts.append(await render_to_sx("nav-link", href=slots_href, icon="fa fa-clock", - label="Slots", select_colours=select_colours)) - if is_admin: - admin_href = url_for("defpage_calendar_admin", calendar_slug=cal_slug) - parts.append(await render_to_sx("nav-link", href=admin_href, icon="fa fa-cog", - select_colours=select_colours)) - return "(<> " + " ".join(parts) + ")" if parts else "" - - -# --------------------------------------------------------------------------- -# Day header -# --------------------------------------------------------------------------- - -async def _day_header_sx(ctx: dict, *, oob: bool = False) -> str: - """Build day detail header row.""" - from quart import url_for - calendar = ctx.get("calendar") - if not calendar: - return "" - cal_slug = getattr(calendar, "slug", "") - day_date = ctx.get("day_date") - if not day_date: - return "" - - link_href = url_for( - "calendar.day.show_day", - calendar_slug=cal_slug, - year=day_date.year, - month=day_date.month, - day=day_date.day, - ) - label_html = await render_to_sx("events-day-label", - date_str=day_date.strftime("%A %d %B %Y")) - - nav_html = await _day_nav_sx(ctx) - - return await render_to_sx("menu-row-sx", id="day-row", level=4, - link_href=link_href, link_label_content=SxExpr(label_html), - nav=SxExpr(nav_html) if nav_html else None, child_id="day-header-child", oob=oob) - - -async def _day_nav_sx(ctx: dict) -> str: - """Day desktop nav: confirmed entries scrolling menu + admin link.""" - from quart import url_for - calendar = ctx.get("calendar") - if not calendar: - return "" - cal_slug = getattr(calendar, "slug", "") - day_date = ctx.get("day_date") - confirmed_entries = ctx.get("confirmed_entries") or [] - rights = ctx.get("rights") or {} - is_admin = getattr(rights, "admin", False) if hasattr(rights, "admin") else rights.get("admin", False) - - parts = [] - # Confirmed entries nav (scrolling menu) - if confirmed_entries: - entry_links = [] - for entry in confirmed_entries: - href = url_for( - "calendar.day.calendar_entries.calendar_entry.get", - calendar_slug=cal_slug, - year=day_date.year, - month=day_date.month, - day=day_date.day, - entry_id=entry.id, - ) - start = entry.start_at.strftime("%H:%M") if entry.start_at else "" - end = f" \u2013 {entry.end_at.strftime('%H:%M')}" if entry.end_at else "" - entry_links.append(await render_to_sx("events-day-entry-link", - href=href, name=entry.name, - time_str=f"{start}{end}")) - inner = "".join(entry_links) - parts.append(await render_to_sx("events-day-entries-nav", inner=SxExpr(inner))) - - if is_admin and day_date: - admin_href = url_for( - "calendar.day.admin.admin", - calendar_slug=cal_slug, - year=day_date.year, - month=day_date.month, - day=day_date.day, - ) - parts.append(await render_to_sx("nav-link", href=admin_href, icon="fa fa-cog")) - return "".join(parts) - - -# --------------------------------------------------------------------------- -# Day admin header -# --------------------------------------------------------------------------- - -async def _day_admin_header_sx(ctx: dict, *, oob: bool = False) -> str: - """Build day admin header row.""" - from quart import url_for - calendar = ctx.get("calendar") - if not calendar: - return "" - cal_slug = getattr(calendar, "slug", "") - day_date = ctx.get("day_date") - if not day_date: - return "" - - link_href = url_for( - "calendar.day.admin.admin", - calendar_slug=cal_slug, - year=day_date.year, - month=day_date.month, - day=day_date.day, - ) - return await render_to_sx("menu-row-sx", id="day-admin-row", level=5, - link_href=link_href, link_label="admin", icon="fa fa-cog", - child_id="day-admin-header-child", oob=oob) - - -# --------------------------------------------------------------------------- -# Calendar admin header -# --------------------------------------------------------------------------- - -async def _calendar_admin_header_sx(ctx: dict, *, oob: bool = False) -> str: - """Build calendar admin header row with nav links.""" - from quart import url_for - calendar = ctx.get("calendar") - cal_slug = getattr(calendar, "slug", "") if calendar else "" - select_colours = ctx.get("select_colours", "") - - nav_parts = [] - if cal_slug: - for endpoint, label in [ - ("defpage_slots_listing", "slots"), - ("calendar.admin.calendar_description_edit", "description"), - ]: - href = url_for(endpoint, calendar_slug=cal_slug) - nav_parts.append(await render_to_sx("nav-link", href=href, label=label, - select_colours=select_colours)) - - nav_html = "".join(nav_parts) - return await render_to_sx("menu-row-sx", id="calendar-admin-row", level=4, - link_label="admin", icon="fa fa-cog", - nav=SxExpr(nav_html) if nav_html else None, child_id="calendar-admin-header-child", oob=oob) - - -# --------------------------------------------------------------------------- -# Markets header -# --------------------------------------------------------------------------- - -async def _markets_header_sx(ctx: dict, *, oob: bool = False) -> str: - """Build the markets section header row.""" - from quart import url_for - link_href = url_for("defpage_events_markets") - return await render_to_sx("menu-row-sx", id="markets-row", level=3, - link_href=link_href, - link_label_content=SxExpr(await render_to_sx("events-markets-label")), - child_id="markets-header-child", oob=oob) - - -# --------------------------------------------------------------------------- -# Calendars main panel -# --------------------------------------------------------------------------- - -async def _calendars_main_panel_sx(ctx: dict) -> str: - """Render the calendars list + create form panel.""" - from quart import url_for - rights = ctx.get("rights") or {} - is_admin = getattr(rights, "admin", False) if hasattr(rights, "admin") else rights.get("admin", False) - has_access = ctx.get("has_access") - can_create = has_access("calendars.create_calendar") if callable(has_access) else is_admin - csrf_token = ctx.get("csrf_token") - csrf = csrf_token() if callable(csrf_token) else (csrf_token or "") - - calendars = ctx.get("calendars") or [] - - form_html = "" - if can_create: - create_url = url_for("calendars.create_calendar") - form_html = await render_to_sx("crud-create-form", - create_url=create_url, csrf=csrf, - errors_id="cal-create-errors", list_id="calendars-list", - placeholder="e.g. Events, Gigs, Meetings", btn_label="Add calendar") - - list_html = await _calendars_list_sx(ctx, calendars) - return await render_to_sx("crud-panel", - form=SxExpr(form_html), list=SxExpr(list_html), - list_id="calendars-list") - - -async def _calendars_list_sx(ctx: dict, calendars: list) -> str: - """Render the calendars list items.""" - from quart import url_for - from shared.utils import route_prefix - csrf_token = ctx.get("csrf_token") - csrf = csrf_token() if callable(csrf_token) else (csrf_token or "") - prefix = route_prefix() - - if not calendars: - return await render_to_sx("empty-state", message="No calendars yet. Create one above.", - cls="text-gray-500 mt-4") - - parts = [] - for cal in calendars: - cal_slug = getattr(cal, "slug", "") - cal_name = getattr(cal, "name", "") - href = prefix + url_for("calendar.get", calendar_slug=cal_slug) - del_url = url_for("calendar.delete", calendar_slug=cal_slug) - csrf_hdr = f'{{"X-CSRFToken":"{csrf}"}}' - parts.append(await render_to_sx("crud-item", - href=href, name=cal_name, slug=cal_slug, - del_url=del_url, csrf_hdr=csrf_hdr, - list_id="calendars-list", - confirm_title="Delete calendar?", - confirm_text="Entries will be hidden (soft delete)")) - return "".join(parts) - - -# --------------------------------------------------------------------------- -# Calendar month grid -# --------------------------------------------------------------------------- - -async def _calendar_main_panel_html(ctx: dict) -> str: - """Render the calendar month grid.""" - from quart import url_for - from quart import session as qsession - - calendar = ctx.get("calendar") - if not calendar: - return "" - cal_slug = getattr(calendar, "slug", "") - hx_select = ctx.get("hx_select_search", "#main-panel") - styles = ctx.get("styles") or {} - pill_cls = getattr(styles, "pill", "") if hasattr(styles, "pill") else styles.get("pill", "") - - year = ctx.get("year", 2024) - month = ctx.get("month", 1) - month_name = ctx.get("month_name", "") - weekday_names = ctx.get("weekday_names", []) - weeks = ctx.get("weeks", []) - prev_month = ctx.get("prev_month", 1) - prev_month_year = ctx.get("prev_month_year", year) - next_month = ctx.get("next_month", 1) - next_month_year = ctx.get("next_month_year", year) - prev_year = ctx.get("prev_year", year - 1) - next_year = ctx.get("next_year", year + 1) - month_entries = ctx.get("month_entries") or [] - user = ctx.get("user") - qs = qsession if "qsession" not in ctx else ctx["qsession"] - - def nav_link(y, m): - return url_for("calendar.get", calendar_slug=cal_slug, year=y, month=m) - - # Month navigation arrows - nav_arrows = [] - for label, yr, mn in [ - ("\u00ab", prev_year, month), - ("\u2039", prev_month_year, prev_month), - ]: - href = nav_link(yr, mn) - nav_arrows.append(await render_to_sx("events-calendar-nav-arrow", - pill_cls=pill_cls, href=href, label=label)) - - nav_arrows.append(await render_to_sx("events-calendar-month-label", - month_name=month_name, year=str(year))) - - for label, yr, mn in [ - ("\u203a", next_month_year, next_month), - ("\u00bb", next_year, month), - ]: - href = nav_link(yr, mn) - nav_arrows.append(await render_to_sx("events-calendar-nav-arrow", - pill_cls=pill_cls, href=href, label=label)) - - # Weekday headers - wd_parts = [] - for wd in weekday_names: - wd_parts.append(await render_to_sx("events-calendar-weekday", name=wd)) - wd_html = "".join(wd_parts) - - # Day cells - cells = [] - for week in weeks: - for day_cell in week: - if isinstance(day_cell, dict): - in_month = day_cell.get("in_month", True) - is_today = day_cell.get("is_today", False) - day_date = day_cell.get("date") - else: - in_month = getattr(day_cell, "in_month", True) - is_today = getattr(day_cell, "is_today", False) - day_date = getattr(day_cell, "date", None) - - cell_cls = "min-h-20 sm:min-h-24 bg-white px-3 py-2 text-xs" - if not in_month: - cell_cls += " bg-stone-50 text-stone-400" - if is_today: - cell_cls += " ring-2 ring-blue-500 z-10 relative" - - # Day number link - day_num_html = "" - day_short_html = "" - if day_date: - day_href = url_for( - "calendar.day.show_day", - calendar_slug=cal_slug, - year=day_date.year, month=day_date.month, day=day_date.day, - ) - day_short_html = await render_to_sx("events-calendar-day-short", - day_str=day_date.strftime("%a")) - day_num_html = await render_to_sx("events-calendar-day-num", - pill_cls=pill_cls, href=day_href, - num=str(day_date.day)) - - # Entry badges for this day - entry_badges = [] - if day_date: - for e in month_entries: - if e.start_at and e.start_at.date() == day_date: - is_mine = ( - (user and e.user_id == user.id) - or (not user and e.session_id == qs.get("calendar_sid")) - ) - if e.state == "confirmed": - bg_cls = "bg-emerald-200 text-emerald-900" if is_mine else "bg-emerald-100 text-emerald-800" - else: - bg_cls = "bg-sky-100 text-sky-800" if is_mine else "bg-stone-100 text-stone-700" - state_label = (e.state or "pending").replace("_", " ") - entry_badges.append(await render_to_sx("events-calendar-entry-badge", - bg_cls=bg_cls, name=e.name, - state_label=state_label)) - - badges_html = "(<> " + "".join(entry_badges) + ")" if entry_badges else "" - cells.append(await render_to_sx("events-calendar-cell", - cell_cls=cell_cls, day_short=SxExpr(day_short_html), - day_num=SxExpr(day_num_html), - badges=SxExpr(badges_html) if badges_html else None)) - - cells_html = "(<> " + "".join(cells) + ")" - arrows_html = "(<> " + "".join(nav_arrows) + ")" - wd_html = "(<> " + wd_html + ")" - return await render_to_sx("events-calendar-grid", - arrows=SxExpr(arrows_html), weekdays=SxExpr(wd_html), - cells=SxExpr(cells_html)) - - -# --------------------------------------------------------------------------- -# Day main panel -# --------------------------------------------------------------------------- - -async def _day_main_panel_html(ctx: dict) -> str: - """Render the day entries table + add button.""" - from quart import url_for - - calendar = ctx.get("calendar") - if not calendar: - return "" - cal_slug = getattr(calendar, "slug", "") - day_entries = ctx.get("day_entries") or [] - day = ctx.get("day") - month = ctx.get("month") - year = ctx.get("year") - hx_select = ctx.get("hx_select_search", "#main-panel") - styles = ctx.get("styles") or {} - list_container = getattr(styles, "list_container", "") if hasattr(styles, "list_container") else styles.get("list_container", "") - pill_cls = getattr(styles, "pill", "") if hasattr(styles, "pill") else styles.get("pill", "") - tr_cls = getattr(styles, "tr", "") if hasattr(styles, "tr") else styles.get("tr", "") - pre_action = getattr(styles, "pre_action_button", "") if hasattr(styles, "pre_action_button") else styles.get("pre_action_button", "") - - rows_html = "" - if day_entries: - row_parts = [] - for entry in day_entries: - row_parts.append(await _day_row_html(ctx, entry)) - rows_html = "".join(row_parts) - else: - rows_html = await render_to_sx("events-day-empty-row") - - add_url = url_for( - "calendar.day.calendar_entries.add_form", - calendar_slug=cal_slug, - day=day, month=month, year=year, - ) - - return await render_to_sx("events-day-table", - list_container=list_container, rows=SxExpr(rows_html), - pre_action=pre_action, add_url=add_url) - - -async def _day_row_html(ctx: dict, entry) -> str: - """Render a single day table row.""" - from quart import url_for - calendar = ctx.get("calendar") - cal_slug = getattr(calendar, "slug", "") - day = ctx.get("day") - month = ctx.get("month") - year = ctx.get("year") - hx_select = ctx.get("hx_select_search", "#main-panel") - styles = ctx.get("styles") or {} - pill_cls = getattr(styles, "pill", "") if hasattr(styles, "pill") else styles.get("pill", "") - tr_cls = getattr(styles, "tr", "") if hasattr(styles, "tr") else styles.get("tr", "") - - entry_href = url_for( - "calendar.day.calendar_entries.calendar_entry.get", - calendar_slug=cal_slug, day=day, month=month, year=year, entry_id=entry.id, - ) - - # Name - name_html = await render_to_sx("events-day-row-name", - href=entry_href, pill_cls=pill_cls, name=entry.name) - - # Slot/Time - slot = getattr(entry, "slot", None) - if slot: - slot_href = url_for("defpage_slot_detail", calendar_slug=cal_slug, slot_id=slot.id) - time_start = slot.time_start.strftime("%H:%M") if slot.time_start else "" - time_end = f" \u2192 {slot.time_end.strftime('%H:%M')}" if slot.time_end else "" - slot_html = await render_to_sx("events-day-row-slot", - href=slot_href, pill_cls=pill_cls, slot_name=slot.name, - time_str=f"({time_start}{time_end})") - else: - start = entry.start_at.strftime("%H:%M") if entry.start_at else "" - end = f" \u2192 {entry.end_at.strftime('%H:%M')}" if entry.end_at else "" - slot_html = await render_to_sx("events-day-row-time", start=start, end=end) - - # State - state = getattr(entry, "state", "pending") or "pending" - state_badge = await _entry_state_badge_html(state) - state_td = await render_to_sx("events-day-row-state", - state_id=f"entry-state-{entry.id}", badge=SxExpr(state_badge)) - - # Cost - cost = getattr(entry, "cost", None) - cost_str = f"\u00a3{cost:.2f}" if cost is not None else "\u00a30.00" - cost_td = await render_to_sx("events-day-row-cost", cost_str=cost_str) - - # Tickets - tp = getattr(entry, "ticket_price", None) - if tp is not None: - tc = getattr(entry, "ticket_count", None) - tc_str = f"{tc} tickets" if tc is not None else "Unlimited" - tickets_td = await render_to_sx("events-day-row-tickets", - price_str=f"\u00a3{tp:.2f}", count_str=tc_str) - else: - tickets_td = await render_to_sx("events-day-row-no-tickets") - - actions_td = await render_to_sx("events-day-row-actions") - - return await render_to_sx("events-day-row", - tr_cls=tr_cls, name=SxExpr(name_html), slot=SxExpr(slot_html), - state=SxExpr(state_td), cost=SxExpr(cost_td), - tickets=SxExpr(tickets_td), actions=SxExpr(actions_td)) - - -async 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 await render_to_sx("badge", cls=cls, label=label) - - -# --------------------------------------------------------------------------- -# Day admin main panel -# --------------------------------------------------------------------------- - -async def _day_admin_main_panel_html(ctx: dict) -> str: - """Render day admin panel (placeholder nav).""" - return await render_to_sx("events-day-admin-panel") - - -# --------------------------------------------------------------------------- -# Calendar admin main panel -# --------------------------------------------------------------------------- - -async def _calendar_admin_main_panel_html(ctx: dict) -> str: - """Render calendar admin config panel with description editor.""" - from quart import url_for - calendar = ctx.get("calendar") - if not calendar: - return "" - csrf_token = ctx.get("csrf_token") - csrf = csrf_token() if callable(csrf_token) else (csrf_token or "") - cal_slug = getattr(calendar, "slug", "") - desc = getattr(calendar, "description", "") or "" - hx_select = ctx.get("hx_select_search", "#main-panel") - - desc_edit_url = url_for("calendar.admin.calendar_description_edit", calendar_slug=cal_slug) - description_html = await _calendar_description_display_html(calendar, desc_edit_url) - - return await render_to_sx("events-calendar-admin-panel", - description_content=SxExpr(description_html), csrf=csrf, - description=desc) - - -async def _calendar_description_display_html(calendar, edit_url: str) -> str: - """Render calendar description display with edit button.""" - desc = getattr(calendar, "description", "") or "" - return await render_to_sx("events-calendar-description-display", - description=desc, edit_url=edit_url) - - -# --------------------------------------------------------------------------- -# Markets main panel -# --------------------------------------------------------------------------- - -async def _markets_main_panel_html(ctx: dict) -> str: - """Render markets list + create form panel.""" - from quart import url_for - rights = ctx.get("rights") or {} - is_admin = getattr(rights, "admin", False) if hasattr(rights, "admin") else rights.get("admin", False) - has_access = ctx.get("has_access") - can_create = has_access("markets.create_market") if callable(has_access) else is_admin - csrf_token = ctx.get("csrf_token") - csrf = csrf_token() if callable(csrf_token) else (csrf_token or "") - markets = ctx.get("markets") or [] - - form_html = "" - if can_create: - create_url = url_for("markets.create_market") - form_html = await render_to_sx("crud-create-form", - create_url=create_url, csrf=csrf, - errors_id="market-create-errors", list_id="markets-list", - placeholder="e.g. Farm Shop, Bakery", btn_label="Add market") - - list_html = await _markets_list_html(ctx, markets) - return await render_to_sx("crud-panel", - form=SxExpr(form_html), list=SxExpr(list_html), - list_id="markets-list") - - -async 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 await render_to_sx("empty-state", message="No markets yet. Create one above.", - cls="text-gray-500 mt-4") - - parts = [] - for m in markets: - m_slug = getattr(m, "slug", "") if hasattr(m, "slug") else m.get("slug", "") - m_name = getattr(m, "name", "") if hasattr(m, "name") else m.get("name", "") - market_href = call_url(ctx, "market_url", f"/{slug}/{m_slug}/") - del_url = url_for("markets.delete_market", market_slug=m_slug) - csrf_hdr = f'{{"X-CSRFToken":"{csrf}"}}' - parts.append(await render_to_sx("crud-item", - href=market_href, name=m_name, - slug=m_slug, del_url=del_url, - csrf_hdr=csrf_hdr, - list_id="markets-list", - confirm_title="Delete market?", - confirm_text="Products will be hidden (soft delete)")) - return "".join(parts) - - -# --------------------------------------------------------------------------- -# Ticket state badge helper -# --------------------------------------------------------------------------- - -async 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 await render_to_sx("badge", cls=cls, label=label) - - -# --------------------------------------------------------------------------- -# Tickets main panel (my tickets) -# --------------------------------------------------------------------------- - -async def _tickets_main_panel_html(ctx: dict, tickets: list) -> str: - """Render my tickets list.""" - from quart import url_for - - ticket_cards = [] - if tickets: - for ticket in tickets: - href = url_for("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')}" - - ticket_cards.append(await render_to_sx("events-ticket-card", - href=href, entry_name=entry_name, - type_name=tt.name if tt else None, - time_str=time_str or None, - cal_name=cal.name if cal else None, - badge=SxExpr(await _ticket_state_badge_html(state)), - code_prefix=ticket.code[:8])) - - cards_html = "".join(ticket_cards) - return await render_to_sx("events-tickets-panel", - list_container=_list_container(ctx), - has_tickets=bool(tickets), cards=SxExpr(cards_html)) - - -# --------------------------------------------------------------------------- -# Ticket detail panel -# --------------------------------------------------------------------------- - -async def _ticket_detail_panel_html(ctx: dict, ticket) -> str: - """Render a single ticket detail with QR code.""" - from quart import url_for - - entry = getattr(ticket, "entry", None) - tt = getattr(ticket, "ticket_type", None) - state = getattr(ticket, "state", "") - code = ticket.code - cal = getattr(entry, "calendar", None) if entry else None - checked_in_at = getattr(ticket, "checked_in_at", None) - - bg_map = {"confirmed": "bg-emerald-50", "checked_in": "bg-blue-50", "reserved": "bg-amber-50"} - header_bg = bg_map.get(state, "bg-stone-50") - entry_name = entry.name if entry else "Ticket" - back_href = url_for("defpage_my_tickets") - - # Badge with larger sizing - badge = (await _ticket_state_badge_html(state)).replace('px-2 py-0.5 text-xs', 'px-3 py-1 text-sm') - - # Time info - time_date = entry.start_at.strftime("%A, %B %d, %Y") if entry and entry.start_at else None - time_range = entry.start_at.strftime("%H:%M") if entry and entry.start_at else None - if time_range and entry.end_at: - time_range += f" \u2013 {entry.end_at.strftime('%H:%M')}" - - tt_desc = f"{tt.name} \u2014 \u00a3{tt.cost:.2f}" if tt and getattr(tt, "cost", None) else None - checkin_str = checked_in_at.strftime("Checked in: %B %d, %Y at %H:%M") if checked_in_at else None - - qr_script = ( - f"(function(){{var c=document.getElementById('ticket-qr-{code}');" - "if(c&&typeof QRCode!=='undefined'){" - "var cv=document.createElement('canvas');" - f"QRCode.toCanvas(cv,'{code}',{{width:200,margin:2,color:{{dark:'#1c1917',light:'#ffffff'}}}},function(e){{if(!e)c.appendChild(cv)}});" - "}})()" - ) - - return await render_to_sx("events-ticket-detail", - list_container=_list_container(ctx), back_href=back_href, - header_bg=header_bg, entry_name=entry_name, - badge=SxExpr(badge), type_name=tt.name if tt else None, - code=code, time_date=time_date, time_range=time_range, - cal_name=cal.name if cal else None, - type_desc=tt_desc, checkin_str=checkin_str, - qr_script=qr_script) - - -# --------------------------------------------------------------------------- -# Ticket admin main panel -# --------------------------------------------------------------------------- - -async def _ticket_admin_main_panel_html(ctx: dict, tickets: list, stats: dict) -> str: - """Render ticket admin dashboard.""" - from quart import url_for - csrf_token = ctx.get("csrf_token") - csrf = csrf_token() if callable(csrf_token) else (csrf_token or "") - lookup_url = url_for("ticket_admin.lookup") - - # Stats cards - stats_html = "" - for label, key, border, bg, text_cls in [ - ("Total", "total", "border-stone-200", "", "text-stone-900"), - ("Confirmed", "confirmed", "border-emerald-200", "bg-emerald-50", "text-emerald-700"), - ("Checked In", "checked_in", "border-blue-200", "bg-blue-50", "text-blue-700"), - ("Reserved", "reserved", "border-amber-200", "bg-amber-50", "text-amber-700"), - ]: - val = stats.get(key, 0) - lbl_cls = text_cls.replace("700", "600").replace("900", "500") if "stone" not in text_cls else "text-stone-500" - stats_html += await render_to_sx("events-ticket-admin-stat", - border=border, bg=bg, text_cls=text_cls, - label_cls=lbl_cls, value=str(val), label=label) - - # Ticket rows - rows_html = "" - for ticket in tickets: - entry = getattr(ticket, "entry", None) - tt = getattr(ticket, "ticket_type", None) - state = getattr(ticket, "state", "") - code = ticket.code - - date_html = "" - if entry and entry.start_at: - date_html = await render_to_sx("events-ticket-admin-date", - date_str=entry.start_at.strftime("%d %b %Y, %H:%M")) - - action_html = "" - if state in ("confirmed", "reserved"): - checkin_url = url_for("ticket_admin.do_checkin", code=code) - action_html = await render_to_sx("events-ticket-admin-checkin-form", - checkin_url=checkin_url, code=code, csrf=csrf) - elif state == "checked_in": - checked_in_at = getattr(ticket, "checked_in_at", None) - t_str = checked_in_at.strftime("%H:%M") if checked_in_at else "" - action_html = await render_to_sx("events-ticket-admin-checked-in", - time_str=t_str) - - rows_html += await render_to_sx("events-ticket-admin-row", - code=code, code_short=code[:12] + "...", - entry_name=entry.name if entry else "\u2014", - date=SxExpr(date_html), - type_name=tt.name if tt else "\u2014", - badge=SxExpr(await _ticket_state_badge_html(state)), - action=SxExpr(action_html)) - - return await render_to_sx("events-ticket-admin-panel", - list_container=_list_container(ctx), stats=SxExpr(stats_html), - lookup_url=lookup_url, has_tickets=bool(tickets), - rows=SxExpr(rows_html)) - - -# --------------------------------------------------------------------------- -# All events / page summary entry cards -# --------------------------------------------------------------------------- - -async def _entry_card_html(entry, page_info: dict, pending_tickets: dict, - ticket_url: str, events_url_fn, *, is_page_scoped: bool = False, - post: dict | None = None) -> str: - """Render a list card for one event entry.""" - pi = page_info.get(getattr(entry, "calendar_container_id", 0), {}) - if is_page_scoped and post: - page_slug = pi.get("slug", post.get("slug", "")) - else: - page_slug = pi.get("slug", "") - page_title = pi.get("title") - - day_href = "" - if page_slug and entry.start_at: - day_href = events_url_fn(f"/{page_slug}/{entry.calendar_slug}/day/{entry.start_at.strftime('%Y/%-m/%-d')}/") - entry_href = f"{day_href}entries/{entry.id}/" if day_href else "" - - # Title (linked or plain) - if entry_href: - title_html = await render_to_sx("events-entry-title-linked", - href=entry_href, name=entry.name) - else: - title_html = await render_to_sx("events-entry-title-plain", name=entry.name) - - # Badges - badges_html = "" - if page_title and (not is_page_scoped or page_title != (post or {}).get("title")): - page_href = events_url_fn(f"/{page_slug}/") - badges_html += await render_to_sx("events-entry-page-badge", - href=page_href, title=page_title) - cal_name = getattr(entry, "calendar_name", "") - if cal_name: - badges_html += await render_to_sx("events-entry-cal-badge", name=cal_name) - - # Time line - time_parts = "" - if day_href and not is_page_scoped: - time_parts += await render_to_sx("events-entry-time-linked", - href=day_href, - date_str=entry.start_at.strftime("%a %-d %b")) - elif not is_page_scoped: - time_parts += await render_to_sx("events-entry-time-plain", - date_str=entry.start_at.strftime("%a %-d %b")) - time_parts += entry.start_at.strftime("%H:%M") - if entry.end_at: - time_parts += f' \u2013 {entry.end_at.strftime("%H:%M")}' - - cost = getattr(entry, "cost", None) - cost_html = await render_to_sx("events-entry-cost", - cost=f"£{cost:.2f}") if cost else "" - - # Ticket widget - tp = getattr(entry, "ticket_price", None) - widget_html = "" - if tp is not None: - qty = pending_tickets.get(entry.id, 0) - widget_html = await render_to_sx("events-entry-widget-wrapper", - widget=SxExpr(await _ticket_widget_html(entry, qty, ticket_url, ctx={}))) - - return await render_to_sx("events-entry-card", - title=SxExpr(title_html), badges=SxExpr(badges_html), - time_parts=SxExpr(time_parts), cost=SxExpr(cost_html), - widget=SxExpr(widget_html)) - - -async def _entry_card_tile_html(entry, page_info: dict, pending_tickets: dict, - ticket_url: str, events_url_fn, *, is_page_scoped: bool = False, - post: dict | None = None) -> str: - """Render a tile card for one event entry.""" - pi = page_info.get(getattr(entry, "calendar_container_id", 0), {}) - if is_page_scoped and post: - page_slug = pi.get("slug", post.get("slug", "")) - else: - page_slug = pi.get("slug", "") - page_title = pi.get("title") - - day_href = "" - if page_slug and entry.start_at: - day_href = events_url_fn(f"/{page_slug}/{entry.calendar_slug}/day/{entry.start_at.strftime('%Y/%-m/%-d')}/") - entry_href = f"{day_href}entries/{entry.id}/" if day_href else "" - - # Title - if entry_href: - title_html = await render_to_sx("events-entry-title-tile-linked", - href=entry_href, name=entry.name) - else: - title_html = await render_to_sx("events-entry-title-tile-plain", name=entry.name) - - # Badges - badges_html = "" - if page_title and (not is_page_scoped or page_title != (post or {}).get("title")): - page_href = events_url_fn(f"/{page_slug}/") - badges_html += await render_to_sx("events-entry-page-badge", - href=page_href, title=page_title) - cal_name = getattr(entry, "calendar_name", "") - if cal_name: - badges_html += await render_to_sx("events-entry-cal-badge", name=cal_name) - - # Time - time_html = "" - if day_href: - time_html += (await render_to_sx("events-entry-time-linked", - href=day_href, - date_str=entry.start_at.strftime("%a %-d %b"))).replace(" · ", "") - else: - time_html += entry.start_at.strftime("%a %-d %b") - time_html += f' \u00b7 {entry.start_at.strftime("%H:%M")}' - if entry.end_at: - time_html += f' \u2013 {entry.end_at.strftime("%H:%M")}' - - cost = getattr(entry, "cost", None) - cost_html = await render_to_sx("events-entry-cost", - cost=f"£{cost:.2f}") if cost else "" - - # Ticket widget - tp = getattr(entry, "ticket_price", None) - widget_html = "" - if tp is not None: - qty = pending_tickets.get(entry.id, 0) - widget_html = await render_to_sx("events-entry-tile-widget-wrapper", - widget=SxExpr(await _ticket_widget_html(entry, qty, ticket_url, ctx={}))) - - return await render_to_sx("events-entry-card-tile", - title=SxExpr(title_html), badges=SxExpr(badges_html), - time=SxExpr(time_html), cost=SxExpr(cost_html), - widget=SxExpr(widget_html)) - - -async def _ticket_widget_html(entry, qty: int, ticket_url: str, *, ctx: dict) -> str: - """Render the inline +/- ticket widget.""" - csrf_token_val = "" - if ctx: - ct = ctx.get("csrf_token") - csrf_token_val = ct() if callable(ct) else (ct or "") - else: - try: - from flask_wtf.csrf import generate_csrf - csrf_token_val = generate_csrf() - except Exception: - pass - - eid = entry.id - tp = getattr(entry, "ticket_price", 0) or 0 - tgt = f"#page-ticket-{eid}" - - async def _tw_form(count_val, btn_html): - return await render_to_sx("events-tw-form", - ticket_url=ticket_url, target=tgt, - csrf=csrf_token_val, entry_id=str(eid), - count_val=str(count_val), btn=SxExpr(btn_html)) - - if qty == 0: - inner = await _tw_form(1, await render_to_sx("events-tw-cart-plus")) - else: - minus = await _tw_form(qty - 1, await render_to_sx("events-tw-minus")) - cart_icon = await render_to_sx("events-tw-cart-icon", qty=str(qty)) - plus = await _tw_form(qty + 1, await render_to_sx("events-tw-plus")) - inner = minus + cart_icon + plus - - return await render_to_sx("events-tw-widget", - entry_id=str(eid), price=f"£{tp:.2f}", - inner=SxExpr(inner)) - - -async 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(await _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(await render_to_sx("events-date-separator", - date_str=entry_date)) - last_date = entry_date - parts.append(await _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(await render_to_sx("sentinel-simple", - id=f"sentinel-{page}", next_url=next_url)) - return "".join(parts) - - -# --------------------------------------------------------------------------- -# All events / page summary main panels -# --------------------------------------------------------------------------- - -_LIST_SVG = None -_TILE_SVG = None - - -async def _get_list_svg(): - global _LIST_SVG - if _LIST_SVG is None: - _LIST_SVG = await render_to_sx("list-svg") - return _LIST_SVG - - -async def _get_tile_svg(): - global _TILE_SVG - if _TILE_SVG is None: - _TILE_SVG = await render_to_sx("tile-svg") - return _TILE_SVG - - -async def _view_toggle_html(ctx: dict, view: str) -> str: - """Render the list/tile view toggle bar.""" - from shared.utils import route_prefix - prefix = route_prefix() - clh = ctx.get("current_local_href", "/") - hx_select = ctx.get("hx_select_search", "#main-panel") - - list_href = prefix + str(clh) - tile_href = prefix + str(clh) - if "?" in list_href: - list_href = list_href.split("?")[0] - if "?" in tile_href: - tile_href = tile_href.split("?")[0] + "?view=tile" - else: - tile_href = tile_href + "?view=tile" - - list_active = 'bg-stone-200 text-stone-800' if view != 'tile' else 'text-stone-400 hover:text-stone-600' - tile_active = 'bg-stone-200 text-stone-800' if view == 'tile' else 'text-stone-400 hover:text-stone-600' - - return await render_to_sx("view-toggle", - list_href=list_href, tile_href=tile_href, - hx_select=hx_select, list_cls=list_active, - tile_cls=tile_active, storage_key="events_view", - list_svg=SxExpr(await _get_list_svg()), tile_svg=SxExpr(await _get_tile_svg())) - - -async def _events_main_panel_html(ctx: dict, entries, has_more, pending_tickets, page_info, - page, view, ticket_url, next_url, events_url_fn, - *, is_page_scoped=False, post=None) -> str: - """Render the events main panel with view toggle + cards.""" - toggle = await _view_toggle_html(ctx, view) - - if entries: - cards = await _entry_cards_html( - entries, page_info, pending_tickets, ticket_url, events_url_fn, - view, page, has_more, next_url, - is_page_scoped=is_page_scoped, post=post, - ) - grid_cls = ("max-w-full px-3 py-3 grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-4" - if view == "tile" else "max-w-full px-3 py-3 space-y-3") - body = await render_to_sx("events-grid", grid_cls=grid_cls, cards=SxExpr(cards)) - else: - body = await render_to_sx("empty-state", icon="fa fa-calendar-xmark", - message="No upcoming events", - cls="px-3 py-12 text-center text-stone-400") - - return await render_to_sx("events-main-panel-body", - toggle=SxExpr(toggle), body=SxExpr(body)) - - -# --------------------------------------------------------------------------- -# Utility -# --------------------------------------------------------------------------- - -def _list_container(ctx: dict) -> str: - styles = ctx.get("styles") or {} - return getattr(styles, "list_container", "") if hasattr(styles, "list_container") else styles.get("list_container", "") - - -# =========================================================================== -# PUBLIC API -# =========================================================================== - - -# --------------------------------------------------------------------------- -# All events -# --------------------------------------------------------------------------- - -async def render_all_events_page(ctx: dict, entries, has_more, pending_tickets, - page_info, page, view) -> str: - """Full page: all events listing.""" - from quart import url_for - from shared.utils import route_prefix - from shared.infrastructure.urls import events_url - - prefix = route_prefix() - view_param = f"&view={view}" if view != "list" else "" - ticket_url = url_for("all_events.adjust_ticket") - next_url = prefix + url_for("all_events.entries_fragment", page=page + 1) + (f"?view={view}" if view != "list" else "") - - content = await _events_main_panel_html( - ctx, entries, has_more, pending_tickets, page_info, - page, view, ticket_url, next_url, events_url, - ) - hdr = await render_to_sx_with_env("layout-root-full", _ctx_to_env(ctx)) - return await full_page_sx(ctx, header_rows=hdr, content=content) - - -async def render_all_events_oob(ctx: dict, entries, has_more, pending_tickets, - page_info, page, view) -> str: - """OOB response: all events listing (htmx nav).""" - from quart import url_for - from shared.utils import route_prefix - from shared.infrastructure.urls import events_url - - prefix = route_prefix() - ticket_url = url_for("all_events.adjust_ticket") - next_url = prefix + url_for("all_events.entries_fragment", page=page + 1) + (f"?view={view}" if view != "list" else "") - - content = await _events_main_panel_html( - ctx, entries, has_more, pending_tickets, page_info, - page, view, ticket_url, next_url, events_url, - ) - return await oob_page_sx(content=content) - - -async def render_all_events_cards(entries, has_more, pending_tickets, - page_info, page, view) -> str: - """Pagination fragment: all events cards only.""" - from quart import url_for - from shared.utils import route_prefix - from shared.infrastructure.urls import events_url - - prefix = route_prefix() - ticket_url = url_for("all_events.adjust_ticket") - next_url = prefix + url_for("all_events.entries_fragment", page=page + 1) + (f"?view={view}" if view != "list" else "") - - return await _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 = await _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 = await render_to_sx_with_env("layout-root-full", _ctx_to_env(ctx)) - hdr += await header_child_sx(await _post_header_sx(ctx)) - return await full_page_sx(ctx, header_rows=hdr, content=content) - - -async def render_page_summary_oob(ctx: dict, entries, has_more, pending_tickets, - page_info, page, view) -> str: - """OOB response: page-scoped events (htmx nav).""" - from quart import url_for - from shared.utils import route_prefix - from shared.infrastructure.urls import events_url - - prefix = route_prefix() - post = ctx.get("post") or {} - ticket_url = url_for("page_summary.adjust_ticket") - next_url = prefix + url_for("page_summary.entries_fragment", page=page + 1) + (f"?view={view}" if view != "list" else "") - - content = await _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 = await _post_header_sx(ctx, oob=True) - oobs += _clear_deeper_oob("post-row", "post-header-child") - return await oob_page_sx(oobs=oobs, content=content) - - -async def render_page_summary_cards(entries, has_more, pending_tickets, - page_info, page, view, post) -> str: - """Pagination fragment: page-scoped events cards only.""" - from quart import url_for - from shared.utils import route_prefix - from shared.infrastructure.urls import events_url - - prefix = route_prefix() - ticket_url = url_for("page_summary.adjust_ticket") - next_url = prefix + url_for("page_summary.entries_fragment", page=page + 1) + (f"?view={view}" if view != "list" else "") - - return await _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 = await _calendars_main_panel_sx(ctx) - ctx = await _ensure_container_nav(ctx) - slug = (ctx.get("post") or {}).get("slug", "") - root_hdr = await render_to_sx_with_env("layout-root-full", _ctx_to_env(ctx)) - post_hdr = await _post_header_sx(ctx) - admin_hdr = await post_admin_header_sx(ctx, slug, selected="calendars") - return await full_page_sx(ctx, header_rows=root_hdr + post_hdr + admin_hdr, content=content) - - -async def render_calendars_oob(ctx: dict) -> str: - """OOB response: calendars listing.""" - content = await _calendars_main_panel_sx(ctx) - ctx = await _ensure_container_nav(ctx) - slug = (ctx.get("post") or {}).get("slug", "") - oobs = await post_admin_header_sx(ctx, slug, oob=True, selected="calendars") - oobs += _clear_deeper_oob("post-row", "post-header-child", - "post-admin-row", "post-admin-header-child") - return await oob_page_sx(oobs=oobs, content=content) - - -# --------------------------------------------------------------------------- -# Calendar month view -# --------------------------------------------------------------------------- - -async def render_calendar_page(ctx: dict) -> str: - """Full page: calendar month view.""" - content = await _calendar_main_panel_html(ctx) - hdr = await render_to_sx_with_env("layout-root-full", _ctx_to_env(ctx)) - child = await _post_header_sx(ctx) + await _calendar_header_sx(ctx) - hdr += await header_child_sx(child) - return await full_page_sx(ctx, header_rows=hdr, content=content) - - -async def render_calendar_oob(ctx: dict) -> str: - """OOB response: calendar month view.""" - content = await _calendar_main_panel_html(ctx) - oobs = await _post_header_sx(ctx, oob=True) - oobs += await oob_header_sx("post-header-child", "calendar-header-child", - await _calendar_header_sx(ctx)) - oobs += _clear_deeper_oob("post-row", "post-header-child", - "calendar-row", "calendar-header-child") - return await oob_page_sx(oobs=oobs, content=content) - - -# --------------------------------------------------------------------------- -# Day detail -# --------------------------------------------------------------------------- - -async def render_day_page(ctx: dict) -> str: - """Full page: day detail.""" - content = await _day_main_panel_html(ctx) - hdr = await render_to_sx_with_env("layout-root-full", _ctx_to_env(ctx)) - child = (await _post_header_sx(ctx) - + await _calendar_header_sx(ctx) + await _day_header_sx(ctx)) - hdr += await header_child_sx(child) - return await full_page_sx(ctx, header_rows=hdr, content=content) - - -async def render_day_oob(ctx: dict) -> str: - """OOB response: day detail.""" - content = await _day_main_panel_html(ctx) - oobs = await _calendar_header_sx(ctx, oob=True) - oobs += await oob_header_sx("calendar-header-child", "day-header-child", - await _day_header_sx(ctx)) - oobs += _clear_deeper_oob("post-row", "post-header-child", - "calendar-row", "calendar-header-child", - "day-row", "day-header-child") - return await oob_page_sx(oobs=oobs, content=content) - - -# --------------------------------------------------------------------------- -# Calendar admin helper -# --------------------------------------------------------------------------- - -async def _events_post_admin_header_sx(ctx: dict, *, oob: bool = False, - selected: str = "") -> str: - """Post-level admin row for events — delegates to shared helper.""" - slug = (ctx.get("post") or {}).get("slug", "") - return await post_admin_header_sx(ctx, slug, oob=oob, selected=selected) - - -# =========================================================================== -# POST / PUT / DELETE response components -# =========================================================================== - - -# --------------------------------------------------------------------------- -# Ticket widget (public wrapper for _ticket_widget_html) -# --------------------------------------------------------------------------- - -async def render_ticket_widget(entry, qty: int, ticket_url: str) -> str: - """Render the +/- ticket widget for page_summary / all_events adjust_ticket.""" - return await _ticket_widget_html(entry, qty, ticket_url, ctx={}) - - -# --------------------------------------------------------------------------- -# Ticket admin: checkin result -# --------------------------------------------------------------------------- - -async def render_checkin_result(success: bool, error: str | None, ticket) -> str: - """Render checkin result: table row on success, error div on failure.""" - if not success: - return await render_to_sx("events-checkin-error", - message=error or "Check-in failed") - if not ticket: - return "" - code = ticket.code - entry = getattr(ticket, "entry", None) - tt = getattr(ticket, "ticket_type", None) - checked_in_at = getattr(ticket, "checked_in_at", None) - time_str = checked_in_at.strftime("%H:%M") if checked_in_at else "Just now" - - date_html = "" - if entry and entry.start_at: - date_html = await render_to_sx("events-ticket-admin-date", - date_str=entry.start_at.strftime("%d %b %Y, %H:%M")) - - return await render_to_sx("events-checkin-success-row", - code=code, code_short=code[:12] + "...", - entry_name=entry.name if entry else "\u2014", - date=SxExpr(date_html), - type_name=tt.name if tt else "\u2014", - badge=SxExpr(await _ticket_state_badge_html("checked_in")), - time_str=time_str) - - -# --------------------------------------------------------------------------- -# Ticket admin: lookup result -# --------------------------------------------------------------------------- - -async 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 await render_to_sx("events-lookup-error", message=error) - if not ticket: - return "" - - entry = getattr(ticket, "entry", None) - tt = getattr(ticket, "ticket_type", None) - state = getattr(ticket, "state", "") - code = ticket.code - checked_in_at = getattr(ticket, "checked_in_at", None) - csrf = generate_csrf_token() - - # Info section - info_html = await render_to_sx("events-lookup-info", - entry_name=entry.name if entry else "Unknown event") - if tt: - info_html += await render_to_sx("events-lookup-type", type_name=tt.name) - if entry and entry.start_at: - info_html += await render_to_sx("events-lookup-date", - date_str=entry.start_at.strftime("%A, %B %d, %Y at %H:%M")) - cal = getattr(entry, "calendar", None) if entry else None - if cal: - info_html += await render_to_sx("events-lookup-cal", cal_name=cal.name) - info_html += await render_to_sx("events-lookup-status", - badge=SxExpr(await _ticket_state_badge_html(state)), code=code) - if checked_in_at: - info_html += await render_to_sx("events-lookup-checkin-time", - date_str=checked_in_at.strftime("%B %d, %Y at %H:%M")) - - # Action area - action_html = "" - if state in ("confirmed", "reserved"): - checkin_url = url_for("ticket_admin.do_checkin", code=code) - action_html = await render_to_sx("events-lookup-checkin-btn", - checkin_url=checkin_url, code=code, csrf=csrf) - elif state == "checked_in": - action_html = await render_to_sx("events-lookup-checked-in") - elif state == "cancelled": - action_html = await render_to_sx("events-lookup-cancelled") - - return await render_to_sx("events-lookup-card", - info=SxExpr(info_html), code=code, action=SxExpr(action_html)) - - -# --------------------------------------------------------------------------- -# Ticket admin: entry tickets table -# --------------------------------------------------------------------------- - -async def render_entry_tickets_admin(entry, tickets: list) -> str: - """Render admin ticket table for a specific entry.""" - from quart import url_for - from shared.browser.app.csrf import generate_csrf_token - csrf = generate_csrf_token() - - count = len(tickets) - suffix = "s" if count != 1 else "" - - rows_html = "" - for ticket in tickets: - tt = getattr(ticket, "ticket_type", None) - state = getattr(ticket, "state", "") - code = ticket.code - checked_in_at = getattr(ticket, "checked_in_at", None) - - action_html = "" - if state in ("confirmed", "reserved"): - checkin_url = url_for("ticket_admin.do_checkin", code=code) - action_html = await render_to_sx("events-entry-tickets-admin-checkin", - checkin_url=checkin_url, code=code, csrf=csrf) - elif state == "checked_in": - t_str = checked_in_at.strftime("%H:%M") if checked_in_at else "" - action_html = await render_to_sx("events-ticket-admin-checked-in", - time_str=t_str) - - rows_html += await render_to_sx("events-entry-tickets-admin-row", - code=code, code_short=code[:12] + "...", - type_name=tt.name if tt else "\u2014", - badge=SxExpr(await _ticket_state_badge_html(state)), - action=SxExpr(action_html)) - - if tickets: - body_html = await render_to_sx("events-entry-tickets-admin-table", - rows=SxExpr(rows_html)) - else: - body_html = await render_to_sx("events-entry-tickets-admin-empty") - - return await render_to_sx("events-entry-tickets-admin-panel", - entry_name=entry.name, - count_label=f"{count} ticket{suffix}", - body=SxExpr(body_html)) - - -# --------------------------------------------------------------------------- -# Day main panel -- public API -# --------------------------------------------------------------------------- - -async def render_day_main_panel(ctx: dict) -> str: - """Public wrapper for day main panel rendering.""" - return await _day_main_panel_html(ctx) - - -# --------------------------------------------------------------------------- -# Entry main panel -# --------------------------------------------------------------------------- - -async def _entry_main_panel_html(ctx: dict) -> str: - """Render the entry detail panel (name, slot, time, state, cost, tickets, - buy form, date, posts, options + edit button).""" - from quart import url_for - - entry = ctx.get("entry") - if not entry: - return "" - - calendar = ctx.get("calendar") - cal_slug = getattr(calendar, "slug", "") if calendar else "" - day = ctx.get("day") - month = ctx.get("month") - year = ctx.get("year") - styles = ctx.get("styles") or {} - list_container = getattr(styles, "list_container", "") if hasattr(styles, "list_container") else styles.get("list_container", "") - pre_action = getattr(styles, "pre_action_button", "") if hasattr(styles, "pre_action_button") else styles.get("pre_action_button", "") - - eid = entry.id - state = getattr(entry, "state", "pending") or "pending" - - async def _field(label, content_html): - return await render_to_sx("events-entry-field", label=label, content=SxExpr(content_html)) - - # Name - name_html = await _field("Name", await render_to_sx("events-entry-name-field", name=entry.name)) - - # Slot - slot = getattr(entry, "slot", None) - if slot: - flex_label = "(flexible)" if getattr(slot, "flexible", False) else "(fixed)" - slot_inner = await render_to_sx("events-entry-slot-assigned", - slot_name=slot.name, flex_label=flex_label) - else: - slot_inner = await render_to_sx("events-entry-slot-none") - slot_html = await _field("Slot", slot_inner) - - # Time Period - start_str = entry.start_at.strftime("%H:%M") if entry.start_at else "" - end_str = f" \u2013 {entry.end_at.strftime('%H:%M')}" if entry.end_at else " \u2013 open-ended" - time_html = await _field("Time Period", await render_to_sx("events-entry-time-field", - time_str=start_str + end_str)) - - # State - state_html = await _field("State", await render_to_sx("events-entry-state-field", - entry_id=str(eid), - badge=SxExpr(await _entry_state_badge_html(state)))) - - # Cost - cost = getattr(entry, "cost", None) - cost_str = f"{cost:.2f}" if cost is not None else "0.00" - cost_html = await _field("Cost", await render_to_sx("events-entry-cost-field", - cost=f"£{cost_str}")) - - # Ticket Configuration (admin) - tickets_html = await _field("Tickets", await render_to_sx("events-entry-tickets-field", - entry_id=str(eid), - tickets_config=SxExpr(await render_entry_tickets_config(entry, calendar, day, month, year)))) - - # Buy Tickets (public-facing) - ticket_remaining = ctx.get("ticket_remaining") - ticket_sold_count = ctx.get("ticket_sold_count", 0) - user_ticket_count = ctx.get("user_ticket_count", 0) - user_ticket_counts_by_type = ctx.get("user_ticket_counts_by_type") or {} - buy_html = await render_buy_form( - entry, ticket_remaining, ticket_sold_count, - user_ticket_count, user_ticket_counts_by_type, - ) - - # Date - date_str = entry.start_at.strftime("%A, %B %d, %Y") if entry.start_at else "" - date_html = await _field("Date", await render_to_sx("events-entry-date-field", date_str=date_str)) - - # Associated Posts - entry_posts = ctx.get("entry_posts") or [] - posts_html = await _field("Associated Posts", await render_to_sx("events-entry-posts-field", - entry_id=str(eid), - posts_panel=SxExpr(await render_entry_posts_panel(entry_posts, entry, calendar, day, month, year)))) - - # Options and Edit Button - edit_url = url_for( - "calendar.day.calendar_entries.calendar_entry.get_edit", - entry_id=eid, calendar_slug=cal_slug, - day=day, month=month, year=year, - ) - - return await render_to_sx("events-entry-panel", - entry_id=str(eid), list_container=list_container, - name=SxExpr(name_html), slot=SxExpr(slot_html), - time=SxExpr(time_html), state=SxExpr(state_html), - cost=SxExpr(cost_html), tickets=SxExpr(tickets_html), - buy=SxExpr(buy_html), date=SxExpr(date_html), - posts=SxExpr(posts_html), - options=SxExpr(await _entry_options_html(entry, calendar, day, month, year)), - pre_action=pre_action, edit_url=edit_url) - - -# --------------------------------------------------------------------------- -# Entry header row -# --------------------------------------------------------------------------- - -async def _entry_header_html(ctx: dict, *, oob: bool = False) -> str: - """Build entry detail header row.""" - from quart import url_for - - calendar = ctx.get("calendar") - if not calendar: - return "" - cal_slug = getattr(calendar, "slug", "") - entry = ctx.get("entry") - if not entry: - return "" - day = ctx.get("day") - month = ctx.get("month") - year = ctx.get("year") - - link_href = url_for( - "calendar.day.calendar_entries.calendar_entry.get", - calendar_slug=cal_slug, - year=year, month=month, day=day, - entry_id=entry.id, - ) - label_html = await render_to_sx("events-entry-label", - entry_id=str(entry.id), - title=SxExpr(await _entry_title_html(entry)), - times=SxExpr(await _entry_times_html(entry))) - - nav_html = await _entry_nav_html(ctx) - - return await render_to_sx("menu-row-sx", id="entry-row", level=5, - link_href=link_href, link_label_content=SxExpr(label_html), - nav=SxExpr(nav_html) if nav_html else None, child_id="entry-header-child", oob=oob) - - -async def _entry_times_html(entry) -> str: - """Render entry times label.""" - start = entry.start_at - end = entry.end_at - if not start: - return "" - start_str = start.strftime("%H:%M") - end_str = f" \u2192 {end.strftime('%H:%M')}" if end else "" - return await render_to_sx("events-entry-times", time_str=start_str + end_str) - - -# --------------------------------------------------------------------------- -# Entry nav (desktop + admin link) -# --------------------------------------------------------------------------- - -async def _entry_nav_html(ctx: dict) -> str: - """Entry desktop nav: associated posts scrolling menu + admin link.""" - from quart import url_for - - calendar = ctx.get("calendar") - if not calendar: - return "" - cal_slug = getattr(calendar, "slug", "") - entry = ctx.get("entry") - if not entry: - return "" - day = ctx.get("day") - month = ctx.get("month") - year = ctx.get("year") - entry_posts = ctx.get("entry_posts") or [] - rights = ctx.get("rights") or {} - is_admin = getattr(rights, "admin", False) if hasattr(rights, "admin") else rights.get("admin", False) - - blog_url_fn = ctx.get("blog_url") - - parts = [] - - # Associated Posts scrolling menu - if entry_posts: - post_links = "" - for ep in entry_posts: - slug = getattr(ep, "slug", "") - title = getattr(ep, "title", "") - feat = getattr(ep, "feature_image", None) - href = blog_url_fn(f"/{slug}/") if blog_url_fn else f"/{slug}/" - if feat: - img_html = await render_to_sx("events-post-img", src=feat, alt=title) - else: - img_html = await render_to_sx("events-post-img-placeholder") - post_links += await render_to_sx("events-entry-nav-post-link", - href=href, img=SxExpr(img_html), title=title) - parts.append((await render_to_sx("events-entry-posts-nav-oob", - items=SxExpr(post_links))).replace(' :hx-swap-oob "true"', '')) - - # Admin link - if is_admin: - admin_url = url_for( - "calendar.day.calendar_entries.calendar_entry.admin.admin", - calendar_slug=cal_slug, - day=day, month=month, year=year, - entry_id=entry.id, - ) - parts.append(await render_to_sx("events-entry-admin-link", href=admin_url)) - - return "".join(parts) - - -# --------------------------------------------------------------------------- -# Entry optioned (confirm/decline/provisional response) -# --------------------------------------------------------------------------- - -async def render_entry_optioned(entry, calendar, day, month, year) -> str: - """Render entry options buttons + OOB title & state swaps.""" - options = await _entry_options_html(entry, calendar, day, month, year) - title = await _entry_title_html(entry) - state = await _entry_state_badge_html(getattr(entry, "state", "pending") or "pending") - - return options + await render_to_sx("events-entry-optioned-oob", - entry_id=str(entry.id), - title=SxExpr(title), state=SxExpr(state)) - - -async def _entry_title_html(entry) -> str: - """Render entry title (icon + name + state badge).""" - state = getattr(entry, "state", "pending") or "pending" - return await render_to_sx("events-entry-title", - name=entry.name, - badge=SxExpr(await _entry_state_badge_html(state))) - - -async 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}" - - async def _make_button(action_name, label, confirm_title, confirm_text, *, trigger_type="submit"): - url = url_for( - f"calendar.day.calendar_entries.calendar_entry.{action_name}", - calendar_slug=cal_slug, day=day, month=month, year=year, entry_id=eid, - ) - btn_type = "button" if trigger_type == "button" else "submit" - return await render_to_sx("events-entry-option-button", - url=url, target=target, csrf=csrf, btn_type=btn_type, - action_btn=action_btn, confirm_title=confirm_title, - confirm_text=confirm_text, label=label, - is_btn=trigger_type == "button") - - buttons_html = "" - if state == "provisional": - buttons_html += await _make_button( - "confirm_entry", "confirm", - "Confirm entry?", "Are you sure you want to confirm this entry?", - ) - buttons_html += await _make_button( - "decline_entry", "decline", - "Decline entry?", "Are you sure you want to decline this entry?", - ) - elif state == "confirmed": - buttons_html += await _make_button( - "provisional_entry", "provisional", - "Provisional entry?", "Are you sure you want to provisional this entry?", - trigger_type="button", - ) - - return await render_to_sx("events-entry-options", - entry_id=str(eid), buttons=SxExpr(buttons_html)) - - -# --------------------------------------------------------------------------- -# Entry tickets config (display + form) -# --------------------------------------------------------------------------- - -async def render_entry_tickets_config(entry, calendar, day, month, year) -> str: - """Render ticket config display + edit form for admin entry view.""" - from quart import url_for - from shared.browser.app.csrf import generate_csrf_token - csrf = generate_csrf_token() - - cal_slug = getattr(calendar, "slug", "") - eid = entry.id - tp = getattr(entry, "ticket_price", None) - tc = getattr(entry, "ticket_count", None) - eid_s = str(eid) - show_js = f"document.getElementById('ticket-form-{eid}').classList.remove('hidden'); this.classList.add('hidden');" - hide_js = (f"document.getElementById('ticket-form-{eid}').classList.add('hidden'); " - f"document.getElementById('entry-tickets-{eid}').querySelectorAll('button:not([type=submit])').forEach(btn => btn.classList.remove('hidden'));") - - if tp is not None: - tc_str = f"{tc} tickets" if tc is not None else "Unlimited" - display_html = await render_to_sx("events-ticket-config-display", - price_str=f"£{tp:.2f}", - count_str=tc_str, show_js=show_js) - else: - display_html = await render_to_sx("events-ticket-config-none", show_js=show_js) - - update_url = url_for( - "calendar.day.calendar_entries.calendar_entry.update_tickets", - entry_id=eid, calendar_slug=cal_slug, day=day, month=month, year=year, - ) - hidden_cls = "" if tp is None else "hidden" - tp_val = f"{tp:.2f}" if tp is not None else "" - tc_val = str(tc) if tc is not None else "" - - form_html = await render_to_sx("events-ticket-config-form", - entry_id=eid_s, hidden_cls=hidden_cls, - update_url=update_url, csrf=csrf, - price_val=tp_val, count_val=tc_val, hide_js=hide_js) - return display_html + form_html - - -# --------------------------------------------------------------------------- -# Entry posts panel -# --------------------------------------------------------------------------- - -async def render_entry_posts_panel(entry_posts, entry, calendar, day, month, year) -> str: - """Render associated posts list with remove buttons and search input.""" - from quart import url_for - from shared.browser.app.csrf import generate_csrf_token - csrf = generate_csrf_token() - - cal_slug = getattr(calendar, "slug", "") - eid = entry.id - eid_s = str(eid) - - posts_html = "" - if entry_posts: - items = "" - for ep in entry_posts: - ep_title = getattr(ep, "title", "") - ep_id = getattr(ep, "id", 0) - feat = getattr(ep, "feature_image", None) - img_html = (await render_to_sx("events-post-img", src=feat, alt=ep_title) - if feat else await render_to_sx("events-post-img-placeholder")) - - del_url = url_for( - "calendar.day.calendar_entries.calendar_entry.remove_post", - calendar_slug=cal_slug, day=day, month=month, year=year, - entry_id=eid, post_id=ep_id, - ) - items += await render_to_sx("events-entry-post-item", - img=SxExpr(img_html), title=ep_title, - del_url=del_url, entry_id=eid_s, - csrf_hdr=f'{{"X-CSRFToken": "{csrf}"}}') - posts_html = await render_to_sx("events-entry-posts-list", items=SxExpr(items)) - else: - posts_html = await render_to_sx("events-entry-posts-none") - - search_url = url_for( - "calendar.day.calendar_entries.calendar_entry.search_posts", - calendar_slug=cal_slug, day=day, month=month, year=year, entry_id=eid, - ) - - return await render_to_sx("events-entry-posts-panel", - posts=SxExpr(posts_html), search_url=search_url, - entry_id=eid_s) - - -# --------------------------------------------------------------------------- -# Entry posts nav OOB -# --------------------------------------------------------------------------- - -async 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 await render_to_sx("events-entry-posts-nav-oob-empty") - - items = "" - for ep in entry_posts: - slug = getattr(ep, "slug", "") - title = getattr(ep, "title", "") - feat = getattr(ep, "feature_image", None) - href = blog_url_fn(f"/{slug}/") if blog_url_fn else f"/{slug}/" - img_html = (await render_to_sx("events-post-img", src=feat, alt=title) - if feat else await render_to_sx("events-post-img-placeholder")) - items += await render_to_sx("events-entry-nav-post", - href=href, nav_btn=nav_btn, - img=SxExpr(img_html), title=title) - - return await render_to_sx("events-entry-posts-nav-oob", items=SxExpr(items)) - - -# --------------------------------------------------------------------------- -# Day entries nav OOB -# --------------------------------------------------------------------------- - -async 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 await render_to_sx("events-day-entries-nav-oob-empty") - - items = "" - for entry in confirmed_entries: - href = url_for( - "calendar.day.calendar_entries.calendar_entry.get", - calendar_slug=cal_slug, - year=day_date.year, month=day_date.month, day=day_date.day, - entry_id=entry.id, - ) - start = entry.start_at.strftime("%H:%M") if entry.start_at else "" - end = f" \u2013 {entry.end_at.strftime('%H:%M')}" if entry.end_at else "" - items += await render_to_sx("events-day-nav-entry", - href=href, nav_btn=nav_btn, - name=entry.name, time_str=start + end) - - return await render_to_sx("events-day-entries-nav-oob", items=SxExpr(items)) - - -# --------------------------------------------------------------------------- -# Post nav entries OOB -# --------------------------------------------------------------------------- - -async 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 await render_to_sx("events-post-nav-oob-empty") - - slug = post.get("slug", "") if isinstance(post, dict) else getattr(post, "slug", "") - - items = "" - if has_entries: - for entry in associated_entries.entries: - entry_path = ( - f"/{slug}/{entry.calendar_slug}/" - f"{entry.start_at.year}/{entry.start_at.month}/{entry.start_at.day}/" - f"entries/{entry.id}/" - ) - href = events_url(entry_path) - time_str = entry.start_at.strftime("%b %d, %Y at %H:%M") - end_str = f" \u2013 {entry.end_at.strftime('%H:%M')}" if entry.end_at else "" - items += await render_to_sx("events-post-nav-entry", - href=href, nav_btn=nav_btn, - name=entry.name, time_str=time_str + end_str) - - if calendars: - for cal in calendars: - cs = getattr(cal, "slug", "") - local_href = events_url(f"/{slug}/{cs}/") - items += await render_to_sx("events-post-nav-calendar", - href=local_href, nav_btn=nav_btn, name=cal.name) - - hs = ("on load or scroll " - "if window.innerWidth >= 640 and my.scrollWidth > my.clientWidth " - "remove .hidden from .entries-nav-arrow add .flex to .entries-nav-arrow " - "else add .hidden to .entries-nav-arrow remove .flex from .entries-nav-arrow end") - - return await render_to_sx("events-post-nav-wrapper", - items=SxExpr(items), hyperscript=hs) - - -# --------------------------------------------------------------------------- -# Calendar description display + edit form -# --------------------------------------------------------------------------- - -async def render_calendar_description(calendar, *, oob: bool = False) -> str: - """Render calendar description display with edit button, optionally with OOB title.""" - from quart import url_for - - cal_slug = getattr(calendar, "slug", "") - edit_url = url_for("calendar.admin.calendar_description_edit", calendar_slug=cal_slug) - html = await _calendar_description_display_html(calendar, edit_url) - - if oob: - desc = getattr(calendar, "description", "") or "" - html += await render_to_sx("events-calendar-description-title-oob", - description=desc) - return html - - -async def render_calendar_description_edit(calendar) -> str: - """Render calendar description edit form.""" - from quart import url_for - from shared.browser.app.csrf import generate_csrf_token - csrf = generate_csrf_token() - cal_slug = getattr(calendar, "slug", "") - desc = getattr(calendar, "description", "") or "" - - save_url = url_for("calendar.admin.calendar_description_save", calendar_slug=cal_slug) - cancel_url = url_for("calendar.admin.calendar_description_view", calendar_slug=cal_slug) - - return await render_to_sx("events-calendar-description-edit-form", - save_url=save_url, cancel_url=cancel_url, - csrf=csrf, description=desc) - - -# --------------------------------------------------------------------------- -# Calendars list panel (for POST create / DELETE) -# --------------------------------------------------------------------------- - -async def render_calendars_list_panel(ctx: dict) -> str: - """Render the calendars main panel HTML for POST/DELETE response.""" - return await _calendars_main_panel_sx(ctx) - - -# --------------------------------------------------------------------------- -# Markets list panel (for POST create / DELETE) -# --------------------------------------------------------------------------- - -async def render_markets_list_panel(ctx: dict) -> str: - """Render the markets main panel HTML for POST/DELETE response.""" - return await _markets_main_panel_html(ctx) - - -# --------------------------------------------------------------------------- -# Slot main panel -# --------------------------------------------------------------------------- - -async def render_slot_main_panel(slot, calendar, *, oob: bool = False) -> str: - """Render slot detail view.""" - from quart import url_for, g - - styles = getattr(g, "styles", None) or {} - list_container = getattr(styles, "list_container", "") if hasattr(styles, "list_container") else styles.get("list_container", "") - pre_action = getattr(styles, "pre_action_button", "") if hasattr(styles, "pre_action_button") else styles.get("pre_action_button", "") - cal_slug = getattr(calendar, "slug", "") - - days_display = getattr(slot, "days_display", "\u2014") - days = days_display.split(", ") - flexible = getattr(slot, "flexible", False) - time_start = slot.time_start.strftime("%H:%M") if slot.time_start else "" - time_end = slot.time_end.strftime("%H:%M") if slot.time_end else "" - cost = getattr(slot, "cost", None) - cost_str = f"{cost:.2f}" if cost is not None else "" - desc = getattr(slot, "description", "") or "" - - edit_url = url_for("calendar.slots.slot.get_edit", slot_id=slot.id, calendar_slug=cal_slug) - - # Days pills - if days and days[0] != "\u2014": - days_inner = "".join( - await render_to_sx("events-slot-day-pill", day=d) for d in days - ) - days_html = await render_to_sx("events-slot-days-pills", days_inner=SxExpr(days_inner)) - else: - days_html = await render_to_sx("events-slot-no-days") - - sid = str(slot.id) - - result = await render_to_sx("events-slot-panel", - slot_id=sid, list_container=list_container, - days=SxExpr(days_html), - flexible="yes" if flexible else "no", - time_str=f"{time_start} \u2014 {time_end}", - cost_str=cost_str, - pre_action=pre_action, edit_url=edit_url) - - if oob: - result += await render_to_sx("events-slot-description-oob", description=desc) - - return result - - -# --------------------------------------------------------------------------- -# Slots table -# --------------------------------------------------------------------------- - -async def render_slots_table(slots, calendar) -> str: - """Render slots table with rows and add button.""" - from quart import url_for, g - from shared.browser.app.csrf import generate_csrf_token - csrf = generate_csrf_token() - - styles = getattr(g, "styles", None) or {} - list_container = getattr(styles, "list_container", "") if hasattr(styles, "list_container") else styles.get("list_container", "") - tr_cls = getattr(styles, "tr", "") if hasattr(styles, "tr") else styles.get("tr", "") - pill_cls = getattr(styles, "pill", "") if hasattr(styles, "pill") else styles.get("pill", "") - action_btn = getattr(styles, "action_button", "") if hasattr(styles, "action_button") else styles.get("action_button", "") - pre_action = getattr(styles, "pre_action_button", "") if hasattr(styles, "pre_action_button") else styles.get("pre_action_button", "") - hx_select = getattr(g, "hx_select_search", "#main-panel") - cal_slug = getattr(calendar, "slug", "") - - rows_html = "" - if slots: - for s in slots: - slot_href = url_for("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(", ") - if day_list and day_list[0] != "\u2014": - days_inner = "".join( - await render_to_sx("events-slot-day-pill", day=d) for d in day_list - ) - days_html = await render_to_sx("events-slot-days-pills", days_inner=SxExpr(days_inner)) - else: - days_html = await render_to_sx("events-slot-no-days") - - time_start = s.time_start.strftime("%H:%M") if s.time_start else "" - time_end = s.time_end.strftime("%H:%M") if s.time_end else "" - cost = getattr(s, "cost", None) - cost_str = f"{cost:.2f}" if cost is not None else "" - - rows_html += await render_to_sx("events-slots-row", - tr_cls=tr_cls, slot_href=slot_href, - pill_cls=pill_cls, hx_select=hx_select, - slot_name=s.name, description=desc, - flexible="yes" if s.flexible else "no", - days=SxExpr(days_html), - time_str=f"{time_start} - {time_end}", - cost_str=cost_str, action_btn=action_btn, - del_url=del_url, - csrf_hdr=f'{{"X-CSRFToken": "{csrf}"}}') - else: - rows_html = await render_to_sx("events-slots-empty-row") - - add_url = url_for("calendar.slots.add_form", calendar_slug=cal_slug) - - return await render_to_sx("events-slots-table", - list_container=list_container, rows=SxExpr(rows_html), - pre_action=pre_action, add_url=add_url) - - -# --------------------------------------------------------------------------- -# Ticket type main panel -# --------------------------------------------------------------------------- - -async def render_ticket_type_main_panel(ticket_type, entry, calendar, day, month, year, *, oob: bool = False) -> str: - """Render ticket type detail view.""" - from quart import url_for, g - - styles = getattr(g, "styles", None) or {} - list_container = getattr(styles, "list_container", "") if hasattr(styles, "list_container") else styles.get("list_container", "") - pre_action = getattr(styles, "pre_action_button", "") if hasattr(styles, "pre_action_button") else styles.get("pre_action_button", "") - cal_slug = getattr(calendar, "slug", "") - - cost = getattr(ticket_type, "cost", None) - cost_str = f"\u00a3{cost:.2f}" if cost is not None else "\u00a30.00" - count = getattr(ticket_type, "count", 0) - tid = str(ticket_type.id) - - edit_url = url_for( - "calendar.day.calendar_entries.calendar_entry.ticket_types.ticket_type.get_edit", - ticket_type_id=ticket_type.id, calendar_slug=cal_slug, - year=year, month=month, day=day, entry_id=entry.id, - ) - - async def _col(label, val): - return await render_to_sx("events-ticket-type-col", label=label, value=val) - - return await render_to_sx("events-ticket-type-panel", - ticket_id=tid, list_container=list_container, - c1=await _col("Name", ticket_type.name), - c2=await _col("Cost", cost_str), - c3=await _col("Count", str(count)), - pre_action=pre_action, edit_url=edit_url) - - -# --------------------------------------------------------------------------- -# Ticket types table -# --------------------------------------------------------------------------- - -async def render_ticket_types_table(ticket_types, entry, calendar, day, month, year) -> str: - """Render ticket types table with rows and add button.""" - from quart import url_for, g - from shared.browser.app.csrf import generate_csrf_token - csrf = generate_csrf_token() - - styles = getattr(g, "styles", None) or {} - list_container = getattr(styles, "list_container", "") if hasattr(styles, "list_container") else styles.get("list_container", "") - tr_cls = getattr(styles, "tr", "") if hasattr(styles, "tr") else styles.get("tr", "") - pill_cls = getattr(styles, "pill", "") if hasattr(styles, "pill") else styles.get("pill", "") - action_btn = getattr(styles, "action_button", "") if hasattr(styles, "action_button") else styles.get("action_button", "") - hx_select = getattr(g, "hx_select_search", "#main-panel") - cal_slug = getattr(calendar, "slug", "") - eid = entry.id - - rows_html = "" - if ticket_types: - for tt in ticket_types: - tt_href = url_for( - "calendar.day.calendar_entries.calendar_entry.ticket_types.ticket_type.get", - calendar_slug=cal_slug, year=year, month=month, day=day, - entry_id=eid, ticket_type_id=tt.id, - ) - del_url = url_for( - "calendar.day.calendar_entries.calendar_entry.ticket_types.ticket_type.delete", - calendar_slug=cal_slug, year=year, month=month, day=day, - entry_id=eid, ticket_type_id=tt.id, - ) - cost = getattr(tt, "cost", None) - cost_str = f"\u00a3{cost:.2f}" if cost is not None else "\u00a30.00" - - rows_html += await render_to_sx("events-ticket-types-row", - tr_cls=tr_cls, tt_href=tt_href, - pill_cls=pill_cls, hx_select=hx_select, - tt_name=tt.name, cost_str=cost_str, - count=str(tt.count), action_btn=action_btn, - del_url=del_url, - csrf_hdr=f'{{"X-CSRFToken": "{csrf}"}}') - else: - rows_html = await render_to_sx("events-ticket-types-empty-row") - - add_url = url_for( - "calendar.day.calendar_entries.calendar_entry.ticket_types.add_form", - calendar_slug=cal_slug, entry_id=eid, year=year, month=month, day=day, - ) - - return await render_to_sx("events-ticket-types-table", - list_container=list_container, rows=SxExpr(rows_html), - action_btn=action_btn, add_url=add_url) - - -# --------------------------------------------------------------------------- -# Buy result (ticket purchase confirmation) -# --------------------------------------------------------------------------- - -async def render_buy_result(entry, created_tickets, remaining, cart_count) -> str: - """Render buy result card with created tickets + OOB cart icon.""" - from quart import url_for - - cart_html = await _cart_icon_oob(cart_count) - - count = len(created_tickets) - suffix = "s" if count != 1 else "" - - tickets_html = "" - for ticket in created_tickets: - href = url_for("defpage_ticket_detail", code=ticket.code) - tickets_html += await render_to_sx("events-buy-result-ticket", - href=href, code_short=ticket.code[:12] + "...") - - remaining_html = "" - if remaining is not None: - r_suffix = "s" if remaining != 1 else "" - remaining_html = await render_to_sx("events-buy-result-remaining", - text=f"{remaining} ticket{r_suffix} remaining") - - my_href = url_for("defpage_my_tickets") - - return cart_html + await render_to_sx("events-buy-result", - entry_id=str(entry.id), - count_label=f"{count} ticket{suffix} reserved", - tickets=SxExpr(tickets_html), - remaining=SxExpr(remaining_html), - my_tickets_href=my_href) - - -# --------------------------------------------------------------------------- -# Buy form (ticket +/- controls) -# --------------------------------------------------------------------------- - -async def render_buy_form(entry, ticket_remaining, ticket_sold_count, - user_ticket_count, user_ticket_counts_by_type) -> str: - """Render the ticket buy/adjust form with +/- controls.""" - from quart import url_for - from shared.browser.app.csrf import generate_csrf_token - csrf = generate_csrf_token() - - eid = entry.id - eid_s = str(eid) - tp = getattr(entry, "ticket_price", None) - state = getattr(entry, "state", "") - ticket_types = getattr(entry, "ticket_types", None) or [] - - if tp is None: - return "" - - if state != "confirmed": - return await render_to_sx("events-buy-not-confirmed", entry_id=eid_s) - - adjust_url = url_for("tickets.adjust_quantity") - target = f"#ticket-buy-{eid}" - - # Info line - info_html = "" - info_items = "" - if ticket_sold_count: - info_items += await render_to_sx("events-buy-info-sold", - count=str(ticket_sold_count)) - if ticket_remaining is not None: - info_items += await render_to_sx("events-buy-info-remaining", - count=str(ticket_remaining)) - if user_ticket_count: - info_items += await render_to_sx("events-buy-info-basket", - count=str(user_ticket_count)) - if info_items: - info_html = await render_to_sx("events-buy-info-bar", items=SxExpr(info_items)) - - active_types = [tt for tt in ticket_types if getattr(tt, "deleted_at", None) is None] - - body_html = "" - if active_types: - type_items = "" - for tt in active_types: - type_count = user_ticket_counts_by_type.get(tt.id, 0) if user_ticket_counts_by_type else 0 - cost_str = f"\u00a3{tt.cost:.2f}" if tt.cost is not None else "\u00a30.00" - type_items += await render_to_sx("events-buy-type-item", - type_name=tt.name, cost_str=cost_str, - adjust_controls=SxExpr(await _ticket_adjust_controls(csrf, adjust_url, target, eid, type_count, ticket_type_id=tt.id))) - body_html = await render_to_sx("events-buy-types-wrapper", items=SxExpr(type_items)) - else: - qty = user_ticket_count or 0 - body_html = await render_to_sx("events-buy-default", - price_str=f"\u00a3{tp:.2f}", - adjust_controls=SxExpr(await _ticket_adjust_controls(csrf, adjust_url, target, eid, qty))) - - return await render_to_sx("events-buy-panel", - entry_id=eid_s, info=SxExpr(info_html), body=SxExpr(body_html)) - - -async def _ticket_adjust_controls(csrf, adjust_url, target, entry_id, count, *, ticket_type_id=None): - """Render +/- ticket controls for buy form.""" - from quart import url_for - - tt_html = await render_to_sx("events-adjust-tt-hidden", - ticket_type_id=str(ticket_type_id)) if ticket_type_id else "" - eid_s = str(entry_id) - - async def _adj_form(count_val, btn_html, *, extra_cls=""): - return await render_to_sx("events-adjust-form", - adjust_url=adjust_url, target=target, - extra_cls=extra_cls, csrf=csrf, - entry_id=eid_s, tt=SxExpr(tt_html) if tt_html else None, - count_val=str(count_val), btn=SxExpr(btn_html)) - - if count == 0: - return await _adj_form(1, await render_to_sx("events-adjust-cart-plus"), - extra_cls="flex items-center") - - my_tickets_href = url_for("defpage_my_tickets") - minus = await _adj_form(count - 1, await render_to_sx("events-adjust-minus")) - cart_icon = await render_to_sx("events-adjust-cart-icon", - href=my_tickets_href, count=str(count)) - plus = await _adj_form(count + 1, await render_to_sx("events-adjust-plus")) - - return await render_to_sx("events-adjust-controls", - minus=SxExpr(minus), cart_icon=SxExpr(cart_icon), plus=SxExpr(plus)) - - -# --------------------------------------------------------------------------- -# Adjust response (OOB cart icon + buy form) -# --------------------------------------------------------------------------- - -async 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 = await _cart_icon_oob(cart_count) - form_html = await render_buy_form( - entry, ticket_remaining, ticket_sold_count, - user_ticket_count, user_ticket_counts_by_type, - ) - return cart_html + form_html - - -async 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 await render_to_sx("events-cart-icon-logo", - blog_href=blog_href, logo=logo) - - cart_href = cart_url_fn("/") if cart_url_fn else "/" - return await render_to_sx("events-cart-icon-badge", - cart_href=cart_href, count=str(count)) - - -# =========================================================================== -# SLOT PICKER JS — shared by entry edit + entry add forms -# =========================================================================== - -_SLOT_PICKER_JS = """\ -""" - - -# =========================================================================== -# Entry edit form -# =========================================================================== - -async def _slot_options_html(day_slots, selected_slot_id=None) -> str: - """Build slot