All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 1m20s
Replace all 676 inline sexp() string calls across 7 services with render(component_name, **kwargs) calls backed by 46 external .sexpr component definition files (587 defcomps total). - Add render() function to shared/sexp/jinja_bridge.py - Add load_service_components() helper and update load_sexp_dir() for *.sexpr - Update parser keyword regex to support HTMX hx-on::event syntax - Convert remaining inline HTML in route files to render() calls - Add shared/sexp/templates/misc.sexp for cross-service utility components Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2613 lines
106 KiB
Python
2613 lines
106 KiB
Python
"""
|
|
Events service s-expression page components.
|
|
|
|
Renders all events, page summary, calendars, calendar month, day, day admin,
|
|
calendar admin, tickets, ticket admin, markets, and payments pages.
|
|
Called from route handlers in place of ``render_template()``.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
from typing import Any
|
|
from markupsafe import escape
|
|
|
|
from shared.sexp.jinja_bridge import render, load_service_components
|
|
from shared.sexp.helpers import (
|
|
call_url, get_asset_url, root_header_html,
|
|
search_mobile_html, search_desktop_html,
|
|
full_page, oob_page,
|
|
)
|
|
|
|
# Load events-specific .sexpr components at import time
|
|
load_service_components(os.path.dirname(os.path.dirname(__file__)))
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# OOB header helper (same pattern as market)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def _oob_header_html(parent_id: str, child_id: str, row_html: str) -> str:
|
|
"""Wrap a header row in OOB div with child placeholder."""
|
|
return render("events-oob-header",
|
|
parent_id=parent_id, child_id=child_id, row_html=row_html)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Post header helpers (mirrors events/templates/_types/post/header/_header.html)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def _post_header_html(ctx: dict, *, oob: bool = False) -> str:
|
|
"""Build the post-level header row."""
|
|
post = ctx.get("post") or {}
|
|
slug = post.get("slug", "")
|
|
title = (post.get("title") or "")[:160]
|
|
feature_image = post.get("feature_image")
|
|
|
|
label_html = render("events-post-label",
|
|
feature_image=feature_image, title=title)
|
|
|
|
nav_parts = []
|
|
page_cart_count = ctx.get("page_cart_count", 0)
|
|
if page_cart_count and page_cart_count > 0:
|
|
cart_href = call_url(ctx, "cart_url", f"/{slug}/")
|
|
nav_parts.append(render("events-post-cart-link",
|
|
href=cart_href, count=str(page_cart_count)))
|
|
|
|
# Post nav: calendar links + admin
|
|
nav_parts.append(_post_nav_html(ctx))
|
|
|
|
nav_html = "".join(nav_parts)
|
|
link_href = call_url(ctx, "blog_url", f"/{slug}/")
|
|
|
|
return render("menu-row", id="post-row", level=1,
|
|
link_href=link_href, link_label_html=label_html,
|
|
nav_html=nav_html, child_id="post-header-child", oob=oob)
|
|
|
|
|
|
def _post_nav_html(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("calendars.calendar.get", calendar_slug=cal_slug)
|
|
is_sel = (cal_slug == current_cal_slug)
|
|
parts.append(render("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_html", "")
|
|
if container_nav:
|
|
parts.append(container_nav)
|
|
|
|
return "".join(parts)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Post admin header
|
|
# ---------------------------------------------------------------------------
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Calendars header
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def _calendars_header_html(ctx: dict, *, oob: bool = False) -> str:
|
|
"""Build the calendars section header row."""
|
|
from quart import url_for
|
|
link_href = url_for("calendars.home")
|
|
return render("menu-row", id="calendars-row", level=3,
|
|
link_href=link_href,
|
|
link_label_html=render("events-calendars-label"),
|
|
child_id="calendars-header-child", oob=oob)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Calendar header
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def _calendar_header_html(ctx: dict, *, oob: bool = False) -> str:
|
|
"""Build a single calendar's header row."""
|
|
from quart import url_for
|
|
calendar = ctx.get("calendar")
|
|
if not calendar:
|
|
return ""
|
|
cal_slug = getattr(calendar, "slug", "")
|
|
cal_name = getattr(calendar, "name", "")
|
|
cal_desc = getattr(calendar, "description", "") or ""
|
|
|
|
link_href = url_for("calendars.calendar.get", calendar_slug=cal_slug)
|
|
label_html = render("events-calendar-label",
|
|
name=cal_name, description=cal_desc)
|
|
|
|
# Desktop nav: slots + admin
|
|
nav_html = _calendar_nav_html(ctx)
|
|
|
|
return render("menu-row", id="calendar-row", level=3,
|
|
link_href=link_href, link_label_html=label_html,
|
|
nav_html=nav_html, child_id="calendar-header-child", oob=oob)
|
|
|
|
|
|
def _calendar_nav_html(ctx: dict) -> str:
|
|
"""Calendar desktop nav: Slots + admin link."""
|
|
from quart import url_for
|
|
calendar = ctx.get("calendar")
|
|
if not calendar:
|
|
return ""
|
|
cal_slug = getattr(calendar, "slug", "")
|
|
rights = ctx.get("rights") or {}
|
|
is_admin = getattr(rights, "admin", False) if hasattr(rights, "admin") else rights.get("admin", False)
|
|
select_colours = ctx.get("select_colours", "")
|
|
|
|
parts = []
|
|
slots_href = url_for("calendars.calendar.slots.get", calendar_slug=cal_slug)
|
|
parts.append(render("nav-link", href=slots_href, icon="fa fa-clock",
|
|
label="Slots", select_colours=select_colours))
|
|
if is_admin:
|
|
admin_href = url_for("calendars.calendar.admin.admin", calendar_slug=cal_slug)
|
|
parts.append(render("nav-link", href=admin_href, icon="fa fa-cog",
|
|
select_colours=select_colours))
|
|
return "".join(parts)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Day header
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def _day_header_html(ctx: dict, *, oob: bool = False) -> str:
|
|
"""Build day detail header row."""
|
|
from quart import url_for
|
|
calendar = ctx.get("calendar")
|
|
if not calendar:
|
|
return ""
|
|
cal_slug = getattr(calendar, "slug", "")
|
|
day_date = ctx.get("day_date")
|
|
if not day_date:
|
|
return ""
|
|
|
|
link_href = url_for(
|
|
"calendars.calendar.day.show_day",
|
|
calendar_slug=cal_slug,
|
|
year=day_date.year,
|
|
month=day_date.month,
|
|
day=day_date.day,
|
|
)
|
|
label_html = render("events-day-label",
|
|
date_str=day_date.strftime("%A %d %B %Y"))
|
|
|
|
nav_html = _day_nav_html(ctx)
|
|
|
|
return render("menu-row", id="day-row", level=4,
|
|
link_href=link_href, link_label_html=label_html,
|
|
nav_html=nav_html, child_id="day-header-child", oob=oob)
|
|
|
|
|
|
def _day_nav_html(ctx: dict) -> str:
|
|
"""Day desktop nav: confirmed entries scrolling menu + admin link."""
|
|
from quart import url_for
|
|
calendar = ctx.get("calendar")
|
|
if not calendar:
|
|
return ""
|
|
cal_slug = getattr(calendar, "slug", "")
|
|
day_date = ctx.get("day_date")
|
|
confirmed_entries = ctx.get("confirmed_entries") or []
|
|
rights = ctx.get("rights") or {}
|
|
is_admin = getattr(rights, "admin", False) if hasattr(rights, "admin") else rights.get("admin", False)
|
|
|
|
parts = []
|
|
# Confirmed entries nav (scrolling menu)
|
|
if confirmed_entries:
|
|
entry_links = []
|
|
for entry in confirmed_entries:
|
|
href = url_for(
|
|
"calendars.calendar.day.calendar_entries.calendar_entry.get",
|
|
calendar_slug=cal_slug,
|
|
year=day_date.year,
|
|
month=day_date.month,
|
|
day=day_date.day,
|
|
entry_id=entry.id,
|
|
)
|
|
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(render("events-day-entry-link",
|
|
href=href, name=entry.name,
|
|
time_str=f"{start}{end}"))
|
|
inner = "".join(entry_links)
|
|
parts.append(render("events-day-entries-nav", inner_html=inner))
|
|
|
|
if is_admin and day_date:
|
|
admin_href = url_for(
|
|
"calendars.calendar.day.admin.admin",
|
|
calendar_slug=cal_slug,
|
|
year=day_date.year,
|
|
month=day_date.month,
|
|
day=day_date.day,
|
|
)
|
|
parts.append(render("nav-link", href=admin_href, icon="fa fa-cog"))
|
|
return "".join(parts)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Day admin header
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def _day_admin_header_html(ctx: dict, *, oob: bool = False) -> str:
|
|
"""Build day admin header row."""
|
|
from quart import url_for
|
|
calendar = ctx.get("calendar")
|
|
if not calendar:
|
|
return ""
|
|
cal_slug = getattr(calendar, "slug", "")
|
|
day_date = ctx.get("day_date")
|
|
if not day_date:
|
|
return ""
|
|
|
|
link_href = url_for(
|
|
"calendars.calendar.day.admin.admin",
|
|
calendar_slug=cal_slug,
|
|
year=day_date.year,
|
|
month=day_date.month,
|
|
day=day_date.day,
|
|
)
|
|
return render("menu-row", id="day-admin-row", level=5,
|
|
link_href=link_href, link_label="admin", icon="fa fa-cog",
|
|
child_id="day-admin-header-child", oob=oob)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Calendar admin header
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def _calendar_admin_header_html(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 [
|
|
("calendars.calendar.slots.get", "slots"),
|
|
("calendars.calendar.admin.calendar_description_edit", "description"),
|
|
]:
|
|
href = url_for(endpoint, calendar_slug=cal_slug)
|
|
nav_parts.append(render("nav-link", href=href, label=label,
|
|
select_colours=select_colours))
|
|
|
|
nav_html = "".join(nav_parts)
|
|
return render("menu-row", id="calendar-admin-row", level=4,
|
|
link_label="admin", icon="fa fa-cog",
|
|
nav_html=nav_html, child_id="calendar-admin-header-child", oob=oob)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Markets header
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def _markets_header_html(ctx: dict, *, oob: bool = False) -> str:
|
|
"""Build the markets section header row."""
|
|
from quart import url_for
|
|
link_href = url_for("markets.home")
|
|
return render("menu-row", id="markets-row", level=3,
|
|
link_href=link_href,
|
|
link_label_html=render("events-markets-label"),
|
|
child_id="markets-header-child", oob=oob)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Payments header
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def _payments_header_html(ctx: dict, *, oob: bool = False) -> str:
|
|
"""Build the payments section header row."""
|
|
from quart import url_for
|
|
link_href = url_for("payments.home")
|
|
return render("menu-row", id="payments-row", level=3,
|
|
link_href=link_href,
|
|
link_label_html=render("events-payments-label"),
|
|
child_id="payments-header-child", oob=oob)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Calendars main panel
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def _calendars_main_panel_html(ctx: dict) -> str:
|
|
"""Render the calendars list + create form panel."""
|
|
from quart import url_for
|
|
rights = ctx.get("rights") or {}
|
|
is_admin = getattr(rights, "admin", False) if hasattr(rights, "admin") else rights.get("admin", False)
|
|
has_access = ctx.get("has_access")
|
|
can_create = has_access("calendars.create_calendar") if callable(has_access) else is_admin
|
|
csrf_token = ctx.get("csrf_token")
|
|
csrf = csrf_token() if callable(csrf_token) else (csrf_token or "")
|
|
|
|
calendars = ctx.get("calendars") or []
|
|
|
|
form_html = ""
|
|
if can_create:
|
|
create_url = url_for("calendars.create_calendar")
|
|
form_html = render("events-calendars-create-form",
|
|
create_url=create_url, csrf=csrf)
|
|
|
|
list_html = _calendars_list_html(ctx, calendars)
|
|
return render("events-calendars-panel",
|
|
form_html=form_html, list_html=list_html)
|
|
|
|
|
|
def _calendars_list_html(ctx: dict, calendars: list) -> str:
|
|
"""Render the calendars list items."""
|
|
from quart import url_for
|
|
from shared.utils import route_prefix
|
|
csrf_token = ctx.get("csrf_token")
|
|
csrf = csrf_token() if callable(csrf_token) else (csrf_token or "")
|
|
prefix = route_prefix()
|
|
|
|
if not calendars:
|
|
return render("events-calendars-empty")
|
|
|
|
parts = []
|
|
for cal in calendars:
|
|
cal_slug = getattr(cal, "slug", "")
|
|
cal_name = getattr(cal, "name", "")
|
|
href = prefix + url_for("calendars.calendar.get", calendar_slug=cal_slug)
|
|
del_url = url_for("calendars.calendar.delete", calendar_slug=cal_slug)
|
|
csrf_hdr = f'{{"X-CSRFToken":"{csrf}"}}'
|
|
parts.append(render("events-calendars-item",
|
|
href=href, cal_name=cal_name, cal_slug=cal_slug,
|
|
del_url=del_url, csrf_hdr=csrf_hdr))
|
|
return "".join(parts)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Calendar month grid
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def _calendar_main_panel_html(ctx: dict) -> str:
|
|
"""Render the calendar month grid."""
|
|
from quart import url_for
|
|
from quart import session as qsession
|
|
|
|
calendar = ctx.get("calendar")
|
|
if not calendar:
|
|
return ""
|
|
cal_slug = getattr(calendar, "slug", "")
|
|
hx_select = ctx.get("hx_select_search", "#main-panel")
|
|
styles = ctx.get("styles") or {}
|
|
pill_cls = getattr(styles, "pill", "") if hasattr(styles, "pill") else styles.get("pill", "")
|
|
|
|
year = ctx.get("year", 2024)
|
|
month = ctx.get("month", 1)
|
|
month_name = ctx.get("month_name", "")
|
|
weekday_names = ctx.get("weekday_names", [])
|
|
weeks = ctx.get("weeks", [])
|
|
prev_month = ctx.get("prev_month", 1)
|
|
prev_month_year = ctx.get("prev_month_year", year)
|
|
next_month = ctx.get("next_month", 1)
|
|
next_month_year = ctx.get("next_month_year", year)
|
|
prev_year = ctx.get("prev_year", year - 1)
|
|
next_year = ctx.get("next_year", year + 1)
|
|
month_entries = ctx.get("month_entries") or []
|
|
user = ctx.get("user")
|
|
qs = qsession if "qsession" not in ctx else ctx["qsession"]
|
|
|
|
def nav_link(y, m):
|
|
return url_for("calendars.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(render("events-calendar-nav-arrow",
|
|
pill_cls=pill_cls, href=href, label=label))
|
|
|
|
nav_arrows.append(render("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(render("events-calendar-nav-arrow",
|
|
pill_cls=pill_cls, href=href, label=label))
|
|
|
|
# Weekday headers
|
|
wd_html = "".join(render("events-calendar-weekday", name=wd) for wd in weekday_names)
|
|
|
|
# Day cells
|
|
cells = []
|
|
for week in weeks:
|
|
for day_cell in week:
|
|
if isinstance(day_cell, dict):
|
|
in_month = day_cell.get("in_month", True)
|
|
is_today = day_cell.get("is_today", False)
|
|
day_date = day_cell.get("date")
|
|
else:
|
|
in_month = getattr(day_cell, "in_month", True)
|
|
is_today = getattr(day_cell, "is_today", False)
|
|
day_date = getattr(day_cell, "date", None)
|
|
|
|
cell_cls = "min-h-20 sm:min-h-24 bg-white px-3 py-2 text-xs"
|
|
if not in_month:
|
|
cell_cls += " bg-stone-50 text-stone-400"
|
|
if is_today:
|
|
cell_cls += " ring-2 ring-blue-500 z-10 relative"
|
|
|
|
# Day number link
|
|
day_num_html = ""
|
|
day_short_html = ""
|
|
if day_date:
|
|
day_href = url_for(
|
|
"calendars.calendar.day.show_day",
|
|
calendar_slug=cal_slug,
|
|
year=day_date.year, month=day_date.month, day=day_date.day,
|
|
)
|
|
day_short_html = render("events-calendar-day-short",
|
|
day_str=day_date.strftime("%a"))
|
|
day_num_html = render("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(render("events-calendar-entry-badge",
|
|
bg_cls=bg_cls, name=e.name,
|
|
state_label=state_label))
|
|
|
|
badges_html = "".join(entry_badges)
|
|
cells.append(render("events-calendar-cell",
|
|
cell_cls=cell_cls, day_short_html=day_short_html,
|
|
day_num_html=day_num_html, badges_html=badges_html))
|
|
|
|
cells_html = "".join(cells)
|
|
arrows_html = "".join(nav_arrows)
|
|
return render("events-calendar-grid",
|
|
arrows_html=arrows_html, weekdays_html=wd_html,
|
|
cells_html=cells_html)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Day main panel
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def _day_main_panel_html(ctx: dict) -> str:
|
|
"""Render the day entries table + add button."""
|
|
from quart import url_for
|
|
|
|
calendar = ctx.get("calendar")
|
|
if not calendar:
|
|
return ""
|
|
cal_slug = getattr(calendar, "slug", "")
|
|
day_entries = ctx.get("day_entries") or []
|
|
day = ctx.get("day")
|
|
month = ctx.get("month")
|
|
year = ctx.get("year")
|
|
hx_select = ctx.get("hx_select_search", "#main-panel")
|
|
styles = ctx.get("styles") or {}
|
|
list_container = getattr(styles, "list_container", "") if hasattr(styles, "list_container") else styles.get("list_container", "")
|
|
pill_cls = getattr(styles, "pill", "") if hasattr(styles, "pill") else styles.get("pill", "")
|
|
tr_cls = getattr(styles, "tr", "") if hasattr(styles, "tr") else styles.get("tr", "")
|
|
pre_action = getattr(styles, "pre_action_button", "") if hasattr(styles, "pre_action_button") else styles.get("pre_action_button", "")
|
|
|
|
rows_html = ""
|
|
if day_entries:
|
|
rows_html = "".join(_day_row_html(ctx, entry) for entry in day_entries)
|
|
else:
|
|
rows_html = render("events-day-empty-row")
|
|
|
|
add_url = url_for(
|
|
"calendars.calendar.day.calendar_entries.add_form",
|
|
calendar_slug=cal_slug,
|
|
day=day, month=month, year=year,
|
|
)
|
|
|
|
return render("events-day-table",
|
|
list_container=list_container, rows_html=rows_html,
|
|
pre_action=pre_action, add_url=add_url)
|
|
|
|
|
|
def _day_row_html(ctx: dict, entry) -> str:
|
|
"""Render a single day table row."""
|
|
from quart import url_for
|
|
calendar = ctx.get("calendar")
|
|
cal_slug = getattr(calendar, "slug", "")
|
|
day = ctx.get("day")
|
|
month = ctx.get("month")
|
|
year = ctx.get("year")
|
|
hx_select = ctx.get("hx_select_search", "#main-panel")
|
|
styles = ctx.get("styles") or {}
|
|
pill_cls = getattr(styles, "pill", "") if hasattr(styles, "pill") else styles.get("pill", "")
|
|
tr_cls = getattr(styles, "tr", "") if hasattr(styles, "tr") else styles.get("tr", "")
|
|
|
|
entry_href = url_for(
|
|
"calendars.calendar.day.calendar_entries.calendar_entry.get",
|
|
calendar_slug=cal_slug, day=day, month=month, year=year, entry_id=entry.id,
|
|
)
|
|
|
|
# Name
|
|
name_html = render("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("calendars.calendar.slots.slot.get", calendar_slug=cal_slug, slot_id=slot.id)
|
|
time_start = slot.time_start.strftime("%H:%M") if slot.time_start else ""
|
|
time_end = f" \u2192 {slot.time_end.strftime('%H:%M')}" if slot.time_end else ""
|
|
slot_html = render("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 = render("events-day-row-time", start=start, end=end)
|
|
|
|
# State
|
|
state = getattr(entry, "state", "pending") or "pending"
|
|
state_badge = _entry_state_badge_html(state)
|
|
state_td = render("events-day-row-state",
|
|
state_id=f"entry-state-{entry.id}", badge_html=state_badge)
|
|
|
|
# Cost
|
|
cost = getattr(entry, "cost", None)
|
|
cost_str = f"\u00a3{cost:.2f}" if cost is not None else "\u00a30.00"
|
|
cost_td = render("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 = render("events-day-row-tickets",
|
|
price_str=f"\u00a3{tp:.2f}", count_str=tc_str)
|
|
else:
|
|
tickets_td = render("events-day-row-no-tickets")
|
|
|
|
actions_td = render("events-day-row-actions")
|
|
|
|
return render("events-day-row",
|
|
tr_cls=tr_cls, name_html=name_html, slot_html=slot_html,
|
|
state_html=state_td, cost_html=cost_td,
|
|
tickets_html=tickets_td, actions_html=actions_td)
|
|
|
|
|
|
def _entry_state_badge_html(state: str) -> str:
|
|
"""Render an entry state badge."""
|
|
state_classes = {
|
|
"confirmed": "bg-emerald-100 text-emerald-800",
|
|
"provisional": "bg-amber-100 text-amber-800",
|
|
"ordered": "bg-sky-100 text-sky-800",
|
|
"pending": "bg-stone-100 text-stone-700",
|
|
"declined": "bg-red-100 text-red-800",
|
|
}
|
|
cls = state_classes.get(state, "bg-stone-100 text-stone-700")
|
|
label = state.replace("_", " ").capitalize()
|
|
return render("events-state-badge", cls=cls, label=label)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Day admin main panel
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def _day_admin_main_panel_html(ctx: dict) -> str:
|
|
"""Render day admin panel (placeholder nav)."""
|
|
return render("events-day-admin-panel")
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Calendar admin main panel
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def _calendar_admin_main_panel_html(ctx: dict) -> str:
|
|
"""Render calendar admin config panel with description editor."""
|
|
from quart import url_for
|
|
calendar = ctx.get("calendar")
|
|
if not calendar:
|
|
return ""
|
|
csrf_token = ctx.get("csrf_token")
|
|
csrf = csrf_token() if callable(csrf_token) else (csrf_token or "")
|
|
cal_slug = getattr(calendar, "slug", "")
|
|
desc = getattr(calendar, "description", "") or ""
|
|
hx_select = ctx.get("hx_select_search", "#main-panel")
|
|
|
|
desc_edit_url = url_for("calendars.calendar.admin.calendar_description_edit", calendar_slug=cal_slug)
|
|
description_html = _calendar_description_display_html(calendar, desc_edit_url)
|
|
|
|
return render("events-calendar-admin-panel",
|
|
description_html=description_html, csrf=csrf,
|
|
description=desc)
|
|
|
|
|
|
def _calendar_description_display_html(calendar, edit_url: str) -> str:
|
|
"""Render calendar description display with edit button."""
|
|
desc = getattr(calendar, "description", "") or ""
|
|
return render("events-calendar-description-display",
|
|
description=desc, edit_url=edit_url)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Markets main panel
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def _markets_main_panel_html(ctx: dict) -> str:
|
|
"""Render markets list + create form panel."""
|
|
from quart import url_for
|
|
rights = ctx.get("rights") or {}
|
|
is_admin = getattr(rights, "admin", False) if hasattr(rights, "admin") else rights.get("admin", False)
|
|
has_access = ctx.get("has_access")
|
|
can_create = has_access("markets.create_market") if callable(has_access) else is_admin
|
|
csrf_token = ctx.get("csrf_token")
|
|
csrf = csrf_token() if callable(csrf_token) else (csrf_token or "")
|
|
markets = ctx.get("markets") or []
|
|
|
|
form_html = ""
|
|
if can_create:
|
|
create_url = url_for("markets.create_market")
|
|
form_html = render("events-markets-create-form",
|
|
create_url=create_url, csrf=csrf)
|
|
|
|
list_html = _markets_list_html(ctx, markets)
|
|
return render("events-markets-panel",
|
|
form_html=form_html, list_html=list_html)
|
|
|
|
|
|
def _markets_list_html(ctx: dict, markets: list) -> str:
|
|
"""Render markets list items."""
|
|
from quart import url_for
|
|
csrf_token = ctx.get("csrf_token")
|
|
csrf = csrf_token() if callable(csrf_token) else (csrf_token or "")
|
|
post = ctx.get("post") or {}
|
|
slug = post.get("slug", "")
|
|
|
|
if not markets:
|
|
return render("events-markets-empty")
|
|
|
|
parts = []
|
|
for m in markets:
|
|
m_slug = getattr(m, "slug", "") if hasattr(m, "slug") else m.get("slug", "")
|
|
m_name = getattr(m, "name", "") if hasattr(m, "name") else m.get("name", "")
|
|
market_href = call_url(ctx, "market_url", f"/{slug}/{m_slug}/")
|
|
del_url = url_for("markets.delete_market", market_slug=m_slug)
|
|
csrf_hdr = f'{{"X-CSRFToken":"{csrf}"}}'
|
|
parts.append(render("events-markets-item",
|
|
href=market_href, market_name=m_name,
|
|
market_slug=m_slug, del_url=del_url,
|
|
csrf_hdr=csrf_hdr))
|
|
return "".join(parts)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Payments main panel
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def _payments_main_panel_html(ctx: dict) -> str:
|
|
"""Render SumUp payment config form."""
|
|
from quart import url_for
|
|
csrf_token = ctx.get("csrf_token")
|
|
csrf = csrf_token() if callable(csrf_token) else (csrf_token or "")
|
|
sumup_configured = ctx.get("sumup_configured", False)
|
|
merchant_code = ctx.get("sumup_merchant_code", "")
|
|
checkout_prefix = ctx.get("sumup_checkout_prefix", "")
|
|
update_url = url_for("payments.update_sumup")
|
|
|
|
placeholder = "--------" if sumup_configured else "sup_sk_..."
|
|
input_cls = "w-full px-3 py-1.5 text-sm border border-stone-300 rounded focus:ring-purple-500 focus:border-purple-500"
|
|
|
|
return render("events-payments-panel",
|
|
update_url=update_url, csrf=csrf,
|
|
merchant_code=merchant_code, placeholder=placeholder,
|
|
input_cls=input_cls, sumup_configured=sumup_configured,
|
|
checkout_prefix=checkout_prefix)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Ticket state badge helper
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def _ticket_state_badge_html(state: str) -> str:
|
|
"""Render a ticket state badge."""
|
|
cls_map = {
|
|
"confirmed": "bg-emerald-100 text-emerald-800",
|
|
"checked_in": "bg-blue-100 text-blue-800",
|
|
"reserved": "bg-amber-100 text-amber-800",
|
|
"cancelled": "bg-red-100 text-red-800",
|
|
}
|
|
cls = cls_map.get(state, "bg-stone-100 text-stone-700")
|
|
label = (state or "").replace("_", " ").capitalize()
|
|
return render("events-state-badge", cls=cls, label=label)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Tickets main panel (my tickets)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def _tickets_main_panel_html(ctx: dict, tickets: list) -> str:
|
|
"""Render my tickets list."""
|
|
from quart import url_for
|
|
|
|
ticket_cards = []
|
|
if tickets:
|
|
for ticket in tickets:
|
|
href = url_for("tickets.ticket_detail", code=ticket.code)
|
|
entry = getattr(ticket, "entry", None)
|
|
entry_name = entry.name if entry else "Unknown event"
|
|
tt = getattr(ticket, "ticket_type", None)
|
|
state = getattr(ticket, "state", "")
|
|
cal = getattr(entry, "calendar", None) if entry else None
|
|
|
|
time_str = ""
|
|
if entry and entry.start_at:
|
|
time_str = entry.start_at.strftime("%A, %B %d, %Y at %H:%M")
|
|
if entry.end_at:
|
|
time_str += f" \u2013 {entry.end_at.strftime('%H:%M')}"
|
|
|
|
ticket_cards.append(render("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_html=_ticket_state_badge_html(state),
|
|
code_prefix=ticket.code[:8]))
|
|
|
|
cards_html = "".join(ticket_cards)
|
|
return render("events-tickets-panel",
|
|
list_container=_list_container(ctx),
|
|
has_tickets=bool(tickets), cards_html=cards_html)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Ticket detail panel
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def _ticket_detail_panel_html(ctx: dict, ticket) -> str:
|
|
"""Render a single ticket detail with QR code."""
|
|
from quart import url_for
|
|
|
|
entry = getattr(ticket, "entry", None)
|
|
tt = getattr(ticket, "ticket_type", None)
|
|
state = getattr(ticket, "state", "")
|
|
code = ticket.code
|
|
cal = getattr(entry, "calendar", None) if entry else None
|
|
checked_in_at = getattr(ticket, "checked_in_at", None)
|
|
|
|
bg_map = {"confirmed": "bg-emerald-50", "checked_in": "bg-blue-50", "reserved": "bg-amber-50"}
|
|
header_bg = bg_map.get(state, "bg-stone-50")
|
|
entry_name = entry.name if entry else "Ticket"
|
|
back_href = url_for("tickets.my_tickets")
|
|
|
|
# Badge with larger sizing
|
|
badge = _ticket_state_badge_html(state).replace('px-2 py-0.5 text-xs', 'px-3 py-1 text-sm')
|
|
|
|
# Time info
|
|
time_date = entry.start_at.strftime("%A, %B %d, %Y") if entry and entry.start_at else None
|
|
time_range = entry.start_at.strftime("%H:%M") if entry and entry.start_at else None
|
|
if time_range and entry.end_at:
|
|
time_range += f" \u2013 {entry.end_at.strftime('%H:%M')}"
|
|
|
|
tt_desc = f"{tt.name} \u2014 \u00a3{tt.cost:.2f}" if tt and getattr(tt, "cost", None) else None
|
|
checkin_str = checked_in_at.strftime("Checked in: %B %d, %Y at %H:%M") if checked_in_at else None
|
|
|
|
qr_script = (
|
|
f"(function(){{var c=document.getElementById('ticket-qr-{code}');"
|
|
"if(c&&typeof QRCode!=='undefined'){"
|
|
"var cv=document.createElement('canvas');"
|
|
f"QRCode.toCanvas(cv,'{code}',{{width:200,margin:2,color:{{dark:'#1c1917',light:'#ffffff'}}}},function(e){{if(!e)c.appendChild(cv)}});"
|
|
"}})()"
|
|
)
|
|
|
|
return render("events-ticket-detail",
|
|
list_container=_list_container(ctx), back_href=back_href,
|
|
header_bg=header_bg, entry_name=entry_name,
|
|
badge_html=badge, type_name=tt.name if tt else None,
|
|
code=code, time_date=time_date, time_range=time_range,
|
|
cal_name=cal.name if cal else None,
|
|
type_desc=tt_desc, checkin_str=checkin_str,
|
|
qr_script=qr_script)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Ticket admin main panel
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def _ticket_admin_main_panel_html(ctx: dict, tickets: list, stats: dict) -> str:
|
|
"""Render ticket admin dashboard."""
|
|
from quart import url_for
|
|
csrf_token = ctx.get("csrf_token")
|
|
csrf = csrf_token() if callable(csrf_token) else (csrf_token or "")
|
|
lookup_url = url_for("ticket_admin.lookup")
|
|
|
|
# Stats cards
|
|
stats_html = ""
|
|
for label, key, border, bg, text_cls in [
|
|
("Total", "total", "border-stone-200", "", "text-stone-900"),
|
|
("Confirmed", "confirmed", "border-emerald-200", "bg-emerald-50", "text-emerald-700"),
|
|
("Checked In", "checked_in", "border-blue-200", "bg-blue-50", "text-blue-700"),
|
|
("Reserved", "reserved", "border-amber-200", "bg-amber-50", "text-amber-700"),
|
|
]:
|
|
val = stats.get(key, 0)
|
|
lbl_cls = text_cls.replace("700", "600").replace("900", "500") if "stone" not in text_cls else "text-stone-500"
|
|
stats_html += render("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 = render("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 = render("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 = render("events-ticket-admin-checked-in",
|
|
time_str=t_str)
|
|
|
|
rows_html += render("events-ticket-admin-row",
|
|
code=code, code_short=code[:12] + "...",
|
|
entry_name=entry.name if entry else "\u2014",
|
|
date_html=date_html,
|
|
type_name=tt.name if tt else "\u2014",
|
|
badge_html=_ticket_state_badge_html(state),
|
|
action_html=action_html)
|
|
|
|
return render("events-ticket-admin-panel",
|
|
list_container=_list_container(ctx), stats_html=stats_html,
|
|
lookup_url=lookup_url, has_tickets=bool(tickets),
|
|
rows_html=rows_html)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# All events / page summary entry cards
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def _entry_card_html(entry, page_info: dict, pending_tickets: dict,
|
|
ticket_url: str, events_url_fn, *, is_page_scoped: bool = False,
|
|
post: dict | None = None) -> str:
|
|
"""Render a list card for one event entry."""
|
|
pi = page_info.get(getattr(entry, "calendar_container_id", 0), {})
|
|
if is_page_scoped and post:
|
|
page_slug = pi.get("slug", post.get("slug", ""))
|
|
else:
|
|
page_slug = pi.get("slug", "")
|
|
page_title = pi.get("title")
|
|
|
|
day_href = ""
|
|
if page_slug and entry.start_at:
|
|
day_href = events_url_fn(f"/{page_slug}/{entry.calendar_slug}/day/{entry.start_at.strftime('%Y/%-m/%-d')}/")
|
|
entry_href = f"{day_href}entries/{entry.id}/" if day_href else ""
|
|
|
|
# Title (linked or plain)
|
|
if entry_href:
|
|
title_html = render("events-entry-title-linked",
|
|
href=entry_href, name=entry.name)
|
|
else:
|
|
title_html = render("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 += render("events-entry-page-badge",
|
|
href=page_href, title=page_title)
|
|
cal_name = getattr(entry, "calendar_name", "")
|
|
if cal_name:
|
|
badges_html += render("events-entry-cal-badge", name=cal_name)
|
|
|
|
# Time line
|
|
time_parts = ""
|
|
if day_href and not is_page_scoped:
|
|
time_parts += render("events-entry-time-linked",
|
|
href=day_href,
|
|
date_str=entry.start_at.strftime("%a %-d %b"))
|
|
elif not is_page_scoped:
|
|
time_parts += render("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 = render("events-entry-cost",
|
|
cost_html=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 = render("events-entry-widget-wrapper",
|
|
widget_html=_ticket_widget_html(entry, qty, ticket_url, ctx={}))
|
|
|
|
return render("events-entry-card",
|
|
title_html=title_html, badges_html=badges_html,
|
|
time_parts=time_parts, cost_html=cost_html,
|
|
widget_html=widget_html)
|
|
|
|
|
|
def _entry_card_tile_html(entry, page_info: dict, pending_tickets: dict,
|
|
ticket_url: str, events_url_fn, *, is_page_scoped: bool = False,
|
|
post: dict | None = None) -> str:
|
|
"""Render a tile card for one event entry."""
|
|
pi = page_info.get(getattr(entry, "calendar_container_id", 0), {})
|
|
if is_page_scoped and post:
|
|
page_slug = pi.get("slug", post.get("slug", ""))
|
|
else:
|
|
page_slug = pi.get("slug", "")
|
|
page_title = pi.get("title")
|
|
|
|
day_href = ""
|
|
if page_slug and entry.start_at:
|
|
day_href = events_url_fn(f"/{page_slug}/{entry.calendar_slug}/day/{entry.start_at.strftime('%Y/%-m/%-d')}/")
|
|
entry_href = f"{day_href}entries/{entry.id}/" if day_href else ""
|
|
|
|
# Title
|
|
if entry_href:
|
|
title_html = render("events-entry-title-tile-linked",
|
|
href=entry_href, name=entry.name)
|
|
else:
|
|
title_html = render("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 += render("events-entry-page-badge",
|
|
href=page_href, title=page_title)
|
|
cal_name = getattr(entry, "calendar_name", "")
|
|
if cal_name:
|
|
badges_html += render("events-entry-cal-badge", name=cal_name)
|
|
|
|
# Time
|
|
time_html = ""
|
|
if day_href:
|
|
time_html += render("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 = render("events-entry-cost",
|
|
cost_html=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 = render("events-entry-tile-widget-wrapper",
|
|
widget_html=_ticket_widget_html(entry, qty, ticket_url, ctx={}))
|
|
|
|
return render("events-entry-card-tile",
|
|
title_html=title_html, badges_html=badges_html,
|
|
time_html=time_html, cost_html=cost_html,
|
|
widget_html=widget_html)
|
|
|
|
|
|
def _ticket_widget_html(entry, qty: int, ticket_url: str, *, ctx: dict) -> str:
|
|
"""Render the inline +/- ticket widget."""
|
|
csrf_token_val = ""
|
|
if ctx:
|
|
ct = ctx.get("csrf_token")
|
|
csrf_token_val = ct() if callable(ct) else (ct or "")
|
|
else:
|
|
try:
|
|
from flask_wtf.csrf import generate_csrf
|
|
csrf_token_val = generate_csrf()
|
|
except Exception:
|
|
pass
|
|
|
|
eid = entry.id
|
|
tp = getattr(entry, "ticket_price", 0) or 0
|
|
tgt = f"#page-ticket-{eid}"
|
|
|
|
def _tw_form(count_val, btn_html):
|
|
return render("events-tw-form",
|
|
ticket_url=ticket_url, target=tgt,
|
|
csrf=csrf_token_val, entry_id=str(eid),
|
|
count_val=str(count_val), btn_html=btn_html)
|
|
|
|
if qty == 0:
|
|
inner = _tw_form(1, render("events-tw-cart-plus"))
|
|
else:
|
|
minus = _tw_form(qty - 1, render("events-tw-minus"))
|
|
cart_icon = render("events-tw-cart-icon", qty=str(qty))
|
|
plus = _tw_form(qty + 1, render("events-tw-plus"))
|
|
inner = minus + cart_icon + plus
|
|
|
|
return render("events-tw-widget",
|
|
entry_id=str(eid), price_html=f"£{tp:.2f}",
|
|
inner_html=inner)
|
|
|
|
|
|
def _entry_cards_html(entries, page_info, pending_tickets, ticket_url,
|
|
events_url_fn, view, page, has_more, next_url,
|
|
*, is_page_scoped=False, post=None) -> str:
|
|
"""Render entry cards (list or tile) with sentinel."""
|
|
parts = []
|
|
last_date = None
|
|
for entry in entries:
|
|
if view == "tile":
|
|
parts.append(_entry_card_tile_html(
|
|
entry, page_info, pending_tickets, ticket_url, events_url_fn,
|
|
is_page_scoped=is_page_scoped, post=post,
|
|
))
|
|
else:
|
|
entry_date = entry.start_at.strftime("%A %-d %B %Y") if entry.start_at else ""
|
|
if entry_date != last_date:
|
|
parts.append(render("events-date-separator",
|
|
date_str=entry_date))
|
|
last_date = entry_date
|
|
parts.append(_entry_card_html(
|
|
entry, page_info, pending_tickets, ticket_url, events_url_fn,
|
|
is_page_scoped=is_page_scoped, post=post,
|
|
))
|
|
|
|
if has_more:
|
|
parts.append(render("events-sentinel",
|
|
page=str(page), next_url=next_url))
|
|
return "".join(parts)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# All events / page summary main panels
|
|
# ---------------------------------------------------------------------------
|
|
|
|
_LIST_SVG = None
|
|
_TILE_SVG = None
|
|
|
|
|
|
def _get_list_svg():
|
|
global _LIST_SVG
|
|
if _LIST_SVG is None:
|
|
_LIST_SVG = render("events-list-svg")
|
|
return _LIST_SVG
|
|
|
|
|
|
def _get_tile_svg():
|
|
global _TILE_SVG
|
|
if _TILE_SVG is None:
|
|
_TILE_SVG = render("events-tile-svg")
|
|
return _TILE_SVG
|
|
|
|
|
|
def _view_toggle_html(ctx: dict, view: str) -> str:
|
|
"""Render the list/tile view toggle bar."""
|
|
from shared.utils import route_prefix
|
|
prefix = route_prefix()
|
|
clh = ctx.get("current_local_href", "/")
|
|
hx_select = ctx.get("hx_select_search", "#main-panel")
|
|
|
|
list_href = prefix + str(clh)
|
|
tile_href = prefix + str(clh)
|
|
if "?" in list_href:
|
|
list_href = list_href.split("?")[0]
|
|
if "?" in tile_href:
|
|
tile_href = tile_href.split("?")[0] + "?view=tile"
|
|
else:
|
|
tile_href = tile_href + "?view=tile"
|
|
|
|
list_active = 'bg-stone-200 text-stone-800' if view != 'tile' else 'text-stone-400 hover:text-stone-600'
|
|
tile_active = 'bg-stone-200 text-stone-800' if view == 'tile' else 'text-stone-400 hover:text-stone-600'
|
|
|
|
return render("events-view-toggle",
|
|
list_href=list_href, tile_href=tile_href,
|
|
hx_select=hx_select, list_active=list_active,
|
|
tile_active=tile_active, list_svg=_get_list_svg(),
|
|
tile_svg=_get_tile_svg())
|
|
|
|
|
|
def _events_main_panel_html(ctx: dict, entries, has_more, pending_tickets, page_info,
|
|
page, view, ticket_url, next_url, events_url_fn,
|
|
*, is_page_scoped=False, post=None) -> str:
|
|
"""Render the events main panel with view toggle + cards."""
|
|
toggle = _view_toggle_html(ctx, view)
|
|
|
|
if entries:
|
|
cards = _entry_cards_html(
|
|
entries, page_info, pending_tickets, ticket_url, events_url_fn,
|
|
view, page, has_more, next_url,
|
|
is_page_scoped=is_page_scoped, post=post,
|
|
)
|
|
grid_cls = ("max-w-full px-3 py-3 grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-4"
|
|
if view == "tile" else "max-w-full px-3 py-3 space-y-3")
|
|
body = render("events-grid", grid_cls=grid_cls, cards_html=cards)
|
|
else:
|
|
body = render("events-empty")
|
|
|
|
return render("events-main-panel-body",
|
|
toggle_html=toggle, body_html=body)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Utility
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def _list_container(ctx: dict) -> str:
|
|
styles = ctx.get("styles") or {}
|
|
return getattr(styles, "list_container", "") if hasattr(styles, "list_container") else styles.get("list_container", "")
|
|
|
|
|
|
# ===========================================================================
|
|
# PUBLIC API
|
|
# ===========================================================================
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# All events
|
|
# ---------------------------------------------------------------------------
|
|
|
|
async def render_all_events_page(ctx: dict, entries, has_more, pending_tickets,
|
|
page_info, page, view) -> str:
|
|
"""Full page: all events listing."""
|
|
from quart import url_for
|
|
from shared.utils import route_prefix
|
|
from shared.infrastructure.urls import events_url
|
|
|
|
prefix = route_prefix()
|
|
view_param = f"&view={view}" if view != "list" else ""
|
|
ticket_url = url_for("all_events.adjust_ticket")
|
|
next_url = prefix + url_for("all_events.entries_fragment", page=page + 1) + (f"?view={view}" if view != "list" else "")
|
|
|
|
content = _events_main_panel_html(
|
|
ctx, entries, has_more, pending_tickets, page_info,
|
|
page, view, ticket_url, next_url, events_url,
|
|
)
|
|
hdr = root_header_html(ctx)
|
|
return full_page(ctx, header_rows_html=hdr, content_html=content)
|
|
|
|
|
|
async def render_all_events_oob(ctx: dict, entries, has_more, pending_tickets,
|
|
page_info, page, view) -> str:
|
|
"""OOB response: all events listing (htmx nav)."""
|
|
from quart import url_for
|
|
from shared.utils import route_prefix
|
|
from shared.infrastructure.urls import events_url
|
|
|
|
prefix = route_prefix()
|
|
ticket_url = url_for("all_events.adjust_ticket")
|
|
next_url = prefix + url_for("all_events.entries_fragment", page=page + 1) + (f"?view={view}" if view != "list" else "")
|
|
|
|
content = _events_main_panel_html(
|
|
ctx, entries, has_more, pending_tickets, page_info,
|
|
page, view, ticket_url, next_url, events_url,
|
|
)
|
|
return oob_page(ctx, content_html=content)
|
|
|
|
|
|
async def render_all_events_cards(entries, has_more, pending_tickets,
|
|
page_info, page, view) -> str:
|
|
"""Pagination fragment: all events cards only."""
|
|
from quart import url_for
|
|
from shared.utils import route_prefix
|
|
from shared.infrastructure.urls import events_url
|
|
|
|
prefix = route_prefix()
|
|
ticket_url = url_for("all_events.adjust_ticket")
|
|
next_url = prefix + url_for("all_events.entries_fragment", page=page + 1) + (f"?view={view}" if view != "list" else "")
|
|
|
|
return _entry_cards_html(
|
|
entries, page_info, pending_tickets, ticket_url, events_url,
|
|
view, page, has_more, next_url,
|
|
)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Page summary
|
|
# ---------------------------------------------------------------------------
|
|
|
|
async def render_page_summary_page(ctx: dict, entries, has_more, pending_tickets,
|
|
page_info, page, view) -> str:
|
|
"""Full page: page-scoped events listing."""
|
|
from quart import url_for
|
|
from shared.utils import route_prefix
|
|
from shared.infrastructure.urls import events_url
|
|
|
|
prefix = route_prefix()
|
|
post = ctx.get("post") or {}
|
|
ticket_url = url_for("page_summary.adjust_ticket")
|
|
next_url = prefix + url_for("page_summary.entries_fragment", page=page + 1) + (f"?view={view}" if view != "list" else "")
|
|
|
|
content = _events_main_panel_html(
|
|
ctx, entries, has_more, pending_tickets, page_info,
|
|
page, view, ticket_url, next_url, events_url,
|
|
is_page_scoped=True, post=post,
|
|
)
|
|
|
|
hdr = root_header_html(ctx)
|
|
hdr += render("events-header-child",
|
|
inner_html=_post_header_html(ctx))
|
|
return full_page(ctx, header_rows_html=hdr, content_html=content)
|
|
|
|
|
|
async def render_page_summary_oob(ctx: dict, entries, has_more, pending_tickets,
|
|
page_info, page, view) -> str:
|
|
"""OOB response: page-scoped events (htmx nav)."""
|
|
from quart import url_for
|
|
from shared.utils import route_prefix
|
|
from shared.infrastructure.urls import events_url
|
|
|
|
prefix = route_prefix()
|
|
post = ctx.get("post") or {}
|
|
ticket_url = url_for("page_summary.adjust_ticket")
|
|
next_url = prefix + url_for("page_summary.entries_fragment", page=page + 1) + (f"?view={view}" if view != "list" else "")
|
|
|
|
content = _events_main_panel_html(
|
|
ctx, entries, has_more, pending_tickets, page_info,
|
|
page, view, ticket_url, next_url, events_url,
|
|
is_page_scoped=True, post=post,
|
|
)
|
|
|
|
oobs = _post_header_html(ctx, oob=True)
|
|
return oob_page(ctx, oobs_html=oobs, content_html=content)
|
|
|
|
|
|
async def render_page_summary_cards(entries, has_more, pending_tickets,
|
|
page_info, page, view, post) -> str:
|
|
"""Pagination fragment: page-scoped events cards only."""
|
|
from quart import url_for
|
|
from shared.utils import route_prefix
|
|
from shared.infrastructure.urls import events_url
|
|
|
|
prefix = route_prefix()
|
|
ticket_url = url_for("page_summary.adjust_ticket")
|
|
next_url = prefix + url_for("page_summary.entries_fragment", page=page + 1) + (f"?view={view}" if view != "list" else "")
|
|
|
|
return _entry_cards_html(
|
|
entries, page_info, pending_tickets, ticket_url, events_url,
|
|
view, page, has_more, next_url,
|
|
is_page_scoped=True, post=post,
|
|
)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Calendars home
|
|
# ---------------------------------------------------------------------------
|
|
|
|
async def render_calendars_page(ctx: dict) -> str:
|
|
"""Full page: calendars listing."""
|
|
content = _calendars_main_panel_html(ctx)
|
|
hdr = root_header_html(ctx)
|
|
child = _post_header_html(ctx) + _calendars_header_html(ctx)
|
|
hdr += render("events-header-child", inner_html=child)
|
|
return full_page(ctx, header_rows_html=hdr, content_html=content)
|
|
|
|
|
|
async def render_calendars_oob(ctx: dict) -> str:
|
|
"""OOB response: calendars listing."""
|
|
content = _calendars_main_panel_html(ctx)
|
|
oobs = _post_header_html(ctx, oob=True)
|
|
oobs += _oob_header_html("post-header-child", "calendars-header-child",
|
|
_calendars_header_html(ctx))
|
|
return oob_page(ctx, oobs_html=oobs, content_html=content)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Calendar month view
|
|
# ---------------------------------------------------------------------------
|
|
|
|
async def render_calendar_page(ctx: dict) -> str:
|
|
"""Full page: calendar month view."""
|
|
content = _calendar_main_panel_html(ctx)
|
|
hdr = root_header_html(ctx)
|
|
child = _post_header_html(ctx) + _calendar_header_html(ctx)
|
|
hdr += render("events-header-child", inner_html=child)
|
|
return full_page(ctx, header_rows_html=hdr, content_html=content)
|
|
|
|
|
|
async def render_calendar_oob(ctx: dict) -> str:
|
|
"""OOB response: calendar month view."""
|
|
content = _calendar_main_panel_html(ctx)
|
|
oobs = _post_header_html(ctx, oob=True)
|
|
oobs += _oob_header_html("post-header-child", "calendar-header-child",
|
|
_calendar_header_html(ctx))
|
|
return oob_page(ctx, oobs_html=oobs, content_html=content)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Day detail
|
|
# ---------------------------------------------------------------------------
|
|
|
|
async def render_day_page(ctx: dict) -> str:
|
|
"""Full page: day detail."""
|
|
content = _day_main_panel_html(ctx)
|
|
hdr = root_header_html(ctx)
|
|
child = (_post_header_html(ctx)
|
|
+ _calendar_header_html(ctx) + _day_header_html(ctx))
|
|
hdr += render("events-header-child", inner_html=child)
|
|
return full_page(ctx, header_rows_html=hdr, content_html=content)
|
|
|
|
|
|
async def render_day_oob(ctx: dict) -> str:
|
|
"""OOB response: day detail."""
|
|
content = _day_main_panel_html(ctx)
|
|
oobs = _calendar_header_html(ctx, oob=True)
|
|
oobs += _oob_header_html("calendar-header-child", "day-header-child",
|
|
_day_header_html(ctx))
|
|
return oob_page(ctx, oobs_html=oobs, content_html=content)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Day admin
|
|
# ---------------------------------------------------------------------------
|
|
|
|
async def render_day_admin_page(ctx: dict) -> str:
|
|
"""Full page: day admin."""
|
|
content = _day_admin_main_panel_html(ctx)
|
|
hdr = root_header_html(ctx)
|
|
child = (_post_header_html(ctx)
|
|
+ _calendar_header_html(ctx) + _day_header_html(ctx)
|
|
+ _day_admin_header_html(ctx))
|
|
hdr += render("events-header-child", inner_html=child)
|
|
return full_page(ctx, header_rows_html=hdr, content_html=content)
|
|
|
|
|
|
async def render_day_admin_oob(ctx: dict) -> str:
|
|
"""OOB response: day admin."""
|
|
content = _day_admin_main_panel_html(ctx)
|
|
oobs = _calendar_header_html(ctx, oob=True)
|
|
oobs += _oob_header_html("day-header-child", "day-admin-header-child",
|
|
_day_admin_header_html(ctx))
|
|
return oob_page(ctx, oobs_html=oobs, content_html=content)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Calendar admin
|
|
# ---------------------------------------------------------------------------
|
|
|
|
async def render_calendar_admin_page(ctx: dict) -> str:
|
|
"""Full page: calendar admin."""
|
|
content = _calendar_admin_main_panel_html(ctx)
|
|
hdr = root_header_html(ctx)
|
|
child = (_post_header_html(ctx)
|
|
+ _calendar_header_html(ctx) + _calendar_admin_header_html(ctx))
|
|
hdr += render("events-header-child", inner_html=child)
|
|
return full_page(ctx, header_rows_html=hdr, content_html=content)
|
|
|
|
|
|
async def render_calendar_admin_oob(ctx: dict) -> str:
|
|
"""OOB response: calendar admin."""
|
|
content = _calendar_admin_main_panel_html(ctx)
|
|
oobs = _calendar_header_html(ctx, oob=True)
|
|
oobs += _oob_header_html("calendar-header-child", "calendar-admin-header-child",
|
|
_calendar_admin_header_html(ctx))
|
|
return oob_page(ctx, oobs_html=oobs, content_html=content)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Slots
|
|
# ---------------------------------------------------------------------------
|
|
|
|
async def render_slots_page(ctx: dict) -> str:
|
|
"""Full page: slots listing."""
|
|
from quart import g
|
|
slots = ctx.get("slots") or []
|
|
calendar = ctx.get("calendar")
|
|
content = render_slots_table(slots, calendar)
|
|
hdr = root_header_html(ctx)
|
|
child = (_post_header_html(ctx)
|
|
+ _calendar_header_html(ctx) + _calendar_admin_header_html(ctx))
|
|
hdr += render("events-header-child", inner_html=child)
|
|
return full_page(ctx, header_rows_html=hdr, content_html=content)
|
|
|
|
|
|
async def render_slots_oob(ctx: dict) -> str:
|
|
"""OOB response: slots listing."""
|
|
slots = ctx.get("slots") or []
|
|
calendar = ctx.get("calendar")
|
|
content = render_slots_table(slots, calendar)
|
|
oobs = _calendar_admin_header_html(ctx, oob=True)
|
|
return oob_page(ctx, oobs_html=oobs, content_html=content)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Tickets
|
|
# ---------------------------------------------------------------------------
|
|
|
|
async def render_tickets_page(ctx: dict, tickets: list) -> str:
|
|
"""Full page: my tickets."""
|
|
content = _tickets_main_panel_html(ctx, tickets)
|
|
hdr = root_header_html(ctx)
|
|
return full_page(ctx, header_rows_html=hdr, content_html=content)
|
|
|
|
|
|
async def render_tickets_oob(ctx: dict, tickets: list) -> str:
|
|
"""OOB response: my tickets."""
|
|
content = _tickets_main_panel_html(ctx, tickets)
|
|
return oob_page(ctx, content_html=content)
|
|
|
|
|
|
async def render_ticket_detail_page(ctx: dict, ticket) -> str:
|
|
"""Full page: ticket detail with QR."""
|
|
content = _ticket_detail_panel_html(ctx, ticket)
|
|
hdr = root_header_html(ctx)
|
|
return full_page(ctx, header_rows_html=hdr, content_html=content)
|
|
|
|
|
|
async def render_ticket_detail_oob(ctx: dict, ticket) -> str:
|
|
"""OOB response: ticket detail."""
|
|
content = _ticket_detail_panel_html(ctx, ticket)
|
|
return oob_page(ctx, content_html=content)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Ticket admin
|
|
# ---------------------------------------------------------------------------
|
|
|
|
async def render_ticket_admin_page(ctx: dict, tickets: list, stats: dict) -> str:
|
|
"""Full page: ticket admin dashboard."""
|
|
content = _ticket_admin_main_panel_html(ctx, tickets, stats)
|
|
hdr = root_header_html(ctx)
|
|
return full_page(ctx, header_rows_html=hdr, content_html=content)
|
|
|
|
|
|
async def render_ticket_admin_oob(ctx: dict, tickets: list, stats: dict) -> str:
|
|
"""OOB response: ticket admin dashboard."""
|
|
content = _ticket_admin_main_panel_html(ctx, tickets, stats)
|
|
return oob_page(ctx, content_html=content)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Markets
|
|
# ---------------------------------------------------------------------------
|
|
|
|
async def render_markets_page(ctx: dict) -> str:
|
|
"""Full page: markets listing."""
|
|
content = _markets_main_panel_html(ctx)
|
|
hdr = root_header_html(ctx)
|
|
child = _post_header_html(ctx) + _markets_header_html(ctx)
|
|
hdr += render("events-header-child", inner_html=child)
|
|
return full_page(ctx, header_rows_html=hdr, content_html=content)
|
|
|
|
|
|
async def render_markets_oob(ctx: dict) -> str:
|
|
"""OOB response: markets listing."""
|
|
content = _markets_main_panel_html(ctx)
|
|
oobs = _post_header_html(ctx, oob=True)
|
|
oobs += _oob_header_html("post-header-child", "markets-header-child",
|
|
_markets_header_html(ctx))
|
|
return oob_page(ctx, oobs_html=oobs, content_html=content)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Payments
|
|
# ---------------------------------------------------------------------------
|
|
|
|
async def render_payments_page(ctx: dict) -> str:
|
|
"""Full page: payments admin."""
|
|
content = _payments_main_panel_html(ctx)
|
|
hdr = root_header_html(ctx)
|
|
child = _post_header_html(ctx) + _payments_header_html(ctx)
|
|
hdr += render("events-header-child", inner_html=child)
|
|
return full_page(ctx, header_rows_html=hdr, content_html=content)
|
|
|
|
|
|
async def render_payments_oob(ctx: dict) -> str:
|
|
"""OOB response: payments admin."""
|
|
content = _payments_main_panel_html(ctx)
|
|
oobs = _post_header_html(ctx, oob=True)
|
|
oobs += _oob_header_html("post-header-child", "payments-header-child",
|
|
_payments_header_html(ctx))
|
|
return oob_page(ctx, oobs_html=oobs, content_html=content)
|
|
|
|
|
|
# ===========================================================================
|
|
# POST / PUT / DELETE response components
|
|
# ===========================================================================
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Ticket widget (public wrapper for _ticket_widget_html)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def render_ticket_widget(entry, qty: int, ticket_url: str) -> str:
|
|
"""Render the +/- ticket widget for page_summary / all_events adjust_ticket."""
|
|
return _ticket_widget_html(entry, qty, ticket_url, ctx={})
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Ticket admin: checkin result
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def render_checkin_result(success: bool, error: str | None, ticket) -> str:
|
|
"""Render checkin result: table row on success, error div on failure."""
|
|
if not success:
|
|
return render("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 = render("events-ticket-admin-date",
|
|
date_str=entry.start_at.strftime("%d %b %Y, %H:%M"))
|
|
|
|
return render("events-checkin-success-row",
|
|
code=code, code_short=code[:12] + "...",
|
|
entry_name=entry.name if entry else "\u2014",
|
|
date_html=date_html,
|
|
type_name=tt.name if tt else "\u2014",
|
|
badge_html=_ticket_state_badge_html("checked_in"),
|
|
time_str=time_str)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Ticket admin: lookup result
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def render_lookup_result(ticket, error: str | None) -> str:
|
|
"""Render ticket lookup result: error div or ticket info card."""
|
|
from quart import url_for
|
|
from shared.browser.app.csrf import generate_csrf_token
|
|
|
|
if error:
|
|
return render("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 = render("events-lookup-info",
|
|
entry_name=entry.name if entry else "Unknown event")
|
|
if tt:
|
|
info_html += render("events-lookup-type", type_name=tt.name)
|
|
if entry and entry.start_at:
|
|
info_html += render("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 += render("events-lookup-cal", cal_name=cal.name)
|
|
info_html += render("events-lookup-status",
|
|
badge_html=_ticket_state_badge_html(state), code=code)
|
|
if checked_in_at:
|
|
info_html += render("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 = render("events-lookup-checkin-btn",
|
|
checkin_url=checkin_url, code=code, csrf=csrf)
|
|
elif state == "checked_in":
|
|
action_html = render("events-lookup-checked-in")
|
|
elif state == "cancelled":
|
|
action_html = render("events-lookup-cancelled")
|
|
|
|
return render("events-lookup-card",
|
|
info_html=info_html, code=code, action_html=action_html)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Ticket admin: entry tickets table
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def render_entry_tickets_admin(entry, tickets: list) -> str:
|
|
"""Render admin ticket table for a specific entry."""
|
|
from quart import url_for
|
|
from shared.browser.app.csrf import generate_csrf_token
|
|
csrf = generate_csrf_token()
|
|
|
|
count = len(tickets)
|
|
suffix = "s" if count != 1 else ""
|
|
|
|
rows_html = ""
|
|
for ticket in tickets:
|
|
tt = getattr(ticket, "ticket_type", None)
|
|
state = getattr(ticket, "state", "")
|
|
code = ticket.code
|
|
checked_in_at = getattr(ticket, "checked_in_at", None)
|
|
|
|
action_html = ""
|
|
if state in ("confirmed", "reserved"):
|
|
checkin_url = url_for("ticket_admin.do_checkin", code=code)
|
|
action_html = render("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 = render("events-ticket-admin-checked-in",
|
|
time_str=t_str)
|
|
|
|
rows_html += render("events-entry-tickets-admin-row",
|
|
code=code, code_short=code[:12] + "...",
|
|
type_name=tt.name if tt else "\u2014",
|
|
badge_html=_ticket_state_badge_html(state),
|
|
action_html=action_html)
|
|
|
|
if tickets:
|
|
body_html = render("events-entry-tickets-admin-table",
|
|
rows_html=rows_html)
|
|
else:
|
|
body_html = render("events-entry-tickets-admin-empty")
|
|
|
|
return render("events-entry-tickets-admin-panel",
|
|
entry_name=entry.name,
|
|
count_label=f"{count} ticket{suffix}",
|
|
body_html=body_html)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Day main panel -- public API
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def render_day_main_panel(ctx: dict) -> str:
|
|
"""Public wrapper for day main panel rendering."""
|
|
return _day_main_panel_html(ctx)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Entry main panel
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def _entry_main_panel_html(ctx: dict) -> str:
|
|
"""Render the entry detail panel (name, slot, time, state, cost, tickets,
|
|
buy form, date, posts, options + edit button)."""
|
|
from quart import url_for
|
|
|
|
entry = ctx.get("entry")
|
|
if not entry:
|
|
return ""
|
|
|
|
calendar = ctx.get("calendar")
|
|
cal_slug = getattr(calendar, "slug", "") if calendar else ""
|
|
day = ctx.get("day")
|
|
month = ctx.get("month")
|
|
year = ctx.get("year")
|
|
styles = ctx.get("styles") or {}
|
|
list_container = getattr(styles, "list_container", "") if hasattr(styles, "list_container") else styles.get("list_container", "")
|
|
pre_action = getattr(styles, "pre_action_button", "") if hasattr(styles, "pre_action_button") else styles.get("pre_action_button", "")
|
|
|
|
eid = entry.id
|
|
state = getattr(entry, "state", "pending") or "pending"
|
|
|
|
def _field(label, content_html):
|
|
return render("events-entry-field", label=label, content_html=content_html)
|
|
|
|
# Name
|
|
name_html = _field("Name", render("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 = render("events-entry-slot-assigned",
|
|
slot_name=slot.name, flex_label=flex_label)
|
|
else:
|
|
slot_inner = render("events-entry-slot-none")
|
|
slot_html = _field("Slot", slot_inner)
|
|
|
|
# Time Period
|
|
start_str = entry.start_at.strftime("%H:%M") if entry.start_at else ""
|
|
end_str = f" \u2013 {entry.end_at.strftime('%H:%M')}" if entry.end_at else " \u2013 open-ended"
|
|
time_html = _field("Time Period", render("events-entry-time-field",
|
|
time_str=start_str + end_str))
|
|
|
|
# State
|
|
state_html = _field("State", render("events-entry-state-field",
|
|
entry_id=str(eid),
|
|
badge_html=_entry_state_badge_html(state)))
|
|
|
|
# Cost
|
|
cost = getattr(entry, "cost", None)
|
|
cost_str = f"{cost:.2f}" if cost is not None else "0.00"
|
|
cost_html = _field("Cost", render("events-entry-cost-field",
|
|
cost_html=f"£{cost_str}"))
|
|
|
|
# Ticket Configuration (admin)
|
|
tickets_html = _field("Tickets", render("events-entry-tickets-field",
|
|
entry_id=str(eid),
|
|
tickets_config_html=render_entry_tickets_config(entry, calendar, day, month, year)))
|
|
|
|
# Buy Tickets (public-facing)
|
|
ticket_remaining = ctx.get("ticket_remaining")
|
|
ticket_sold_count = ctx.get("ticket_sold_count", 0)
|
|
user_ticket_count = ctx.get("user_ticket_count", 0)
|
|
user_ticket_counts_by_type = ctx.get("user_ticket_counts_by_type") or {}
|
|
buy_html = render_buy_form(
|
|
entry, ticket_remaining, ticket_sold_count,
|
|
user_ticket_count, user_ticket_counts_by_type,
|
|
)
|
|
|
|
# Date
|
|
date_str = entry.start_at.strftime("%A, %B %d, %Y") if entry.start_at else ""
|
|
date_html = _field("Date", render("events-entry-date-field", date_str=date_str))
|
|
|
|
# Associated Posts
|
|
entry_posts = ctx.get("entry_posts") or []
|
|
posts_html = _field("Associated Posts", render("events-entry-posts-field",
|
|
entry_id=str(eid),
|
|
posts_panel_html=render_entry_posts_panel(entry_posts, entry, calendar, day, month, year)))
|
|
|
|
# Options and Edit Button
|
|
edit_url = url_for(
|
|
"calendars.calendar.day.calendar_entries.calendar_entry.get_edit",
|
|
entry_id=eid, calendar_slug=cal_slug,
|
|
day=day, month=month, year=year,
|
|
)
|
|
|
|
return render("events-entry-panel",
|
|
entry_id=str(eid), list_container=list_container,
|
|
name_html=name_html, slot_html=slot_html,
|
|
time_html=time_html, state_html=state_html,
|
|
cost_html=cost_html, tickets_html=tickets_html,
|
|
buy_html=buy_html, date_html=date_html,
|
|
posts_html=posts_html,
|
|
options_html=_entry_options_html(entry, calendar, day, month, year),
|
|
pre_action=pre_action, edit_url=edit_url)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Entry header row
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def _entry_header_html(ctx: dict, *, oob: bool = False) -> str:
|
|
"""Build entry detail header row."""
|
|
from quart import url_for
|
|
|
|
calendar = ctx.get("calendar")
|
|
if not calendar:
|
|
return ""
|
|
cal_slug = getattr(calendar, "slug", "")
|
|
entry = ctx.get("entry")
|
|
if not entry:
|
|
return ""
|
|
day = ctx.get("day")
|
|
month = ctx.get("month")
|
|
year = ctx.get("year")
|
|
|
|
link_href = url_for(
|
|
"calendars.calendar.day.calendar_entries.calendar_entry.get",
|
|
calendar_slug=cal_slug,
|
|
year=year, month=month, day=day,
|
|
entry_id=entry.id,
|
|
)
|
|
label_html = render("events-entry-label",
|
|
entry_id=str(entry.id),
|
|
title_html=_entry_title_html(entry),
|
|
times_html=_entry_times_html(entry))
|
|
|
|
nav_html = _entry_nav_html(ctx)
|
|
|
|
return render("menu-row", id="entry-row", level=5,
|
|
link_href=link_href, link_label_html=label_html,
|
|
nav_html=nav_html, child_id="entry-header-child", oob=oob)
|
|
|
|
|
|
def _entry_times_html(entry) -> str:
|
|
"""Render entry times label."""
|
|
start = entry.start_at
|
|
end = entry.end_at
|
|
if not start:
|
|
return ""
|
|
start_str = start.strftime("%H:%M")
|
|
end_str = f" \u2192 {end.strftime('%H:%M')}" if end else ""
|
|
return render("events-entry-times", time_str=start_str + end_str)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Entry nav (desktop + admin link)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def _entry_nav_html(ctx: dict) -> str:
|
|
"""Entry desktop nav: associated posts scrolling menu + admin link."""
|
|
from quart import url_for
|
|
|
|
calendar = ctx.get("calendar")
|
|
if not calendar:
|
|
return ""
|
|
cal_slug = getattr(calendar, "slug", "")
|
|
entry = ctx.get("entry")
|
|
if not entry:
|
|
return ""
|
|
day = ctx.get("day")
|
|
month = ctx.get("month")
|
|
year = ctx.get("year")
|
|
entry_posts = ctx.get("entry_posts") or []
|
|
rights = ctx.get("rights") or {}
|
|
is_admin = getattr(rights, "admin", False) if hasattr(rights, "admin") else rights.get("admin", False)
|
|
|
|
blog_url_fn = ctx.get("blog_url")
|
|
|
|
parts = []
|
|
|
|
# Associated Posts scrolling menu
|
|
if entry_posts:
|
|
post_links = ""
|
|
for ep in entry_posts:
|
|
slug = getattr(ep, "slug", "")
|
|
title = getattr(ep, "title", "")
|
|
feat = getattr(ep, "feature_image", None)
|
|
href = blog_url_fn(f"/{slug}/") if blog_url_fn else f"/{slug}/"
|
|
if feat:
|
|
img_html = render("events-post-img", src=feat, alt=title)
|
|
else:
|
|
img_html = render("events-post-img-placeholder")
|
|
post_links += render("events-entry-nav-post-link",
|
|
href=href, img_html=img_html, title=title)
|
|
parts.append(render("events-entry-posts-nav-oob",
|
|
items_html=post_links).replace(' :hx-swap-oob "true"', ''))
|
|
|
|
# Admin link
|
|
if is_admin:
|
|
admin_url = url_for(
|
|
"calendars.calendar.day.calendar_entries.calendar_entry.admin.admin",
|
|
calendar_slug=cal_slug,
|
|
day=day, month=month, year=year,
|
|
entry_id=entry.id,
|
|
)
|
|
parts.append(render("events-entry-admin-link", href=admin_url))
|
|
|
|
return "".join(parts)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Entry page / OOB rendering
|
|
# ---------------------------------------------------------------------------
|
|
|
|
async def render_entry_page(ctx: dict) -> str:
|
|
"""Full page: entry detail."""
|
|
content = _entry_main_panel_html(ctx)
|
|
hdr = root_header_html(ctx)
|
|
child = (_post_header_html(ctx)
|
|
+ _calendar_header_html(ctx) + _day_header_html(ctx)
|
|
+ _entry_header_html(ctx))
|
|
hdr += render("events-header-child", inner_html=child)
|
|
nav_html = _entry_nav_html(ctx)
|
|
return full_page(ctx, header_rows_html=hdr, content_html=content, menu_html=nav_html)
|
|
|
|
|
|
async def render_entry_oob(ctx: dict) -> str:
|
|
"""OOB response: entry detail."""
|
|
content = _entry_main_panel_html(ctx)
|
|
oobs = _day_header_html(ctx, oob=True)
|
|
oobs += _oob_header_html("day-header-child", "entry-header-child",
|
|
_entry_header_html(ctx))
|
|
nav_html = _entry_nav_html(ctx)
|
|
return oob_page(ctx, oobs_html=oobs, content_html=content, menu_html=nav_html)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Entry optioned (confirm/decline/provisional response)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def render_entry_optioned(entry, calendar, day, month, year) -> str:
|
|
"""Render entry options buttons + OOB title & state swaps."""
|
|
options = _entry_options_html(entry, calendar, day, month, year)
|
|
title = _entry_title_html(entry)
|
|
state = _entry_state_badge_html(getattr(entry, "state", "pending") or "pending")
|
|
|
|
return options + render("events-entry-optioned-oob",
|
|
entry_id=str(entry.id),
|
|
title_html=title, state_html=state)
|
|
|
|
|
|
def _entry_title_html(entry) -> str:
|
|
"""Render entry title (icon + name + state badge)."""
|
|
state = getattr(entry, "state", "pending") or "pending"
|
|
return render("events-entry-title",
|
|
name=entry.name,
|
|
badge_html=_entry_state_badge_html(state))
|
|
|
|
|
|
def _entry_options_html(entry, calendar, day, month, year) -> str:
|
|
"""Render confirm/decline/provisional buttons based on entry state."""
|
|
from quart import url_for, g
|
|
from shared.browser.app.csrf import generate_csrf_token
|
|
csrf = generate_csrf_token()
|
|
|
|
styles = getattr(g, "styles", None) or {}
|
|
action_btn = getattr(styles, "action_button", "") if hasattr(styles, "action_button") else styles.get("action_button", "")
|
|
|
|
cal_slug = getattr(calendar, "slug", "")
|
|
eid = entry.id
|
|
state = getattr(entry, "state", "pending") or "pending"
|
|
target = f"#calendar_entry_options_{eid}"
|
|
|
|
def _make_button(action_name, label, confirm_title, confirm_text, *, trigger_type="submit"):
|
|
url = url_for(
|
|
f"calendars.calendar.day.calendar_entries.calendar_entry.{action_name}",
|
|
calendar_slug=cal_slug, day=day, month=month, year=year, entry_id=eid,
|
|
)
|
|
btn_type = "button" if trigger_type == "button" else "submit"
|
|
return render("events-entry-option-button",
|
|
url=url, target=target, csrf=csrf, btn_type=btn_type,
|
|
action_btn=action_btn, confirm_title=confirm_title,
|
|
confirm_text=confirm_text, label=label,
|
|
is_btn=trigger_type == "button")
|
|
|
|
buttons_html = ""
|
|
if state == "provisional":
|
|
buttons_html += _make_button(
|
|
"confirm_entry", "confirm",
|
|
"Confirm entry?", "Are you sure you want to confirm this entry?",
|
|
)
|
|
buttons_html += _make_button(
|
|
"decline_entry", "decline",
|
|
"Decline entry?", "Are you sure you want to decline this entry?",
|
|
)
|
|
elif state == "confirmed":
|
|
buttons_html += _make_button(
|
|
"provisional_entry", "provisional",
|
|
"Provisional entry?", "Are you sure you want to provisional this entry?",
|
|
trigger_type="button",
|
|
)
|
|
|
|
return render("events-entry-options",
|
|
entry_id=str(eid), buttons_html=buttons_html)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Entry tickets config (display + form)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def render_entry_tickets_config(entry, calendar, day, month, year) -> str:
|
|
"""Render ticket config display + edit form for admin entry view."""
|
|
from quart import url_for
|
|
from shared.browser.app.csrf import generate_csrf_token
|
|
csrf = generate_csrf_token()
|
|
|
|
cal_slug = getattr(calendar, "slug", "")
|
|
eid = entry.id
|
|
tp = getattr(entry, "ticket_price", None)
|
|
tc = getattr(entry, "ticket_count", None)
|
|
eid_s = str(eid)
|
|
show_js = f"document.getElementById('ticket-form-{eid}').classList.remove('hidden'); this.classList.add('hidden');"
|
|
hide_js = (f"document.getElementById('ticket-form-{eid}').classList.add('hidden'); "
|
|
f"document.getElementById('entry-tickets-{eid}').querySelectorAll('button:not([type=submit])').forEach(btn => btn.classList.remove('hidden'));")
|
|
|
|
if tp is not None:
|
|
tc_str = f"{tc} tickets" if tc is not None else "Unlimited"
|
|
display_html = render("events-ticket-config-display",
|
|
price_str=f"£{tp:.2f}",
|
|
count_str=tc_str, show_js=show_js)
|
|
else:
|
|
display_html = render("events-ticket-config-none", show_js=show_js)
|
|
|
|
update_url = url_for(
|
|
"calendars.calendar.day.calendar_entries.calendar_entry.update_tickets",
|
|
entry_id=eid, calendar_slug=cal_slug, day=day, month=month, year=year,
|
|
)
|
|
hidden_cls = "" if tp is None else "hidden"
|
|
tp_val = f"{tp:.2f}" if tp is not None else ""
|
|
tc_val = str(tc) if tc is not None else ""
|
|
|
|
form_html = render("events-ticket-config-form",
|
|
entry_id=eid_s, hidden_cls=hidden_cls,
|
|
update_url=update_url, csrf=csrf,
|
|
price_val=tp_val, count_val=tc_val, hide_js=hide_js)
|
|
return display_html + form_html
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Entry posts panel
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def render_entry_posts_panel(entry_posts, entry, calendar, day, month, year) -> str:
|
|
"""Render associated posts list with remove buttons and search input."""
|
|
from quart import url_for
|
|
from shared.browser.app.csrf import generate_csrf_token
|
|
csrf = generate_csrf_token()
|
|
|
|
cal_slug = getattr(calendar, "slug", "")
|
|
eid = entry.id
|
|
eid_s = str(eid)
|
|
|
|
posts_html = ""
|
|
if entry_posts:
|
|
items = ""
|
|
for ep in entry_posts:
|
|
ep_title = getattr(ep, "title", "")
|
|
ep_id = getattr(ep, "id", 0)
|
|
feat = getattr(ep, "feature_image", None)
|
|
img_html = (render("events-post-img", src=feat, alt=ep_title)
|
|
if feat else render("events-post-img-placeholder"))
|
|
|
|
del_url = url_for(
|
|
"calendars.calendar.day.calendar_entries.calendar_entry.remove_post",
|
|
calendar_slug=cal_slug, day=day, month=month, year=year,
|
|
entry_id=eid, post_id=ep_id,
|
|
)
|
|
items += render("events-entry-post-item",
|
|
img_html=img_html, title=ep_title,
|
|
del_url=del_url, entry_id=eid_s,
|
|
csrf_hdr=f'{{"X-CSRFToken": "{csrf}"}}')
|
|
posts_html = render("events-entry-posts-list", items_html=items)
|
|
else:
|
|
posts_html = render("events-entry-posts-none")
|
|
|
|
search_url = url_for(
|
|
"calendars.calendar.day.calendar_entries.calendar_entry.search_posts",
|
|
calendar_slug=cal_slug, day=day, month=month, year=year, entry_id=eid,
|
|
)
|
|
|
|
return render("events-entry-posts-panel",
|
|
posts_html=posts_html, search_url=search_url,
|
|
entry_id=eid_s)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Entry posts nav OOB
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def render_entry_posts_nav_oob(entry_posts) -> str:
|
|
"""Render OOB nav for entry posts (scrolling menu)."""
|
|
from quart import g
|
|
styles = getattr(g, "styles", None) or {}
|
|
nav_btn = getattr(styles, "nav_button", "") if hasattr(styles, "nav_button") else styles.get("nav_button", "")
|
|
blog_url_fn = getattr(g, "blog_url", None)
|
|
|
|
if not entry_posts:
|
|
return render("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 = (render("events-post-img", src=feat, alt=title)
|
|
if feat else render("events-post-img-placeholder"))
|
|
items += render("events-entry-nav-post",
|
|
href=href, nav_btn=nav_btn,
|
|
img_html=img_html, title=title)
|
|
|
|
return render("events-entry-posts-nav-oob", items_html=items)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Day entries nav OOB
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def render_day_entries_nav_oob(confirmed_entries, calendar, day_date) -> str:
|
|
"""Render OOB nav for confirmed entries in a day."""
|
|
from quart import url_for, g
|
|
|
|
styles = getattr(g, "styles", None) or {}
|
|
nav_btn = getattr(styles, "nav_button", "") if hasattr(styles, "nav_button") else styles.get("nav_button", "")
|
|
cal_slug = getattr(calendar, "slug", "")
|
|
|
|
if not confirmed_entries:
|
|
return render("events-day-entries-nav-oob-empty")
|
|
|
|
items = ""
|
|
for entry in confirmed_entries:
|
|
href = url_for(
|
|
"calendars.calendar.day.calendar_entries.calendar_entry.get",
|
|
calendar_slug=cal_slug,
|
|
year=day_date.year, month=day_date.month, day=day_date.day,
|
|
entry_id=entry.id,
|
|
)
|
|
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 += render("events-day-nav-entry",
|
|
href=href, nav_btn=nav_btn,
|
|
name=entry.name, time_str=start + end)
|
|
|
|
return render("events-day-entries-nav-oob", items_html=items)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Post nav entries OOB
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def render_post_nav_entries_oob(associated_entries, calendars, post) -> str:
|
|
"""Render OOB nav for associated entries and calendars of a post."""
|
|
from quart import g
|
|
from shared.infrastructure.urls import events_url
|
|
|
|
styles = getattr(g, "styles", None) or {}
|
|
nav_btn = getattr(styles, "nav_button_less_pad", "") if hasattr(styles, "nav_button_less_pad") else styles.get("nav_button_less_pad", "")
|
|
|
|
has_entries = associated_entries and getattr(associated_entries, "entries", None)
|
|
has_items = has_entries or calendars
|
|
|
|
if not has_items:
|
|
return render("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 += render("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 += render("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 render("events-post-nav-wrapper",
|
|
items_html=items, hyperscript=hs)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Calendar description display + edit form
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def render_calendar_description(calendar, *, oob: bool = False) -> str:
|
|
"""Render calendar description display with edit button, optionally with OOB title."""
|
|
from quart import url_for
|
|
|
|
cal_slug = getattr(calendar, "slug", "")
|
|
edit_url = url_for("calendars.calendar.admin.calendar_description_edit", calendar_slug=cal_slug)
|
|
html = _calendar_description_display_html(calendar, edit_url)
|
|
|
|
if oob:
|
|
desc = getattr(calendar, "description", "") or ""
|
|
html += render("events-calendar-description-title-oob",
|
|
description=desc)
|
|
return html
|
|
|
|
|
|
def render_calendar_description_edit(calendar) -> str:
|
|
"""Render calendar description edit form."""
|
|
from quart import url_for
|
|
from shared.browser.app.csrf import generate_csrf_token
|
|
csrf = generate_csrf_token()
|
|
cal_slug = getattr(calendar, "slug", "")
|
|
desc = getattr(calendar, "description", "") or ""
|
|
|
|
save_url = url_for("calendars.calendar.admin.calendar_description_save", calendar_slug=cal_slug)
|
|
cancel_url = url_for("calendars.calendar.admin.calendar_description_view", calendar_slug=cal_slug)
|
|
|
|
return render("events-calendar-description-edit-form",
|
|
save_url=save_url, cancel_url=cancel_url,
|
|
csrf=csrf, description=desc)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Payments panel (public wrapper)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def render_payments_panel(ctx: dict) -> str:
|
|
"""Render the payments config panel for PUT response."""
|
|
return _payments_main_panel_html(ctx)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Calendars list panel (for POST create / DELETE)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def render_calendars_list_panel(ctx: dict) -> str:
|
|
"""Render the calendars main panel HTML for POST/DELETE response."""
|
|
return _calendars_main_panel_html(ctx)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Markets list panel (for POST create / DELETE)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def render_markets_list_panel(ctx: dict) -> str:
|
|
"""Render the markets main panel HTML for POST/DELETE response."""
|
|
return _markets_main_panel_html(ctx)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Slot main panel
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def render_slot_main_panel(slot, calendar, *, oob: bool = False) -> str:
|
|
"""Render slot detail view."""
|
|
from quart import url_for, g
|
|
|
|
styles = getattr(g, "styles", None) or {}
|
|
list_container = getattr(styles, "list_container", "") if hasattr(styles, "list_container") else styles.get("list_container", "")
|
|
pre_action = getattr(styles, "pre_action_button", "") if hasattr(styles, "pre_action_button") else styles.get("pre_action_button", "")
|
|
cal_slug = getattr(calendar, "slug", "")
|
|
|
|
days_display = getattr(slot, "days_display", "\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("calendars.calendar.slots.slot.get_edit", slot_id=slot.id, calendar_slug=cal_slug)
|
|
|
|
# Days pills
|
|
if days and days[0] != "\u2014":
|
|
days_inner = "".join(
|
|
render("events-slot-day-pill", day=d) for d in days
|
|
)
|
|
days_html = render("events-slot-days-pills", days_inner_html=days_inner)
|
|
else:
|
|
days_html = render("events-slot-no-days")
|
|
|
|
sid = str(slot.id)
|
|
|
|
result = render("events-slot-panel",
|
|
slot_id=sid, list_container=list_container,
|
|
days_html=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 += render("events-slot-description-oob", description=desc)
|
|
|
|
return result
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Slots table
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def render_slots_table(slots, calendar) -> str:
|
|
"""Render slots table with rows and add button."""
|
|
from quart import url_for, g
|
|
from shared.browser.app.csrf import generate_csrf_token
|
|
csrf = generate_csrf_token()
|
|
|
|
styles = getattr(g, "styles", None) or {}
|
|
list_container = getattr(styles, "list_container", "") if hasattr(styles, "list_container") else styles.get("list_container", "")
|
|
tr_cls = getattr(styles, "tr", "") if hasattr(styles, "tr") else styles.get("tr", "")
|
|
pill_cls = getattr(styles, "pill", "") if hasattr(styles, "pill") else styles.get("pill", "")
|
|
action_btn = getattr(styles, "action_button", "") if hasattr(styles, "action_button") else styles.get("action_button", "")
|
|
pre_action = getattr(styles, "pre_action_button", "") if hasattr(styles, "pre_action_button") else styles.get("pre_action_button", "")
|
|
hx_select = getattr(g, "hx_select_search", "#main-panel")
|
|
cal_slug = getattr(calendar, "slug", "")
|
|
|
|
rows_html = ""
|
|
if slots:
|
|
for s in slots:
|
|
slot_href = url_for("calendars.calendar.slots.slot.get", calendar_slug=cal_slug, slot_id=s.id)
|
|
del_url = url_for("calendars.calendar.slots.slot.slot_delete", calendar_slug=cal_slug, slot_id=s.id)
|
|
desc = getattr(s, "description", "") or ""
|
|
|
|
days_display = getattr(s, "days_display", "\u2014")
|
|
day_list = days_display.split(", ")
|
|
if day_list and day_list[0] != "\u2014":
|
|
days_inner = "".join(
|
|
render("events-slot-day-pill", day=d) for d in day_list
|
|
)
|
|
days_html = render("events-slot-days-pills", days_inner_html=days_inner)
|
|
else:
|
|
days_html = render("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 += render("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_html=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 = render("events-slots-empty-row")
|
|
|
|
add_url = url_for("calendars.calendar.slots.add_form", calendar_slug=cal_slug)
|
|
|
|
return render("events-slots-table",
|
|
list_container=list_container, rows_html=rows_html,
|
|
pre_action=pre_action, add_url=add_url)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Ticket type main panel
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def render_ticket_type_main_panel(ticket_type, entry, calendar, day, month, year, *, oob: bool = False) -> str:
|
|
"""Render ticket type detail view."""
|
|
from quart import url_for, g
|
|
|
|
styles = getattr(g, "styles", None) or {}
|
|
list_container = getattr(styles, "list_container", "") if hasattr(styles, "list_container") else styles.get("list_container", "")
|
|
pre_action = getattr(styles, "pre_action_button", "") if hasattr(styles, "pre_action_button") else styles.get("pre_action_button", "")
|
|
cal_slug = getattr(calendar, "slug", "")
|
|
|
|
cost = getattr(ticket_type, "cost", None)
|
|
cost_str = f"\u00a3{cost:.2f}" if cost is not None else "\u00a30.00"
|
|
count = getattr(ticket_type, "count", 0)
|
|
tid = str(ticket_type.id)
|
|
|
|
edit_url = url_for(
|
|
"calendars.calendar.day.calendar_entries.calendar_entry.ticket_types.ticket_type.get_edit",
|
|
ticket_type_id=ticket_type.id, calendar_slug=cal_slug,
|
|
year=year, month=month, day=day, entry_id=entry.id,
|
|
)
|
|
|
|
def _col(label, val):
|
|
return render("events-ticket-type-col", label=label, value=val)
|
|
|
|
return render("events-ticket-type-panel",
|
|
ticket_id=tid, list_container=list_container,
|
|
c1=_col("Name", ticket_type.name),
|
|
c2=_col("Cost", cost_str),
|
|
c3=_col("Count", str(count)),
|
|
pre_action=pre_action, edit_url=edit_url)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Ticket types table
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def render_ticket_types_table(ticket_types, entry, calendar, day, month, year) -> str:
|
|
"""Render ticket types table with rows and add button."""
|
|
from quart import url_for, g
|
|
from shared.browser.app.csrf import generate_csrf_token
|
|
csrf = generate_csrf_token()
|
|
|
|
styles = getattr(g, "styles", None) or {}
|
|
list_container = getattr(styles, "list_container", "") if hasattr(styles, "list_container") else styles.get("list_container", "")
|
|
tr_cls = getattr(styles, "tr", "") if hasattr(styles, "tr") else styles.get("tr", "")
|
|
pill_cls = getattr(styles, "pill", "") if hasattr(styles, "pill") else styles.get("pill", "")
|
|
action_btn = getattr(styles, "action_button", "") if hasattr(styles, "action_button") else styles.get("action_button", "")
|
|
hx_select = getattr(g, "hx_select_search", "#main-panel")
|
|
cal_slug = getattr(calendar, "slug", "")
|
|
eid = entry.id
|
|
|
|
rows_html = ""
|
|
if ticket_types:
|
|
for tt in ticket_types:
|
|
tt_href = url_for(
|
|
"calendars.calendar.day.calendar_entries.calendar_entry.ticket_types.ticket_type.get",
|
|
calendar_slug=cal_slug, year=year, month=month, day=day,
|
|
entry_id=eid, ticket_type_id=tt.id,
|
|
)
|
|
del_url = url_for(
|
|
"calendars.calendar.day.calendar_entries.calendar_entry.ticket_types.ticket_type.delete",
|
|
calendar_slug=cal_slug, year=year, month=month, day=day,
|
|
entry_id=eid, ticket_type_id=tt.id,
|
|
)
|
|
cost = getattr(tt, "cost", None)
|
|
cost_str = f"\u00a3{cost:.2f}" if cost is not None else "\u00a30.00"
|
|
|
|
rows_html += render("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 = render("events-ticket-types-empty-row")
|
|
|
|
add_url = url_for(
|
|
"calendars.calendar.day.calendar_entries.calendar_entry.ticket_types.add_form",
|
|
calendar_slug=cal_slug, entry_id=eid, year=year, month=month, day=day,
|
|
)
|
|
|
|
return render("events-ticket-types-table",
|
|
list_container=list_container, rows_html=rows_html,
|
|
action_btn=action_btn, add_url=add_url)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Buy result (ticket purchase confirmation)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def render_buy_result(entry, created_tickets, remaining, cart_count) -> str:
|
|
"""Render buy result card with created tickets + OOB cart icon."""
|
|
from quart import url_for
|
|
|
|
cart_html = _cart_icon_oob(cart_count)
|
|
|
|
count = len(created_tickets)
|
|
suffix = "s" if count != 1 else ""
|
|
|
|
tickets_html = ""
|
|
for ticket in created_tickets:
|
|
href = url_for("tickets.ticket_detail", code=ticket.code)
|
|
tickets_html += render("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 = render("events-buy-result-remaining",
|
|
text=f"{remaining} ticket{r_suffix} remaining")
|
|
|
|
my_href = url_for("tickets.my_tickets")
|
|
|
|
return cart_html + render("events-buy-result",
|
|
entry_id=str(entry.id),
|
|
count_label=f"{count} ticket{suffix} reserved",
|
|
tickets_html=tickets_html,
|
|
remaining_html=remaining_html,
|
|
my_tickets_href=my_href)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Buy form (ticket +/- controls)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def render_buy_form(entry, ticket_remaining, ticket_sold_count,
|
|
user_ticket_count, user_ticket_counts_by_type) -> str:
|
|
"""Render the ticket buy/adjust form with +/- controls."""
|
|
from quart import url_for
|
|
from shared.browser.app.csrf import generate_csrf_token
|
|
csrf = generate_csrf_token()
|
|
|
|
eid = entry.id
|
|
eid_s = str(eid)
|
|
tp = getattr(entry, "ticket_price", None)
|
|
state = getattr(entry, "state", "")
|
|
ticket_types = getattr(entry, "ticket_types", None) or []
|
|
|
|
if tp is None:
|
|
return ""
|
|
|
|
if state != "confirmed":
|
|
return render("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 += render("events-buy-info-sold",
|
|
count=str(ticket_sold_count))
|
|
if ticket_remaining is not None:
|
|
info_items += render("events-buy-info-remaining",
|
|
count=str(ticket_remaining))
|
|
if user_ticket_count:
|
|
info_items += render("events-buy-info-basket",
|
|
count=str(user_ticket_count))
|
|
if info_items:
|
|
info_html = render("events-buy-info-bar", items_html=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 += render("events-buy-type-item",
|
|
type_name=tt.name, cost_str=cost_str,
|
|
adjust_controls_html=_ticket_adjust_controls(csrf, adjust_url, target, eid, type_count, ticket_type_id=tt.id))
|
|
body_html = render("events-buy-types-wrapper", items_html=type_items)
|
|
else:
|
|
qty = user_ticket_count or 0
|
|
body_html = render("events-buy-default",
|
|
price_str=f"\u00a3{tp:.2f}",
|
|
adjust_controls_html=_ticket_adjust_controls(csrf, adjust_url, target, eid, qty))
|
|
|
|
return render("events-buy-panel",
|
|
entry_id=eid_s, info_html=info_html, body_html=body_html)
|
|
|
|
|
|
def _ticket_adjust_controls(csrf, adjust_url, target, entry_id, count, *, ticket_type_id=None):
|
|
"""Render +/- ticket controls for buy form."""
|
|
from quart import url_for
|
|
|
|
tt_html = render("events-adjust-tt-hidden",
|
|
ticket_type_id=str(ticket_type_id)) if ticket_type_id else ""
|
|
eid_s = str(entry_id)
|
|
|
|
def _adj_form(count_val, btn_html, *, extra_cls=""):
|
|
return render("events-adjust-form",
|
|
adjust_url=adjust_url, target=target,
|
|
extra_cls=extra_cls, csrf=csrf,
|
|
entry_id=eid_s, tt_html=tt_html,
|
|
count_val=str(count_val), btn_html=btn_html)
|
|
|
|
if count == 0:
|
|
return _adj_form(1, render("events-adjust-cart-plus"),
|
|
extra_cls="flex items-center")
|
|
|
|
my_tickets_href = url_for("tickets.my_tickets")
|
|
minus = _adj_form(count - 1, render("events-adjust-minus"))
|
|
cart_icon = render("events-adjust-cart-icon",
|
|
href=my_tickets_href, count=str(count))
|
|
plus = _adj_form(count + 1, render("events-adjust-plus"))
|
|
|
|
return render("events-adjust-controls",
|
|
minus_html=minus, cart_icon_html=cart_icon, plus_html=plus)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Adjust response (OOB cart icon + buy form)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def render_adjust_response(entry, ticket_remaining, ticket_sold_count,
|
|
user_ticket_count, user_ticket_counts_by_type,
|
|
cart_count) -> str:
|
|
"""Render ticket adjust response: OOB cart icon + buy form."""
|
|
cart_html = _cart_icon_oob(cart_count)
|
|
form_html = render_buy_form(
|
|
entry, ticket_remaining, ticket_sold_count,
|
|
user_ticket_count, user_ticket_counts_by_type,
|
|
)
|
|
return cart_html + form_html
|
|
|
|
|
|
def _cart_icon_oob(count: int) -> str:
|
|
"""Render the OOB cart icon/badge swap."""
|
|
from quart import g
|
|
|
|
blog_url_fn = getattr(g, "blog_url", None)
|
|
cart_url_fn = getattr(g, "cart_url", None)
|
|
site_fn = getattr(g, "site", None)
|
|
logo = ""
|
|
if site_fn:
|
|
site_obj = site_fn() if callable(site_fn) else site_fn
|
|
logo = getattr(site_obj, "logo", "") if site_obj else ""
|
|
|
|
if count == 0:
|
|
blog_href = blog_url_fn("/") if blog_url_fn else "/"
|
|
return render("events-cart-icon-logo",
|
|
blog_href=blog_href, logo=logo)
|
|
|
|
cart_href = cart_url_fn("/") if cart_url_fn else "/"
|
|
return render("events-cart-icon-badge",
|
|
cart_href=cart_href, count=str(count))
|