SxExpr is now a str subclass so it works everywhere a plain string does (join, isinstance, f-strings) while serialize() still emits it unquoted. sx_call() and all internal render functions (_render_to_sx, async_eval_to_sx, etc.) return SxExpr, eliminating the "forgot to wrap" bug class that caused the sx_content leak and list serialization bugs. - Phase 0: SxExpr(str) with .source property, __add__/__radd__ - Phase 1: sx_call returns SxExpr (drop-in, all 200+ sites unchanged) - Phase 2: async_eval_to_sx, async_eval_slot_to_sx, _render_to_sx, mobile_menu_sx return SxExpr; remove isinstance(str) workaround - Phase 3: Remove ~150 redundant SxExpr() wrappings across 45 files - Phase 4: serialize() docstring, handler return docs, ;; returns: sx Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
171 lines
5.9 KiB
Python
171 lines
5.9 KiB
Python
"""Badge helpers, OOB helpers, formatting utilities, list containers."""
|
|
from __future__ import annotations
|
|
|
|
from shared.sx.helpers import sx_call
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# OOB header helper — delegates to shared
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Post header helpers — thin wrapper over shared post_header_sx
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def _clear_oob(*ids: str) -> str:
|
|
"""Generate OOB swaps to remove orphaned header rows/children."""
|
|
from shared.sx.helpers import sx_call
|
|
return "".join(sx_call("clear-oob-div", id=i) for i in ids)
|
|
|
|
|
|
# All possible header row/child IDs at each depth (deepest first)
|
|
_EVENTS_DEEP_IDS = [
|
|
"entry-admin-row", "entry-admin-header-child",
|
|
"entry-row", "entry-header-child",
|
|
"day-admin-row", "day-admin-header-child",
|
|
"day-row", "day-header-child",
|
|
"calendar-admin-row", "calendar-admin-header-child",
|
|
"calendar-row", "calendar-header-child",
|
|
"calendars-row", "calendars-header-child",
|
|
"post-admin-row", "post-admin-header-child",
|
|
]
|
|
|
|
|
|
def _clear_deeper_oob(*keep_ids: str) -> str:
|
|
"""Clear all events header rows/children NOT in keep_ids."""
|
|
to_clear = [i for i in _EVENTS_DEEP_IDS if i not in keep_ids]
|
|
return _clear_oob(*to_clear)
|
|
|
|
|
|
async def _ensure_container_nav(ctx: dict) -> dict:
|
|
"""Fetch container_nav if not already present (for post header row)."""
|
|
if ctx.get("container_nav"):
|
|
return ctx
|
|
post = ctx.get("post") or {}
|
|
post_id = post.get("id")
|
|
slug = post.get("slug", "")
|
|
if not post_id:
|
|
return ctx
|
|
from quart import g
|
|
from shared.infrastructure.fragments import fetch_fragments
|
|
current_cal = getattr(g, "calendar_slug", "") or ""
|
|
nav_params = {
|
|
"container_type": "page",
|
|
"container_id": str(post_id),
|
|
"post_slug": slug,
|
|
"current_calendar": current_cal,
|
|
}
|
|
events_nav, market_nav = await fetch_fragments([
|
|
("events", "container-nav", nav_params),
|
|
("market", "container-nav", nav_params),
|
|
], required=False)
|
|
return {**ctx, "container_nav": events_nav + market_nav}
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 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", "")
|
|
|
|
|
|
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 sx_call("badge", cls=cls, label=label)
|
|
|
|
|
|
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 sx_call("badge", cls=cls, label=label)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# View toggle + SVG caching
|
|
# ---------------------------------------------------------------------------
|
|
|
|
_LIST_SVG = None
|
|
_TILE_SVG = None
|
|
|
|
|
|
def _get_list_svg():
|
|
global _LIST_SVG
|
|
if _LIST_SVG is None:
|
|
_LIST_SVG = sx_call("list-svg")
|
|
return _LIST_SVG
|
|
|
|
|
|
def _get_tile_svg():
|
|
global _TILE_SVG
|
|
if _TILE_SVG is None:
|
|
_TILE_SVG = sx_call("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 sx_call("view-toggle",
|
|
list_href=list_href, tile_href=tile_href,
|
|
hx_select=hx_select, list_cls=list_active,
|
|
tile_cls=tile_active, storage_key="events_view",
|
|
list_svg=_get_list_svg(), tile_svg=_get_tile_svg())
|
|
|
|
|
|
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 sx_call("events-cart-icon-logo",
|
|
blog_href=blog_href, logo=logo)
|
|
|
|
cart_href = cart_url_fn("/") if cart_url_fn else "/"
|
|
return sx_call("events-cart-icon-badge",
|
|
cart_href=cart_href, count=str(count))
|