Move events/market/blog composition from Python to .sx defcomps (Phase 9)
Some checks failed
Build and Deploy / build-and-deploy (push) Failing after 2m33s

Continues the pattern of eliminating Python sx_call tree-building in favour
of data-driven .sx defcomps. POST/PUT/DELETE routes now pass plain data
(dicts, lists, scalars) and let .sx handle iteration, conditionals, and
layout via map/let/when/if. Single response components wrap OOB swaps.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-05 08:17:09 +00:00
parent 877e776977
commit 51ebf347ba
23 changed files with 1841 additions and 1423 deletions

View File

@@ -8,8 +8,8 @@ from shared.sx.helpers import (
from shared.sx.parser import SxExpr
from .utils import (
_clear_deeper_oob, _ensure_container_nav,
_entry_state_badge_html, _list_container,
_ensure_container_nav,
_list_container,
)
@@ -27,45 +27,41 @@ def _post_nav_sx(ctx: dict) -> str:
"""Post desktop nav: calendar links + container nav (markets, etc.)."""
from quart import url_for, g
calendars = ctx.get("calendars") or []
calendars_orm = ctx.get("calendars") or []
select_colours = ctx.get("select_colours", "")
current_cal_slug = getattr(g, "calendar_slug", None)
parts = []
for cal in calendars:
calendars_data = []
for cal in calendars_orm:
cal_slug = getattr(cal, "slug", "") if hasattr(cal, "slug") else cal.get("slug", "")
cal_name = getattr(cal, "name", "") if hasattr(cal, "name") else cal.get("name", "")
href = url_for("calendar.get", calendar_slug=cal_slug)
is_sel = (cal_slug == current_cal_slug)
parts.append(sx_call("nav-link", href=href, icon="fa fa-calendar",
label=cal_name, select_colours=select_colours,
is_selected=is_sel))
# Container nav fragments (markets, etc.)
container_nav = ctx.get("container_nav", "")
if container_nav:
parts.append(container_nav)
calendars_data.append({
"href": href, "name": cal_name,
"is-selected": True if cal_slug == current_cal_slug else None,
})
container_nav = ctx.get("container_nav", "") or None
# Admin cog → blog admin for this post (cross-domain, no HTMX)
rights = ctx.get("rights") or {}
has_admin = rights.get("admin") if isinstance(rights, dict) else getattr(rights, "admin", False)
admin_href = None
aclass = None
if has_admin:
post = ctx.get("post") or {}
slug = post.get("slug", "")
styles = ctx.get("styles") or {}
nav_btn = styles.get("nav_button", "") if isinstance(styles, dict) else getattr(styles, "nav_button", "")
select_colours = ctx.get("select_colours", "")
admin_href = call_url(ctx, "blog_url", f"/{slug}/admin/")
aclass = f"{nav_btn} {select_colours}".strip() or (
"justify-center cursor-pointer flex flex-row items-center gap-2 "
"rounded bg-stone-200 text-black p-3"
)
parts.append(
f'<div class="relative nav-group">'
f'<a href="{admin_href}" class="{aclass}">'
f'<i class="fa fa-cog" aria-hidden="true"></i></a></div>'
)
return "".join(parts)
return sx_call("events-post-nav-from-data",
calendars=calendars_data or None, container_nav=container_nav,
select_colours=select_colours,
has_admin=has_admin or None, admin_href=admin_href, aclass=aclass)
# ---------------------------------------------------------------------------
@@ -119,15 +115,13 @@ def _calendar_nav_sx(ctx: dict) -> str:
is_admin = getattr(rights, "admin", False) if hasattr(rights, "admin") else rights.get("admin", False)
select_colours = ctx.get("select_colours", "")
parts = []
slots_href = url_for("defpage_slots_listing", calendar_slug=cal_slug)
parts.append(sx_call("nav-link", href=slots_href, icon="fa fa-clock",
label="Slots", select_colours=select_colours))
if is_admin:
admin_href = url_for("defpage_calendar_admin", calendar_slug=cal_slug)
parts.append(sx_call("nav-link", href=admin_href, icon="fa fa-cog",
select_colours=select_colours))
return "(<> " + " ".join(parts) + ")" if parts else ""
admin_href = url_for("defpage_calendar_admin", calendar_slug=cal_slug) if is_admin else None
return sx_call("events-calendar-nav-from-data",
slots_href=slots_href, admin_href=admin_href,
select_colours=select_colours,
is_admin=is_admin or None)
# ---------------------------------------------------------------------------
@@ -174,27 +168,21 @@ def _day_nav_sx(ctx: dict) -> str:
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(
"defpage_entry_detail",
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(sx_call("events-day-entry-link",
href=href, name=entry.name,
time_str=f"{start}{end}"))
inner = "".join(entry_links)
parts.append(sx_call("events-day-entries-nav", inner=SxExpr(inner)))
entries_data = []
for entry in confirmed_entries:
href = url_for(
"defpage_entry_detail",
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 ""
entries_data.append({"href": href, "name": entry.name, "time-str": f"{start}{end}"})
admin_href = None
if is_admin and day_date:
admin_href = url_for(
"defpage_day_admin",
@@ -203,8 +191,10 @@ def _day_nav_sx(ctx: dict) -> str:
month=day_date.month,
day=day_date.day,
)
parts.append(sx_call("nav-link", href=admin_href, icon="fa fa-cog"))
return "".join(parts)
return sx_call("events-day-nav-from-data",
entries=entries_data or None,
is_admin=is_admin or None, admin_href=admin_href)
# ---------------------------------------------------------------------------
@@ -245,17 +235,16 @@ def _calendar_admin_header_sx(ctx: dict, *, oob: bool = False) -> str:
cal_slug = getattr(calendar, "slug", "") if calendar else ""
select_colours = ctx.get("select_colours", "")
nav_parts = []
links_data = []
if cal_slug:
for endpoint, label in [
("defpage_slots_listing", "slots"),
("calendar.admin.calendar_description_edit", "description"),
]:
href = url_for(endpoint, calendar_slug=cal_slug)
nav_parts.append(sx_call("nav-link", href=href, label=label,
select_colours=select_colours))
links_data.append({"href": url_for(endpoint, calendar_slug=cal_slug), "label": label})
nav_html = "".join(nav_parts)
nav_html = sx_call("events-calendar-admin-nav-from-data",
links=links_data or None, select_colours=select_colours) if links_data else ""
return sx_call("menu-row-sx", id="calendar-admin-row", level=4,
link_label="admin", icon="fa fa-cog",
nav=SxExpr(nav_html) if nav_html else None, child_id="calendar-admin-header-child", oob=oob)
@@ -282,55 +271,36 @@ def _markets_header_sx(ctx: dict, *, oob: bool = False) -> str:
def _calendars_main_panel_sx(ctx: dict) -> str:
"""Render the calendars list + create form panel."""
from quart import url_for
from shared.utils import route_prefix
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 = sx_call("crud-create-form",
create_url=create_url, csrf=csrf,
errors_id="cal-create-errors", list_id="calendars-list",
placeholder="e.g. Events, Gigs, Meetings", btn_label="Add calendar")
list_html = _calendars_list_sx(ctx, calendars)
return sx_call("crud-panel",
form=SxExpr(form_html), list=SxExpr(list_html),
list_id="calendars-list")
def _calendars_list_sx(ctx: dict, calendars: list) -> str:
"""Render the calendars list items."""
from quart import url_for
from shared.utils import route_prefix
csrf_token = ctx.get("csrf_token")
csrf = csrf_token() if callable(csrf_token) else (csrf_token or "")
prefix = route_prefix()
if not calendars:
return sx_call("empty-state", message="No calendars yet. Create one above.",
cls="text-gray-500 mt-4")
parts = []
calendars = ctx.get("calendars") or []
items_data = []
for cal in calendars:
cal_slug = getattr(cal, "slug", "")
cal_name = getattr(cal, "name", "")
href = prefix + url_for("calendar.get", calendar_slug=cal_slug)
del_url = url_for("calendar.delete", calendar_slug=cal_slug)
csrf_hdr = f'{{"X-CSRFToken":"{csrf}"}}'
parts.append(sx_call("crud-item",
href=href, name=cal_name, slug=cal_slug,
del_url=del_url, csrf_hdr=csrf_hdr,
list_id="calendars-list",
confirm_title="Delete calendar?",
confirm_text="Entries will be hidden (soft delete)"))
return "".join(parts)
items_data.append({
"href": prefix + url_for("calendar.get", calendar_slug=cal_slug),
"name": cal_name, "slug": cal_slug,
"del-url": url_for("calendar.delete", calendar_slug=cal_slug),
"csrf-hdr": f'{{"X-CSRFToken":"{csrf}"}}',
"confirm-title": "Delete calendar?",
"confirm-text": "Entries will be hidden (soft delete)",
})
return sx_call("events-crud-panel-from-data",
can_create=can_create or None,
create_url=url_for("calendars.create_calendar") if can_create else None,
csrf=csrf, errors_id="cal-create-errors", list_id="calendars-list",
placeholder="e.g. Events, Gigs, Meetings", btn_label="Add calendar",
items=items_data or None,
empty_msg="No calendars yet. Create one above.")
# ---------------------------------------------------------------------------
@@ -338,7 +308,7 @@ def _calendars_list_sx(ctx: dict, calendars: list) -> str:
# ---------------------------------------------------------------------------
def _calendar_main_panel_html(ctx: dict) -> str:
"""Render the calendar month grid."""
"""Render the calendar month grid via data extraction + sx defcomp."""
from quart import url_for
from quart import session as qsession
@@ -346,7 +316,6 @@ def _calendar_main_panel_html(ctx: dict) -> str:
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", "")
@@ -368,35 +337,8 @@ def _calendar_main_panel_html(ctx: dict) -> str:
def nav_link(y, m):
return url_for("calendar.get", calendar_slug=cal_slug, year=y, month=m)
# Month navigation arrows
nav_arrows = []
for label, yr, mn in [
("\u00ab", prev_year, month),
("\u2039", prev_month_year, prev_month),
]:
href = nav_link(yr, mn)
nav_arrows.append(sx_call("events-calendar-nav-arrow",
pill_cls=pill_cls, href=href, label=label))
nav_arrows.append(sx_call("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(sx_call("events-calendar-nav-arrow",
pill_cls=pill_cls, href=href, label=label))
# Weekday headers
wd_parts = []
for wd in weekday_names:
wd_parts.append(sx_call("events-calendar-weekday", name=wd))
wd_html = "".join(wd_parts)
# Day cells
cells = []
# Day cells data
cells_data = []
for week in weeks:
for day_cell in week:
if isinstance(day_cell, dict):
@@ -414,24 +356,18 @@ def _calendar_main_panel_html(ctx: dict) -> str:
if is_today:
cell_cls += " ring-2 ring-blue-500 z-10 relative"
# Day number link
day_num_html = ""
day_short_html = ""
cell = {"cell-cls": cell_cls}
if day_date:
day_href = url_for(
cell["day-str"] = day_date.strftime("%a")
cell["day-href"] = url_for(
"calendar.day.show_day",
calendar_slug=cal_slug,
year=day_date.year, month=day_date.month, day=day_date.day,
)
day_short_html = sx_call("events-calendar-day-short",
day_str=day_date.strftime("%a"))
day_num_html = sx_call("events-calendar-day-num",
pill_cls=pill_cls, href=day_href,
num=str(day_date.day))
cell["day-num"] = str(day_date.day)
# Entry badges for this day
entry_badges = []
if day_date:
# Entry badges for this day
badges = []
for e in month_entries:
if e.start_at and e.start_at.date() == day_date:
is_mine = (
@@ -442,23 +378,23 @@ def _calendar_main_panel_html(ctx: dict) -> str:
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(sx_call("events-calendar-entry-badge",
bg_cls=bg_cls, name=e.name,
state_label=state_label))
badges.append({
"bg-cls": bg_cls, "name": e.name,
"state-label": (e.state or "pending").replace("_", " "),
})
if badges:
cell["badges"] = badges
badges_html = "(<> " + "".join(entry_badges) + ")" if entry_badges else ""
cells.append(sx_call("events-calendar-cell",
cell_cls=cell_cls, day_short=SxExpr(day_short_html),
day_num=SxExpr(day_num_html),
badges=SxExpr(badges_html) if badges_html else None))
cells_data.append(cell)
cells_html = "(<> " + "".join(cells) + ")"
arrows_html = "(<> " + "".join(nav_arrows) + ")"
wd_html = "(<> " + wd_html + ")"
return sx_call("events-calendar-grid",
arrows=SxExpr(arrows_html), weekdays=SxExpr(wd_html),
cells=SxExpr(cells_html))
return sx_call("events-calendar-grid-from-data",
pill_cls=pill_cls, month_name=month_name, year=str(year),
prev_year_href=nav_link(prev_year, month),
prev_month_href=nav_link(prev_month_year, prev_month),
next_month_href=nav_link(next_month_year, next_month),
next_year_href=nav_link(next_year, month),
weekday_names=weekday_names or None,
cells=cells_data or None)
# ---------------------------------------------------------------------------
@@ -466,7 +402,7 @@ def _calendar_main_panel_html(ctx: dict) -> str:
# ---------------------------------------------------------------------------
def _day_main_panel_html(ctx: dict) -> str:
"""Render the day entries table + add button."""
"""Render the day entries table via data extraction + sx defcomp."""
from quart import url_for
calendar = ctx.get("calendar")
@@ -477,21 +413,49 @@ def _day_main_panel_html(ctx: dict) -> str:
day = ctx.get("day")
month = ctx.get("month")
year = ctx.get("year")
hx_select = ctx.get("hx_select_search", "#main-panel")
styles = ctx.get("styles") or {}
list_container = getattr(styles, "list_container", "") if hasattr(styles, "list_container") else styles.get("list_container", "")
pill_cls = getattr(styles, "pill", "") if hasattr(styles, "pill") else styles.get("pill", "")
tr_cls = getattr(styles, "tr", "") if hasattr(styles, "tr") else styles.get("tr", "")
pre_action = getattr(styles, "pre_action_button", "") if hasattr(styles, "pre_action_button") else styles.get("pre_action_button", "")
rows_html = ""
if day_entries:
row_parts = []
for entry in day_entries:
row_parts.append(_day_row_html(ctx, entry))
rows_html = "".join(row_parts)
else:
rows_html = sx_call("events-day-empty-row")
rows_data = []
for entry in day_entries:
entry_href = url_for(
"defpage_entry_detail",
calendar_slug=cal_slug, day=day, month=month, year=year, entry_id=entry.id,
)
row = {
"href": entry_href, "name": entry.name,
"state-id": f"entry-state-{entry.id}",
"state": getattr(entry, "state", "pending") or "pending",
}
# Slot/Time
slot = getattr(entry, "slot", None)
if slot:
row["slot-name"] = slot.name
row["slot-href"] = url_for("defpage_slot_detail", calendar_slug=cal_slug, slot_id=slot.id)
time_start = slot.time_start.strftime("%H:%M") if slot.time_start else ""
time_end = f" \u2192 {slot.time_end.strftime('%H:%M')}" if slot.time_end else ""
row["slot-time"] = f"({time_start}{time_end})"
else:
row["start"] = entry.start_at.strftime("%H:%M") if entry.start_at else ""
row["end"] = f" \u2192 {entry.end_at.strftime('%H:%M')}" if entry.end_at else ""
# Cost
cost = getattr(entry, "cost", None)
row["cost-str"] = f"\u00a3{cost:.2f}" if cost is not None else "\u00a30.00"
# Tickets
tp = getattr(entry, "ticket_price", None)
if tp is not None:
tc = getattr(entry, "ticket_count", None)
row["has-tickets"] = True
row["price-str"] = f"\u00a3{tp:.2f}"
row["count-str"] = f"{tc} tickets" if tc is not None else "Unlimited"
rows_data.append(row)
add_url = url_for(
"calendar.day.calendar_entries.add_form",
@@ -499,74 +463,10 @@ def _day_main_panel_html(ctx: dict) -> str:
day=day, month=month, year=year,
)
return sx_call("events-day-table",
list_container=list_container, rows=SxExpr(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(
"defpage_entry_detail",
calendar_slug=cal_slug, day=day, month=month, year=year, entry_id=entry.id,
)
# Name
name_html = sx_call("events-day-row-name",
href=entry_href, pill_cls=pill_cls, name=entry.name)
# Slot/Time
slot = getattr(entry, "slot", None)
if slot:
slot_href = url_for("defpage_slot_detail", calendar_slug=cal_slug, slot_id=slot.id)
time_start = slot.time_start.strftime("%H:%M") if slot.time_start else ""
time_end = f" \u2192 {slot.time_end.strftime('%H:%M')}" if slot.time_end else ""
slot_html = sx_call("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 = sx_call("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 = sx_call("events-day-row-state",
state_id=f"entry-state-{entry.id}", badge=state_badge)
# Cost
cost = getattr(entry, "cost", None)
cost_str = f"\u00a3{cost:.2f}" if cost is not None else "\u00a30.00"
cost_td = sx_call("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 = sx_call("events-day-row-tickets",
price_str=f"\u00a3{tp:.2f}", count_str=tc_str)
else:
tickets_td = sx_call("events-day-row-no-tickets")
actions_td = sx_call("events-day-row-actions")
return sx_call("events-day-row",
tr_cls=tr_cls, name=name_html, slot=slot_html,
state=state_td, cost=cost_td,
tickets=tickets_td, actions=actions_td)
return sx_call("events-day-table-from-data",
list_container=list_container, pre_action=pre_action,
add_url=add_url, tr_cls=tr_cls, pill_cls=pill_cls,
rows=rows_data or None)
# ---------------------------------------------------------------------------
@@ -614,7 +514,7 @@ def _calendar_description_display_html(calendar, edit_url: str) -> str:
# ---------------------------------------------------------------------------
def _markets_main_panel_html(ctx: dict) -> str:
"""Render markets list + create form panel."""
"""Render markets list + create form panel via data extraction + sx defcomp."""
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)
@@ -623,48 +523,29 @@ def _markets_main_panel_html(ctx: dict) -> str:
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 = sx_call("crud-create-form",
create_url=create_url, csrf=csrf,
errors_id="market-create-errors", list_id="markets-list",
placeholder="e.g. Farm Shop, Bakery", btn_label="Add market")
list_html = _markets_list_html(ctx, markets)
return sx_call("crud-panel",
form=SxExpr(form_html), list=SxExpr(list_html),
list_id="markets-list")
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 sx_call("empty-state", message="No markets yet. Create one above.",
cls="text-gray-500 mt-4")
parts = []
items_data = []
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(sx_call("crud-item",
href=market_href, name=m_name,
slug=m_slug, del_url=del_url,
csrf_hdr=csrf_hdr,
list_id="markets-list",
confirm_title="Delete market?",
confirm_text="Products will be hidden (soft delete)"))
return "".join(parts)
items_data.append({
"href": call_url(ctx, "market_url", f"/{slug}/{m_slug}/"),
"name": m_name, "slug": m_slug,
"del-url": url_for("markets.delete_market", market_slug=m_slug),
"csrf-hdr": f'{{"X-CSRFToken":"{csrf}"}}',
"confirm-title": "Delete market?",
"confirm-text": "Products will be hidden (soft delete)",
})
return sx_call("events-crud-panel-from-data",
can_create=can_create or None,
create_url=url_for("markets.create_market") if can_create else None,
csrf=csrf, errors_id="market-create-errors", list_id="markets-list",
placeholder="e.g. Farm Shop, Bakery", btn_label="Add market",
items=items_data or None,
empty_msg="No markets yet. Create one above.")
# ---------------------------------------------------------------------------