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

@@ -3,7 +3,6 @@ from __future__ import annotations
from shared.sx.helpers import sx_call
from shared.sx.parser import SxExpr
# ===========================================================================
@@ -111,9 +110,9 @@ _SLOT_PICKER_JS = """\
# Slot options (shared by entry edit + add forms)
# ---------------------------------------------------------------------------
def _slot_options_html(day_slots, selected_slot_id=None) -> str:
"""Build slot <option> elements."""
parts = []
def _slot_options_data(day_slots, selected_slot_id=None) -> list:
"""Extract slot option data for sx composition."""
result = []
for slot in day_slots:
start = slot.time_start.strftime("%H:%M") if slot.time_start else ""
end = slot.time_end.strftime("%H:%M") if slot.time_end else ""
@@ -127,16 +126,15 @@ def _slot_options_html(day_slots, selected_slot_id=None) -> str:
label_parts.append("\u2013open-ended)")
if flexible:
label_parts.append("[flexible]")
label = " ".join(label_parts)
parts.append(sx_call("events-slot-option",
value=str(slot.id),
data_start=start, data_end=end,
data_flexible="1" if flexible else "0",
data_cost=cost_str,
selected="selected" if selected_slot_id == slot.id else None,
label=label))
return "".join(parts)
result.append({
"value": str(slot.id),
"data-start": start, "data-end": end,
"data-flexible": "1" if flexible else "0",
"data-cost": cost_str,
"selected": "selected" if selected_slot_id == slot.id else None,
"label": " ".join(label_parts),
})
return result
# ---------------------------------------------------------------------------
@@ -169,7 +167,7 @@ def _slot_header_html(ctx: dict, *, oob: bool = False) -> str:
# ---------------------------------------------------------------------------
def render_slot_main_panel(slot, calendar, *, oob: bool = False) -> str:
"""Render slot detail view."""
"""Render slot detail view via data extraction + sx defcomp."""
from quart import url_for, g
styles = getattr(g, "styles", None) or {}
@@ -179,38 +177,23 @@ def render_slot_main_panel(slot, calendar, *, oob: bool = False) -> str:
days_display = getattr(slot, "days_display", "\u2014")
days = days_display.split(", ")
flexible = getattr(slot, "flexible", False)
if days and days[0] == "\u2014":
days = []
time_start = slot.time_start.strftime("%H:%M") if slot.time_start else ""
time_end = slot.time_end.strftime("%H:%M") if slot.time_end else ""
cost = getattr(slot, "cost", None)
cost_str = f"{cost:.2f}" if cost is not None else ""
desc = getattr(slot, "description", "") or ""
edit_url = url_for("calendar.slots.slot.get_edit", slot_id=slot.id, calendar_slug=cal_slug)
# Days pills
if days and days[0] != "\u2014":
days_inner = "".join(
sx_call("events-slot-day-pill", day=d) for d in days
)
days_html = sx_call("events-slot-days-pills", days_inner=SxExpr(days_inner))
else:
days_html = sx_call("events-slot-no-days")
sid = str(slot.id)
result = sx_call("events-slot-panel",
slot_id=sid, list_container=list_container,
days=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 += sx_call("events-slot-description-oob", description=desc)
return result
return sx_call("events-slot-panel-from-data",
slot_id=str(slot.id), list_container=list_container,
days=days or None,
flexible="yes" if getattr(slot, "flexible", False) else "no",
time_str=f"{time_start} \u2014 {time_end}",
cost_str=f"{cost:.2f}" if cost is not None else "",
pre_action=pre_action,
edit_url=url_for("calendar.slots.slot.get_edit", slot_id=slot.id, calendar_slug=cal_slug),
description=getattr(slot, "description", "") or "",
oob=oob or None)
# ---------------------------------------------------------------------------
@@ -218,7 +201,7 @@ def render_slot_main_panel(slot, calendar, *, oob: bool = False) -> str:
# ---------------------------------------------------------------------------
def render_slots_table(slots, calendar) -> str:
"""Render slots table with rows and add button."""
"""Render slots table via data extraction + sx defcomp."""
from quart import url_for, g
from shared.browser.app.csrf import generate_csrf_token
csrf = generate_csrf_token()
@@ -232,46 +215,34 @@ def render_slots_table(slots, calendar) -> str:
hx_select = getattr(g, "hx_select_search", "#main-panel")
cal_slug = getattr(calendar, "slug", "")
rows_html = ""
slots_data = []
if slots:
for s in slots:
slot_href = url_for("defpage_slot_detail", calendar_slug=cal_slug, slot_id=s.id)
del_url = url_for("calendar.slots.slot.slot_delete", calendar_slug=cal_slug, slot_id=s.id)
desc = getattr(s, "description", "") or ""
days_display = getattr(s, "days_display", "\u2014")
day_list = days_display.split(", ")
if day_list and day_list[0] != "\u2014":
days_inner = "".join(
sx_call("events-slot-day-pill", day=d) for d in day_list
)
days_html = sx_call("events-slot-days-pills", days_inner=SxExpr(days_inner))
else:
days_html = sx_call("events-slot-no-days")
if day_list and day_list[0] == "\u2014":
day_list = []
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 ""
slots_data.append({
"slot-href": url_for("defpage_slot_detail", calendar_slug=cal_slug, slot_id=s.id),
"slot-name": s.name,
"description": getattr(s, "description", "") or "",
"flexible": "yes" if s.flexible else "no",
"days": day_list or None,
"time-str": f"{time_start} - {time_end}",
"cost-str": f"{cost:.2f}" if cost is not None else "",
"del-url": url_for("calendar.slots.slot.slot_delete", calendar_slug=cal_slug, slot_id=s.id),
})
rows_html += sx_call("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=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 = sx_call("events-slots-empty-row")
add_url = url_for("calendar.slots.add_form", calendar_slug=cal_slug)
return sx_call("events-slots-table",
list_container=list_container, rows=SxExpr(rows_html),
pre_action=pre_action, add_url=add_url)
return sx_call("events-slots-table-from-data",
list_container=list_container, slots=slots_data or None,
pre_action=pre_action,
add_url=url_for("calendar.slots.add_form", calendar_slug=cal_slug),
tr_cls=tr_cls, pill_cls=pill_cls, action_btn=action_btn,
hx_select=hx_select,
csrf_hdr=f'{{"X-CSRFToken": "{csrf}"}}')
# ---------------------------------------------------------------------------
@@ -279,7 +250,7 @@ def render_slots_table(slots, calendar) -> str:
# ---------------------------------------------------------------------------
def render_slot_edit_form(slot, calendar) -> str:
"""Render slot edit form (replaces _types/slot/_edit.html)."""
"""Render slot edit form via data extraction + sx defcomp."""
from quart import url_for, g
from shared.browser.app.csrf import generate_csrf_token
csrf = generate_csrf_token()
@@ -291,38 +262,25 @@ def render_slot_edit_form(slot, calendar) -> str:
cal_slug = getattr(calendar, "slug", "")
sid = slot.id
put_url = url_for("calendar.slots.slot.put", calendar_slug=cal_slug, slot_id=sid)
cancel_url = url_for("calendar.slots.slot.get_view", calendar_slug=cal_slug, slot_id=sid)
cost = getattr(slot, "cost", None)
cost_val = f"{cost:.2f}" if cost is not None else ""
start_val = slot.time_start.strftime("%H:%M") if slot.time_start else ""
end_val = slot.time_end.strftime("%H:%M") if slot.time_end else ""
desc_val = getattr(slot, "description", "") or ""
# Days checkboxes
day_keys = [("mon", "Mon"), ("tue", "Tue"), ("wed", "Wed"), ("thu", "Thu"),
("fri", "Fri"), ("sat", "Sat"), ("sun", "Sun")]
days_data = [{"name": k, "label": lbl, "checked": getattr(slot, k, False) or None}
for k, lbl in day_keys]
all_checked = all(getattr(slot, k, False) for k, _ in day_keys)
days_parts = [sx_call("events-day-all-checkbox",
checked="checked" if all_checked else None)]
for key, label in day_keys:
checked = getattr(slot, key, False)
days_parts.append(sx_call("events-day-checkbox",
name=key, label=label,
checked="checked" if checked else None))
days_html = "".join(days_parts)
flexible = getattr(slot, "flexible", False)
return sx_call("events-slot-edit-form",
return sx_call("events-slot-edit-form-from-data",
slot_id=str(sid), list_container=list_container,
put_url=put_url, cancel_url=cancel_url, csrf=csrf,
name_val=slot.name or "", cost_val=cost_val,
start_val=start_val, end_val=end_val,
desc_val=desc_val, days=SxExpr(days_html),
flexible_checked="checked" if flexible else None,
put_url=url_for("calendar.slots.slot.put", calendar_slug=cal_slug, slot_id=sid),
cancel_url=url_for("calendar.slots.slot.get_view", calendar_slug=cal_slug, slot_id=sid),
csrf=csrf,
name_val=slot.name or "",
cost_val=f"{cost:.2f}" if cost is not None else "",
start_val=slot.time_start.strftime("%H:%M") if slot.time_start else "",
end_val=slot.time_end.strftime("%H:%M") if slot.time_end else "",
desc_val=getattr(slot, "description", "") or "",
days_data=days_data, all_checked=all_checked or None,
flexible_checked="checked" if getattr(slot, "flexible", False) else None,
action_btn=action_btn, cancel_btn=cancel_btn)
@@ -331,7 +289,7 @@ def render_slot_edit_form(slot, calendar) -> str:
# ---------------------------------------------------------------------------
def render_slot_add_form(calendar) -> str:
"""Render slot add form (replaces _types/slots/_add.html)."""
"""Render slot add form via data extraction + sx defcomp."""
from quart import url_for, g
from shared.browser.app.csrf import generate_csrf_token
csrf = generate_csrf_token()
@@ -341,23 +299,16 @@ def render_slot_add_form(calendar) -> str:
cancel_btn = styles.get("cancel_button", "") if isinstance(styles, dict) else getattr(styles, "cancel_button", "")
cal_slug = getattr(calendar, "slug", "")
post_url = url_for("calendar.slots.post", calendar_slug=cal_slug)
cancel_url = url_for("calendar.slots.add_button", calendar_slug=cal_slug)
csrf_hdr = f'{{"X-CSRFToken": "{csrf}"}}'
# Days checkboxes (all unchecked for add)
day_keys = [("mon", "Mon"), ("tue", "Tue"), ("wed", "Wed"), ("thu", "Thu"),
("fri", "Fri"), ("sat", "Sat"), ("sun", "Sun")]
days_parts = [sx_call("events-day-all-checkbox", checked=None)]
for key, label in day_keys:
days_parts.append(sx_call("events-day-checkbox", name=key, label=label, checked=None))
days_html = "".join(days_parts)
days_data = [{"name": k, "label": lbl} for k, lbl in day_keys]
return sx_call("events-slot-add-form",
post_url=post_url, csrf=csrf_hdr,
days=SxExpr(days_html),
return sx_call("events-slot-add-form-from-data",
post_url=url_for("calendar.slots.post", calendar_slug=cal_slug),
csrf=f'{{"X-CSRFToken": "{csrf}"}}',
days_data=days_data,
action_btn=action_btn, cancel_btn=cancel_btn,
cancel_url=cancel_url)
cancel_url=url_for("calendar.slots.add_button", calendar_slug=cal_slug))
def render_slot_add_button(calendar) -> str: