Remove render_to_sx from public API: enforce sx_call for all service code
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 1m44s

Replace ~250 render_to_sx calls across all services with sync sx_call,
converting many async functions to sync where no other awaits remained.
Make render_to_sx/render_to_sx_with_env private (_render_to_sx).
Add (post-header-ctx) IO primitive and shared post/post-admin defmacros.
Convert built-in post/post-admin layouts from Python to register_sx_layout
with .sx defcomps. Remove dead post_admin_mobile_nav_sx.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-04 19:30:45 +00:00
parent 57e0d0c341
commit 959e63d440
61 changed files with 1352 additions and 1208 deletions

View File

@@ -3,7 +3,7 @@ from __future__ import annotations
from markupsafe import escape
from shared.sx.helpers import render_to_sx, render_to_sx_with_env
from shared.sx.helpers import sx_call
from shared.sx.parser import SxExpr
from .utils import (
@@ -16,7 +16,7 @@ from .utils import (
# All events / page summary entry cards
# ---------------------------------------------------------------------------
async def _entry_card_html(entry, page_info: dict, pending_tickets: dict,
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."""
@@ -35,36 +35,36 @@ async def _entry_card_html(entry, page_info: dict, pending_tickets: dict,
# Title (linked or plain)
if entry_href:
title_html = await render_to_sx("events-entry-title-linked",
title_html = sx_call("events-entry-title-linked",
href=entry_href, name=entry.name)
else:
title_html = await render_to_sx("events-entry-title-plain", name=entry.name)
title_html = sx_call("events-entry-title-plain", name=entry.name)
# Badges
badges_html = ""
if page_title and (not is_page_scoped or page_title != (post or {}).get("title")):
page_href = events_url_fn(f"/{page_slug}/")
badges_html += await render_to_sx("events-entry-page-badge",
badges_html += sx_call("events-entry-page-badge",
href=page_href, title=page_title)
cal_name = getattr(entry, "calendar_name", "")
if cal_name:
badges_html += await render_to_sx("events-entry-cal-badge", name=cal_name)
badges_html += sx_call("events-entry-cal-badge", name=cal_name)
# Time line
time_parts = ""
if day_href and not is_page_scoped:
time_parts += await render_to_sx("events-entry-time-linked",
time_parts += sx_call("events-entry-time-linked",
href=day_href,
date_str=entry.start_at.strftime("%a %-d %b"))
elif not is_page_scoped:
time_parts += await render_to_sx("events-entry-time-plain",
time_parts += sx_call("events-entry-time-plain",
date_str=entry.start_at.strftime("%a %-d %b"))
time_parts += entry.start_at.strftime("%H:%M")
if entry.end_at:
time_parts += f' \u2013 {entry.end_at.strftime("%H:%M")}'
cost = getattr(entry, "cost", None)
cost_html = await render_to_sx("events-entry-cost",
cost_html = sx_call("events-entry-cost",
cost=f"\u00a3{cost:.2f}") if cost else ""
# Ticket widget
@@ -72,16 +72,16 @@ async def _entry_card_html(entry, page_info: dict, pending_tickets: dict,
widget_html = ""
if tp is not None:
qty = pending_tickets.get(entry.id, 0)
widget_html = await render_to_sx("events-entry-widget-wrapper",
widget=SxExpr(await _ticket_widget_html(entry, qty, ticket_url, ctx={})))
widget_html = sx_call("events-entry-widget-wrapper",
widget=SxExpr(_ticket_widget_html(entry, qty, ticket_url, ctx={})))
return await render_to_sx("events-entry-card",
return sx_call("events-entry-card",
title=SxExpr(title_html), badges=SxExpr(badges_html),
time_parts=SxExpr(time_parts), cost=SxExpr(cost_html),
widget=SxExpr(widget_html))
async def _entry_card_tile_html(entry, page_info: dict, pending_tickets: dict,
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."""
@@ -100,25 +100,25 @@ async def _entry_card_tile_html(entry, page_info: dict, pending_tickets: dict,
# Title
if entry_href:
title_html = await render_to_sx("events-entry-title-tile-linked",
title_html = sx_call("events-entry-title-tile-linked",
href=entry_href, name=entry.name)
else:
title_html = await render_to_sx("events-entry-title-tile-plain", name=entry.name)
title_html = sx_call("events-entry-title-tile-plain", name=entry.name)
# Badges
badges_html = ""
if page_title and (not is_page_scoped or page_title != (post or {}).get("title")):
page_href = events_url_fn(f"/{page_slug}/")
badges_html += await render_to_sx("events-entry-page-badge",
badges_html += sx_call("events-entry-page-badge",
href=page_href, title=page_title)
cal_name = getattr(entry, "calendar_name", "")
if cal_name:
badges_html += await render_to_sx("events-entry-cal-badge", name=cal_name)
badges_html += sx_call("events-entry-cal-badge", name=cal_name)
# Time
time_html = ""
if day_href:
time_html += (await render_to_sx("events-entry-time-linked",
time_html += (sx_call("events-entry-time-linked",
href=day_href,
date_str=entry.start_at.strftime("%a %-d %b"))).replace(" &middot; ", "")
else:
@@ -128,7 +128,7 @@ async def _entry_card_tile_html(entry, page_info: dict, pending_tickets: dict,
time_html += f' \u2013 {entry.end_at.strftime("%H:%M")}'
cost = getattr(entry, "cost", None)
cost_html = await render_to_sx("events-entry-cost",
cost_html = sx_call("events-entry-cost",
cost=f"\u00a3{cost:.2f}") if cost else ""
# Ticket widget
@@ -136,16 +136,16 @@ async def _entry_card_tile_html(entry, page_info: dict, pending_tickets: dict,
widget_html = ""
if tp is not None:
qty = pending_tickets.get(entry.id, 0)
widget_html = await render_to_sx("events-entry-tile-widget-wrapper",
widget=SxExpr(await _ticket_widget_html(entry, qty, ticket_url, ctx={})))
widget_html = sx_call("events-entry-tile-widget-wrapper",
widget=SxExpr(_ticket_widget_html(entry, qty, ticket_url, ctx={})))
return await render_to_sx("events-entry-card-tile",
return sx_call("events-entry-card-tile",
title=SxExpr(title_html), badges=SxExpr(badges_html),
time=SxExpr(time_html), cost=SxExpr(cost_html),
widget=SxExpr(widget_html))
async def _entry_cards_html(entries, page_info, pending_tickets, ticket_url,
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."""
@@ -153,23 +153,23 @@ async def _entry_cards_html(entries, page_info, pending_tickets, ticket_url,
last_date = None
for entry in entries:
if view == "tile":
parts.append(await _entry_card_tile_html(
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(await render_to_sx("events-date-separator",
parts.append(sx_call("events-date-separator",
date_str=entry_date))
last_date = entry_date
parts.append(await _entry_card_html(
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(await render_to_sx("sentinel-simple",
parts.append(sx_call("sentinel-simple",
id=f"sentinel-{page}", next_url=next_url))
return "".join(parts)
@@ -178,27 +178,27 @@ async def _entry_cards_html(entries, page_info, pending_tickets, ticket_url,
# All events / page summary main panels
# ---------------------------------------------------------------------------
async def _events_main_panel_html(ctx: dict, entries, has_more, pending_tickets, page_info,
def _events_main_panel_html(ctx: dict, entries, has_more, pending_tickets, page_info,
page, view, ticket_url, next_url, events_url_fn,
*, is_page_scoped=False, post=None) -> str:
"""Render the events main panel with view toggle + cards."""
toggle = await _view_toggle_html(ctx, view)
toggle = _view_toggle_html(ctx, view)
if entries:
cards = await _entry_cards_html(
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 = await render_to_sx("events-grid", grid_cls=grid_cls, cards=SxExpr(cards))
body = sx_call("events-grid", grid_cls=grid_cls, cards=SxExpr(cards))
else:
body = await render_to_sx("empty-state", icon="fa fa-calendar-xmark",
body = sx_call("empty-state", icon="fa fa-calendar-xmark",
message="No upcoming events",
cls="px-3 py-12 text-center text-stone-400")
return await render_to_sx("events-main-panel-body",
return sx_call("events-main-panel-body",
toggle=SxExpr(toggle), body=SxExpr(body))
@@ -206,7 +206,7 @@ async def _events_main_panel_html(ctx: dict, entries, has_more, pending_tickets,
# Entry main panel
# ---------------------------------------------------------------------------
async def _entry_main_panel_html(ctx: dict) -> str:
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
@@ -228,63 +228,63 @@ async def _entry_main_panel_html(ctx: dict) -> str:
eid = entry.id
state = getattr(entry, "state", "pending") or "pending"
async def _field(label, content_html):
return await render_to_sx("events-entry-field", label=label, content=SxExpr(content_html))
def _field(label, content_html):
return sx_call("events-entry-field", label=label, content=SxExpr(content_html))
# Name
name_html = await _field("Name", await render_to_sx("events-entry-name-field", name=entry.name))
name_html = _field("Name", sx_call("events-entry-name-field", name=entry.name))
# Slot
slot = getattr(entry, "slot", None)
if slot:
flex_label = "(flexible)" if getattr(slot, "flexible", False) else "(fixed)"
slot_inner = await render_to_sx("events-entry-slot-assigned",
slot_inner = sx_call("events-entry-slot-assigned",
slot_name=slot.name, flex_label=flex_label)
else:
slot_inner = await render_to_sx("events-entry-slot-none")
slot_html = await _field("Slot", slot_inner)
slot_inner = sx_call("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 = await _field("Time Period", await render_to_sx("events-entry-time-field",
time_html = _field("Time Period", sx_call("events-entry-time-field",
time_str=start_str + end_str))
# State
state_html = await _field("State", await render_to_sx("events-entry-state-field",
state_html = _field("State", sx_call("events-entry-state-field",
entry_id=str(eid),
badge=SxExpr(await _entry_state_badge_html(state))))
badge=SxExpr(_entry_state_badge_html(state))))
# Cost
cost = getattr(entry, "cost", None)
cost_str = f"{cost:.2f}" if cost is not None else "0.00"
cost_html = await _field("Cost", await render_to_sx("events-entry-cost-field",
cost_html = _field("Cost", sx_call("events-entry-cost-field",
cost=f"\u00a3{cost_str}"))
# Ticket Configuration (admin)
tickets_html = await _field("Tickets", await render_to_sx("events-entry-tickets-field",
tickets_html = _field("Tickets", sx_call("events-entry-tickets-field",
entry_id=str(eid),
tickets_config=SxExpr(await render_entry_tickets_config(entry, calendar, day, month, year))))
tickets_config=SxExpr(render_entry_tickets_config(entry, calendar, day, month, year))))
# Buy Tickets (public-facing)
ticket_remaining = ctx.get("ticket_remaining")
ticket_sold_count = ctx.get("ticket_sold_count", 0)
user_ticket_count = ctx.get("user_ticket_count", 0)
user_ticket_counts_by_type = ctx.get("user_ticket_counts_by_type") or {}
buy_html = await render_buy_form(
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 = await _field("Date", await render_to_sx("events-entry-date-field", date_str=date_str))
date_html = _field("Date", sx_call("events-entry-date-field", date_str=date_str))
# Associated Posts
entry_posts = ctx.get("entry_posts") or []
posts_html = await _field("Associated Posts", await render_to_sx("events-entry-posts-field",
posts_html = _field("Associated Posts", sx_call("events-entry-posts-field",
entry_id=str(eid),
posts_panel=SxExpr(await render_entry_posts_panel(entry_posts, entry, calendar, day, month, year))))
posts_panel=SxExpr(render_entry_posts_panel(entry_posts, entry, calendar, day, month, year))))
# Options and Edit Button
edit_url = url_for(
@@ -293,14 +293,14 @@ async def _entry_main_panel_html(ctx: dict) -> str:
day=day, month=month, year=year,
)
return await render_to_sx("events-entry-panel",
return sx_call("events-entry-panel",
entry_id=str(eid), list_container=list_container,
name=SxExpr(name_html), slot=SxExpr(slot_html),
time=SxExpr(time_html), state=SxExpr(state_html),
cost=SxExpr(cost_html), tickets=SxExpr(tickets_html),
buy=SxExpr(buy_html), date=SxExpr(date_html),
posts=SxExpr(posts_html),
options=SxExpr(await _entry_options_html(entry, calendar, day, month, year)),
options=SxExpr(_entry_options_html(entry, calendar, day, month, year)),
pre_action=pre_action, edit_url=edit_url)
@@ -308,7 +308,7 @@ async def _entry_main_panel_html(ctx: dict) -> str:
# Entry header row
# ---------------------------------------------------------------------------
async def _entry_header_html(ctx: dict, *, oob: bool = False) -> str:
def _entry_header_html(ctx: dict, *, oob: bool = False) -> str:
"""Build entry detail header row."""
from quart import url_for
@@ -329,19 +329,19 @@ async def _entry_header_html(ctx: dict, *, oob: bool = False) -> str:
year=year, month=month, day=day,
entry_id=entry.id,
)
label_html = await render_to_sx("events-entry-label",
label_html = sx_call("events-entry-label",
entry_id=str(entry.id),
title=SxExpr(await _entry_title_html(entry)),
times=SxExpr(await _entry_times_html(entry)))
title=SxExpr(_entry_title_html(entry)),
times=SxExpr(_entry_times_html(entry)))
nav_html = await _entry_nav_html(ctx)
nav_html = _entry_nav_html(ctx)
return await render_to_sx("menu-row-sx", id="entry-row", level=5,
return sx_call("menu-row-sx", id="entry-row", level=5,
link_href=link_href, link_label_content=SxExpr(label_html),
nav=SxExpr(nav_html) if nav_html else None, child_id="entry-header-child", oob=oob)
async def _entry_times_html(entry) -> str:
def _entry_times_html(entry) -> str:
"""Render entry times label."""
start = entry.start_at
end = entry.end_at
@@ -349,14 +349,14 @@ async def _entry_times_html(entry) -> str:
return ""
start_str = start.strftime("%H:%M")
end_str = f" \u2192 {end.strftime('%H:%M')}" if end else ""
return await render_to_sx("events-entry-times", time_str=start_str + end_str)
return sx_call("events-entry-times", time_str=start_str + end_str)
# ---------------------------------------------------------------------------
# Entry nav (desktop + admin link)
# ---------------------------------------------------------------------------
async def _entry_nav_html(ctx: dict) -> str:
def _entry_nav_html(ctx: dict) -> str:
"""Entry desktop nav: associated posts scrolling menu + admin link."""
from quart import url_for
@@ -387,12 +387,12 @@ async def _entry_nav_html(ctx: dict) -> str:
feat = getattr(ep, "feature_image", None)
href = blog_url_fn(f"/{slug}/") if blog_url_fn else f"/{slug}/"
if feat:
img_html = await render_to_sx("events-post-img", src=feat, alt=title)
img_html = sx_call("events-post-img", src=feat, alt=title)
else:
img_html = await render_to_sx("events-post-img-placeholder")
post_links += await render_to_sx("events-entry-nav-post-link",
img_html = sx_call("events-post-img-placeholder")
post_links += sx_call("events-entry-nav-post-link",
href=href, img=SxExpr(img_html), title=title)
parts.append((await render_to_sx("events-entry-posts-nav-oob",
parts.append((sx_call("events-entry-posts-nav-oob",
items=SxExpr(post_links))).replace(' :hx-swap-oob "true"', ''))
# Admin link
@@ -403,7 +403,7 @@ async def _entry_nav_html(ctx: dict) -> str:
day=day, month=month, year=year,
entry_id=entry.id,
)
parts.append(await render_to_sx("events-entry-admin-link", href=admin_url))
parts.append(sx_call("events-entry-admin-link", href=admin_url))
return "".join(parts)
@@ -412,26 +412,26 @@ async def _entry_nav_html(ctx: dict) -> str:
# Entry optioned (confirm/decline/provisional response)
# ---------------------------------------------------------------------------
async def render_entry_optioned(entry, calendar, day, month, year) -> str:
def render_entry_optioned(entry, calendar, day, month, year) -> str:
"""Render entry options buttons + OOB title & state swaps."""
options = await _entry_options_html(entry, calendar, day, month, year)
title = await _entry_title_html(entry)
state = await _entry_state_badge_html(getattr(entry, "state", "pending") or "pending")
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 + await render_to_sx("events-entry-optioned-oob",
return options + sx_call("events-entry-optioned-oob",
entry_id=str(entry.id),
title=SxExpr(title), state=SxExpr(state))
async def _entry_title_html(entry) -> str:
def _entry_title_html(entry) -> str:
"""Render entry title (icon + name + state badge)."""
state = getattr(entry, "state", "pending") or "pending"
return await render_to_sx("events-entry-title",
return sx_call("events-entry-title",
name=entry.name,
badge=SxExpr(await _entry_state_badge_html(state)))
badge=SxExpr(_entry_state_badge_html(state)))
async def _entry_options_html(entry, calendar, day, month, year) -> str:
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
@@ -445,13 +445,13 @@ async def _entry_options_html(entry, calendar, day, month, year) -> str:
state = getattr(entry, "state", "pending") or "pending"
target = f"#calendar_entry_options_{eid}"
async def _make_button(action_name, label, confirm_title, confirm_text, *, trigger_type="submit"):
def _make_button(action_name, label, confirm_title, confirm_text, *, trigger_type="submit"):
url = url_for(
f"calendar.day.calendar_entries.calendar_entry.{action_name}",
calendar_slug=cal_slug, day=day, month=month, year=year, entry_id=eid,
)
btn_type = "button" if trigger_type == "button" else "submit"
return await render_to_sx("events-entry-option-button",
return sx_call("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,
@@ -459,22 +459,22 @@ async def _entry_options_html(entry, calendar, day, month, year) -> str:
buttons_html = ""
if state == "provisional":
buttons_html += await _make_button(
buttons_html += _make_button(
"confirm_entry", "confirm",
"Confirm entry?", "Are you sure you want to confirm this entry?",
)
buttons_html += await _make_button(
buttons_html += _make_button(
"decline_entry", "decline",
"Decline entry?", "Are you sure you want to decline this entry?",
)
elif state == "confirmed":
buttons_html += await _make_button(
buttons_html += _make_button(
"provisional_entry", "provisional",
"Provisional entry?", "Are you sure you want to provisional this entry?",
trigger_type="button",
)
return await render_to_sx("events-entry-options",
return sx_call("events-entry-options",
entry_id=str(eid), buttons=SxExpr(buttons_html))
@@ -482,7 +482,7 @@ async def _entry_options_html(entry, calendar, day, month, year) -> str:
# Entry tickets config (display + form)
# ---------------------------------------------------------------------------
async def render_entry_tickets_config(entry, calendar, day, month, year) -> str:
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
@@ -499,11 +499,11 @@ async def render_entry_tickets_config(entry, calendar, day, month, year) -> str:
if tp is not None:
tc_str = f"{tc} tickets" if tc is not None else "Unlimited"
display_html = await render_to_sx("events-ticket-config-display",
display_html = sx_call("events-ticket-config-display",
price_str=f"\u00a3{tp:.2f}",
count_str=tc_str, show_js=show_js)
else:
display_html = await render_to_sx("events-ticket-config-none", show_js=show_js)
display_html = sx_call("events-ticket-config-none", show_js=show_js)
update_url = url_for(
"calendar.day.calendar_entries.calendar_entry.update_tickets",
@@ -513,7 +513,7 @@ async def render_entry_tickets_config(entry, calendar, day, month, year) -> str:
tp_val = f"{tp:.2f}" if tp is not None else ""
tc_val = str(tc) if tc is not None else ""
form_html = await render_to_sx("events-ticket-config-form",
form_html = sx_call("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)
@@ -524,7 +524,7 @@ async def render_entry_tickets_config(entry, calendar, day, month, year) -> str:
# Entry posts panel
# ---------------------------------------------------------------------------
async def render_entry_posts_panel(entry_posts, entry, calendar, day, month, year) -> str:
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
@@ -541,28 +541,28 @@ async def render_entry_posts_panel(entry_posts, entry, calendar, day, month, yea
ep_title = getattr(ep, "title", "")
ep_id = getattr(ep, "id", 0)
feat = getattr(ep, "feature_image", None)
img_html = (await render_to_sx("events-post-img", src=feat, alt=ep_title)
if feat else await render_to_sx("events-post-img-placeholder"))
img_html = (sx_call("events-post-img", src=feat, alt=ep_title)
if feat else sx_call("events-post-img-placeholder"))
del_url = url_for(
"calendar.day.calendar_entries.calendar_entry.remove_post",
calendar_slug=cal_slug, day=day, month=month, year=year,
entry_id=eid, post_id=ep_id,
)
items += await render_to_sx("events-entry-post-item",
items += sx_call("events-entry-post-item",
img=SxExpr(img_html), title=ep_title,
del_url=del_url, entry_id=eid_s,
csrf_hdr=f'{{"X-CSRFToken": "{csrf}"}}')
posts_html = await render_to_sx("events-entry-posts-list", items=SxExpr(items))
posts_html = sx_call("events-entry-posts-list", items=SxExpr(items))
else:
posts_html = await render_to_sx("events-entry-posts-none")
posts_html = sx_call("events-entry-posts-none")
search_url = url_for(
"calendar.day.calendar_entries.calendar_entry.search_posts",
calendar_slug=cal_slug, day=day, month=month, year=year, entry_id=eid,
)
return await render_to_sx("events-entry-posts-panel",
return sx_call("events-entry-posts-panel",
posts=SxExpr(posts_html), search_url=search_url,
entry_id=eid_s)
@@ -571,7 +571,7 @@ async def render_entry_posts_panel(entry_posts, entry, calendar, day, month, yea
# Entry posts nav OOB
# ---------------------------------------------------------------------------
async def render_entry_posts_nav_oob(entry_posts) -> str:
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 {}
@@ -579,7 +579,7 @@ async def render_entry_posts_nav_oob(entry_posts) -> str:
blog_url_fn = getattr(g, "blog_url", None)
if not entry_posts:
return await render_to_sx("events-entry-posts-nav-oob-empty")
return sx_call("events-entry-posts-nav-oob-empty")
items = ""
for ep in entry_posts:
@@ -587,20 +587,20 @@ async def render_entry_posts_nav_oob(entry_posts) -> str:
title = getattr(ep, "title", "")
feat = getattr(ep, "feature_image", None)
href = blog_url_fn(f"/{slug}/") if blog_url_fn else f"/{slug}/"
img_html = (await render_to_sx("events-post-img", src=feat, alt=title)
if feat else await render_to_sx("events-post-img-placeholder"))
items += await render_to_sx("events-entry-nav-post",
img_html = (sx_call("events-post-img", src=feat, alt=title)
if feat else sx_call("events-post-img-placeholder"))
items += sx_call("events-entry-nav-post",
href=href, nav_btn=nav_btn,
img=SxExpr(img_html), title=title)
return await render_to_sx("events-entry-posts-nav-oob", items=SxExpr(items))
return sx_call("events-entry-posts-nav-oob", items=SxExpr(items))
# ---------------------------------------------------------------------------
# Day entries nav OOB
# ---------------------------------------------------------------------------
async def render_day_entries_nav_oob(confirmed_entries, calendar, day_date) -> str:
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
@@ -609,7 +609,7 @@ async def render_day_entries_nav_oob(confirmed_entries, calendar, day_date) -> s
cal_slug = getattr(calendar, "slug", "")
if not confirmed_entries:
return await render_to_sx("events-day-entries-nav-oob-empty")
return sx_call("events-day-entries-nav-oob-empty")
items = ""
for entry in confirmed_entries:
@@ -621,18 +621,18 @@ async def render_day_entries_nav_oob(confirmed_entries, calendar, day_date) -> s
)
start = entry.start_at.strftime("%H:%M") if entry.start_at else ""
end = f" \u2013 {entry.end_at.strftime('%H:%M')}" if entry.end_at else ""
items += await render_to_sx("events-day-nav-entry",
items += sx_call("events-day-nav-entry",
href=href, nav_btn=nav_btn,
name=entry.name, time_str=start + end)
return await render_to_sx("events-day-entries-nav-oob", items=SxExpr(items))
return sx_call("events-day-entries-nav-oob", items=SxExpr(items))
# ---------------------------------------------------------------------------
# Post nav entries OOB
# ---------------------------------------------------------------------------
async def render_post_nav_entries_oob(associated_entries, calendars, post) -> str:
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
@@ -644,7 +644,7 @@ async def render_post_nav_entries_oob(associated_entries, calendars, post) -> st
has_items = has_entries or calendars
if not has_items:
return await render_to_sx("events-post-nav-oob-empty")
return sx_call("events-post-nav-oob-empty")
slug = post.get("slug", "") if isinstance(post, dict) else getattr(post, "slug", "")
@@ -659,7 +659,7 @@ async def render_post_nav_entries_oob(associated_entries, calendars, post) -> st
href = events_url(entry_path)
time_str = entry.start_at.strftime("%b %d, %Y at %H:%M")
end_str = f" \u2013 {entry.end_at.strftime('%H:%M')}" if entry.end_at else ""
items += await render_to_sx("events-post-nav-entry",
items += sx_call("events-post-nav-entry",
href=href, nav_btn=nav_btn,
name=entry.name, time_str=time_str + end_str)
@@ -667,7 +667,7 @@ async def render_post_nav_entries_oob(associated_entries, calendars, post) -> st
for cal in calendars:
cs = getattr(cal, "slug", "")
local_href = events_url(f"/{slug}/{cs}/")
items += await render_to_sx("events-post-nav-calendar",
items += sx_call("events-post-nav-calendar",
href=local_href, nav_btn=nav_btn, name=cal.name)
hs = ("on load or scroll "
@@ -675,7 +675,7 @@ async def render_post_nav_entries_oob(associated_entries, calendars, post) -> st
"remove .hidden from .entries-nav-arrow add .flex to .entries-nav-arrow "
"else add .hidden to .entries-nav-arrow remove .flex from .entries-nav-arrow end")
return await render_to_sx("events-post-nav-wrapper",
return sx_call("events-post-nav-wrapper",
items=SxExpr(items), hyperscript=hs)
@@ -683,23 +683,23 @@ async def render_post_nav_entries_oob(associated_entries, calendars, post) -> st
# Calendar description display + edit form
# ---------------------------------------------------------------------------
async def render_calendar_description(calendar, *, oob: bool = False) -> str:
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
from .calendar import _calendar_description_display_html
cal_slug = getattr(calendar, "slug", "")
edit_url = url_for("calendar.admin.calendar_description_edit", calendar_slug=cal_slug)
html = await _calendar_description_display_html(calendar, edit_url)
html = _calendar_description_display_html(calendar, edit_url)
if oob:
desc = getattr(calendar, "description", "") or ""
html += await render_to_sx("events-calendar-description-title-oob",
html += sx_call("events-calendar-description-title-oob",
description=desc)
return html
async def render_calendar_description_edit(calendar) -> str:
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
@@ -710,7 +710,7 @@ async def render_calendar_description_edit(calendar) -> str:
save_url = url_for("calendar.admin.calendar_description_save", calendar_slug=cal_slug)
cancel_url = url_for("calendar.admin.calendar_description_view", calendar_slug=cal_slug)
return await render_to_sx("events-calendar-description-edit-form",
return sx_call("events-calendar-description-edit-form",
save_url=save_url, cancel_url=cancel_url,
csrf=csrf, description=desc)
@@ -719,7 +719,7 @@ async def render_calendar_description_edit(calendar) -> str:
# Entry admin page / OOB
# ---------------------------------------------------------------------------
async def _entry_admin_header_html(ctx: dict, *, oob: bool = False) -> str:
def _entry_admin_header_html(ctx: dict, *, oob: bool = False) -> str:
"""Build the entry admin header row."""
from quart import url_for
@@ -739,14 +739,14 @@ async def _entry_admin_header_html(ctx: dict, *, oob: bool = False) -> str:
calendar_slug=cal_slug, day=day, month=month, year=year, entry_id=entry.id,
)
# Nav: ticket_types link
nav_html = await _entry_admin_nav_html(ctx)
nav_html = _entry_admin_nav_html(ctx)
return await render_to_sx("menu-row-sx", id="entry-admin-row", level=6,
return sx_call("menu-row-sx", id="entry-admin-row", level=6,
link_href=link_href, link_label="admin", icon="fa fa-cog",
nav=SxExpr(nav_html) if nav_html else None, child_id="entry-admin-header-child", oob=oob)
async def _entry_admin_nav_html(ctx: dict) -> str:
def _entry_admin_nav_html(ctx: dict) -> str:
"""Entry admin nav: ticket_types link."""
from quart import url_for
@@ -765,11 +765,11 @@ async def _entry_admin_nav_html(ctx: dict) -> str:
href = url_for("calendar.day.calendar_entries.calendar_entry.ticket_types.get",
calendar_slug=cal_slug, entry_id=entry.id,
year=year, month=month, day=day)
return await render_to_sx("nav-link", href=href, label="ticket_types",
return sx_call("nav-link", href=href, label="ticket_types",
select_colours=select_colours)
async def _entry_admin_main_panel_html(ctx: dict) -> str:
def _entry_admin_main_panel_html(ctx: dict) -> str:
"""Entry admin main panel: just a ticket_types link."""
from quart import url_for
@@ -789,7 +789,7 @@ async def _entry_admin_main_panel_html(ctx: dict) -> str:
href = url_for("calendar.day.calendar_entries.calendar_entry.ticket_types.get",
calendar_slug=cal_slug, entry_id=entry.id,
year=year, month=month, day=day)
return await render_to_sx("nav-link", href=href, label="ticket_types",
return sx_call("nav-link", href=href, label="ticket_types",
select_colours=select_colours, aclass=nav_btn,
is_selected=False)
@@ -798,7 +798,7 @@ async def _entry_admin_main_panel_html(ctx: dict) -> str:
# Post search results
# ---------------------------------------------------------------------------
async def render_post_search_results(search_posts, search_query, page, total_pages,
def render_post_search_results(search_posts, search_query, page, total_pages,
entry, calendar, day, month, year) -> str:
"""Render post search results (replaces _types/entry/_post_search_results.html)."""
from quart import url_for
@@ -816,11 +816,11 @@ async def render_post_search_results(search_posts, search_query, page, total_pag
feat = getattr(sp, "feature_image", None)
title = getattr(sp, "title", "")
if feat:
img_html = await render_to_sx("events-post-img", src=feat, alt=title)
img_html = sx_call("events-post-img", src=feat, alt=title)
else:
img_html = await render_to_sx("events-post-img-placeholder")
img_html = sx_call("events-post-img-placeholder")
parts.append(await render_to_sx("events-post-search-item",
parts.append(sx_call("events-post-search-item",
post_url=post_url, entry_id=str(eid), csrf=csrf,
post_id=str(sp.id), img=SxExpr(img_html), title=title))
@@ -830,10 +830,10 @@ async def render_post_search_results(search_posts, search_query, page, total_pag
next_url = url_for("calendar.day.calendar_entries.calendar_entry.search_posts",
calendar_slug=cal_slug, day=day, month=month, year=year,
entry_id=eid, q=search_query, page=page + 1)
result += await render_to_sx("events-post-search-sentinel",
result += sx_call("events-post-search-sentinel",
page=str(page), next_url=next_url)
elif search_posts:
result += await render_to_sx("events-post-search-end")
result += sx_call("events-post-search-end")
return result
@@ -842,7 +842,7 @@ async def render_post_search_results(search_posts, search_query, page, total_pag
# Entry edit form
# ---------------------------------------------------------------------------
async def render_entry_edit_form(entry, calendar, day, month, year, day_slots) -> str:
def render_entry_edit_form(entry, calendar, day, month, year, day_slots) -> str:
"""Render entry edit form (replaces _types/entry/_edit.html)."""
from quart import url_for, g
from shared.browser.app.csrf import generate_csrf_token
@@ -863,11 +863,11 @@ async def render_entry_edit_form(entry, calendar, day, month, year, day_slots) -
# Slot picker
if day_slots:
options_html = await _slot_options_html(day_slots, selected_slot_id=getattr(entry, "slot_id", None))
slot_picker_html = await render_to_sx("events-slot-picker",
options_html = _slot_options_html(day_slots, selected_slot_id=getattr(entry, "slot_id", None))
slot_picker_html = sx_call("events-slot-picker",
id=f"entry-slot-{eid}", options=SxExpr(options_html))
else:
slot_picker_html = await render_to_sx("events-no-slots")
slot_picker_html = sx_call("events-no-slots")
# Values
start_val = entry.start_at.strftime("%H:%M") if entry.start_at else ""
@@ -879,7 +879,7 @@ async def render_entry_edit_form(entry, calendar, day, month, year, day_slots) -
tp_val = f"{tp:.2f}" if tp is not None else ""
tc_val = str(tc) if tc is not None else ""
html = await render_to_sx("events-entry-edit-form",
html = sx_call("events-entry-edit-form",
entry_id=str(eid), list_container=list_container,
put_url=put_url, cancel_url=cancel_url, csrf=csrf,
name_val=entry.name or "", slot_picker=SxExpr(slot_picker_html),
@@ -893,7 +893,7 @@ async def render_entry_edit_form(entry, calendar, day, month, year, day_slots) -
# Entry add form / button
# ---------------------------------------------------------------------------
async def render_entry_add_form(calendar, day, month, year, day_slots) -> str:
def render_entry_add_form(calendar, day, month, year, day_slots) -> str:
"""Render entry add form (replaces _types/day/_add.html)."""
from quart import url_for, g
from shared.browser.app.csrf import generate_csrf_token
@@ -912,13 +912,13 @@ async def render_entry_add_form(calendar, day, month, year, day_slots) -> str:
# Slot picker
if day_slots:
options_html = await _slot_options_html(day_slots)
slot_picker_html = await render_to_sx("events-slot-picker",
options_html = _slot_options_html(day_slots)
slot_picker_html = sx_call("events-slot-picker",
id="entry-slot-new", options=SxExpr(options_html))
else:
slot_picker_html = await render_to_sx("events-no-slots")
slot_picker_html = sx_call("events-no-slots")
html = await render_to_sx("events-entry-add-form",
html = sx_call("events-entry-add-form",
post_url=post_url, csrf=csrf,
slot_picker=SxExpr(slot_picker_html),
action_btn=action_btn, cancel_btn=cancel_btn,
@@ -926,7 +926,7 @@ async def render_entry_add_form(calendar, day, month, year, day_slots) -> str:
return html + _SLOT_PICKER_JS
async def render_entry_add_button(calendar, day, month, year) -> str:
def render_entry_add_button(calendar, day, month, year) -> str:
"""Render entry add button (replaces _types/day/_add_button.html)."""
from quart import url_for, g
@@ -936,14 +936,14 @@ async def render_entry_add_button(calendar, day, month, year) -> str:
add_url = url_for("calendar.day.calendar_entries.add_form",
calendar_slug=cal_slug, day=day, month=month, year=year)
return await render_to_sx("events-entry-add-button", pre_action=pre_action, add_url=add_url)
return sx_call("events-entry-add-button", pre_action=pre_action, add_url=add_url)
# ---------------------------------------------------------------------------
# Fragment: container cards entries
# ---------------------------------------------------------------------------
async def render_fragment_container_cards(batch, post_ids, slug_map) -> str:
def render_fragment_container_cards(batch, post_ids, slug_map) -> str:
"""Render container cards entries (replaces fragments/container_cards_entries.html)."""
from shared.infrastructure.urls import events_url
@@ -963,12 +963,12 @@ async def render_fragment_container_cards(batch, post_ids, slug_map) -> str:
time_str = entry.start_at.strftime("%H:%M")
if entry.end_at:
time_str += f" \u2013 {entry.end_at.strftime('%H:%M')}"
cards_html += await render_to_sx("events-frag-entry-card",
cards_html += sx_call("events-frag-entry-card",
href=events_url(_entry_path),
name=entry.name,
date_str=entry.start_at.strftime("%a, %b %d"),
time_str=time_str)
parts.append(await render_to_sx("events-frag-entries-widget", cards=SxExpr(cards_html)))
parts.append(sx_call("events-frag-entries-widget", cards=SxExpr(cards_html)))
parts.append(f"<!-- /card-widget:{post_id} -->")
return "\n".join(parts)
@@ -978,7 +978,7 @@ async def render_fragment_container_cards(batch, post_ids, slug_map) -> str:
# Fragment: account page tickets
# ---------------------------------------------------------------------------
async def render_fragment_account_tickets(tickets) -> str:
def render_fragment_account_tickets(tickets) -> str:
"""Render account page tickets (replaces fragments/account_page_tickets.html)."""
from shared.infrastructure.urls import events_url
@@ -993,25 +993,25 @@ async def render_fragment_account_tickets(tickets) -> str:
type_name = ""
if getattr(ticket, "ticket_type_name", None):
type_name = f'<span>&middot; {escape(ticket.ticket_type_name)}</span>'
badge_html = await render_to_sx("status-pill",
badge_html = sx_call("status-pill",
status=getattr(ticket, "state", ""))
items_html += await render_to_sx("events-frag-ticket-item",
items_html += sx_call("events-frag-ticket-item",
href=href, entry_name=ticket.entry_name,
date_str=date_str, calendar_name=cal_name,
type_name=type_name, badge=SxExpr(badge_html))
body = await render_to_sx("events-frag-tickets-list", items=SxExpr(items_html))
body = sx_call("events-frag-tickets-list", items=SxExpr(items_html))
else:
body = await render_to_sx("empty-state", message="No tickets yet.",
body = sx_call("empty-state", message="No tickets yet.",
cls="text-sm text-stone-500")
return await render_to_sx("events-frag-tickets-panel", items=SxExpr(body))
return sx_call("events-frag-tickets-panel", items=SxExpr(body))
# ---------------------------------------------------------------------------
# Fragment: account page bookings
# ---------------------------------------------------------------------------
async def render_fragment_account_bookings(bookings) -> str:
def render_fragment_account_bookings(bookings) -> str:
"""Render account page bookings (replaces fragments/account_page_bookings.html)."""
if bookings:
items_html = ""
@@ -1027,16 +1027,16 @@ async def render_fragment_account_bookings(bookings) -> str:
cost_str = ""
if getattr(booking, "cost", None):
cost_str = f'<span>&middot; &pound;{escape(str(booking.cost))}</span>'
badge_html = await render_to_sx("status-pill",
badge_html = sx_call("status-pill",
status=getattr(booking, "state", ""))
items_html += await render_to_sx("events-frag-booking-item",
items_html += sx_call("events-frag-booking-item",
name=booking.name,
date_str=date_str + date_str_extra,
calendar_name=cal_name, cost_str=cost_str,
badge=SxExpr(badge_html))
body = await render_to_sx("events-frag-bookings-list", items=SxExpr(items_html))
body = sx_call("events-frag-bookings-list", items=SxExpr(items_html))
else:
body = await render_to_sx("empty-state", message="No bookings yet.",
body = sx_call("empty-state", message="No bookings yet.",
cls="text-sm text-stone-500")
return await render_to_sx("events-frag-bookings-panel", items=SxExpr(body))
return sx_call("events-frag-bookings-panel", items=SxExpr(body))