Replace sx_call() with render_to_sx() across all services
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 2m6s

Python no longer generates s-expression strings. All SX rendering now
goes through render_to_sx() which builds AST from native Python values
and evaluates via async_eval_to_sx() — no SX string literals in Python.

- Add render_to_sx()/render_to_html() infrastructure in shared/sx/helpers.py
- Add (abort status msg) IO primitive in shared/sx/primitives_io.py
- Convert all 9 services: ~650 sx_call() invocations replaced
- Convert shared helpers (root_header_sx, full_page_sx, etc.) to async
- Fix likes service import bug (likes.models → models)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-04 00:08:33 +00:00
parent 0554f8a113
commit e085fe43b4
51 changed files with 1824 additions and 1742 deletions

View File

@@ -56,6 +56,6 @@ def register(url_prefix="/"):
await g.s.flush()
from sx.sx_components import render_newsletter_toggle
return sx_response(render_newsletter_toggle(un))
return sx_response(await render_newsletter_toggle(un))
return account_bp

View File

@@ -11,7 +11,7 @@ from typing import Any
from shared.sx.jinja_bridge import load_service_components
from shared.sx.helpers import (
sx_call, SxExpr,
render_to_sx,
root_header_sx, full_page_sx,
)
@@ -28,9 +28,10 @@ async def render_login_page(ctx: dict) -> str:
"""Full page: login form."""
error = ctx.get("error", "")
email = ctx.get("email", "")
hdr = root_header_sx(ctx)
content = sx_call("account-login-content", error=error or None, email=email)
return full_page_sx(ctx, header_rows=hdr,
hdr = await root_header_sx(ctx)
content = await render_to_sx("account-login-content",
error=error or None, email=email)
return await full_page_sx(ctx, header_rows=hdr,
content=content,
meta_html='<title>Login \u2014 Rose Ash</title>')
@@ -39,18 +40,19 @@ async def render_device_page(ctx: dict) -> str:
"""Full page: device authorization form."""
error = ctx.get("error", "")
code = ctx.get("code", "")
hdr = root_header_sx(ctx)
content = sx_call("account-device-content", error=error or None, code=code)
return full_page_sx(ctx, header_rows=hdr,
hdr = await root_header_sx(ctx)
content = await render_to_sx("account-device-content",
error=error or None, code=code)
return await full_page_sx(ctx, header_rows=hdr,
content=content,
meta_html='<title>Authorize Device \u2014 Rose Ash</title>')
async def render_device_approved_page(ctx: dict) -> str:
"""Full page: device approved."""
hdr = root_header_sx(ctx)
content = sx_call("account-device-approved")
return full_page_sx(ctx, header_rows=hdr,
hdr = await root_header_sx(ctx)
content = await render_to_sx("account-device-approved")
return await full_page_sx(ctx, header_rows=hdr,
content=content,
meta_html='<title>Device Authorized \u2014 Rose Ash</title>')
@@ -59,10 +61,10 @@ async def render_check_email_page(ctx: dict) -> str:
"""Full page: check email after magic link sent."""
email = ctx.get("email", "")
email_error = ctx.get("email_error")
hdr = root_header_sx(ctx)
content = sx_call("account-check-email-content",
email=email, email_error=email_error)
return full_page_sx(ctx, header_rows=hdr,
hdr = await root_header_sx(ctx)
content = await render_to_sx("account-check-email-content",
email=email, email_error=email_error)
return await full_page_sx(ctx, header_rows=hdr,
content=content,
meta_html='<title>Check your email \u2014 Rose Ash</title>')
@@ -71,7 +73,7 @@ async def render_check_email_page(ctx: dict) -> str:
# Public API: Fragment renderers for POST handlers
# ---------------------------------------------------------------------------
def render_newsletter_toggle(un) -> str:
async def render_newsletter_toggle(un) -> str:
"""Render a newsletter toggle switch for POST response."""
from shared.browser.app.csrf import generate_csrf_token
@@ -94,7 +96,7 @@ def render_newsletter_toggle(un) -> str:
translate = "translate-x-1"
checked = "false"
return sx_call(
return await render_to_sx(
"account-newsletter-toggle",
id=f"nl-{nid}", url=toggle_url,
hdrs=f'{{"X-CSRFToken": "{csrf}"}}',
@@ -103,27 +105,3 @@ def render_newsletter_toggle(un) -> str:
checked=checked,
knob_cls=f"inline-block h-4 w-4 rounded-full bg-white shadow transform transition-transform {translate}",
)
# ---------------------------------------------------------------------------
# Internal helpers
# ---------------------------------------------------------------------------
def _fragment_content(frag: object) -> str:
"""Convert a fragment response to sx content string.
SxExpr (from text/sx responses) is embedded as-is; plain strings
(from text/html) are wrapped in ``~rich-text``.
"""
from shared.sx.parser import SxExpr
if isinstance(frag, SxExpr):
return frag.source
s = str(frag) if frag else ""
if not s:
return ""
return f'(~rich-text :html "{_sx_escape(s)}")'
def _sx_escape(s: str) -> str:
"""Escape a string for embedding in sx string literals."""
return s.replace("\\", "\\\\").replace('"', '\\"')

View File

@@ -26,43 +26,51 @@ def _register_account_layouts() -> None:
register_custom_layout("account", _account_full, _account_oob, _account_mobile)
def _account_full(ctx: dict, **kw: Any) -> str:
from shared.sx.helpers import root_header_sx, header_child_sx, call_url, sx_call, SxExpr
async def _account_full(ctx: dict, **kw: Any) -> str:
from shared.sx.helpers import root_header_sx, header_child_sx, render_to_sx
root_hdr = root_header_sx(ctx)
auth_hdr = sx_call("auth-header-row",
account_url=call_url(ctx, "account_url", ""),
root_hdr = await root_header_sx(ctx)
auth_hdr = await render_to_sx("auth-header-row",
account_url=_call_url(ctx, "account_url", ""),
select_colours=ctx.get("select_colours", ""),
account_nav=_as_sx_nav(ctx),
)
hdr_child = header_child_sx(auth_hdr)
hdr_child = await header_child_sx(auth_hdr)
return "(<> " + root_hdr + " " + hdr_child + ")"
def _account_oob(ctx: dict, **kw: Any) -> str:
from shared.sx.helpers import root_header_sx, call_url, sx_call
async def _account_oob(ctx: dict, **kw: Any) -> str:
from shared.sx.helpers import root_header_sx, render_to_sx
auth_hdr = sx_call("auth-header-row",
account_url=call_url(ctx, "account_url", ""),
auth_hdr = await render_to_sx("auth-header-row",
account_url=_call_url(ctx, "account_url", ""),
select_colours=ctx.get("select_colours", ""),
account_nav=_as_sx_nav(ctx),
oob=True,
)
return "(<> " + auth_hdr + " " + root_header_sx(ctx, oob=True) + ")"
return "(<> " + auth_hdr + " " + await root_header_sx(ctx, oob=True) + ")"
def _account_mobile(ctx: dict, **kw: Any) -> str:
from shared.sx.helpers import mobile_menu_sx, mobile_root_nav_sx, sx_call, SxExpr, call_url
async def _account_mobile(ctx: dict, **kw: Any) -> str:
from shared.sx.helpers import mobile_menu_sx, mobile_root_nav_sx, render_to_sx
from shared.sx.parser import SxExpr
ctx = _inject_account_nav(ctx)
nav_items = sx_call("auth-nav-items",
account_url=call_url(ctx, "account_url", ""),
nav_items = await render_to_sx("auth-nav-items",
account_url=_call_url(ctx, "account_url", ""),
select_colours=ctx.get("select_colours", ""),
account_nav=_as_sx_nav(ctx),
)
auth_section = sx_call("mobile-menu-section",
auth_section = await render_to_sx("mobile-menu-section",
label="account", href="/", level=1, colour="sky",
items=SxExpr(nav_items))
return mobile_menu_sx(auth_section, mobile_root_nav_sx(ctx))
return mobile_menu_sx(auth_section, await mobile_root_nav_sx(ctx))
def _call_url(ctx: dict, key: str, path: str = "/") -> str:
fn = ctx.get(key)
if callable(fn):
return fn(path)
return str(fn or "") + path
def _inject_account_nav(ctx: dict) -> dict:
@@ -75,7 +83,7 @@ def _inject_account_nav(ctx: dict) -> dict:
def _as_sx_nav(ctx: dict) -> Any:
"""Convert account_nav fragment to SxExpr for use in sx_call."""
"""Convert account_nav fragment to SxExpr for use in component calls."""
from shared.sx.helpers import _as_sx
ctx = _inject_account_nav(ctx)
return _as_sx(ctx.get("account_nav"))
@@ -100,8 +108,7 @@ async def _h_newsletters_content(**kw):
from sqlalchemy import select
from shared.models import UserNewsletter
from shared.models.ghost_membership_entities import GhostNewsletter
from shared.sx.helpers import sx_call, SxExpr
from shared.sx.parser import serialize
from shared.sx.helpers import render_to_sx
result = await g.s.execute(
select(GhostNewsletter).order_by(GhostNewsletter.name)
@@ -131,8 +138,8 @@ async def _h_newsletters_content(**kw):
# Call account_url to get the base URL string
account_url_str = account_url("") if callable(account_url) else str(account_url or "")
return sx_call("account-newsletters-content",
newsletter_list=SxExpr(serialize(newsletter_list)),
return await render_to_sx("account-newsletters-content",
newsletter_list=newsletter_list,
account_url=account_url_str)
@@ -148,5 +155,11 @@ async def _h_fragment_content(slug=None, **kw):
)
if not fragment_html:
abort(404)
from sx.sx_components import _fragment_content
return _fragment_content(fragment_html)
from shared.sx.parser import SxExpr
if isinstance(fragment_html, SxExpr):
return fragment_html.source
s = str(fragment_html) if fragment_html else ""
if not s:
return ""
from shared.sx.helpers import render_to_sx
return await render_to_sx("rich-text", html=s)

View File

@@ -235,7 +235,7 @@ def register(url_prefix, title):
from shared.sx.page import get_template_context
from sx.sx_components import render_new_post_page, render_editor_panel
tctx = await get_template_context()
tctx["editor_html"] = render_editor_panel(save_error="Invalid JSON in editor content.")
tctx["editor_html"] = await render_editor_panel(save_error="Invalid JSON in editor content.")
html = await render_new_post_page(tctx)
return await make_response(html, 400)
@@ -244,7 +244,7 @@ def register(url_prefix, title):
from shared.sx.page import get_template_context
from sx.sx_components import render_new_post_page, render_editor_panel
tctx = await get_template_context()
tctx["editor_html"] = render_editor_panel(save_error=reason)
tctx["editor_html"] = await render_editor_panel(save_error=reason)
html = await render_new_post_page(tctx)
return await make_response(html, 400)
@@ -291,7 +291,7 @@ def register(url_prefix, title):
from shared.sx.page import get_template_context
from sx.sx_components import render_new_post_page, render_editor_panel
tctx = await get_template_context()
tctx["editor_html"] = render_editor_panel(save_error="Invalid JSON in editor content.", is_page=True)
tctx["editor_html"] = await render_editor_panel(save_error="Invalid JSON in editor content.", is_page=True)
tctx["is_page"] = True
html = await render_new_post_page(tctx)
return await make_response(html, 400)
@@ -301,7 +301,7 @@ def register(url_prefix, title):
from shared.sx.page import get_template_context
from sx.sx_components import render_new_post_page, render_editor_panel
tctx = await get_template_context()
tctx["editor_html"] = render_editor_panel(save_error=reason, is_page=True)
tctx["editor_html"] = await render_editor_panel(save_error=reason, is_page=True)
tctx["is_page"] = True
html = await render_new_post_page(tctx)
return await make_response(html, 400)

View File

@@ -17,10 +17,10 @@ from shared.sx.helpers import sx_response
def register():
bp = Blueprint("menu_items", __name__, url_prefix='/settings/menu_items')
def get_menu_items_nav_oob_sync(menu_items):
async def get_menu_items_nav_oob_async(menu_items):
"""Helper to generate OOB update for root nav menu items"""
from sx.sx_components import render_menu_items_nav_oob
return render_menu_items_nav_oob(menu_items)
return await render_menu_items_nav_oob(menu_items)
@bp.get("/new/")
@require_admin
@@ -51,8 +51,8 @@ def register():
# Get updated list and nav OOB
menu_items = await get_all_menu_items(g.s)
from sx.sx_components import render_menu_items_list
html = render_menu_items_list(menu_items)
nav_oob = get_menu_items_nav_oob_sync(menu_items)
html = await render_menu_items_list(menu_items)
nav_oob = await get_menu_items_nav_oob_async(menu_items)
return sx_response(html + nav_oob)
except MenuItemError as e:
@@ -91,8 +91,8 @@ def register():
# Get updated list and nav OOB
menu_items = await get_all_menu_items(g.s)
from sx.sx_components import render_menu_items_list
html = render_menu_items_list(menu_items)
nav_oob = get_menu_items_nav_oob_sync(menu_items)
html = await render_menu_items_list(menu_items)
nav_oob = await get_menu_items_nav_oob_async(menu_items)
return sx_response(html + nav_oob)
except MenuItemError as e:
@@ -112,8 +112,8 @@ def register():
# Get updated list and nav OOB
menu_items = await get_all_menu_items(g.s)
from sx.sx_components import render_menu_items_list
html = render_menu_items_list(menu_items)
nav_oob = get_menu_items_nav_oob_sync(menu_items)
html = await render_menu_items_list(menu_items)
nav_oob = await get_menu_items_nav_oob_async(menu_items)
return sx_response(html + nav_oob)
@bp.get("/pages/search/")
@@ -128,7 +128,7 @@ def register():
has_more = (page * per_page) < total
from sx.sx_components import render_page_search_results
return sx_response(render_page_search_results(pages, query, page, has_more))
return sx_response(await render_page_search_results(pages, query, page, has_more))
@bp.post("/reorder/")
@require_admin
@@ -153,8 +153,8 @@ def register():
# Get updated list and nav OOB
menu_items = await get_all_menu_items(g.s)
from sx.sx_components import render_menu_items_list
html = render_menu_items_list(menu_items)
nav_oob = get_menu_items_nav_oob_sync(menu_items)
html = await render_menu_items_list(menu_items)
nav_oob = await get_menu_items_nav_oob_async(menu_items)
return sx_response(html + nav_oob)
return bp

View File

@@ -90,7 +90,7 @@ def register():
features = result.get("features", {})
from sx.sx_components import render_features_panel
html = render_features_panel(
html = await render_features_panel(
features, post,
sumup_configured=result.get("sumup_configured", False),
sumup_merchant_code=result.get("sumup_merchant_code") or "",
@@ -129,7 +129,7 @@ def register():
features = result.get("features", {})
from sx.sx_components import render_features_panel
html = render_features_panel(
html = await render_features_panel(
features, post,
sumup_configured=result.get("sumup_configured", False),
sumup_merchant_code=result.get("sumup_merchant_code") or "",
@@ -259,8 +259,8 @@ def register():
from sx.sx_components import render_associated_entries, render_nav_entries_oob
post = g.post_data["post"]
admin_list = render_associated_entries(all_calendars, associated_entry_ids, post["slug"])
nav_entries_html = render_nav_entries_oob(associated_entries, calendars, post)
admin_list = await render_associated_entries(all_calendars, associated_entry_ids, post["slug"])
nav_entries_html = await render_nav_entries_oob(associated_entries, calendars, post)
return sx_response(admin_list + nav_entries_html)
@@ -436,7 +436,7 @@ def register():
page_markets = await _fetch_page_markets(post_id)
from sx.sx_components import render_markets_panel
return sx_response(render_markets_panel(page_markets, post))
return sx_response(await render_markets_panel(page_markets, post))
@bp.post("/markets/new/")
@require_admin
@@ -462,7 +462,7 @@ def register():
page_markets = await _fetch_page_markets(post_id)
from sx.sx_components import render_markets_panel
return sx_response(render_markets_panel(page_markets, post))
return sx_response(await render_markets_panel(page_markets, post))
@bp.delete("/markets/<market_slug>/")
@require_admin
@@ -482,6 +482,6 @@ def register():
page_markets = await _fetch_page_markets(post_id)
from sx.sx_components import render_markets_panel
return sx_response(render_markets_panel(page_markets, post))
return sx_response(await render_markets_panel(page_markets, post))
return bp

View File

@@ -125,7 +125,7 @@ def register():
# Get post_id from g.post_data
if not g.user:
return sx_response(render_like_toggle_button(slug, False, like_url), status=403)
return sx_response(await render_like_toggle_button(slug, False, like_url), status=403)
post_id = g.post_data["post"]["id"]
user_id = g.user.id
@@ -135,7 +135,7 @@ def register():
})
liked = result["liked"]
return sx_response(render_like_toggle_button(slug, liked, like_url))
return sx_response(await render_like_toggle_button(slug, liked, like_url))
@bp.get("/w/<widget_domain>/")
async def widget_paginate(slug: str, widget_domain: str):

View File

@@ -47,7 +47,7 @@ def register():
snippets = await _visible_snippets(g.s)
from sx.sx_components import render_snippets_list
return sx_response(render_snippets_list(snippets, is_admin))
return sx_response(await render_snippets_list(snippets, is_admin))
@bp.patch("/<int:snippet_id>/visibility/")
@require_login
@@ -71,6 +71,6 @@ def register():
snippets = await _visible_snippets(g.s)
from sx.sx_components import render_snippets_list
return sx_response(render_snippets_list(snippets, True))
return sx_response(await render_snippets_list(snippets, True))
return bp

File diff suppressed because it is too large Load Diff

View File

@@ -129,148 +129,148 @@ def _register_blog_layouts() -> None:
# --- Blog layout (root + blog header) ---
def _blog_full(ctx: dict, **kw: Any) -> str:
async def _blog_full(ctx: dict, **kw: Any) -> str:
from shared.sx.helpers import root_header_sx
from sx.sx_components import _blog_header_sx
root_hdr = root_header_sx(ctx)
blog_hdr = _blog_header_sx(ctx)
root_hdr = await root_header_sx(ctx)
blog_hdr = await _blog_header_sx(ctx)
return "(<> " + root_hdr + " " + blog_hdr + ")"
def _blog_oob(ctx: dict, **kw: Any) -> str:
async def _blog_oob(ctx: dict, **kw: Any) -> str:
from shared.sx.helpers import root_header_sx, oob_header_sx
from sx.sx_components import _blog_header_sx
root_hdr = root_header_sx(ctx)
blog_hdr = _blog_header_sx(ctx)
root_hdr = await root_header_sx(ctx)
blog_hdr = await _blog_header_sx(ctx)
rows = "(<> " + root_hdr + " " + blog_hdr + ")"
return oob_header_sx("root-header-child", "blog-header-child", rows)
return await oob_header_sx("root-header-child", "blog-header-child", rows)
# --- Settings layout (root + settings header) ---
def _settings_full(ctx: dict, **kw: Any) -> str:
async def _settings_full(ctx: dict, **kw: Any) -> str:
from shared.sx.helpers import root_header_sx
from sx.sx_components import _settings_header_sx
root_hdr = root_header_sx(ctx)
settings_hdr = _settings_header_sx(ctx)
root_hdr = await root_header_sx(ctx)
settings_hdr = await _settings_header_sx(ctx)
return "(<> " + root_hdr + " " + settings_hdr + ")"
def _settings_oob(ctx: dict, **kw: Any) -> str:
async def _settings_oob(ctx: dict, **kw: Any) -> str:
from shared.sx.helpers import root_header_sx, oob_header_sx
from sx.sx_components import _settings_header_sx
root_hdr = root_header_sx(ctx)
settings_hdr = _settings_header_sx(ctx)
root_hdr = await root_header_sx(ctx)
settings_hdr = await _settings_header_sx(ctx)
rows = "(<> " + root_hdr + " " + settings_hdr + ")"
return oob_header_sx("root-header-child", "root-settings-header-child", rows)
return await oob_header_sx("root-header-child", "root-settings-header-child", rows)
def _settings_mobile(ctx: dict, **kw: Any) -> str:
async def _settings_mobile(ctx: dict, **kw: Any) -> str:
from sx.sx_components import _settings_nav_sx
return _settings_nav_sx(ctx)
return await _settings_nav_sx(ctx)
# --- Sub-settings helpers ---
def _sub_settings_full(ctx: dict, row_id: str, child_id: str,
async def _sub_settings_full(ctx: dict, row_id: str, child_id: str,
endpoint: str, icon: str, label: str) -> str:
from shared.sx.helpers import root_header_sx
from sx.sx_components import _settings_header_sx, _sub_settings_header_sx
from quart import url_for as qurl
root_hdr = root_header_sx(ctx)
settings_hdr = _settings_header_sx(ctx)
sub_hdr = _sub_settings_header_sx(row_id, child_id,
root_hdr = await root_header_sx(ctx)
settings_hdr = await _settings_header_sx(ctx)
sub_hdr = await _sub_settings_header_sx(row_id, child_id,
qurl(endpoint), icon, label, ctx)
return "(<> " + root_hdr + " " + settings_hdr + " " + sub_hdr + ")"
def _sub_settings_oob(ctx: dict, row_id: str, child_id: str,
async def _sub_settings_oob(ctx: dict, row_id: str, child_id: str,
endpoint: str, icon: str, label: str) -> str:
from shared.sx.helpers import oob_header_sx
from sx.sx_components import _settings_header_sx, _sub_settings_header_sx
from quart import url_for as qurl
settings_hdr_oob = _settings_header_sx(ctx, oob=True)
sub_hdr = _sub_settings_header_sx(row_id, child_id,
settings_hdr_oob = await _settings_header_sx(ctx, oob=True)
sub_hdr = await _sub_settings_header_sx(row_id, child_id,
qurl(endpoint), icon, label, ctx)
sub_oob = oob_header_sx("root-settings-header-child", child_id, sub_hdr)
sub_oob = await oob_header_sx("root-settings-header-child", child_id, sub_hdr)
return "(<> " + settings_hdr_oob + " " + sub_oob + ")"
# --- Cache ---
def _cache_full(ctx: dict, **kw: Any) -> str:
return _sub_settings_full(ctx, "cache-row", "cache-header-child",
async def _cache_full(ctx: dict, **kw: Any) -> str:
return await _sub_settings_full(ctx, "cache-row", "cache-header-child",
"defpage_cache_page", "refresh", "Cache")
def _cache_oob(ctx: dict, **kw: Any) -> str:
return _sub_settings_oob(ctx, "cache-row", "cache-header-child",
async def _cache_oob(ctx: dict, **kw: Any) -> str:
return await _sub_settings_oob(ctx, "cache-row", "cache-header-child",
"defpage_cache_page", "refresh", "Cache")
# --- Snippets ---
def _snippets_full(ctx: dict, **kw: Any) -> str:
return _sub_settings_full(ctx, "snippets-row", "snippets-header-child",
async def _snippets_full(ctx: dict, **kw: Any) -> str:
return await _sub_settings_full(ctx, "snippets-row", "snippets-header-child",
"defpage_snippets_page", "puzzle-piece", "Snippets")
def _snippets_oob(ctx: dict, **kw: Any) -> str:
return _sub_settings_oob(ctx, "snippets-row", "snippets-header-child",
async def _snippets_oob(ctx: dict, **kw: Any) -> str:
return await _sub_settings_oob(ctx, "snippets-row", "snippets-header-child",
"defpage_snippets_page", "puzzle-piece", "Snippets")
# --- Menu Items ---
def _menu_items_full(ctx: dict, **kw: Any) -> str:
return _sub_settings_full(ctx, "menu_items-row", "menu_items-header-child",
async def _menu_items_full(ctx: dict, **kw: Any) -> str:
return await _sub_settings_full(ctx, "menu_items-row", "menu_items-header-child",
"defpage_menu_items_page", "bars", "Menu Items")
def _menu_items_oob(ctx: dict, **kw: Any) -> str:
return _sub_settings_oob(ctx, "menu_items-row", "menu_items-header-child",
async def _menu_items_oob(ctx: dict, **kw: Any) -> str:
return await _sub_settings_oob(ctx, "menu_items-row", "menu_items-header-child",
"defpage_menu_items_page", "bars", "Menu Items")
# --- Tag Groups ---
def _tag_groups_full(ctx: dict, **kw: Any) -> str:
return _sub_settings_full(ctx, "tag-groups-row", "tag-groups-header-child",
async def _tag_groups_full(ctx: dict, **kw: Any) -> str:
return await _sub_settings_full(ctx, "tag-groups-row", "tag-groups-header-child",
"defpage_tag_groups_page", "tags", "Tag Groups")
def _tag_groups_oob(ctx: dict, **kw: Any) -> str:
return _sub_settings_oob(ctx, "tag-groups-row", "tag-groups-header-child",
async def _tag_groups_oob(ctx: dict, **kw: Any) -> str:
return await _sub_settings_oob(ctx, "tag-groups-row", "tag-groups-header-child",
"defpage_tag_groups_page", "tags", "Tag Groups")
# --- Tag Group Edit ---
def _tag_group_edit_full(ctx: dict, **kw: Any) -> str:
async def _tag_group_edit_full(ctx: dict, **kw: Any) -> str:
from quart import request
g_id = (request.view_args or {}).get("id")
from quart import url_for as qurl
from shared.sx.helpers import root_header_sx
from sx.sx_components import _settings_header_sx, _sub_settings_header_sx
root_hdr = root_header_sx(ctx)
settings_hdr = _settings_header_sx(ctx)
sub_hdr = _sub_settings_header_sx("tag-groups-row", "tag-groups-header-child",
root_hdr = await root_header_sx(ctx)
settings_hdr = await _settings_header_sx(ctx)
sub_hdr = await _sub_settings_header_sx("tag-groups-row", "tag-groups-header-child",
qurl("defpage_tag_group_edit", id=g_id),
"tags", "Tag Groups", ctx)
return "(<> " + root_hdr + " " + settings_hdr + " " + sub_hdr + ")"
def _tag_group_edit_oob(ctx: dict, **kw: Any) -> str:
async def _tag_group_edit_oob(ctx: dict, **kw: Any) -> str:
from quart import request
g_id = (request.view_args or {}).get("id")
from quart import url_for as qurl
from shared.sx.helpers import oob_header_sx
from sx.sx_components import _settings_header_sx, _sub_settings_header_sx
settings_hdr_oob = _settings_header_sx(ctx, oob=True)
sub_hdr = _sub_settings_header_sx("tag-groups-row", "tag-groups-header-child",
settings_hdr_oob = await _settings_header_sx(ctx, oob=True)
sub_hdr = await _sub_settings_header_sx("tag-groups-row", "tag-groups-header-child",
qurl("defpage_tag_group_edit", id=g_id),
"tags", "Tag Groups", ctx)
sub_oob = oob_header_sx("root-settings-header-child", "tag-groups-header-child", sub_hdr)
sub_oob = await oob_header_sx("root-settings-header-child", "tag-groups-header-child", sub_hdr)
return "(<> " + settings_hdr_oob + " " + sub_oob + ")"
@@ -302,12 +302,12 @@ def _register_blog_helpers() -> None:
async def _h_editor_content(**kw):
from sx.sx_components import render_editor_panel
return render_editor_panel()
return await render_editor_panel()
async def _h_editor_page_content(**kw):
from sx.sx_components import render_editor_panel
return render_editor_panel(is_page=True)
return await render_editor_panel(is_page=True)
# --- Post admin helpers ---
@@ -391,7 +391,7 @@ async def _h_post_preview_content(slug=None, **kw):
from sx.sx_components import _preview_main_panel_sx
tctx = await get_template_context()
tctx.update(preview_ctx)
return _preview_main_panel_sx(tctx)
return await _preview_main_panel_sx(tctx)
async def _h_post_entries_content(slug=None, **kw):
@@ -415,7 +415,7 @@ async def _h_post_entries_content(slug=None, **kw):
tctx = await get_template_context()
tctx["all_calendars"] = all_calendars
tctx["associated_entry_ids"] = associated_entry_ids
return _post_entries_content_sx(tctx)
return await _post_entries_content_sx(tctx)
async def _h_post_settings_content(slug=None, **kw):
@@ -468,7 +468,7 @@ async def _h_post_edit_content(slug=None, **kw):
tctx["save_success"] = save_success
tctx["save_error"] = save_error
tctx["newsletters"] = newsletters
return _post_edit_content_sx(tctx)
return await _post_edit_content_sx(tctx)
# --- Settings helpers ---
@@ -484,7 +484,7 @@ async def _h_cache_content(**kw):
from shared.sx.page import get_template_context
from sx.sx_components import _cache_main_panel_sx
tctx = await get_template_context()
return _cache_main_panel_sx(tctx)
return await _cache_main_panel_sx(tctx)
# --- Snippets helper ---
@@ -506,7 +506,7 @@ async def _h_snippets_content(**kw):
tctx = await get_template_context()
tctx["snippets"] = rows
tctx["is_admin"] = is_admin
return _snippets_main_panel_sx(tctx)
return await _snippets_main_panel_sx(tctx)
# --- Menu Items helper ---
@@ -519,7 +519,7 @@ async def _h_menu_items_content(**kw):
from sx.sx_components import _menu_items_main_panel_sx
tctx = await get_template_context()
tctx["menu_items"] = menu_items
return _menu_items_main_panel_sx(tctx)
return await _menu_items_main_panel_sx(tctx)
# --- Tag Groups helpers ---
@@ -539,7 +539,7 @@ async def _h_tag_groups_content(**kw):
from sx.sx_components import _tag_groups_main_panel_sx
tctx = await get_template_context()
tctx.update({"groups": groups, "unassigned_tags": unassigned})
return _tag_groups_main_panel_sx(tctx)
return await _tag_groups_main_panel_sx(tctx)
async def _h_tag_group_edit_content(id=None, **kw):
@@ -571,4 +571,4 @@ async def _h_tag_group_edit_content(id=None, **kw):
"all_tags": all_tags,
"assigned_tag_ids": set(assigned_rows),
})
return _tag_groups_edit_main_panel_sx(tctx)
return await _tag_groups_edit_main_panel_sx(tctx)

View File

@@ -49,7 +49,7 @@ def register():
from shared.sx.page import get_template_context
from sx.sx_components import render_cart_payments_panel
ctx = await get_template_context()
html = render_cart_payments_panel(ctx)
html = await render_cart_payments_panel(ctx)
return sx_response(html)
return bp

View File

@@ -16,9 +16,9 @@ from shared.sx.helpers import (
post_header_sx as _shared_post_header_sx,
search_desktop_sx, search_mobile_sx,
full_page_sx, oob_page_sx, header_child_sx,
sx_call, SxExpr,
render_to_sx,
)
from shared.sx.parser import serialize
from shared.sx.parser import SxExpr
from shared.infrastructure.urls import cart_url
# Load cart-specific .sx components + handlers at import time
@@ -69,12 +69,12 @@ async def _post_header_sx(ctx: dict, page_post: Any, *, oob: bool = False) -> st
"""Build post-level header row from page_post DTO, using shared helper."""
ctx = _ensure_post_ctx(ctx, page_post)
ctx = await _ensure_container_nav(ctx)
return _shared_post_header_sx(ctx, oob=oob)
return await _shared_post_header_sx(ctx, oob=oob)
def _cart_header_sx(ctx: dict, *, oob: bool = False) -> str:
async def _cart_header_sx(ctx: dict, *, oob: bool = False) -> str:
"""Build the cart section header row."""
return sx_call(
return await render_to_sx(
"menu-row-sx",
id="cart-row", level=1, colour="sky",
link_href=call_url(ctx, "cart_url", "/"),
@@ -83,17 +83,17 @@ def _cart_header_sx(ctx: dict, *, oob: bool = False) -> str:
)
def _page_cart_header_sx(ctx: dict, page_post: Any, *, oob: bool = False) -> str:
async def _page_cart_header_sx(ctx: dict, page_post: Any, *, oob: bool = False) -> str:
"""Build the per-page cart header row."""
slug = page_post.slug if page_post else ""
title = ((page_post.title if page_post else None) or "")[:160]
label_parts = []
if page_post and page_post.feature_image:
label_parts.append(sx_call("cart-page-label-img", src=page_post.feature_image))
label_parts.append(await render_to_sx("cart-page-label-img", src=page_post.feature_image))
label_parts.append(f'(span "{escape(title)}")')
label_sx = "(<> " + " ".join(label_parts) + ")"
nav_sx = sx_call("cart-all-carts-link", href=call_url(ctx, "cart_url", "/"))
return sx_call(
nav_sx = await render_to_sx("cart-all-carts-link", href=call_url(ctx, "cart_url", "/"))
return await render_to_sx(
"menu-row-sx",
id="page-cart-row", level=2, colour="sky",
link_href=call_url(ctx, "cart_url", f"/{slug}/"),
@@ -102,26 +102,26 @@ def _page_cart_header_sx(ctx: dict, page_post: Any, *, oob: bool = False) -> str
)
def _auth_header_sx(ctx: dict, *, oob: bool = False) -> str:
async def _auth_header_sx(ctx: dict, *, oob: bool = False) -> str:
"""Build the account section header row (for orders)."""
return sx_call(
return await render_to_sx(
"auth-header-row-simple",
account_url=call_url(ctx, "account_url", ""),
oob=oob,
)
def _orders_header_sx(ctx: dict, list_url: str) -> str:
async def _orders_header_sx(ctx: dict, list_url: str) -> str:
"""Build the orders section header row."""
return sx_call("orders-header-row", list_url=list_url)
return await render_to_sx("orders-header-row", list_url=list_url)
def _cart_page_admin_header_sx(ctx: dict, page_post: Any, *, oob: bool = False,
async def _cart_page_admin_header_sx(ctx: dict, page_post: Any, *, oob: bool = False,
selected: str = "") -> str:
"""Build the page-level admin header row."""
slug = page_post.slug if page_post else ""
ctx = _ensure_post_ctx(ctx, page_post)
return post_admin_header_sx(ctx, slug, oob=oob, selected=selected)
return await post_admin_header_sx(ctx, slug, oob=oob, selected=selected)
# ---------------------------------------------------------------------------
@@ -190,24 +190,25 @@ async def render_orders_page(ctx: dict, orders: list, page: int,
detail_url_prefix = pfx + url_for_fn("orders.order.order_detail", order_id=0).rsplit("0/", 1)[0]
order_dicts = [_serialize_order(o) for o in orders]
content = sx_call("orders-list-content",
orders=SxExpr(serialize(order_dicts)),
content = await render_to_sx("orders-list-content",
orders=order_dicts,
page=page, total_pages=total_pages,
rows_url=rows_url, detail_url_prefix=detail_url_prefix)
hdr = root_header_sx(ctx)
auth = _auth_header_sx(ctx)
orders_hdr = _orders_header_sx(ctx, list_url)
auth_child = sx_call(
hdr = await root_header_sx(ctx)
auth = await _auth_header_sx(ctx)
orders_hdr = await _orders_header_sx(ctx, list_url)
auth_child_inner = await render_to_sx("header-child-sx", id="auth-header-child", inner=SxExpr(orders_hdr))
auth_child = await render_to_sx(
"header-child-sx",
inner=SxExpr("(<> " + auth + " " + sx_call("header-child-sx", id="auth-header-child", inner=SxExpr(orders_hdr)) + ")"),
inner=SxExpr("(<> " + auth + " " + auth_child_inner + ")"),
)
header_rows = "(<> " + hdr + " " + auth_child + ")"
filt = sx_call("order-list-header", search_mobile=SxExpr(search_mobile_sx(ctx)))
return full_page_sx(ctx, header_rows=header_rows,
filt = await render_to_sx("order-list-header", search_mobile=SxExpr(await search_mobile_sx(ctx)))
return await full_page_sx(ctx, header_rows=header_rows,
filter=filt,
aside=search_desktop_sx(ctx),
aside=await search_desktop_sx(ctx),
content=content)
@@ -222,20 +223,21 @@ async def render_orders_rows(ctx: dict, orders: list, page: int,
detail_url_prefix = pfx + url_for_fn("orders.order.order_detail", order_id=0).rsplit("0/", 1)[0]
order_dicts = [_serialize_order(o) for o in orders]
parts = [sx_call("order-row-pair",
order=SxExpr(serialize(od)),
detail_url_prefix=detail_url_prefix)
for od in order_dicts]
parts = []
for od in order_dicts:
parts.append(await render_to_sx("order-row-pair",
order=od,
detail_url_prefix=detail_url_prefix))
if page < total_pages:
next_url = list_url + qs_fn(page=page + 1)
parts.append(sx_call(
parts.append(await render_to_sx(
"infinite-scroll",
url=next_url, page=page, total_pages=total_pages,
id_prefix="orders", colspan=5,
))
else:
parts.append(sx_call("order-end-row"))
parts.append(await render_to_sx("order-end-row"))
return "(<> " + " ".join(parts) + ")"
@@ -255,24 +257,25 @@ async def render_orders_oob(ctx: dict, orders: list, page: int,
detail_url_prefix = pfx + url_for_fn("orders.order.order_detail", order_id=0).rsplit("0/", 1)[0]
order_dicts = [_serialize_order(o) for o in orders]
content = sx_call("orders-list-content",
orders=SxExpr(serialize(order_dicts)),
content = await render_to_sx("orders-list-content",
orders=order_dicts,
page=page, total_pages=total_pages,
rows_url=rows_url, detail_url_prefix=detail_url_prefix)
auth_oob = _auth_header_sx(ctx, oob=True)
auth_child_oob = sx_call(
auth_oob = await _auth_header_sx(ctx, oob=True)
orders_hdr = await _orders_header_sx(ctx, list_url)
auth_child_oob = await render_to_sx(
"oob-header-sx",
parent_id="auth-header-child",
row=SxExpr(_orders_header_sx(ctx, list_url)),
row=SxExpr(orders_hdr),
)
root_oob = root_header_sx(ctx, oob=True)
root_oob = await root_header_sx(ctx, oob=True)
oobs = "(<> " + auth_oob + " " + auth_child_oob + " " + root_oob + ")"
filt = sx_call("order-list-header", search_mobile=SxExpr(search_mobile_sx(ctx)))
return oob_page_sx(oobs=oobs,
filt = await render_to_sx("order-list-header", search_mobile=SxExpr(await search_mobile_sx(ctx)))
return await oob_page_sx(oobs=oobs,
filter=filt,
aside=search_desktop_sx(ctx),
aside=await search_desktop_sx(ctx),
content=content)
@@ -296,29 +299,32 @@ async def render_order_page(ctx: dict, order: Any,
order_data = _serialize_order(order)
cal_data = [_serialize_calendar_entry(e) for e in (calendar_entries or [])]
main = sx_call("order-detail-content",
order=SxExpr(serialize(order_data)),
calendar_entries=SxExpr(serialize(cal_data)))
filt = sx_call("order-detail-filter-content",
order=SxExpr(serialize(order_data)),
main = await render_to_sx("order-detail-content",
order=order_data,
calendar_entries=cal_data)
filt = await render_to_sx("order-detail-filter-content",
order=order_data,
list_url=list_url, recheck_url=recheck_url,
pay_url=pay_url, csrf=generate_csrf_token())
hdr = root_header_sx(ctx)
order_row = sx_call(
hdr = await root_header_sx(ctx)
order_row = await render_to_sx(
"menu-row-sx",
id="order-row", level=3, colour="sky",
link_href=detail_url, link_label=f"Order {order.id}", icon="fa fa-gbp",
)
order_child = sx_call(
auth = await _auth_header_sx(ctx)
orders_hdr = await _orders_header_sx(ctx, list_url)
orders_child = await render_to_sx("header-child-sx", id="orders-header-child", inner=SxExpr(order_row))
auth_inner = "(<> " + orders_hdr + " " + orders_child + ")"
auth_child = await render_to_sx("header-child-sx", id="auth-header-child", inner=SxExpr(auth_inner))
order_child = await render_to_sx(
"header-child-sx",
inner=SxExpr("(<> " + _auth_header_sx(ctx) + " " + sx_call("header-child-sx", id="auth-header-child", inner=SxExpr(
"(<> " + _orders_header_sx(ctx, list_url) + " " + sx_call("header-child-sx", id="orders-header-child", inner=SxExpr(order_row)) + ")"
)) + ")"),
inner=SxExpr("(<> " + auth + " " + auth_child + ")"),
)
header_rows = "(<> " + hdr + " " + order_child + ")"
return full_page_sx(ctx, header_rows=header_rows, filter=filt, content=main)
return await full_page_sx(ctx, header_rows=header_rows, filter=filt, content=main)
async def render_order_oob(ctx: dict, order: Any,
@@ -337,27 +343,27 @@ async def render_order_oob(ctx: dict, order: Any,
order_data = _serialize_order(order)
cal_data = [_serialize_calendar_entry(e) for e in (calendar_entries or [])]
main = sx_call("order-detail-content",
order=SxExpr(serialize(order_data)),
calendar_entries=SxExpr(serialize(cal_data)))
filt = sx_call("order-detail-filter-content",
order=SxExpr(serialize(order_data)),
main = await render_to_sx("order-detail-content",
order=order_data,
calendar_entries=cal_data)
filt = await render_to_sx("order-detail-filter-content",
order=order_data,
list_url=list_url, recheck_url=recheck_url,
pay_url=pay_url, csrf=generate_csrf_token())
order_row_oob = sx_call(
order_row_oob = await render_to_sx(
"menu-row-sx",
id="order-row", level=3, colour="sky",
link_href=detail_url, link_label=f"Order {order.id}", icon="fa fa-gbp",
oob=True,
)
orders_child_oob = sx_call("oob-header-sx",
orders_child_oob = await render_to_sx("oob-header-sx",
parent_id="orders-header-child",
row=SxExpr(order_row_oob))
root_oob = root_header_sx(ctx, oob=True)
root_oob = await root_header_sx(ctx, oob=True)
oobs = "(<> " + orders_child_oob + " " + root_oob + ")"
return oob_page_sx(oobs=oobs, filter=filt, content=main)
return await oob_page_sx(oobs=oobs, filter=filt, content=main)
# ---------------------------------------------------------------------------
@@ -370,25 +376,25 @@ async def render_checkout_error_page(ctx: dict, error: str | None = None,
err_msg = error or "Unexpected error while creating the hosted checkout session."
order_sx = None
if order:
order_sx = sx_call("checkout-error-order-id", oid=f"#{order.id}")
order_sx = await render_to_sx("checkout-error-order-id", oid=f"#{order.id}")
back_url = cart_url("/")
hdr = root_header_sx(ctx)
filt = sx_call("checkout-error-header")
content = sx_call(
hdr = await root_header_sx(ctx)
filt = await render_to_sx("checkout-error-header")
content = await render_to_sx(
"checkout-error-content",
msg=err_msg,
order=SxExpr(order_sx) if order_sx else None,
back_url=back_url,
)
return full_page_sx(ctx, header_rows=hdr, filter=filt, content=content)
return await full_page_sx(ctx, header_rows=hdr, filter=filt, content=content)
# ---------------------------------------------------------------------------
# Public API: POST response renderers
# ---------------------------------------------------------------------------
def render_cart_payments_panel(ctx: dict) -> str:
async def render_cart_payments_panel(ctx: dict) -> str:
"""Render the payments config panel for PUT response."""
page_config = ctx.get("page_config")
pc_data = None
@@ -398,5 +404,5 @@ def render_cart_payments_panel(ctx: dict) -> str:
"sumup_merchant_code": getattr(page_config, "sumup_merchant_code", None) or "",
"sumup_checkout_prefix": getattr(page_config, "sumup_checkout_prefix", None) or "",
}
return sx_call("cart-payments-content",
page_config=SxExpr(serialize(pc_data)) if pc_data else None)
return await render_to_sx("cart-payments-content",
page_config=pc_data)

View File

@@ -27,31 +27,35 @@ def _register_cart_layouts() -> None:
register_custom_layout("cart-admin", _cart_admin_full, _cart_admin_oob)
def _cart_page_full(ctx: dict, **kw: Any) -> str:
from shared.sx.helpers import root_header_sx, sx_call, SxExpr
async def _cart_page_full(ctx: dict, **kw: Any) -> str:
from shared.sx.helpers import root_header_sx, render_to_sx
from shared.sx.parser import SxExpr
from sx.sx_components import _cart_header_sx, _page_cart_header_sx
page_post = ctx.get("page_post")
root_hdr = root_header_sx(ctx)
child = _cart_header_sx(ctx)
page_hdr = _page_cart_header_sx(ctx, page_post)
nested = sx_call(
root_hdr = await root_header_sx(ctx)
child = await _cart_header_sx(ctx)
page_hdr = await _page_cart_header_sx(ctx, page_post)
inner_child = await render_to_sx("header-child-sx", id="cart-header-child", inner=SxExpr(page_hdr))
nested = await render_to_sx(
"header-child-sx",
inner=SxExpr("(<> " + child + " " + sx_call("header-child-sx", id="cart-header-child", inner=SxExpr(page_hdr)) + ")"),
inner=SxExpr("(<> " + child + " " + inner_child + ")"),
)
return "(<> " + root_hdr + " " + nested + ")"
def _cart_page_oob(ctx: dict, **kw: Any) -> str:
from shared.sx.helpers import root_header_sx, sx_call, SxExpr
async def _cart_page_oob(ctx: dict, **kw: Any) -> str:
from shared.sx.helpers import root_header_sx, render_to_sx
from shared.sx.parser import SxExpr
from sx.sx_components import _cart_header_sx, _page_cart_header_sx
page_post = ctx.get("page_post")
child_oob = sx_call("oob-header-sx",
page_hdr = await _page_cart_header_sx(ctx, page_post)
child_oob = await render_to_sx("oob-header-sx",
parent_id="cart-header-child",
row=SxExpr(_page_cart_header_sx(ctx, page_post)))
cart_hdr_oob = _cart_header_sx(ctx, oob=True)
root_hdr_oob = root_header_sx(ctx, oob=True)
row=SxExpr(page_hdr))
cart_hdr_oob = await _cart_header_sx(ctx, oob=True)
root_hdr_oob = await root_header_sx(ctx, oob=True)
return "(<> " + child_oob + " " + cart_hdr_oob + " " + root_hdr_oob + ")"
@@ -61,9 +65,9 @@ async def _cart_admin_full(ctx: dict, **kw: Any) -> str:
page_post = ctx.get("page_post")
selected = kw.get("selected", "")
root_hdr = root_header_sx(ctx)
root_hdr = await root_header_sx(ctx)
post_hdr = await _post_header_sx(ctx, page_post)
admin_hdr = _cart_page_admin_header_sx(ctx, page_post, selected=selected)
admin_hdr = await _cart_page_admin_header_sx(ctx, page_post, selected=selected)
return "(<> " + root_hdr + " " + post_hdr + " " + admin_hdr + ")"
@@ -72,7 +76,7 @@ async def _cart_admin_oob(ctx: dict, **kw: Any) -> str:
page_post = ctx.get("page_post")
selected = kw.get("selected", "")
return _cart_page_admin_header_sx(ctx, page_post, oob=True, selected=selected)
return await _cart_page_admin_header_sx(ctx, page_post, oob=True, selected=selected)
# ---------------------------------------------------------------------------
@@ -244,22 +248,21 @@ def _build_summary_data(ctx: dict, cart: list, cal_entries: list, tickets: list,
async def _h_overview_content(**kw):
from quart import g
from shared.sx.helpers import sx_call, SxExpr
from shared.sx.parser import serialize
from shared.sx.helpers import render_to_sx
from shared.infrastructure.urls import cart_url
from bp.cart.services import get_cart_grouped_by_page
page_groups = await get_cart_grouped_by_page(g.s)
grp_dicts = [d for d in (_serialize_page_group(grp) for grp in page_groups) if d]
return sx_call("cart-overview-content",
page_groups=SxExpr(serialize(grp_dicts)),
return await render_to_sx("cart-overview-content",
page_groups=grp_dicts,
cart_url_base=cart_url(""))
async def _h_page_cart_content(page_slug=None, **kw):
from quart import g
from shared.sx.helpers import sx_call, SxExpr
from shared.sx.parser import serialize
from shared.sx.helpers import render_to_sx
from shared.sx.parser import SxExpr
from shared.sx.page import get_template_context
from bp.cart.services import total, calendar_total, ticket_total
from bp.cart.services.page_cart import (
@@ -277,7 +280,7 @@ async def _h_page_cart_content(page_slug=None, **kw):
sd = _build_summary_data(ctx, cart, cal_entries, page_tickets,
total, calendar_total, ticket_total)
summary_sx = sx_call("cart-summary-from-data",
summary_sx = await render_to_sx("cart-summary-from-data",
item_count=sd["item_count"],
grand_total=sd["grand_total"],
symbol=sd["symbol"],
@@ -286,10 +289,10 @@ async def _h_page_cart_content(page_slug=None, **kw):
login_href=sd.get("login_href"),
user_email=sd.get("user_email"))
return sx_call("cart-page-cart-content",
cart_items=SxExpr(serialize([_serialize_cart_item(i) for i in cart])),
cal_entries=SxExpr(serialize([_serialize_cal_entry(e) for e in cal_entries])),
ticket_groups=SxExpr(serialize([_serialize_ticket_group(tg) for tg in ticket_groups])),
return await render_to_sx("cart-page-cart-content",
cart_items=[_serialize_cart_item(i) for i in cart],
cal_entries=[_serialize_cal_entry(e) for e in cal_entries],
ticket_groups=[_serialize_ticket_group(tg) for tg in ticket_groups],
summary=SxExpr(summary_sx))
@@ -299,8 +302,7 @@ async def _h_cart_admin_content(page_slug=None, **kw):
async def _h_cart_payments_content(page_slug=None, **kw):
from shared.sx.page import get_template_context
from shared.sx.helpers import sx_call, SxExpr
from shared.sx.parser import serialize
from shared.sx.helpers import render_to_sx
ctx = await get_template_context()
page_config = ctx.get("page_config")
@@ -311,5 +313,5 @@ async def _h_cart_payments_content(page_slug=None, **kw):
"sumup_merchant_code": getattr(page_config, "sumup_merchant_code", None) or "",
"sumup_checkout_prefix": getattr(page_config, "sumup_checkout_prefix", None) or "",
}
return sx_call("cart-payments-content",
page_config=SxExpr(serialize(pc_data)) if pc_data else None)
return await render_to_sx("cart-payments-content",
page_config=pc_data)

View File

@@ -126,7 +126,7 @@ def register() -> Blueprint:
frag_params["session_id"] = ident["session_id"]
from sx.sx_components import render_ticket_widget
widget_html = render_ticket_widget(entry, qty, "/all-tickets/adjust")
widget_html = await render_ticket_widget(entry, qty, "/all-tickets/adjust")
mini_html = await fetch_fragment("cart", "cart-mini", params=frag_params, required=False)
return sx_response(widget_html + (mini_html or ""))

View File

@@ -19,7 +19,7 @@ def register():
@require_admin
async def calendar_description_edit(calendar_slug: str, **kwargs):
from sx.sx_components import render_calendar_description_edit
html = render_calendar_description_edit(g.calendar)
html = await render_calendar_description_edit(g.calendar)
return sx_response(html)
@@ -35,7 +35,7 @@ def register():
await g.s.flush()
from sx.sx_components import render_calendar_description
html = render_calendar_description(g.calendar, oob=True)
html = await render_calendar_description(g.calendar, oob=True)
return sx_response(html)
@@ -43,7 +43,7 @@ def register():
@require_admin
async def calendar_description_view(calendar_slug: str, **kwargs):
from sx.sx_components import render_calendar_description
html = render_calendar_description(g.calendar)
html = await render_calendar_description(g.calendar)
return sx_response(html)
return bp

View File

@@ -201,7 +201,7 @@ def register():
from shared.sx.page import get_template_context
from sx.sx_components import _calendar_admin_main_panel_html
ctx = await get_template_context()
html = _calendar_admin_main_panel_html(ctx)
html = await _calendar_admin_main_panel_html(ctx)
return sx_response(html)
@@ -220,7 +220,7 @@ def register():
from shared.sx.page import get_template_context
from sx.sx_components import render_calendars_list_panel
ctx = await get_template_context()
html = render_calendars_list_panel(ctx)
html = await render_calendars_list_panel(ctx)
if post_data:
from shared.services.entry_associations import get_associated_entries
@@ -236,7 +236,7 @@ def register():
).scalars().all()
associated_entries = await get_associated_entries(post_id)
nav_oob = render_post_nav_entries_oob(associated_entries, cals, post_data["post"])
nav_oob = await render_post_nav_entries_oob(associated_entries, cals, post_data["post"])
html = html + nav_oob
return sx_response(html)

View File

@@ -259,7 +259,7 @@ def register():
}
from sx.sx_components import render_day_main_panel
html = render_day_main_panel(ctx)
html = await render_day_main_panel(ctx)
mini_html = await fetch_fragment("cart", "cart-mini", params=frag_params, required=False)
return sx_response(html + (mini_html or ""))
@@ -280,12 +280,12 @@ def register():
day_slots = list(result.scalars())
from sx.sx_components import render_entry_add_form
return sx_response(render_entry_add_form(g.calendar, day, month, year, day_slots))
return sx_response(await render_entry_add_form(g.calendar, day, month, year, day_slots))
@bp.get("/add-button/")
async def add_button(day: int, month: int, year: int, **kwargs):
from sx.sx_components import render_entry_add_button
return sx_response(render_entry_add_button(g.calendar, day, month, year))
return sx_response(await render_entry_add_button(g.calendar, day, month, year))

View File

@@ -112,7 +112,7 @@ def register():
# Render OOB nav
from sx.sx_components import render_day_entries_nav_oob
return render_day_entries_nav_oob(visible.confirmed_entries, calendar, day_date)
return await render_day_entries_nav_oob(visible.confirmed_entries, calendar, day_date)
async def get_post_nav_oob(entry_id: int):
"""Helper to generate OOB update for post entries nav when entry state changes"""
@@ -149,7 +149,7 @@ def register():
# Render OOB nav for this post
from sx.sx_components import render_post_nav_entries_oob
nav_oob = render_post_nav_entries_oob(associated_entries, calendars, post)
nav_oob = await render_post_nav_entries_oob(associated_entries, calendars, post)
nav_oobs.append(nav_oob)
return "".join(nav_oobs)
@@ -257,7 +257,7 @@ def register():
day_slots = list(result.scalars())
from sx.sx_components import render_entry_edit_form
return sx_response(render_entry_edit_form(g.entry, g.calendar, day, month, year, day_slots))
return sx_response(await render_entry_edit_form(g.entry, g.calendar, day, month, year, day_slots))
@bp.put("/")
@require_admin
@@ -423,7 +423,7 @@ def register():
from sx.sx_components import _entry_main_panel_html
tctx = await get_template_context()
html = _entry_main_panel_html(tctx)
html = await _entry_main_panel_html(tctx)
return sx_response(html + nav_oob)
@@ -449,7 +449,7 @@ def register():
# Re-read entry to get updated state
await g.s.refresh(g.entry)
from sx.sx_components import render_entry_optioned
html = render_entry_optioned(g.entry, g.calendar, day, month, year)
html = await render_entry_optioned(g.entry, g.calendar, day, month, year)
return sx_response(html + day_nav_oob + post_nav_oob)
@bp.post("/decline/")
@@ -474,7 +474,7 @@ def register():
# Re-read entry to get updated state
await g.s.refresh(g.entry)
from sx.sx_components import render_entry_optioned
html = render_entry_optioned(g.entry, g.calendar, day, month, year)
html = await render_entry_optioned(g.entry, g.calendar, day, month, year)
return sx_response(html + day_nav_oob + post_nav_oob)
@bp.post("/provisional/")
@@ -499,7 +499,7 @@ def register():
# Re-read entry to get updated state
await g.s.refresh(g.entry)
from sx.sx_components import render_entry_optioned
html = render_entry_optioned(g.entry, g.calendar, day, month, year)
html = await render_entry_optioned(g.entry, g.calendar, day, month, year)
return sx_response(html + day_nav_oob + post_nav_oob)
@bp.post("/tickets/")
@@ -543,7 +543,7 @@ def register():
# Return just the tickets fragment (targeted by hx-target="#entry-tickets-...")
await g.s.refresh(g.entry)
from sx.sx_components import render_entry_tickets_config
html = render_entry_tickets_config(g.entry, g.calendar, request.view_args.get("day"), request.view_args.get("month"), request.view_args.get("year"))
html = await render_entry_tickets_config(g.entry, g.calendar, request.view_args.get("day"), request.view_args.get("month"), request.view_args.get("year"))
return sx_response(html)
@bp.get("/posts/search/")
@@ -559,7 +559,7 @@ def register():
va = request.view_args or {}
from sx.sx_components import render_post_search_results
return sx_response(render_post_search_results(
return sx_response(await render_post_search_results(
search_posts, query, page, total_pages,
g.entry, g.calendar,
va.get("day"), va.get("month"), va.get("year"),
@@ -594,8 +594,8 @@ def register():
# Return updated posts list + OOB nav update
from sx.sx_components import render_entry_posts_panel, render_entry_posts_nav_oob
va = request.view_args or {}
html = render_entry_posts_panel(entry_posts, g.entry, g.calendar, va.get("day"), va.get("month"), va.get("year"))
nav_oob = render_entry_posts_nav_oob(entry_posts)
html = await render_entry_posts_panel(entry_posts, g.entry, g.calendar, va.get("day"), va.get("month"), va.get("year"))
nav_oob = await render_entry_posts_nav_oob(entry_posts)
return sx_response(html + nav_oob)
@bp.delete("/posts/<int:post_id>/")
@@ -616,8 +616,8 @@ def register():
# Return updated posts list + OOB nav update
from sx.sx_components import render_entry_posts_panel, render_entry_posts_nav_oob
va = request.view_args or {}
html = render_entry_posts_panel(entry_posts, g.entry, g.calendar, va.get("day"), va.get("month"), va.get("year"))
nav_oob = render_entry_posts_nav_oob(entry_posts)
html = await render_entry_posts_panel(entry_posts, g.entry, g.calendar, va.get("day"), va.get("month"), va.get("year"))
nav_oob = await render_entry_posts_nav_oob(entry_posts)
return sx_response(html + nav_oob)
return bp

View File

@@ -69,7 +69,7 @@ def register():
from shared.sx.page import get_template_context
from sx.sx_components import render_calendars_list_panel
ctx = await get_template_context()
html = render_calendars_list_panel(ctx)
html = await render_calendars_list_panel(ctx)
# Blog-embedded mode: also update post nav
if post_data:
@@ -85,7 +85,7 @@ def register():
).scalars().all()
associated_entries = await get_associated_entries(post_id)
nav_oob = render_post_nav_entries_oob(associated_entries, cals, post_data["post"])
nav_oob = await render_post_nav_entries_oob(associated_entries, cals, post_data["post"])
html = html + nav_oob
return sx_response(html)

View File

@@ -44,7 +44,7 @@ def register():
from shared.sx.page import get_template_context
from sx.sx_components import render_markets_list_panel
ctx = await get_template_context()
return sx_response(render_markets_list_panel(ctx))
return sx_response(await render_markets_list_panel(ctx))
@bp.delete("/<market_slug>/")
@require_admin
@@ -57,6 +57,6 @@ def register():
from shared.sx.page import get_template_context
from sx.sx_components import render_markets_list_panel
ctx = await get_template_context()
return sx_response(render_markets_list_panel(ctx))
return sx_response(await render_markets_list_panel(ctx))
return bp

View File

@@ -107,7 +107,7 @@ def register() -> Blueprint:
frag_params["session_id"] = ident["session_id"]
from sx.sx_components import render_ticket_widget
widget_html = render_ticket_widget(entry, qty, f"/{g.post_slug}/tickets/adjust")
widget_html = await render_ticket_widget(entry, qty, f"/{g.post_slug}/tickets/adjust")
mini_html = await fetch_fragment("cart", "cart-mini", params=frag_params, required=False)
return sx_response(widget_html + (mini_html or ""))

View File

@@ -36,7 +36,7 @@ def register():
if not slot:
return await make_response("Not found", 404)
from sx.sx_components import render_slot_edit_form
return sx_response(render_slot_edit_form(slot, g.calendar))
return sx_response(await render_slot_edit_form(slot, g.calendar))
@bp.get("/view/")
@require_admin
@@ -45,7 +45,7 @@ def register():
if not slot:
return await make_response("Not found", 404)
from sx.sx_components import render_slot_main_panel
return sx_response(render_slot_main_panel(slot, g.calendar))
return sx_response(await render_slot_main_panel(slot, g.calendar))
@bp.delete("/")
@require_admin
@@ -54,7 +54,7 @@ def register():
await svc_delete_slot(g.s, slot_id)
slots = await svc_list_slots(g.s, g.calendar.id)
from sx.sx_components import render_slots_table
return sx_response(render_slots_table(slots, g.calendar))
return sx_response(await render_slots_table(slots, g.calendar))
@bp.put("/")
@require_admin
@@ -136,7 +136,7 @@ def register():
), 422
from sx.sx_components import render_slot_main_panel
return sx_response(render_slot_main_panel(slot, g.calendar, oob=True))
return sx_response(await render_slot_main_panel(slot, g.calendar, oob=True))

View File

@@ -111,19 +111,19 @@ def register():
# Success → re-render the slots table
slots = await svc_list_slots(g.s, g.calendar.id)
from sx.sx_components import render_slots_table
return sx_response(render_slots_table(slots, g.calendar))
return sx_response(await render_slots_table(slots, g.calendar))
@bp.get("/add")
@require_admin
async def add_form(**kwargs):
from sx.sx_components import render_slot_add_form
return sx_response(render_slot_add_form(g.calendar))
return sx_response(await render_slot_add_form(g.calendar))
@bp.get("/add-button")
@require_admin
async def add_button(**kwargs):
from sx.sx_components import render_slot_add_button
return sx_response(render_slot_add_button(g.calendar))
return sx_response(await render_slot_add_button(g.calendar))
return bp

View File

@@ -54,7 +54,7 @@ def register() -> Blueprint:
tickets = await get_tickets_for_entry(g.s, entry_id)
from sx.sx_components import render_entry_tickets_admin
html = render_entry_tickets_admin(entry, tickets)
html = await render_entry_tickets_admin(entry, tickets)
return sx_response(html)
@bp.get("/lookup/")
@@ -71,9 +71,9 @@ def register() -> Blueprint:
ticket = await get_ticket_by_code(g.s, code)
from sx.sx_components import render_lookup_result
if not ticket:
return sx_response(render_lookup_result(None, "Ticket not found"))
return sx_response(await render_lookup_result(None, "Ticket not found"))
return sx_response(render_lookup_result(ticket, None))
return sx_response(await render_lookup_result(ticket, None))
@bp.post("/<code>/checkin/")
@require_admin
@@ -84,9 +84,9 @@ def register() -> Blueprint:
from sx.sx_components import render_checkin_result
if not success:
return sx_response(render_checkin_result(False, error, None))
return sx_response(await render_checkin_result(False, error, None))
ticket = await get_ticket_by_code(g.s, code)
return sx_response(render_checkin_result(True, None, ticket))
return sx_response(await render_checkin_result(True, None, ticket))
return bp

View File

@@ -32,7 +32,7 @@ def register():
from sx.sx_components import render_ticket_type_edit_form
va = request.view_args or {}
return sx_response(render_ticket_type_edit_form(
return sx_response(await render_ticket_type_edit_form(
ticket_type, g.entry, g.calendar,
va.get("day"), va.get("month"), va.get("year"),
))
@@ -47,7 +47,7 @@ def register():
from sx.sx_components import render_ticket_type_main_panel
va = request.view_args or {}
return sx_response(render_ticket_type_main_panel(
return sx_response(await render_ticket_type_main_panel(
ticket_type, g.entry, g.calendar,
va.get("day"), va.get("month"), va.get("year"),
))
@@ -114,7 +114,7 @@ def register():
# Return updated view with OOB flag
from sx.sx_components import render_ticket_type_main_panel
va = request.view_args or {}
return sx_response(render_ticket_type_main_panel(
return sx_response(await render_ticket_type_main_panel(
ticket_type, g.entry, g.calendar,
va.get("day"), va.get("month"), va.get("year"),
oob=True,
@@ -133,7 +133,7 @@ def register():
ticket_types = await svc_list_ticket_types(g.s, g.entry.id)
from sx.sx_components import render_ticket_types_table
va = request.view_args or {}
return sx_response(render_ticket_types_table(
return sx_response(await render_ticket_types_table(
ticket_types, g.entry, g.calendar,
va.get("day"), va.get("month"), va.get("year"),
))

View File

@@ -95,7 +95,7 @@ def register():
ticket_types = await svc_list_ticket_types(g.s, g.entry.id)
from sx.sx_components import render_ticket_types_table
va = request.view_args or {}
return sx_response(render_ticket_types_table(
return sx_response(await render_ticket_types_table(
ticket_types, g.entry, g.calendar,
va.get("day"), va.get("month"), va.get("year"),
))
@@ -106,7 +106,7 @@ def register():
"""Show the add ticket type form."""
from sx.sx_components import render_ticket_type_add_form
va = request.view_args or {}
return sx_response(render_ticket_type_add_form(
return sx_response(await render_ticket_type_add_form(
g.entry, g.calendar,
va.get("day"), va.get("month"), va.get("year"),
))
@@ -117,7 +117,7 @@ def register():
"""Show the add ticket type button."""
from sx.sx_components import render_ticket_type_add_button
va = request.view_args or {}
return sx_response(render_ticket_type_add_button(
return sx_response(await render_ticket_type_add_button(
g.entry, g.calendar,
va.get("day"), va.get("month"), va.get("year"),
))

View File

@@ -127,7 +127,7 @@ def register() -> Blueprint:
cart_count = summary.count + summary.calendar_count + summary.ticket_count
from sx.sx_components import render_buy_result
return sx_response(render_buy_result(entry, created, remaining, cart_count))
return sx_response(await render_buy_result(entry, created, remaining, cart_count))
@bp.post("/adjust/")
@clear_cache(tag="calendars", tag_scope="all")
@@ -250,7 +250,7 @@ def register() -> Blueprint:
cart_count = summary.count + summary.calendar_count + summary.ticket_count
from sx.sx_components import render_adjust_response
return sx_response(render_adjust_response(
return sx_response(await render_adjust_response(
entry, ticket_remaining, ticket_sold_count,
user_ticket_count, user_ticket_counts_by_type, cart_count,
))

File diff suppressed because it is too large Load Diff

View File

@@ -44,11 +44,11 @@ async def _cal_admin_full(ctx: dict, **kw: Any) -> str:
)
ctx = await _ensure_container_nav(ctx)
slug = (ctx.get("post") or {}).get("slug", "")
root_hdr = root_header_sx(ctx)
post_hdr = _post_header_sx(ctx)
admin_hdr = post_admin_header_sx(ctx, slug, selected="calendars")
child = admin_hdr + _calendar_header_sx(ctx) + _calendar_admin_header_sx(ctx)
return root_hdr + post_hdr + header_child_sx(child)
root_hdr = await root_header_sx(ctx)
post_hdr = await _post_header_sx(ctx)
admin_hdr = await post_admin_header_sx(ctx, slug, selected="calendars")
child = admin_hdr + await _calendar_header_sx(ctx) + await _calendar_admin_header_sx(ctx)
return root_hdr + post_hdr + await header_child_sx(child)
async def _cal_admin_oob(ctx: dict, **kw: Any) -> str:
@@ -59,10 +59,10 @@ async def _cal_admin_oob(ctx: dict, **kw: Any) -> str:
)
ctx = await _ensure_container_nav(ctx)
slug = (ctx.get("post") or {}).get("slug", "")
oobs = (post_admin_header_sx(ctx, slug, oob=True, selected="calendars")
+ _calendar_header_sx(ctx, oob=True))
oobs += oob_header_sx("calendar-header-child", "calendar-admin-header-child",
_calendar_admin_header_sx(ctx))
oobs = (await post_admin_header_sx(ctx, slug, oob=True, selected="calendars")
+ await _calendar_header_sx(ctx, oob=True))
oobs += await oob_header_sx("calendar-header-child", "calendar-admin-header-child",
await _calendar_admin_header_sx(ctx))
oobs += _clear_deeper_oob("post-row", "post-header-child",
"post-admin-row", "post-admin-header-child",
"calendar-row", "calendar-header-child",
@@ -83,8 +83,8 @@ async def _slots_oob(ctx: dict, **kw: Any) -> str:
)
ctx = await _ensure_container_nav({**ctx, "is_admin_section": True})
slug = (ctx.get("post") or {}).get("slug", "")
oobs = (post_admin_header_sx(ctx, slug, oob=True, selected="calendars")
+ _calendar_admin_header_sx(ctx, oob=True))
oobs = (await post_admin_header_sx(ctx, slug, oob=True, selected="calendars")
+ await _calendar_admin_header_sx(ctx, oob=True))
oobs += _clear_deeper_oob("post-row", "post-header-child",
"post-admin-row", "post-admin-header-child",
"calendar-row", "calendar-header-child",
@@ -102,12 +102,12 @@ async def _slot_full(ctx: dict, **kw: Any) -> str:
)
ctx = await _ensure_container_nav({**ctx, "is_admin_section": True})
slug = (ctx.get("post") or {}).get("slug", "")
root_hdr = root_header_sx(ctx)
post_hdr = _post_header_sx(ctx)
admin_hdr = post_admin_header_sx(ctx, slug, selected="calendars")
child = (admin_hdr + _calendar_header_sx(ctx)
+ _calendar_admin_header_sx(ctx) + _slot_header_html(ctx))
return root_hdr + post_hdr + header_child_sx(child)
root_hdr = await root_header_sx(ctx)
post_hdr = await _post_header_sx(ctx)
admin_hdr = await post_admin_header_sx(ctx, slug, selected="calendars")
child = (admin_hdr + await _calendar_header_sx(ctx)
+ await _calendar_admin_header_sx(ctx) + await _slot_header_html(ctx))
return root_hdr + post_hdr + await header_child_sx(child)
async def _slot_oob(ctx: dict, **kw: Any) -> str:
@@ -118,10 +118,10 @@ async def _slot_oob(ctx: dict, **kw: Any) -> str:
)
ctx = await _ensure_container_nav({**ctx, "is_admin_section": True})
slug = (ctx.get("post") or {}).get("slug", "")
oobs = (post_admin_header_sx(ctx, slug, oob=True, selected="calendars")
+ _calendar_admin_header_sx(ctx, oob=True))
oobs += oob_header_sx("calendar-admin-header-child", "slot-header-child",
_slot_header_html(ctx))
oobs = (await post_admin_header_sx(ctx, slug, oob=True, selected="calendars")
+ await _calendar_admin_header_sx(ctx, oob=True))
oobs += await oob_header_sx("calendar-admin-header-child", "slot-header-child",
await _slot_header_html(ctx))
oobs += _clear_deeper_oob("post-row", "post-header-child",
"post-admin-row", "post-admin-header-child",
"calendar-row", "calendar-header-child",
@@ -140,12 +140,12 @@ async def _day_admin_full(ctx: dict, **kw: Any) -> str:
)
ctx = await _ensure_container_nav(ctx)
slug = (ctx.get("post") or {}).get("slug", "")
root_hdr = root_header_sx(ctx)
post_hdr = _post_header_sx(ctx)
admin_hdr = post_admin_header_sx(ctx, slug, selected="calendars")
child = (admin_hdr + _calendar_header_sx(ctx) + _day_header_sx(ctx)
+ _day_admin_header_sx(ctx))
return root_hdr + post_hdr + header_child_sx(child)
root_hdr = await root_header_sx(ctx)
post_hdr = await _post_header_sx(ctx)
admin_hdr = await post_admin_header_sx(ctx, slug, selected="calendars")
child = (admin_hdr + await _calendar_header_sx(ctx) + await _day_header_sx(ctx)
+ await _day_admin_header_sx(ctx))
return root_hdr + post_hdr + await header_child_sx(child)
async def _day_admin_oob(ctx: dict, **kw: Any) -> str:
@@ -156,10 +156,10 @@ async def _day_admin_oob(ctx: dict, **kw: Any) -> str:
)
ctx = await _ensure_container_nav(ctx)
slug = (ctx.get("post") or {}).get("slug", "")
oobs = (post_admin_header_sx(ctx, slug, oob=True, selected="calendars")
+ _calendar_header_sx(ctx, oob=True))
oobs += oob_header_sx("day-header-child", "day-admin-header-child",
_day_admin_header_sx(ctx))
oobs = (await post_admin_header_sx(ctx, slug, oob=True, selected="calendars")
+ await _calendar_header_sx(ctx, oob=True))
oobs += await oob_header_sx("day-header-child", "day-admin-header-child",
await _day_admin_header_sx(ctx))
oobs += _clear_deeper_oob("post-row", "post-header-child",
"post-admin-row", "post-admin-header-child",
"calendar-row", "calendar-header-child",
@@ -170,26 +170,26 @@ async def _day_admin_oob(ctx: dict, **kw: Any) -> str:
# --- Entry layout (root + child(post + cal + day + entry), + menu) ---
def _entry_full(ctx: dict, **kw: Any) -> str:
async def _entry_full(ctx: dict, **kw: Any) -> str:
from shared.sx.helpers import root_header_sx, header_child_sx
from sx.sx_components import (
_post_header_sx, _calendar_header_sx,
_day_header_sx, _entry_header_html,
)
root_hdr = root_header_sx(ctx)
child = (_post_header_sx(ctx) + _calendar_header_sx(ctx)
+ _day_header_sx(ctx) + _entry_header_html(ctx))
return root_hdr + header_child_sx(child)
root_hdr = await root_header_sx(ctx)
child = (await _post_header_sx(ctx) + await _calendar_header_sx(ctx)
+ await _day_header_sx(ctx) + await _entry_header_html(ctx))
return root_hdr + await header_child_sx(child)
def _entry_oob(ctx: dict, **kw: Any) -> str:
async def _entry_oob(ctx: dict, **kw: Any) -> str:
from shared.sx.helpers import oob_header_sx
from sx.sx_components import (
_day_header_sx, _entry_header_html, _clear_deeper_oob,
)
oobs = _day_header_sx(ctx, oob=True)
oobs += oob_header_sx("day-header-child", "entry-header-child",
_entry_header_html(ctx))
oobs = await _day_header_sx(ctx, oob=True)
oobs += await oob_header_sx("day-header-child", "entry-header-child",
await _entry_header_html(ctx))
oobs += _clear_deeper_oob("post-row", "post-header-child",
"calendar-row", "calendar-header-child",
"day-row", "day-header-child",
@@ -208,12 +208,12 @@ async def _entry_admin_full(ctx: dict, **kw: Any) -> str:
)
ctx = await _ensure_container_nav(ctx)
slug = (ctx.get("post") or {}).get("slug", "")
root_hdr = root_header_sx(ctx)
post_hdr = _post_header_sx(ctx)
admin_hdr = post_admin_header_sx(ctx, slug, selected="calendars")
child = (admin_hdr + _calendar_header_sx(ctx) + _day_header_sx(ctx)
+ _entry_header_html(ctx) + _entry_admin_header_html(ctx))
return root_hdr + post_hdr + header_child_sx(child)
root_hdr = await root_header_sx(ctx)
post_hdr = await _post_header_sx(ctx)
admin_hdr = await post_admin_header_sx(ctx, slug, selected="calendars")
child = (admin_hdr + await _calendar_header_sx(ctx) + await _day_header_sx(ctx)
+ await _entry_header_html(ctx) + await _entry_admin_header_html(ctx))
return root_hdr + post_hdr + await header_child_sx(child)
async def _entry_admin_oob(ctx: dict, **kw: Any) -> str:
@@ -224,10 +224,10 @@ async def _entry_admin_oob(ctx: dict, **kw: Any) -> str:
)
ctx = await _ensure_container_nav(ctx)
slug = (ctx.get("post") or {}).get("slug", "")
oobs = (post_admin_header_sx(ctx, slug, oob=True, selected="calendars")
+ _entry_header_html(ctx, oob=True))
oobs += oob_header_sx("entry-header-child", "entry-admin-header-child",
_entry_admin_header_html(ctx))
oobs = (await post_admin_header_sx(ctx, slug, oob=True, selected="calendars")
+ await _entry_header_html(ctx, oob=True))
oobs += await oob_header_sx("entry-header-child", "entry-admin-header-child",
await _entry_admin_header_html(ctx))
oobs += _clear_deeper_oob("post-row", "post-header-child",
"post-admin-row", "post-admin-header-child",
"calendar-row", "calendar-header-child",
@@ -239,75 +239,75 @@ async def _entry_admin_oob(ctx: dict, **kw: Any) -> str:
# --- Ticket types layout (extends entry admin with ticket-types header, + menu) ---
def _ticket_types_full(ctx: dict, **kw: Any) -> str:
async def _ticket_types_full(ctx: dict, **kw: Any) -> str:
from shared.sx.helpers import root_header_sx, header_child_sx
from sx.sx_components import (
_post_header_sx, _calendar_header_sx, _day_header_sx,
_entry_header_html, _entry_admin_header_html,
_ticket_types_header_html,
)
root_hdr = root_header_sx(ctx)
child = (_post_header_sx(ctx) + _calendar_header_sx(ctx)
+ _day_header_sx(ctx) + _entry_header_html(ctx)
+ _entry_admin_header_html(ctx) + _ticket_types_header_html(ctx))
return root_hdr + header_child_sx(child)
root_hdr = await root_header_sx(ctx)
child = (await _post_header_sx(ctx) + await _calendar_header_sx(ctx)
+ await _day_header_sx(ctx) + await _entry_header_html(ctx)
+ await _entry_admin_header_html(ctx) + await _ticket_types_header_html(ctx))
return root_hdr + await header_child_sx(child)
def _ticket_types_oob(ctx: dict, **kw: Any) -> str:
async def _ticket_types_oob(ctx: dict, **kw: Any) -> str:
from shared.sx.helpers import oob_header_sx
from sx.sx_components import (
_entry_admin_header_html, _ticket_types_header_html, _clear_deeper_oob,
)
oobs = _entry_admin_header_html(ctx, oob=True)
oobs += oob_header_sx("entry-admin-header-child", "ticket_types-header-child",
_ticket_types_header_html(ctx))
oobs = await _entry_admin_header_html(ctx, oob=True)
oobs += await oob_header_sx("entry-admin-header-child", "ticket_types-header-child",
await _ticket_types_header_html(ctx))
return oobs
# --- Ticket type detail layout (extends ticket types with ticket-type header, + menu) ---
def _ticket_type_full(ctx: dict, **kw: Any) -> str:
async def _ticket_type_full(ctx: dict, **kw: Any) -> str:
from shared.sx.helpers import root_header_sx, header_child_sx
from sx.sx_components import (
_post_header_sx, _calendar_header_sx, _day_header_sx,
_entry_header_html, _entry_admin_header_html,
_ticket_types_header_html, _ticket_type_header_html,
)
root_hdr = root_header_sx(ctx)
child = (_post_header_sx(ctx) + _calendar_header_sx(ctx)
+ _day_header_sx(ctx) + _entry_header_html(ctx)
+ _entry_admin_header_html(ctx) + _ticket_types_header_html(ctx)
+ _ticket_type_header_html(ctx))
return root_hdr + header_child_sx(child)
root_hdr = await root_header_sx(ctx)
child = (await _post_header_sx(ctx) + await _calendar_header_sx(ctx)
+ await _day_header_sx(ctx) + await _entry_header_html(ctx)
+ await _entry_admin_header_html(ctx) + await _ticket_types_header_html(ctx)
+ await _ticket_type_header_html(ctx))
return root_hdr + await header_child_sx(child)
def _ticket_type_oob(ctx: dict, **kw: Any) -> str:
async def _ticket_type_oob(ctx: dict, **kw: Any) -> str:
from shared.sx.helpers import oob_header_sx
from sx.sx_components import (
_ticket_types_header_html, _ticket_type_header_html,
)
oobs = _ticket_types_header_html(ctx, oob=True)
oobs += oob_header_sx("ticket_types-header-child", "ticket_type-header-child",
_ticket_type_header_html(ctx))
oobs = await _ticket_types_header_html(ctx, oob=True)
oobs += await oob_header_sx("ticket_types-header-child", "ticket_type-header-child",
await _ticket_type_header_html(ctx))
return oobs
# --- Markets layout (root + child(post + markets)) ---
def _markets_full(ctx: dict, **kw: Any) -> str:
async def _markets_full(ctx: dict, **kw: Any) -> str:
from shared.sx.helpers import root_header_sx, header_child_sx
from sx.sx_components import _post_header_sx, _markets_header_sx
root_hdr = root_header_sx(ctx)
child = _post_header_sx(ctx) + _markets_header_sx(ctx)
return root_hdr + header_child_sx(child)
root_hdr = await root_header_sx(ctx)
child = await _post_header_sx(ctx) + await _markets_header_sx(ctx)
return root_hdr + await header_child_sx(child)
def _markets_oob(ctx: dict, **kw: Any) -> str:
async def _markets_oob(ctx: dict, **kw: Any) -> str:
from shared.sx.helpers import oob_header_sx
from sx.sx_components import _post_header_sx, _markets_header_sx
oobs = _post_header_sx(ctx, oob=True)
oobs += oob_header_sx("post-header-child", "markets-header-child",
_markets_header_sx(ctx))
oobs = await _post_header_sx(ctx, oob=True)
oobs += await oob_header_sx("post-header-child", "markets-header-child",
await _markets_header_sx(ctx))
return oobs
@@ -518,7 +518,7 @@ async def _h_calendar_admin_content(calendar_slug=None, **kw):
from shared.sx.page import get_template_context
from sx.sx_components import _calendar_admin_main_panel_html
ctx = await get_template_context()
return _calendar_admin_main_panel_html(ctx)
return await _calendar_admin_main_panel_html(ctx)
async def _h_day_admin_content(calendar_slug=None, year=None, month=None, day=None, **kw):
@@ -526,7 +526,7 @@ async def _h_day_admin_content(calendar_slug=None, year=None, month=None, day=No
if year is not None:
await _ensure_day_data(int(year), int(month), int(day))
from sx.sx_components import _day_admin_main_panel_html
return _day_admin_main_panel_html({})
return await _day_admin_main_panel_html({})
async def _h_slots_content(calendar_slug=None, **kw):
@@ -537,7 +537,7 @@ async def _h_slots_content(calendar_slug=None, **kw):
slots = await svc_list_slots(g.s, calendar.id) if calendar else []
_add_to_defpage_ctx(slots=slots)
from sx.sx_components import render_slots_table
return render_slots_table(slots, calendar)
return await render_slots_table(slots, calendar)
async def _h_slot_content(calendar_slug=None, slot_id=None, **kw):
@@ -551,7 +551,7 @@ async def _h_slot_content(calendar_slug=None, slot_id=None, **kw):
_add_to_defpage_ctx(slot=slot)
calendar = getattr(g, "calendar", None)
from sx.sx_components import render_slot_main_panel
return render_slot_main_panel(slot, calendar)
return await render_slot_main_panel(slot, calendar)
async def _h_entry_content(calendar_slug=None, entry_id=None, **kw):
@@ -560,7 +560,7 @@ async def _h_entry_content(calendar_slug=None, entry_id=None, **kw):
from shared.sx.page import get_template_context
from sx.sx_components import _entry_main_panel_html
ctx = await get_template_context()
return _entry_main_panel_html(ctx)
return await _entry_main_panel_html(ctx)
async def _h_entry_menu(calendar_slug=None, entry_id=None, **kw):
@@ -569,7 +569,7 @@ async def _h_entry_menu(calendar_slug=None, entry_id=None, **kw):
from shared.sx.page import get_template_context
from sx.sx_components import _entry_nav_html
ctx = await get_template_context()
return _entry_nav_html(ctx)
return await _entry_nav_html(ctx)
async def _h_entry_admin_content(calendar_slug=None, entry_id=None, **kw):
@@ -578,12 +578,12 @@ async def _h_entry_admin_content(calendar_slug=None, entry_id=None, **kw):
from shared.sx.page import get_template_context
from sx.sx_components import _entry_admin_main_panel_html
ctx = await get_template_context()
return _entry_admin_main_panel_html(ctx)
return await _entry_admin_main_panel_html(ctx)
def _h_admin_menu():
from shared.sx.helpers import sx_call
return sx_call("events-admin-placeholder-nav")
async def _h_admin_menu():
from shared.sx.helpers import render_to_sx
return await render_to_sx("events-admin-placeholder-nav")
async def _h_ticket_types_content(calendar_slug=None, entry_id=None,
@@ -597,7 +597,7 @@ async def _h_ticket_types_content(calendar_slug=None, entry_id=None,
ticket_types = await svc_list_ticket_types(g.s, entry.id) if entry else []
_add_to_defpage_ctx(ticket_types=ticket_types)
from sx.sx_components import render_ticket_types_table
return render_ticket_types_table(ticket_types, entry, calendar, day, month, year)
return await render_ticket_types_table(ticket_types, entry, calendar, day, month, year)
async def _h_ticket_type_content(calendar_slug=None, entry_id=None,
@@ -614,7 +614,7 @@ async def _h_ticket_type_content(calendar_slug=None, entry_id=None,
entry = getattr(g, "entry", None)
calendar = getattr(g, "calendar", None)
from sx.sx_components import render_ticket_type_main_panel
return render_ticket_type_main_panel(ticket_type, entry, calendar, day, month, year)
return await render_ticket_type_main_panel(ticket_type, entry, calendar, day, month, year)
async def _h_tickets_content(**kw):
@@ -630,7 +630,7 @@ async def _h_tickets_content(**kw):
from shared.sx.page import get_template_context
from sx.sx_components import _tickets_main_panel_html
ctx = await get_template_context()
return _tickets_main_panel_html(ctx, tickets)
return await _tickets_main_panel_html(ctx, tickets)
async def _h_ticket_detail_content(code=None, **kw):
@@ -653,7 +653,7 @@ async def _h_ticket_detail_content(code=None, **kw):
from shared.sx.page import get_template_context
from sx.sx_components import _ticket_detail_panel_html
ctx = await get_template_context()
return _ticket_detail_panel_html(ctx, ticket)
return await _ticket_detail_panel_html(ctx, ticket)
async def _h_ticket_admin_content(**kw):
@@ -693,11 +693,11 @@ async def _h_ticket_admin_content(**kw):
from shared.sx.page import get_template_context
from sx.sx_components import _ticket_admin_main_panel_html
ctx = await get_template_context()
return _ticket_admin_main_panel_html(ctx, tickets, stats)
return await _ticket_admin_main_panel_html(ctx, tickets, stats)
async def _h_markets_content(**kw):
from shared.sx.page import get_template_context
from sx.sx_components import _markets_main_panel_html
ctx = await get_template_context()
return _markets_main_panel_html(ctx)
return await _markets_main_panel_html(ctx)

View File

@@ -156,7 +156,7 @@ def register(url_prefix="/social"):
else:
list_type = "following"
from sx.sx_components import render_actor_card
return sx_response(render_actor_card(remote_dto, actor, followed_urls, list_type=list_type))
return sx_response(await render_actor_card(remote_dto, actor, followed_urls, list_type=list_type))
# -- Interactions ----------------------------------------------------------
@@ -243,7 +243,7 @@ def register(url_prefix="/social"):
)).scalar())
from sx.sx_components import render_interaction_buttons
return sx_response(render_interaction_buttons(
return sx_response(await render_interaction_buttons(
object_id=object_id,
author_inbox=author_inbox,
like_count=like_count,

View File

@@ -13,9 +13,8 @@ from typing import Any
from markupsafe import escape
from shared.sx.jinja_bridge import load_service_components
from shared.sx.parser import serialize
from shared.sx.helpers import (
sx_call, SxExpr,
render_to_sx,
root_header_sx, full_page_sx, header_child_sx,
)
@@ -81,16 +80,16 @@ def _serialize_remote_actor(a) -> dict:
# Social page shell
# ---------------------------------------------------------------------------
def _social_page(ctx: dict, actor: Any, *, content: str,
async def _social_page(ctx: dict, actor: Any, *, content: str,
title: str = "Rose Ash", meta_html: str = "") -> str:
from shared.sx.parser import SxExpr
actor_data = _serialize_actor(actor)
nav = sx_call("federation-social-nav",
actor=SxExpr(serialize(actor_data)) if actor_data else None)
social_hdr = sx_call("federation-social-header", nav=SxExpr(nav))
hdr = root_header_sx(ctx)
child = header_child_sx(social_hdr)
nav = await render_to_sx("federation-social-nav", actor=actor_data)
social_hdr = await render_to_sx("federation-social-header", nav=SxExpr(nav))
hdr = await root_header_sx(ctx)
child = await header_child_sx(social_hdr)
header_rows = "(<> " + hdr + " " + child + ")"
return full_page_sx(ctx, header_rows=header_rows, content=content,
return await full_page_sx(ctx, header_rows=header_rows, content=content,
meta_html=meta_html or f'<title>{escape(title)}</title>')
@@ -99,24 +98,24 @@ def _social_page(ctx: dict, actor: Any, *, content: str,
# ---------------------------------------------------------------------------
async def render_federation_home(ctx: dict) -> str:
hdr = root_header_sx(ctx)
return full_page_sx(ctx, header_rows=hdr)
hdr = await root_header_sx(ctx)
return await full_page_sx(ctx, header_rows=hdr)
async def render_login_page(ctx: dict) -> str:
error = ctx.get("error", "")
email = ctx.get("email", "")
content = sx_call("account-login-content",
content = await render_to_sx("account-login-content",
error=error or None, email=str(escape(email)))
return _social_page(ctx, None, content=content, title="Login \u2014 Rose Ash")
return await _social_page(ctx, None, content=content, title="Login \u2014 Rose Ash")
async def render_check_email_page(ctx: dict) -> str:
email = ctx.get("email", "")
email_error = ctx.get("email_error")
content = sx_call("account-check-email-content",
content = await render_to_sx("account-check-email-content",
email=str(escape(email)), email_error=email_error)
return _social_page(ctx, None, content=content,
return await _social_page(ctx, None, content=content,
title="Check your email \u2014 Rose Ash")
@@ -124,6 +123,7 @@ async def render_choose_username_page(ctx: dict) -> str:
from shared.browser.app.csrf import generate_csrf_token
from quart import url_for
from shared.config import config
from shared.sx.parser import SxExpr
csrf = generate_csrf_token()
error = ctx.get("error", "")
@@ -132,15 +132,15 @@ async def render_choose_username_page(ctx: dict) -> str:
check_url = url_for("identity.check_username")
actor = ctx.get("actor")
error_sx = sx_call("auth-error-banner", error=error) if error else ""
content = sx_call(
error_sx = await render_to_sx("auth-error-banner", error=error) if error else ""
content = await render_to_sx(
"federation-choose-username",
domain=str(escape(ap_domain)),
error=SxExpr(error_sx) if error_sx else None,
csrf=csrf, username=str(escape(username)),
check_url=check_url,
)
return _social_page(ctx, actor, content=content,
return await _social_page(ctx, actor, content=content,
title="Choose Username \u2014 Rose Ash")
@@ -164,10 +164,10 @@ async def render_timeline_items(items: list, timeline_type: str,
else:
next_url = url_for(f"social.{timeline_type}_timeline_page", before=before)
return sx_call("federation-timeline-items",
items=SxExpr(serialize(item_dicts)),
return await render_to_sx("federation-timeline-items",
items=item_dicts,
timeline_type=timeline_type,
actor=SxExpr(serialize(actor_data)) if actor_data else None,
actor=actor_data,
next_url=next_url)
@@ -178,14 +178,14 @@ async def render_search_results(actors: list, query: str, page: int,
actor_data = _serialize_actor(actor)
parts = []
for ad in actor_dicts:
parts.append(sx_call("federation-actor-card-from-data",
a=SxExpr(serialize(ad)),
actor=SxExpr(serialize(actor_data)) if actor_data else None,
followed_urls=SxExpr(serialize(list(followed_urls))),
parts.append(await render_to_sx("federation-actor-card-from-data",
a=ad,
actor=actor_data,
followed_urls=list(followed_urls),
list_type="search"))
if len(actors) >= 20:
next_url = url_for("social.search_page", q=query, page=page + 1)
parts.append(sx_call("federation-scroll-sentinel", url=next_url))
parts.append(await render_to_sx("federation-scroll-sentinel", url=next_url))
return "(<> " + " ".join(parts) + ")" if parts else ""
@@ -195,14 +195,14 @@ async def render_following_items(actors: list, page: int, actor: Any) -> str:
actor_data = _serialize_actor(actor)
parts = []
for ad in actor_dicts:
parts.append(sx_call("federation-actor-card-from-data",
a=SxExpr(serialize(ad)),
actor=SxExpr(serialize(actor_data)) if actor_data else None,
followed_urls=SxExpr(serialize([])),
parts.append(await render_to_sx("federation-actor-card-from-data",
a=ad,
actor=actor_data,
followed_urls=[],
list_type="following"))
if len(actors) >= 20:
next_url = url_for("social.following_list_page", page=page + 1)
parts.append(sx_call("federation-scroll-sentinel", url=next_url))
parts.append(await render_to_sx("federation-scroll-sentinel", url=next_url))
return "(<> " + " ".join(parts) + ")" if parts else ""
@@ -213,14 +213,14 @@ async def render_followers_items(actors: list, page: int,
actor_data = _serialize_actor(actor)
parts = []
for ad in actor_dicts:
parts.append(sx_call("federation-actor-card-from-data",
a=SxExpr(serialize(ad)),
actor=SxExpr(serialize(actor_data)) if actor_data else None,
followed_urls=SxExpr(serialize(list(followed_urls))),
parts.append(await render_to_sx("federation-actor-card-from-data",
a=ad,
actor=actor_data,
followed_urls=list(followed_urls),
list_type="followers"))
if len(actors) >= 20:
next_url = url_for("social.followers_list_page", page=page + 1)
parts.append(sx_call("federation-scroll-sentinel", url=next_url))
parts.append(await render_to_sx("federation-scroll-sentinel", url=next_url))
return "(<> " + " ".join(parts) + ")" if parts else ""
@@ -233,13 +233,14 @@ async def render_actor_timeline_items(items: list, actor_id: int,
# Public API: POST handler fragment renderers
# ---------------------------------------------------------------------------
def render_interaction_buttons(object_id: str, author_inbox: str,
async def render_interaction_buttons(object_id: str, author_inbox: str,
like_count: int, boost_count: int,
liked_by_me: bool, boosted_by_me: bool,
actor: Any) -> str:
"""Render interaction buttons fragment for POST response."""
from shared.browser.app.csrf import generate_csrf_token
from quart import url_for
from shared.sx.parser import SxExpr
csrf = generate_csrf_token()
safe_id = object_id.replace("/", "_").replace(":", "_")
@@ -262,31 +263,31 @@ def render_interaction_buttons(object_id: str, author_inbox: str,
boost_cls = "hover:text-green-600"
reply_url = url_for("social.defpage_compose_form", reply_to=object_id) if object_id else ""
reply_sx = sx_call("federation-reply-link", url=reply_url) if reply_url else ""
reply_sx = await render_to_sx("federation-reply-link", url=reply_url) if reply_url else ""
like_form = sx_call("federation-like-form",
like_form = await render_to_sx("federation-like-form",
action=like_action, target=target, oid=object_id, ainbox=author_inbox,
csrf=csrf, cls=f"flex items-center gap-1 {like_cls}",
icon=like_icon, count=str(like_count))
boost_form = sx_call("federation-boost-form",
boost_form = await render_to_sx("federation-boost-form",
action=boost_action, target=target, oid=object_id, ainbox=author_inbox,
csrf=csrf, cls=f"flex items-center gap-1 {boost_cls}",
count=str(boost_count))
return sx_call("federation-interaction-buttons",
return await render_to_sx("federation-interaction-buttons",
like=SxExpr(like_form),
boost=SxExpr(boost_form),
reply=SxExpr(reply_sx) if reply_sx else None)
def render_actor_card(actor_dto: Any, actor: Any, followed_urls: set,
async def render_actor_card(actor_dto: Any, actor: Any, followed_urls: set,
*, list_type: str = "following") -> str:
"""Render a single actor card fragment for POST response."""
actor_data = _serialize_actor(actor)
ad = _serialize_remote_actor(actor_dto)
return sx_call("federation-actor-card-from-data",
a=SxExpr(serialize(ad)),
actor=SxExpr(serialize(actor_data)) if actor_data else None,
followed_urls=SxExpr(serialize(list(followed_urls))),
return await render_to_sx("federation-actor-card-from-data",
a=ad,
actor=actor_data,
followed_urls=list(followed_urls),
list_type=list_type)

View File

@@ -26,33 +26,31 @@ def _register_federation_layouts() -> None:
register_custom_layout("social", _social_full, _social_oob)
def _social_full(ctx: dict, **kw: Any) -> str:
from shared.sx.helpers import root_header_sx, header_child_sx, sx_call, SxExpr
from shared.sx.parser import serialize
async def _social_full(ctx: dict, **kw: Any) -> str:
from shared.sx.helpers import root_header_sx, header_child_sx, render_to_sx
from shared.sx.parser import SxExpr
actor = ctx.get("actor")
actor_data = _serialize_actor(actor) if actor else None
nav = sx_call("federation-social-nav",
actor=SxExpr(serialize(actor_data)) if actor_data else None)
social_hdr = sx_call("federation-social-header", nav=SxExpr(nav))
root_hdr = root_header_sx(ctx)
child = header_child_sx(social_hdr)
nav = await render_to_sx("federation-social-nav", actor=actor_data)
social_hdr = await render_to_sx("federation-social-header", nav=SxExpr(nav))
root_hdr = await root_header_sx(ctx)
child = await header_child_sx(social_hdr)
return "(<> " + root_hdr + " " + child + ")"
def _social_oob(ctx: dict, **kw: Any) -> str:
from shared.sx.helpers import root_header_sx, sx_call, SxExpr
from shared.sx.parser import serialize
async def _social_oob(ctx: dict, **kw: Any) -> str:
from shared.sx.helpers import root_header_sx, render_to_sx
from shared.sx.parser import SxExpr
actor = ctx.get("actor")
actor_data = _serialize_actor(actor) if actor else None
nav = sx_call("federation-social-nav",
actor=SxExpr(serialize(actor_data)) if actor_data else None)
social_hdr = sx_call("federation-social-header", nav=SxExpr(nav))
child_oob = sx_call("oob-header-sx",
nav = await render_to_sx("federation-social-nav", actor=actor_data)
social_hdr = await render_to_sx("federation-social-header", nav=SxExpr(nav))
child_oob = await render_to_sx("oob-header-sx",
parent_id="root-header-child",
row=SxExpr(social_hdr))
root_hdr_oob = root_header_sx(ctx, oob=True)
root_hdr_oob = await root_header_sx(ctx, oob=True)
return "(<> " + child_oob + " " + root_hdr_oob + ")"
@@ -145,43 +143,40 @@ def _require_actor():
async def _h_home_timeline_content(**kw):
from quart import g
from shared.services.registry import services
from shared.sx.helpers import sx_call, SxExpr
from shared.sx.parser import serialize
from shared.sx.helpers import render_to_sx
actor = _require_actor()
items = await services.federation.get_home_timeline(g.s, actor.id)
return sx_call("federation-timeline-content",
items=SxExpr(serialize([_serialize_timeline_item(i) for i in items])),
return await render_to_sx("federation-timeline-content",
items=[_serialize_timeline_item(i) for i in items],
timeline_type="home",
actor=SxExpr(serialize(_serialize_actor(actor))))
actor=_serialize_actor(actor))
async def _h_public_timeline_content(**kw):
from quart import g
from shared.services.registry import services
from shared.sx.helpers import sx_call, SxExpr
from shared.sx.parser import serialize
from shared.sx.helpers import render_to_sx
actor = _get_actor()
items = await services.federation.get_public_timeline(g.s)
return sx_call("federation-timeline-content",
items=SxExpr(serialize([_serialize_timeline_item(i) for i in items])),
return await render_to_sx("federation-timeline-content",
items=[_serialize_timeline_item(i) for i in items],
timeline_type="public",
actor=SxExpr(serialize(_serialize_actor(actor))) if actor else None)
actor=_serialize_actor(actor))
async def _h_compose_content(**kw):
from quart import request
from shared.sx.helpers import sx_call
from shared.sx.helpers import render_to_sx
_require_actor()
reply_to = request.args.get("reply_to")
return sx_call("federation-compose-content",
return await render_to_sx("federation-compose-content",
reply_to=reply_to or None)
async def _h_search_content(**kw):
from quart import g, request
from shared.services.registry import services
from shared.sx.helpers import sx_call, SxExpr
from shared.sx.parser import serialize
from shared.sx.helpers import render_to_sx
actor = _get_actor()
query = request.args.get("q", "").strip()
actors_list = []
@@ -194,34 +189,32 @@ async def _h_search_content(**kw):
g.s, actor.preferred_username, page=1, per_page=1000,
)
followed_urls = {a.actor_url for a in following}
return sx_call("federation-search-content",
return await render_to_sx("federation-search-content",
query=query,
actors=SxExpr(serialize([_serialize_remote_actor(a) for a in actors_list])),
actors=[_serialize_remote_actor(a) for a in actors_list],
total=total,
followed_urls=SxExpr(serialize(list(followed_urls))),
actor=SxExpr(serialize(_serialize_actor(actor))) if actor else None)
followed_urls=list(followed_urls),
actor=_serialize_actor(actor))
async def _h_following_content(**kw):
from quart import g
from shared.services.registry import services
from shared.sx.helpers import sx_call, SxExpr
from shared.sx.parser import serialize
from shared.sx.helpers import render_to_sx
actor = _require_actor()
actors_list, total = await services.federation.get_following(
g.s, actor.preferred_username,
)
return sx_call("federation-following-content",
actors=SxExpr(serialize([_serialize_remote_actor(a) for a in actors_list])),
return await render_to_sx("federation-following-content",
actors=[_serialize_remote_actor(a) for a in actors_list],
total=total,
actor=SxExpr(serialize(_serialize_actor(actor))))
actor=_serialize_actor(actor))
async def _h_followers_content(**kw):
from quart import g
from shared.services.registry import services
from shared.sx.helpers import sx_call, SxExpr
from shared.sx.parser import serialize
from shared.sx.helpers import render_to_sx
actor = _require_actor()
actors_list, total = await services.federation.get_followers_paginated(
g.s, actor.preferred_username,
@@ -230,18 +223,17 @@ async def _h_followers_content(**kw):
g.s, actor.preferred_username, page=1, per_page=1000,
)
followed_urls = {a.actor_url for a in following}
return sx_call("federation-followers-content",
actors=SxExpr(serialize([_serialize_remote_actor(a) for a in actors_list])),
return await render_to_sx("federation-followers-content",
actors=[_serialize_remote_actor(a) for a in actors_list],
total=total,
followed_urls=SxExpr(serialize(list(followed_urls))),
actor=SxExpr(serialize(_serialize_actor(actor))))
followed_urls=list(followed_urls),
actor=_serialize_actor(actor))
async def _h_actor_timeline_content(id=None, **kw):
from quart import g, abort
from shared.services.registry import services
from shared.sx.helpers import sx_call, SxExpr
from shared.sx.parser import serialize
from shared.sx.helpers import render_to_sx
actor = _get_actor()
actor_id = id
from shared.models.federation import RemoteActor
@@ -268,18 +260,17 @@ async def _h_actor_timeline_content(id=None, **kw):
)
).scalar_one_or_none()
is_following = existing is not None
return sx_call("federation-actor-timeline-content",
remote_actor=SxExpr(serialize(_serialize_remote_actor(remote_dto))),
items=SxExpr(serialize([_serialize_timeline_item(i) for i in items])),
return await render_to_sx("federation-actor-timeline-content",
remote_actor=_serialize_remote_actor(remote_dto),
items=[_serialize_timeline_item(i) for i in items],
is_following=is_following,
actor=SxExpr(serialize(_serialize_actor(actor))) if actor else None)
actor=_serialize_actor(actor))
async def _h_notifications_content(**kw):
from quart import g
from shared.services.registry import services
from shared.sx.helpers import sx_call, SxExpr
from shared.sx.parser import serialize
from shared.sx.helpers import render_to_sx
actor = _require_actor()
items = await services.federation.get_notifications(g.s, actor.id)
await services.federation.mark_notifications_read(g.s, actor.id)
@@ -298,5 +289,5 @@ async def _h_notifications_content(**kw):
"read": getattr(n, "read", True),
"app_domain": getattr(n, "app_domain", ""),
})
return sx_call("federation-notifications-content",
notifications=SxExpr(serialize(notif_dicts)))
return await render_to_sx("federation-notifications-content",
notifications=notif_dicts)

View File

@@ -31,7 +31,7 @@ def register() -> Blueprint:
async def _is_liked():
"""Check if a user has liked a specific target."""
from sqlalchemy import select
from likes.models.like import Like
from models.like import Like
user_id = request.args.get("user_id", type=int)
target_type = request.args.get("target_type", "")
@@ -62,7 +62,7 @@ def register() -> Blueprint:
async def _liked_slugs():
"""Return all liked target_slugs for a user + target_type."""
from sqlalchemy import select
from likes.models.like import Like
from models.like import Like
user_id = request.args.get("user_id", type=int)
target_type = request.args.get("target_type", "")
@@ -86,7 +86,7 @@ def register() -> Blueprint:
async def _liked_ids():
"""Return all liked target_ids for a user + target_type."""
from sqlalchemy import select
from likes.models.like import Like
from models.like import Like
user_id = request.args.get("user_id", type=int)
target_type = request.args.get("target_type", "")

View File

@@ -129,7 +129,7 @@ def register():
from sx.sx_components import render_like_toggle_button
if not g.user:
return sx_response(render_like_toggle_button(product_slug, False), status=403)
return sx_response(await render_like_toggle_button(product_slug, False), status=403)
user_id = g.user.id
@@ -138,7 +138,7 @@ def register():
})
liked = result["liked"]
return sx_response(render_like_toggle_button(product_slug, liked))
return sx_response(await render_like_toggle_button(product_slug, liked))
@@ -257,7 +257,7 @@ def register():
from sx.sx_components import render_cart_added_response
item_data = getattr(g, "item_data", {})
d = item_data.get("d", {})
return sx_response(render_cart_added_response(g.cart, ci_ns, d))
return sx_response(await render_cart_added_response(g.cart, ci_ns, d))
# normal POST: go to cart page
from shared.infrastructure.urls import cart_url

File diff suppressed because it is too large Load Diff

View File

@@ -27,55 +27,55 @@ def _register_market_layouts() -> None:
register_custom_layout("market-admin", _market_admin_full, _market_admin_oob)
def _market_full(ctx: dict, **kw: Any) -> str:
async def _market_full(ctx: dict, **kw: Any) -> str:
from shared.sx.helpers import root_header_sx, header_child_sx
from sx.sx_components import _post_header_sx, _market_header_sx
root_hdr = root_header_sx(ctx)
child = "(<> " + _post_header_sx(ctx) + " " + _market_header_sx(ctx) + ")"
return "(<> " + root_hdr + " " + header_child_sx(child) + ")"
root_hdr = await root_header_sx(ctx)
child = "(<> " + await _post_header_sx(ctx) + " " + await _market_header_sx(ctx) + ")"
return "(<> " + root_hdr + " " + await header_child_sx(child) + ")"
def _market_oob(ctx: dict, **kw: Any) -> str:
async def _market_oob(ctx: dict, **kw: Any) -> str:
from shared.sx.helpers import oob_header_sx
from sx.sx_components import _post_header_sx, _market_header_sx, _clear_deeper_oob
oobs = oob_header_sx("post-header-child", "market-header-child",
_market_header_sx(ctx))
oobs = "(<> " + oobs + " " + _post_header_sx(ctx, oob=True) + " "
oobs = await oob_header_sx("post-header-child", "market-header-child",
await _market_header_sx(ctx))
oobs = "(<> " + oobs + " " + await _post_header_sx(ctx, oob=True) + " "
oobs += _clear_deeper_oob("post-row", "post-header-child",
"market-row", "market-header-child") + ")"
return oobs
def _market_mobile(ctx: dict, **kw: Any) -> str:
async def _market_mobile(ctx: dict, **kw: Any) -> str:
from sx.sx_components import _mobile_nav_panel_sx
return _mobile_nav_panel_sx(ctx)
return await _mobile_nav_panel_sx(ctx)
def _market_admin_full(ctx: dict, **kw: Any) -> str:
async def _market_admin_full(ctx: dict, **kw: Any) -> str:
from shared.sx.helpers import root_header_sx, header_child_sx
from sx.sx_components import (
_post_header_sx, _market_header_sx, _market_admin_header_sx,
)
selected = kw.get("selected", "")
root_hdr = root_header_sx(ctx)
child = "(<> " + _post_header_sx(ctx) + " " + _market_header_sx(ctx) + " "
child += _market_admin_header_sx(ctx, selected=selected) + ")"
return "(<> " + root_hdr + " " + header_child_sx(child) + ")"
root_hdr = await root_header_sx(ctx)
child = "(<> " + await _post_header_sx(ctx) + " " + await _market_header_sx(ctx) + " "
child += await _market_admin_header_sx(ctx, selected=selected) + ")"
return "(<> " + root_hdr + " " + await header_child_sx(child) + ")"
def _market_admin_oob(ctx: dict, **kw: Any) -> str:
async def _market_admin_oob(ctx: dict, **kw: Any) -> str:
from shared.sx.helpers import oob_header_sx
from sx.sx_components import (
_market_header_sx, _market_admin_header_sx, _clear_deeper_oob,
)
selected = kw.get("selected", "")
oobs = "(<> " + _market_header_sx(ctx, oob=True) + " "
oobs += oob_header_sx("market-header-child", "market-admin-header-child",
_market_admin_header_sx(ctx, selected=selected)) + " "
oobs = "(<> " + await _market_header_sx(ctx, oob=True) + " "
oobs += await oob_header_sx("market-header-child", "market-admin-header-child",
await _market_admin_header_sx(ctx, selected=selected)) + " "
oobs += _clear_deeper_oob("post-row", "post-header-child",
"market-row", "market-header-child",
"market-admin-row", "market-admin-header-child") + ")"
@@ -123,14 +123,14 @@ async def _h_all_markets_content(**kw):
if not markets:
from sx.sx_components import _no_markets_sx
return _no_markets_sx()
return await _no_markets_sx()
prefix = route_prefix()
next_url = prefix + url_for("all_markets.markets_fragment", page=page + 1)
from sx.sx_components import _market_cards_sx, _markets_grid
cards = _market_cards_sx(markets, page_info, page, has_more, next_url)
content = _markets_grid(cards)
cards = await _market_cards_sx(markets, page_info, page, has_more, next_url)
content = await _markets_grid(cards)
return "(<> " + content + " " + '(div :class "pb-8")' + ")"
@@ -148,15 +148,15 @@ async def _h_page_markets_content(slug=None, **kw):
if not markets:
from sx.sx_components import _no_markets_sx
return _no_markets_sx("No markets for this page")
return await _no_markets_sx("No markets for this page")
prefix = route_prefix()
next_url = prefix + url_for("page_markets.markets_fragment", page=page + 1)
from sx.sx_components import _market_cards_sx, _markets_grid
cards = _market_cards_sx(markets, {}, page, has_more, next_url,
cards = await _market_cards_sx(markets, {}, page, has_more, next_url,
show_page_badge=False, post_slug=post_slug)
content = _markets_grid(cards)
content = await _markets_grid(cards)
return "(<> " + content + " " + '(div :class "pb-8")' + ")"
@@ -168,12 +168,12 @@ async def _h_page_admin_content(slug=None, **kw):
return '(div :id "main-panel" ' + content + ')'
def _h_market_home_content(page_slug=None, market_slug=None, **kw):
async def _h_market_home_content(page_slug=None, market_slug=None, **kw):
from quart import g
post_data = getattr(g, "post_data", {})
post = post_data.get("post", {})
from sx.sx_components import _market_landing_content_sx
return _market_landing_content_sx(post)
return await _market_landing_content_sx(post)
def _h_market_admin_content(page_slug=None, market_slug=None, **kw):

View File

@@ -73,8 +73,7 @@ def register(url_prefix: str) -> Blueprint:
result = await g.s.execute(stmt)
orders = result.scalars().all()
from shared.sx.helpers import sx_response, sx_call, SxExpr
from shared.sx.parser import serialize
from shared.sx.helpers import sx_response, render_to_sx
from shared.utils import route_prefix
pfx = route_prefix()
@@ -96,16 +95,16 @@ def register(url_prefix: str) -> Blueprint:
# Build just the rows fragment (not full table) for infinite scroll
parts = []
for od in order_dicts:
parts.append(sx_call("order-row-pair",
order=SxExpr(serialize(od)),
parts.append(await render_to_sx("order-row-pair",
order=od,
detail_url_prefix=detail_prefix))
if page < total_pages:
parts.append(sx_call("infinite-scroll",
parts.append(await render_to_sx("infinite-scroll",
url=rows_url + qs_fn(page=page + 1),
page=page, total_pages=total_pages,
id_prefix="orders", colspan=5))
else:
parts.append(sx_call("order-end-row"))
parts.append(await render_to_sx("order-end-row"))
sx_src = "(<> " + " ".join(parts) + ")"
resp = sx_response(sx_src)

View File

@@ -12,7 +12,7 @@ from typing import Any
from shared.sx.jinja_bridge import load_service_components
from shared.sx.helpers import (
call_url, sx_call, SxExpr,
call_url, render_to_sx,
root_header_sx, full_page_sx, header_child_sx,
)
from shared.infrastructure.urls import market_product_url, cart_url
@@ -29,24 +29,24 @@ load_service_components(os.path.dirname(os.path.dirname(__file__)),
async def render_checkout_error_page(ctx: dict, error: str | None = None, order: Any | None = None) -> str:
"""Full page: checkout error (sx wire format)."""
account_url = call_url(ctx, "account_url", "")
auth_hdr = sx_call("auth-header-row", account_url=account_url)
hdr = root_header_sx(ctx)
hdr = "(<> " + hdr + " " + header_child_sx(auth_hdr) + ")"
filt = sx_call("checkout-error-header")
auth_hdr = await render_to_sx("auth-header-row", account_url=account_url)
hdr = await root_header_sx(ctx)
hdr = "(<> " + hdr + " " + await header_child_sx(auth_hdr) + ")"
filt = await render_to_sx("checkout-error-header")
err_msg = error or "Unexpected error while creating the hosted checkout session."
order_sx = ""
if order:
order_sx = sx_call("checkout-error-order-id", oid=f"#{order.id}")
back_url = cart_url("/")
content = sx_call(
order_sx = await render_to_sx("checkout-error-order-id", oid=f"#{order.id}")
from shared.sx.parser import SxExpr
content = await render_to_sx(
"checkout-error-content",
msg=err_msg,
order=SxExpr(order_sx) if order_sx else None,
back_url=back_url,
back_url=cart_url("/"),
)
return full_page_sx(ctx, header_rows=hdr, filter=filt, content=content)
return await full_page_sx(ctx, header_rows=hdr, filter=filt, content=content)
# ---------------------------------------------------------------------------
@@ -58,12 +58,12 @@ async def render_checkout_return_page(ctx: dict, order: Any | None,
calendar_entries: list | None = None,
order_tickets: list | None = None) -> str:
"""Full page: checkout return after SumUp payment (sx wire format)."""
from shared.sx.parser import serialize
from shared.sx.parser import SxExpr
filt = sx_call("checkout-return-header", status=status)
filt = await render_to_sx("checkout-return-header", status=status)
if not order:
content = sx_call("checkout-return-missing")
content = await render_to_sx("checkout-return-missing")
else:
# Serialize order data for defcomp
order_dict = {
@@ -87,7 +87,7 @@ async def render_checkout_return_page(ctx: dict, order: Any | None,
],
}
summary = sx_call("order-summary-card",
summary = await render_to_sx("order-summary-card",
order_id=order.id,
created_at=order_dict["created_at_formatted"],
description=order.description, status=order.status,
@@ -101,19 +101,19 @@ async def render_checkout_return_page(ctx: dict, order: Any | None,
item_parts = []
for item_d in order_dict["items"]:
if item_d["product_image"]:
img = sx_call("order-item-image",
img = await render_to_sx("order-item-image",
src=item_d["product_image"],
alt=item_d["product_title"] or "Product image")
else:
img = sx_call("order-item-no-image")
item_parts.append(sx_call("order-item-row",
img = await render_to_sx("order-item-no-image")
item_parts.append(await render_to_sx("order-item-row",
href=item_d["product_url"], img=SxExpr(img),
title=item_d["product_title"] or "Unknown product",
pid=f"Product ID: {item_d['product_id']}",
qty=f"Qty: {item_d['quantity']}",
price=f"{item_d['currency'] or order.currency or 'GBP'} {item_d['unit_price_formatted']}",
))
items = sx_call("order-items-panel",
items = await render_to_sx("order-items-panel",
items=SxExpr("(<> " + " ".join(item_parts) + ")"))
# Calendar entries
@@ -131,13 +131,13 @@ async def render_checkout_return_page(ctx: dict, order: Any | None,
ds = e.start_at.strftime("%-d %b %Y, %H:%M") if e.start_at else ""
if e.end_at:
ds += f" \u2013 {e.end_at.strftime('%-d %b %Y, %H:%M')}"
cal_parts.append(sx_call("order-calendar-entry",
cal_parts.append(await render_to_sx("order-calendar-entry",
name=e.name,
pill=f"inline-flex items-center rounded-full px-2 py-0.5 text-[11px] font-medium {pill}",
status=st.capitalize(), date_str=ds,
cost=f"\u00a3{e.cost or 0:.2f}",
))
calendar = sx_call("order-calendar-section",
calendar = await render_to_sx("order-calendar-section",
items=SxExpr("(<> " + " ".join(cal_parts) + ")"))
# Tickets
@@ -156,24 +156,24 @@ async def render_checkout_return_page(ctx: dict, order: Any | None,
ds = tk.entry_start_at.strftime("%-d %b %Y, %H:%M") if tk.entry_start_at else ""
if tk.entry_end_at:
ds += f" \u2013 {tk.entry_end_at.strftime('%-d %b %Y, %H:%M')}"
tk_parts.append(sx_call("checkout-return-ticket",
tk_parts.append(await render_to_sx("checkout-return-ticket",
name=tk.entry_name, pill=pill_cls,
state=st.replace("_", " ").capitalize(),
type_name=tk.ticket_type_name or None,
date_str=ds, code=tk.code,
price=f"\u00a3{tk.price or 0:.2f}",
))
tickets = sx_call("checkout-return-tickets",
tickets = await render_to_sx("checkout-return-tickets",
items=SxExpr("(<> " + " ".join(tk_parts) + ")"))
# Status message
status_msg = ""
if order.status == "failed":
status_msg = sx_call("checkout-return-failed", order_id=order.id)
status_msg = await render_to_sx("checkout-return-failed", order_id=order.id)
elif order.status == "paid":
status_msg = sx_call("checkout-return-paid")
status_msg = await render_to_sx("checkout-return-paid")
content = sx_call("checkout-return-content",
content = await render_to_sx("checkout-return-content",
summary=SxExpr(summary),
items=SxExpr(items) if items else None,
calendar=SxExpr(calendar) if calendar else None,
@@ -182,8 +182,8 @@ async def render_checkout_return_page(ctx: dict, order: Any | None,
)
account_url = call_url(ctx, "account_url", "")
auth_hdr = sx_call("auth-header-row", account_url=account_url)
hdr = root_header_sx(ctx)
hdr = "(<> " + hdr + " " + header_child_sx(auth_hdr) + ")"
auth_hdr = await render_to_sx("auth-header-row", account_url=account_url)
hdr = await root_header_sx(ctx)
hdr = "(<> " + hdr + " " + await header_child_sx(auth_hdr) + ")"
return full_page_sx(ctx, header_rows=hdr, filter=filt, content=content)
return await full_page_sx(ctx, header_rows=hdr, filter=filt, content=content)

View File

@@ -28,94 +28,101 @@ def _register_orders_layouts() -> None:
register_custom_layout("order-detail", _order_detail_full, _order_detail_oob, _order_detail_mobile)
def _orders_full(ctx: dict, **kw: Any) -> str:
from shared.sx.helpers import root_header_sx, header_child_sx, call_url, sx_call, SxExpr
async def _orders_full(ctx: dict, **kw: Any) -> str:
from shared.sx.helpers import root_header_sx, header_child_sx, call_url, render_to_sx
list_url = kw.get("list_url", "/")
account_url = call_url(ctx, "account_url", "")
root_hdr = root_header_sx(ctx)
auth_hdr = sx_call("auth-header-row",
root_hdr = await root_header_sx(ctx)
auth_hdr = await render_to_sx("auth-header-row",
account_url=account_url,
select_colours=ctx.get("select_colours", ""),
account_nav=_as_sx_nav(ctx),
)
orders_hdr = sx_call("orders-header-row", list_url=list_url)
orders_hdr = await render_to_sx("orders-header-row", list_url=list_url)
inner = "(<> " + auth_hdr + " " + orders_hdr + ")"
return "(<> " + root_hdr + " " + header_child_sx(inner) + ")"
return "(<> " + root_hdr + " " + await header_child_sx(inner) + ")"
def _orders_oob(ctx: dict, **kw: Any) -> str:
from shared.sx.helpers import root_header_sx, sx_call, SxExpr, call_url
async def _orders_oob(ctx: dict, **kw: Any) -> str:
from shared.sx.helpers import root_header_sx, render_to_sx
from shared.sx.helpers import call_url
from shared.sx.parser import SxExpr
list_url = kw.get("list_url", "/")
account_url = call_url(ctx, "account_url", "")
auth_hdr = sx_call("auth-header-row",
auth_hdr = await render_to_sx("auth-header-row",
account_url=account_url,
select_colours=ctx.get("select_colours", ""),
account_nav=_as_sx_nav(ctx),
oob=True,
)
auth_child_oob = sx_call("oob-header-sx",
parent_id="auth-header-child",
row=SxExpr(sx_call("orders-header-row", list_url=list_url)))
root_hdr = root_header_sx(ctx, oob=True)
orders_hdr = await render_to_sx("orders-header-row", list_url=list_url)
auth_child_oob = await render_to_sx("oob-header-sx",
parent_id="auth-header-child",
row=SxExpr(orders_hdr))
root_hdr = await root_header_sx(ctx, oob=True)
return "(<> " + auth_hdr + " " + auth_child_oob + " " + root_hdr + ")"
def _orders_mobile(ctx: dict, **kw: Any) -> str:
async def _orders_mobile(ctx: dict, **kw: Any) -> str:
from shared.sx.helpers import mobile_menu_sx, mobile_root_nav_sx
return mobile_menu_sx(mobile_root_nav_sx(ctx))
return mobile_menu_sx(await mobile_root_nav_sx(ctx))
def _order_detail_full(ctx: dict, **kw: Any) -> str:
from shared.sx.helpers import root_header_sx, sx_call, SxExpr, call_url
async def _order_detail_full(ctx: dict, **kw: Any) -> str:
from shared.sx.helpers import root_header_sx, render_to_sx
from shared.sx.helpers import call_url
from shared.sx.parser import SxExpr
list_url = kw.get("list_url", "/")
detail_url = kw.get("detail_url", "/")
account_url = call_url(ctx, "account_url", "")
root_hdr = root_header_sx(ctx)
order_row = sx_call(
root_hdr = await root_header_sx(ctx)
order_row = await render_to_sx(
"menu-row-sx",
id="order-row", level=3, colour="sky", link_href=detail_url,
link_label="Order", icon="fa fa-gbp",
)
auth_hdr = sx_call("auth-header-row",
auth_hdr = await render_to_sx("auth-header-row",
account_url=account_url,
select_colours=ctx.get("select_colours", ""),
account_nav=_as_sx_nav(ctx),
)
detail_header = sx_call(
orders_hdr = await render_to_sx("orders-header-row", list_url=list_url)
detail_header = await render_to_sx(
"order-detail-header-stack",
auth=SxExpr(auth_hdr),
orders=SxExpr(sx_call("orders-header-row", list_url=list_url)),
orders=SxExpr(orders_hdr),
order=SxExpr(order_row),
)
return "(<> " + root_hdr + " " + detail_header + ")"
def _order_detail_oob(ctx: dict, **kw: Any) -> str:
from shared.sx.helpers import root_header_sx, sx_call, SxExpr
async def _order_detail_oob(ctx: dict, **kw: Any) -> str:
from shared.sx.helpers import root_header_sx, render_to_sx
from shared.sx.parser import SxExpr
detail_url = kw.get("detail_url", "/")
order_row_oob = sx_call(
order_row_oob = await render_to_sx(
"menu-row-sx",
id="order-row", level=3, colour="sky", link_href=detail_url,
link_label="Order", icon="fa fa-gbp", oob=True,
)
header_child_oob = sx_call("oob-header-sx",
header_child_oob = await render_to_sx("oob-header-sx",
parent_id="orders-header-child",
row=SxExpr(order_row_oob))
root_hdr = root_header_sx(ctx, oob=True)
root_hdr = await root_header_sx(ctx, oob=True)
return "(<> " + header_child_oob + " " + root_hdr + ")"
def _order_detail_mobile(ctx: dict, **kw: Any) -> str:
async def _order_detail_mobile(ctx: dict, **kw: Any) -> str:
from shared.sx.helpers import mobile_menu_sx, mobile_root_nav_sx
return mobile_menu_sx(mobile_root_nav_sx(ctx))
return mobile_menu_sx(await mobile_root_nav_sx(ctx))
def _as_sx_nav(ctx: dict) -> Any:
"""Convert account_nav fragment to SxExpr for use in sx_call."""
"""Convert account_nav fragment to SxExpr for use in component calls."""
from shared.sx.helpers import _as_sx
return _as_sx(ctx.get("account_nav"))
@@ -279,11 +286,10 @@ async def _ensure_order_detail(order_id):
async def _h_orders_list_content(**kw):
await _ensure_orders_list()
from quart import g
from shared.sx.helpers import sx_call, SxExpr
from shared.sx.parser import serialize
from shared.sx.helpers import render_to_sx
d = getattr(g, "orders_page_data", None)
if not d:
return sx_call("order-empty-state")
return await render_to_sx("order-empty-state")
orders = d["orders"]
url_for_fn = d["url_for_fn"]
@@ -305,8 +311,8 @@ async def _h_orders_list_content(**kw):
detail_prefix = rpfx + url_for_fn("orders.defpage_order_detail", order_id=0).rsplit("0/", 1)[0]
rows_url = rpfx + url_for_fn("orders.orders_rows")
return sx_call("orders-list-content",
orders=SxExpr(serialize(order_dicts)),
return await render_to_sx("orders-list-content",
orders=order_dicts,
page=d["page"],
total_pages=d["total_pages"],
rows_url=rows_url,
@@ -316,30 +322,31 @@ async def _h_orders_list_content(**kw):
async def _h_orders_list_filter(**kw):
await _ensure_orders_list()
from quart import g
from shared.sx.helpers import sx_call, SxExpr
from shared.sx.helpers import render_to_sx
from shared.sx.page import SEARCH_HEADERS_MOBILE
from shared.sx.parser import SxExpr
d = getattr(g, "orders_page_data", None)
search = d.get("search", "") if d else ""
search_count = d.get("search_count", "") if d else ""
search_mobile = sx_call("search-mobile",
search_mobile = await render_to_sx("search-mobile",
current_local_href="/",
search=search or "",
search_count=search_count or "",
hx_select="#main-panel",
search_headers_mobile=SEARCH_HEADERS_MOBILE,
)
return sx_call("order-list-header", search_mobile=SxExpr(search_mobile))
return await render_to_sx("order-list-header", search_mobile=SxExpr(search_mobile))
async def _h_orders_list_aside(**kw):
await _ensure_orders_list()
from quart import g
from shared.sx.helpers import sx_call
from shared.sx.helpers import render_to_sx
from shared.sx.page import SEARCH_HEADERS_DESKTOP
d = getattr(g, "orders_page_data", None)
search = d.get("search", "") if d else ""
search_count = d.get("search_count", "") if d else ""
return sx_call("search-desktop",
return await render_to_sx("search-desktop",
current_local_href="/",
search=search or "",
search_count=search_count or "",
@@ -358,8 +365,7 @@ async def _h_orders_list_url(**kw):
async def _h_order_detail_content(order_id=None, **kw):
await _ensure_order_detail(order_id)
from quart import g
from shared.sx.helpers import sx_call, SxExpr
from shared.sx.parser import serialize
from shared.sx.helpers import render_to_sx
from shared.infrastructure.urls import market_product_url
d = getattr(g, "order_detail_data", None)
if not d:
@@ -402,16 +408,15 @@ async def _h_order_detail_content(order_id=None, **kw):
"cost_formatted": f"{e.cost or 0:.2f}",
})
return sx_call("order-detail-content",
order=SxExpr(serialize(order_dict)),
calendar_entries=SxExpr(serialize(cal_dicts)) if cal_dicts else None)
return await render_to_sx("order-detail-content",
order=order_dict,
calendar_entries=cal_dicts)
async def _h_order_detail_filter(order_id=None, **kw):
await _ensure_order_detail(order_id)
from quart import g
from shared.sx.helpers import sx_call, SxExpr
from shared.sx.parser import serialize
from shared.sx.helpers import render_to_sx
d = getattr(g, "order_detail_data", None)
if not d:
return ""
@@ -422,8 +427,8 @@ async def _h_order_detail_filter(order_id=None, **kw):
"created_at_formatted": order.created_at.strftime("%-d %b %Y, %H:%M") if order.created_at else "\u2014",
}
return sx_call("order-detail-filter-content",
order=SxExpr(serialize(order_dict)),
return await render_to_sx("order-detail-filter-content",
order=order_dict,
list_url=d["list_url"],
recheck_url=d["recheck_url"],
pay_url=d["pay_url"],

View File

@@ -149,22 +149,22 @@ async def _rich_error_page(errnum: str, message: str, image: str | None = None)
# Root header (site nav bar)
from shared.sx.helpers import (
root_header_sx, post_header_sx,
header_child_sx, full_page_sx, sx_call,
header_child_sx, full_page_sx, render_to_sx,
)
hdr = root_header_sx(ctx)
hdr = await root_header_sx(ctx)
# Post breadcrumb if we resolved a post
post = (post_data or {}).get("post") or ctx.get("post") or {}
if post.get("slug"):
ctx["post"] = post
post_row = post_header_sx(ctx)
post_row = await post_header_sx(ctx)
if post_row:
hdr = "(<> " + hdr + " " + header_child_sx(post_row) + ")"
hdr = "(<> " + hdr + " " + await header_child_sx(post_row) + ")"
# Error content
error_content = sx_call("error-content", errnum=errnum, message=message, image=image)
error_content = await render_to_sx("error-content", errnum=errnum, message=message, image=image)
return full_page_sx(ctx, header_rows=hdr, content=error_content)
return await full_page_sx(ctx, header_rows=hdr, content=error_content)
except Exception:
current_app.logger.debug("Rich error page failed, falling back", exc_info=True)
return None

View File

@@ -114,18 +114,18 @@ async def _render_profile_sx(actor, activities, total):
# Import federation layout for OOB headers
try:
from federation.sxc.pages import _social_oob
oob_headers = _social_oob(tctx)
oob_headers = await _social_oob(tctx)
except ImportError:
oob_headers = ""
return sx_response(oob_page_sx(oobs=oob_headers, content=content))
return sx_response(await oob_page_sx(oobs=oob_headers, content=content))
else:
try:
from federation.sxc.pages import _social_full
header_rows = _social_full(tctx)
header_rows = await _social_full(tctx)
except ImportError:
from shared.sx.helpers import root_header_sx
header_rows = root_header_sx(tctx)
return full_page_sx(tctx, header_rows=header_rows, content=content)
header_rows = await root_header_sx(tctx)
return await full_page_sx(tctx, header_rows=header_rows, content=content)
def create_activitypub_blueprint(app_name: str) -> Blueprint:

View File

@@ -92,14 +92,14 @@ def create_ap_social_blueprint(app_name: str) -> Blueprint:
kw = {"actor": actor}
if is_htmx_request():
oob_headers = _social_oob_headers(tctx, **kw)
return sx_response(oob_page_sx(
oob_headers = await _social_oob_headers(tctx, **kw)
return sx_response(await oob_page_sx(
oobs=oob_headers,
content=content,
))
else:
header_rows = _social_full_headers(tctx, **kw)
return full_page_sx(tctx, header_rows=header_rows, content=content)
header_rows = await _social_full_headers(tctx, **kw)
return await full_page_sx(tctx, header_rows=header_rows, content=content)
# -- Index ----------------------------------------------------------------

View File

@@ -14,10 +14,9 @@ from typing import Any
from markupsafe import escape
from shared.sx.helpers import (
sx_call, root_header_sx, oob_header_sx,
root_header_sx, oob_header_sx,
mobile_menu_sx, mobile_root_nav_sx, full_page_sx, oob_page_sx,
)
from shared.sx.parser import SxExpr
# ---------------------------------------------------------------------------
@@ -91,23 +90,23 @@ def _social_header_row(actor: Any) -> str:
)
def _social_full_headers(ctx: dict, **kw: Any) -> str:
root_hdr = root_header_sx(ctx)
async def _social_full_headers(ctx: dict, **kw: Any) -> str:
root_hdr = await root_header_sx(ctx)
actor = kw.get("actor")
social_row = _social_header_row(actor)
return "(<> " + root_hdr + " " + social_row + ")"
def _social_oob_headers(ctx: dict, **kw: Any) -> str:
root_hdr = root_header_sx(ctx)
async def _social_oob_headers(ctx: dict, **kw: Any) -> str:
root_hdr = await root_header_sx(ctx)
actor = kw.get("actor")
social_row = _social_header_row(actor)
rows = "(<> " + root_hdr + " " + social_row + ")"
return oob_header_sx("root-header-child", "social-lite-header-child", rows)
return await oob_header_sx("root-header-child", "social-lite-header-child", rows)
def _social_mobile(ctx: dict, **kw: Any) -> str:
return mobile_menu_sx(mobile_root_nav_sx(ctx))
async def _social_mobile(ctx: dict, **kw: Any) -> str:
return mobile_menu_sx(await mobile_root_nav_sx(ctx))
# ---------------------------------------------------------------------------

View File

@@ -83,12 +83,12 @@ def _as_sx(val: Any) -> SxExpr | None:
return SxExpr(f'(~rich-text :html "{escaped}")')
def root_header_sx(ctx: dict, *, oob: bool = False) -> str:
"""Build the root header row as a sx call string."""
async def root_header_sx(ctx: dict, *, oob: bool = False) -> str:
"""Build the root header row as sx wire format."""
rights = ctx.get("rights") or {}
is_admin = rights.get("admin") if isinstance(rights, dict) else getattr(rights, "admin", False)
settings_url = call_url(ctx, "blog_url", "/settings/") if is_admin else ""
return sx_call("header-row-sx",
return await render_to_sx("header-row-sx",
cart_mini=_as_sx(ctx.get("cart_mini")),
blog_url=call_url(ctx, "blog_url", ""),
site_title=ctx.get("base_title", ""),
@@ -108,13 +108,13 @@ def mobile_menu_sx(*sections: str) -> str:
return "(<> " + " ".join(parts) + ")" if parts else ""
def mobile_root_nav_sx(ctx: dict) -> str:
async def mobile_root_nav_sx(ctx: dict) -> str:
"""Root-level mobile nav via ~mobile-root-nav component."""
nav_tree = ctx.get("nav_tree") or ""
auth_menu = ctx.get("auth_menu") or ""
if not nav_tree and not auth_menu:
return ""
return sx_call("mobile-root-nav",
return await render_to_sx("mobile-root-nav",
nav_tree=_as_sx(nav_tree),
auth_menu=_as_sx(auth_menu),
)
@@ -124,7 +124,7 @@ def mobile_root_nav_sx(ctx: dict) -> str:
# Shared nav-item builders — used by BOTH desktop headers and mobile menus
# ---------------------------------------------------------------------------
def _post_nav_items_sx(ctx: dict) -> str:
async def _post_nav_items_sx(ctx: dict) -> str:
"""Build post-level nav items (container_nav + admin cog). Shared by
``post_header_sx`` (desktop) and ``post_mobile_nav_sx`` (mobile)."""
post = ctx.get("post") or {}
@@ -135,7 +135,7 @@ def _post_nav_items_sx(ctx: dict) -> str:
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}/")
parts.append(sx_call("page-cart-badge", href=cart_href,
parts.append(await render_to_sx("page-cart-badge", href=cart_href,
count=str(page_cart_count)))
container_nav = str(ctx.get("container_nav") or "").strip()
@@ -171,7 +171,7 @@ def _post_nav_items_sx(ctx: dict) -> str:
return "(<> " + " ".join(parts) + ")" if parts else ""
def _post_admin_nav_items_sx(ctx: dict, slug: str,
async def _post_admin_nav_items_sx(ctx: dict, slug: str,
selected: str = "") -> str:
"""Build post-admin nav items (calendars, markets, etc.). Shared by
``post_admin_header_sx`` (desktop) and mobile menu."""
@@ -193,7 +193,7 @@ def _post_admin_nav_items_sx(ctx: dict, slug: str,
continue
href = url_fn(path)
is_sel = label == selected
parts.append(sx_call("nav-link", href=href, label=label,
parts.append(await render_to_sx("nav-link", href=href, label=label,
select_colours=select_colours,
is_selected=is_sel or None))
return "(<> " + " ".join(parts) + ")" if parts else ""
@@ -203,15 +203,15 @@ def _post_admin_nav_items_sx(ctx: dict, slug: str,
# Mobile menu section builders — wrap shared nav items for hamburger panel
# ---------------------------------------------------------------------------
def post_mobile_nav_sx(ctx: dict) -> str:
async def post_mobile_nav_sx(ctx: dict) -> str:
"""Post-level mobile menu section."""
nav = _post_nav_items_sx(ctx)
nav = await _post_nav_items_sx(ctx)
if not nav:
return ""
post = ctx.get("post") or {}
slug = post.get("slug", "")
title = (post.get("title") or slug)[:40]
return sx_call("mobile-menu-section",
return await render_to_sx("mobile-menu-section",
label=title,
href=call_url(ctx, "blog_url", f"/{slug}/"),
level=1,
@@ -219,22 +219,22 @@ def post_mobile_nav_sx(ctx: dict) -> str:
)
def post_admin_mobile_nav_sx(ctx: dict, slug: str,
async def post_admin_mobile_nav_sx(ctx: dict, slug: str,
selected: str = "") -> str:
"""Post-admin mobile menu section."""
nav = _post_admin_nav_items_sx(ctx, slug, selected)
nav = await _post_admin_nav_items_sx(ctx, slug, selected)
if not nav:
return ""
admin_href = call_url(ctx, "blog_url", f"/{slug}/admin/")
return sx_call("mobile-menu-section",
return await render_to_sx("mobile-menu-section",
label="admin", href=admin_href, level=2,
items=SxExpr(nav),
)
def search_mobile_sx(ctx: dict) -> str:
"""Build mobile search input as sx call string."""
return sx_call("search-mobile",
async def search_mobile_sx(ctx: dict) -> str:
"""Build mobile search input as sx wire format."""
return await render_to_sx("search-mobile",
current_local_href=ctx.get("current_local_href", "/"),
search=ctx.get("search", ""),
search_count=ctx.get("search_count", ""),
@@ -243,9 +243,9 @@ def search_mobile_sx(ctx: dict) -> str:
)
def search_desktop_sx(ctx: dict) -> str:
"""Build desktop search input as sx call string."""
return sx_call("search-desktop",
async def search_desktop_sx(ctx: dict) -> str:
"""Build desktop search input as sx wire format."""
return await render_to_sx("search-desktop",
current_local_href=ctx.get("current_local_href", "/"),
search=ctx.get("search", ""),
search_count=ctx.get("search_count", ""),
@@ -254,8 +254,8 @@ def search_desktop_sx(ctx: dict) -> str:
)
def post_header_sx(ctx: dict, *, oob: bool = False, child: str = "") -> str:
"""Build the post-level header row as sx call string."""
async def post_header_sx(ctx: dict, *, oob: bool = False, child: str = "") -> str:
"""Build the post-level header row as sx wire format."""
post = ctx.get("post") or {}
slug = post.get("slug", "")
if not slug:
@@ -263,11 +263,11 @@ def post_header_sx(ctx: dict, *, oob: bool = False, child: str = "") -> str:
title = (post.get("title") or "")[:160]
feature_image = post.get("feature_image")
label_sx = sx_call("post-label", feature_image=feature_image, title=title)
nav_sx = _post_nav_items_sx(ctx) or None
label_sx = await render_to_sx("post-label", feature_image=feature_image, title=title)
nav_sx = await _post_nav_items_sx(ctx) or None
link_href = call_url(ctx, "blog_url", f"/{slug}/")
return sx_call("menu-row-sx",
return await render_to_sx("menu-row-sx",
id="post-row", level=1,
link_href=link_href,
link_label_content=SxExpr(label_sx),
@@ -278,22 +278,22 @@ def post_header_sx(ctx: dict, *, oob: bool = False, child: str = "") -> str:
)
def post_admin_header_sx(ctx: dict, slug: str, *, oob: bool = False,
async def post_admin_header_sx(ctx: dict, slug: str, *, oob: bool = False,
selected: str = "", admin_href: str = "") -> str:
"""Post admin header row as sx call string."""
"""Post admin header row as sx wire format."""
# Label
label_parts = ['(i :class "fa fa-shield-halved" :aria-hidden "true")', '" admin"']
if selected:
label_parts.append(f'(span :class "text-white" "{escape(selected)}")')
label_sx = "(<> " + " ".join(label_parts) + ")"
nav_sx = _post_admin_nav_items_sx(ctx, slug, selected) or None
nav_sx = await _post_admin_nav_items_sx(ctx, slug, selected) or None
if not admin_href:
blog_fn = ctx.get("blog_url")
admin_href = blog_fn(f"/{slug}/admin/") if callable(blog_fn) else f"/{slug}/admin/"
return sx_call("menu-row-sx",
return await render_to_sx("menu-row-sx",
id="post-admin-row", level=2,
link_href=admin_href,
link_label_content=SxExpr(label_sx),
@@ -302,29 +302,29 @@ def post_admin_header_sx(ctx: dict, slug: str, *, oob: bool = False,
)
def oob_header_sx(parent_id: str, child_id: str, row_sx: str) -> str:
async def oob_header_sx(parent_id: str, child_id: str, row_sx: str) -> str:
"""Wrap a header row sx in an OOB swap.
child_id is accepted for call-site compatibility but no longer used —
the child placeholder is created by ~menu-row-sx itself.
"""
return sx_call("oob-header-sx",
return await render_to_sx("oob-header-sx",
parent_id=parent_id,
row=SxExpr(row_sx),
)
def header_child_sx(inner_sx: str, *, id: str = "root-header-child") -> str:
async def header_child_sx(inner_sx: str, *, id: str = "root-header-child") -> str:
"""Wrap inner sx in a header-child div."""
return sx_call("header-child-sx",
return await render_to_sx("header-child-sx",
id=id, inner=SxExpr(f"(<> {inner_sx})"),
)
def oob_page_sx(*, oobs: str = "", filter: str = "", aside: str = "",
async def oob_page_sx(*, oobs: str = "", filter: str = "", aside: str = "",
content: str = "", menu: str = "") -> str:
"""Build OOB response as sx call string."""
return sx_call("oob-sx",
"""Build OOB response as sx wire format."""
return await render_to_sx("oob-sx",
oobs=SxExpr(f"(<> {oobs})") if oobs else None,
filter=SxExpr(filter) if filter else None,
aside=SxExpr(aside) if aside else None,
@@ -333,7 +333,7 @@ def oob_page_sx(*, oobs: str = "", filter: str = "", aside: str = "",
)
def full_page_sx(ctx: dict, *, header_rows: str,
async def full_page_sx(ctx: dict, *, header_rows: str,
filter: str = "", aside: str = "",
content: str = "", menu: str = "",
meta_html: str = "", meta: str = "") -> str:
@@ -344,8 +344,8 @@ def full_page_sx(ctx: dict, *, header_rows: str,
"""
# Auto-generate mobile nav from context when no menu provided
if not menu:
menu = mobile_root_nav_sx(ctx)
body_sx = sx_call("app-body",
menu = await mobile_root_nav_sx(ctx)
body_sx = await render_to_sx("app-body",
header_rows=SxExpr(f"(<> {header_rows})") if header_rows else None,
filter=SxExpr(filter) if filter else None,
aside=SxExpr(aside) if aside else None,
@@ -359,6 +359,64 @@ def full_page_sx(ctx: dict, *, header_rows: str,
return sx_page(ctx, body_sx, meta_html=meta_html)
def _build_component_ast(__name: str, **kwargs: Any) -> list:
"""Build an AST list for a component call from Python kwargs.
Returns e.g. [Symbol("~card"), Keyword("title"), "hello", Keyword("count"), 3]
No SX string generation — values stay as native Python objects.
"""
from .types import Symbol, Keyword, NIL
comp_sym = Symbol(__name if __name.startswith("~") else f"~{__name}")
ast: list = [comp_sym]
for key, val in kwargs.items():
kebab = key.replace("_", "-")
ast.append(Keyword(kebab))
if val is None:
ast.append(NIL)
elif isinstance(val, SxExpr):
# SxExpr values need to be parsed into AST
from .parser import parse
ast.append(parse(val.source))
else:
ast.append(val)
return ast
async def render_to_sx(__name: str, **kwargs: Any) -> str:
"""Call a defcomp and get SX wire format back. No SX string literals.
Builds an AST from Python values and evaluates it through the SX
evaluator, which resolves IO primitives and serializes component/tag
calls as SX wire format.
await render_to_sx("card", title="hello", count=3)
# equivalent to old: sx_call("card", title="hello", count=3)
# but values flow as native objects, not serialized strings
"""
from .jinja_bridge import get_component_env, _get_request_context
from .async_eval import async_eval_to_sx
ast = _build_component_ast(__name, **kwargs)
env = dict(get_component_env())
ctx = _get_request_context()
return await async_eval_to_sx(ast, env, ctx)
async def render_to_html(__name: str, **kwargs: Any) -> str:
"""Call a defcomp and get HTML back. No SX string literals.
Same as render_to_sx() but produces HTML output instead of SX wire
format. Used by route renders that need HTML (full pages, fragments).
"""
from .jinja_bridge import get_component_env, _get_request_context
from .async_eval import async_render
ast = _build_component_ast(__name, **kwargs)
env = dict(get_component_env())
ctx = _get_request_context()
return await async_render(ast, env, ctx)
def sx_call(component_name: str, **kwargs: Any) -> str:
"""Build an s-expression component call string from Python kwargs.
@@ -428,27 +486,19 @@ def components_for_request() -> str:
return "\n".join(parts)
def sx_response(source_or_component: str, status: int = 200,
headers: dict | None = None, **kwargs: Any):
def sx_response(source: str, status: int = 200,
headers: dict | None = None):
"""Return an s-expression wire-format response.
Can be called with a raw sx string::
Takes a raw sx string::
return sx_response('(~test-row :nodeid "foo")')
Or with a component name + kwargs (builds the sx call)::
return sx_response("test-row", nodeid="foo", outcome="passed")
For SX requests, missing component definitions are prepended as a
``<script type="text/sx" data-components>`` block so the client
can process them before rendering OOB content.
"""
from quart import request, Response
if kwargs:
source = sx_call(source_or_component, **kwargs)
else:
source = source_or_component
body = source
# Validate the sx source parses as a single expression

View File

@@ -87,61 +87,61 @@ def get_layout(name: str) -> Layout | None:
# Built-in layouts
# ---------------------------------------------------------------------------
def _root_full(ctx: dict, **kw: Any) -> str:
return root_header_sx(ctx)
async def _root_full(ctx: dict, **kw: Any) -> str:
return await root_header_sx(ctx)
def _root_oob(ctx: dict, **kw: Any) -> str:
root_hdr = root_header_sx(ctx)
return oob_header_sx("root-header-child", "root-header-child", root_hdr)
async def _root_oob(ctx: dict, **kw: Any) -> str:
root_hdr = await root_header_sx(ctx)
return await oob_header_sx("root-header-child", "root-header-child", root_hdr)
def _post_full(ctx: dict, **kw: Any) -> str:
root_hdr = root_header_sx(ctx)
post_hdr = post_header_sx(ctx)
async def _post_full(ctx: dict, **kw: Any) -> str:
root_hdr = await root_header_sx(ctx)
post_hdr = await post_header_sx(ctx)
return "(<> " + root_hdr + " " + post_hdr + ")"
def _post_oob(ctx: dict, **kw: Any) -> str:
post_hdr = post_header_sx(ctx, oob=True)
async def _post_oob(ctx: dict, **kw: Any) -> str:
post_hdr = await post_header_sx(ctx, oob=True)
# Also replace #post-header-child (empty — clears any nested admin rows)
child_oob = oob_header_sx("post-header-child", "", "")
child_oob = await oob_header_sx("post-header-child", "", "")
return "(<> " + post_hdr + " " + child_oob + ")"
def _post_admin_full(ctx: dict, **kw: Any) -> str:
async def _post_admin_full(ctx: dict, **kw: Any) -> str:
slug = ctx.get("post", {}).get("slug", "")
selected = kw.get("selected", "")
root_hdr = root_header_sx(ctx)
admin_hdr = post_admin_header_sx(ctx, slug, selected=selected)
post_hdr = post_header_sx(ctx, child=admin_hdr)
root_hdr = await root_header_sx(ctx)
admin_hdr = await post_admin_header_sx(ctx, slug, selected=selected)
post_hdr = await post_header_sx(ctx, child=admin_hdr)
return "(<> " + root_hdr + " " + post_hdr + ")"
def _post_admin_oob(ctx: dict, **kw: Any) -> str:
async def _post_admin_oob(ctx: dict, **kw: Any) -> str:
slug = ctx.get("post", {}).get("slug", "")
selected = kw.get("selected", "")
post_hdr = post_header_sx(ctx, oob=True)
admin_hdr = post_admin_header_sx(ctx, slug, selected=selected)
admin_oob = oob_header_sx("post-header-child", "post-admin-header-child", admin_hdr)
post_hdr = await post_header_sx(ctx, oob=True)
admin_hdr = await post_admin_header_sx(ctx, slug, selected=selected)
admin_oob = await oob_header_sx("post-header-child", "post-admin-header-child", admin_hdr)
return "(<> " + post_hdr + " " + admin_oob + ")"
def _root_mobile(ctx: dict, **kw: Any) -> str:
return mobile_root_nav_sx(ctx)
async def _root_mobile(ctx: dict, **kw: Any) -> str:
return await mobile_root_nav_sx(ctx)
def _post_mobile(ctx: dict, **kw: Any) -> str:
return mobile_menu_sx(post_mobile_nav_sx(ctx), mobile_root_nav_sx(ctx))
async def _post_mobile(ctx: dict, **kw: Any) -> str:
return mobile_menu_sx(await post_mobile_nav_sx(ctx), await mobile_root_nav_sx(ctx))
def _post_admin_mobile(ctx: dict, **kw: Any) -> str:
async def _post_admin_mobile(ctx: dict, **kw: Any) -> str:
slug = ctx.get("post", {}).get("slug", "")
selected = kw.get("selected", "")
return mobile_menu_sx(
post_admin_mobile_nav_sx(ctx, slug, selected),
post_mobile_nav_sx(ctx),
mobile_root_nav_sx(ctx),
await post_admin_mobile_nav_sx(ctx, slug, selected),
await post_mobile_nav_sx(ctx),
await mobile_root_nav_sx(ctx),
)

View File

@@ -268,7 +268,7 @@ async def execute_page(
is_htmx = is_htmx_request()
if is_htmx:
return sx_response(oob_page_sx(
return sx_response(await oob_page_sx(
oobs=oob_headers if oob_headers else "",
filter=filter_sx,
aside=aside_sx,
@@ -276,7 +276,7 @@ async def execute_page(
menu=menu_sx,
))
else:
return full_page_sx(
return await full_page_sx(
tctx,
header_rows=header_rows,
filter=filter_sx,

View File

@@ -42,6 +42,7 @@ IO_PRIMITIVES: frozenset[str] = frozenset({
"get-children",
"g",
"csrf-token",
"abort",
})
@@ -328,6 +329,22 @@ async def _io_csrf_token(
return ""
async def _io_abort(
args: list[Any], kwargs: dict[str, Any], ctx: RequestContext
) -> Any:
"""``(abort 403 "message")`` — raise HTTP error from SX.
Allows defpages to abort with HTTP error codes for auth/ownership
checks without needing a Python page helper.
"""
if not args:
raise ValueError("abort requires a status code")
from quart import abort
status = int(args[0])
message = str(args[1]) if len(args) > 1 else ""
abort(status, message)
_IO_HANDLERS: dict[str, Any] = {
"frag": _io_frag,
"query": _io_query,
@@ -341,4 +358,5 @@ _IO_HANDLERS: dict[str, Any] = {
"get-children": _io_get_children,
"g": _io_g,
"csrf-token": _io_csrf_token,
"abort": _io_abort,
}

View File

@@ -34,30 +34,30 @@ def _register_sx_layouts() -> None:
register_custom_layout("sx-section", _sx_section_full_headers, _sx_section_oob_headers, _sx_section_mobile)
def _sx_full_headers(ctx: dict, **kw: Any) -> str:
async def _sx_full_headers(ctx: dict, **kw: Any) -> str:
"""Full headers for sx home page: root + sx menu row."""
from shared.sx.helpers import root_header_sx
from sxc.sx_components import _sx_header_sx, _main_nav_sx
main_nav = _main_nav_sx(kw.get("section"))
root_hdr = root_header_sx(ctx)
sx_row = _sx_header_sx(main_nav)
main_nav = await _main_nav_sx(kw.get("section"))
root_hdr = await root_header_sx(ctx)
sx_row = await _sx_header_sx(main_nav)
return "(<> " + root_hdr + " " + sx_row + ")"
def _sx_oob_headers(ctx: dict, **kw: Any) -> str:
async def _sx_oob_headers(ctx: dict, **kw: Any) -> str:
"""OOB headers for sx home page."""
from shared.sx.helpers import root_header_sx, oob_header_sx
from sxc.sx_components import _sx_header_sx, _main_nav_sx
root_hdr = root_header_sx(ctx)
main_nav = _main_nav_sx(kw.get("section"))
sx_row = _sx_header_sx(main_nav)
root_hdr = await root_header_sx(ctx)
main_nav = await _main_nav_sx(kw.get("section"))
sx_row = await _sx_header_sx(main_nav)
rows = "(<> " + root_hdr + " " + sx_row + ")"
return oob_header_sx("root-header-child", "sx-header-child", rows)
return await oob_header_sx("root-header-child", "sx-header-child", rows)
def _sx_section_full_headers(ctx: dict, **kw: Any) -> str:
async def _sx_section_full_headers(ctx: dict, **kw: Any) -> str:
"""Full headers for sx section pages: root + sx row + sub row."""
from shared.sx.helpers import root_header_sx
from sxc.sx_components import (
@@ -70,14 +70,14 @@ def _sx_section_full_headers(ctx: dict, **kw: Any) -> str:
sub_nav = kw.get("sub_nav", "")
selected = kw.get("selected", "")
root_hdr = root_header_sx(ctx)
main_nav = _main_nav_sx(section)
sub_row = _sub_row_sx(sub_label, sub_href, sub_nav, selected)
sx_row = _sx_header_sx(main_nav, child=sub_row)
root_hdr = await root_header_sx(ctx)
main_nav = await _main_nav_sx(section)
sub_row = await _sub_row_sx(sub_label, sub_href, sub_nav, selected)
sx_row = await _sx_header_sx(main_nav, child=sub_row)
return "(<> " + root_hdr + " " + sx_row + ")"
def _sx_section_oob_headers(ctx: dict, **kw: Any) -> str:
async def _sx_section_oob_headers(ctx: dict, **kw: Any) -> str:
"""OOB headers for sx section pages."""
from shared.sx.helpers import root_header_sx, oob_header_sx
from sxc.sx_components import (
@@ -90,34 +90,34 @@ def _sx_section_oob_headers(ctx: dict, **kw: Any) -> str:
sub_nav = kw.get("sub_nav", "")
selected = kw.get("selected", "")
root_hdr = root_header_sx(ctx)
main_nav = _main_nav_sx(section)
sub_row = _sub_row_sx(sub_label, sub_href, sub_nav, selected)
sx_row = _sx_header_sx(main_nav, child=sub_row)
root_hdr = await root_header_sx(ctx)
main_nav = await _main_nav_sx(section)
sub_row = await _sub_row_sx(sub_label, sub_href, sub_nav, selected)
sx_row = await _sx_header_sx(main_nav, child=sub_row)
rows = "(<> " + root_hdr + " " + sx_row + ")"
return oob_header_sx("root-header-child", "sx-header-child", rows)
return await oob_header_sx("root-header-child", "sx-header-child", rows)
def _sx_mobile(ctx: dict, **kw: Any) -> str:
async def _sx_mobile(ctx: dict, **kw: Any) -> str:
"""Mobile menu for sx home page: main nav + root."""
from shared.sx.helpers import (
mobile_menu_sx, mobile_root_nav_sx, sx_call, SxExpr,
mobile_menu_sx, mobile_root_nav_sx, render_to_sx, SxExpr,
)
from sxc.sx_components import _main_nav_sx
main_nav = _main_nav_sx(kw.get("section"))
main_nav = await _main_nav_sx(kw.get("section"))
return mobile_menu_sx(
sx_call("mobile-menu-section",
await render_to_sx("mobile-menu-section",
label="sx", href="/", level=1, colour="violet",
items=SxExpr(main_nav)),
mobile_root_nav_sx(ctx),
await mobile_root_nav_sx(ctx),
)
def _sx_section_mobile(ctx: dict, **kw: Any) -> str:
async def _sx_section_mobile(ctx: dict, **kw: Any) -> str:
"""Mobile menu for sx section pages: sub nav + main nav + root."""
from shared.sx.helpers import (
mobile_menu_sx, mobile_root_nav_sx, sx_call, SxExpr,
mobile_menu_sx, mobile_root_nav_sx, render_to_sx, SxExpr,
)
from sxc.sx_components import _main_nav_sx
@@ -125,17 +125,17 @@ def _sx_section_mobile(ctx: dict, **kw: Any) -> str:
sub_label = kw.get("sub_label", section)
sub_href = kw.get("sub_href", "/")
sub_nav = kw.get("sub_nav", "")
main_nav = _main_nav_sx(section)
main_nav = await _main_nav_sx(section)
parts = []
if sub_nav:
parts.append(sx_call("mobile-menu-section",
parts.append(await render_to_sx("mobile-menu-section",
label=sub_label, href=sub_href, level=2, colour="violet",
items=SxExpr(sub_nav)))
parts.append(sx_call("mobile-menu-section",
parts.append(await render_to_sx("mobile-menu-section",
label="sx", href="/", level=1, colour="violet",
items=SxExpr(main_nav)))
parts.append(mobile_root_nav_sx(ctx))
parts.append(await mobile_root_nav_sx(ctx))
return mobile_menu_sx(*parts)

View File

@@ -5,7 +5,7 @@ import os
from shared.sx.jinja_bridge import load_sx_dir, watch_sx_dir
from shared.sx.helpers import (
sx_call, SxExpr, get_asset_url,
render_to_sx, SxExpr, get_asset_url,
)
from content.highlight import highlight
@@ -108,11 +108,11 @@ def _full_wire_text(sx_src: str, *comp_names: str) -> str:
# Navigation helpers
# ---------------------------------------------------------------------------
def _nav_items_sx(items: list[tuple[str, str]], current: str | None = None) -> str:
async def _nav_items_sx(items: list[tuple[str, str]], current: str | None = None) -> str:
"""Build nav link items as sx."""
parts = []
for label, href in items:
parts.append(sx_call("nav-link",
parts.append(await render_to_sx("nav-link",
href=href, label=label,
is_selected="true" if current == label else None,
select_colours="aria-selected:bg-violet-200 aria-selected:text-violet-900",
@@ -120,9 +120,9 @@ def _nav_items_sx(items: list[tuple[str, str]], current: str | None = None) -> s
return "(<> " + " ".join(parts) + ")"
def _sx_header_sx(nav: str | None = None, *, child: str | None = None) -> str:
async def _sx_header_sx(nav: str | None = None, *, child: str | None = None) -> str:
"""Build the sx docs menu-row."""
return sx_call("menu-row-sx",
return await render_to_sx("menu-row-sx",
id="sx-row", level=1, colour="violet",
link_href="/", link_label="sx",
link_label_content=SxExpr('(span :class "font-mono" "(</>) sx")'),
@@ -132,40 +132,40 @@ def _sx_header_sx(nav: str | None = None, *, child: str | None = None) -> str:
)
def _docs_nav_sx(current: str | None = None) -> str:
async def _docs_nav_sx(current: str | None = None) -> str:
from content.pages import DOCS_NAV
return _nav_items_sx(DOCS_NAV, current)
return await _nav_items_sx(DOCS_NAV, current)
def _reference_nav_sx(current: str | None = None) -> str:
async def _reference_nav_sx(current: str | None = None) -> str:
from content.pages import REFERENCE_NAV
return _nav_items_sx(REFERENCE_NAV, current)
return await _nav_items_sx(REFERENCE_NAV, current)
def _protocols_nav_sx(current: str | None = None) -> str:
async def _protocols_nav_sx(current: str | None = None) -> str:
from content.pages import PROTOCOLS_NAV
return _nav_items_sx(PROTOCOLS_NAV, current)
return await _nav_items_sx(PROTOCOLS_NAV, current)
def _examples_nav_sx(current: str | None = None) -> str:
async def _examples_nav_sx(current: str | None = None) -> str:
from content.pages import EXAMPLES_NAV
return _nav_items_sx(EXAMPLES_NAV, current)
return await _nav_items_sx(EXAMPLES_NAV, current)
def _essays_nav_sx(current: str | None = None) -> str:
async def _essays_nav_sx(current: str | None = None) -> str:
from content.pages import ESSAYS_NAV
return _nav_items_sx(ESSAYS_NAV, current)
return await _nav_items_sx(ESSAYS_NAV, current)
def _main_nav_sx(current_section: str | None = None) -> str:
async def _main_nav_sx(current_section: str | None = None) -> str:
from content.pages import MAIN_NAV
return _nav_items_sx(MAIN_NAV, current_section)
return await _nav_items_sx(MAIN_NAV, current_section)
def _sub_row_sx(sub_label: str, sub_href: str, sub_nav: str,
async def _sub_row_sx(sub_label: str, sub_href: str, sub_nav: str,
selected: str = "") -> str:
"""Build the level-2 sub-section menu-row."""
return sx_call("menu-row-sx",
return await render_to_sx("menu-row-sx",
id="sx-sub-row", level=2, colour="violet",
link_href=sub_href, link_label=sub_label,
selected=selected or None,
@@ -178,22 +178,22 @@ def _sub_row_sx(sub_label: str, sub_href: str, sub_nav: str,
# Content builders — return sx source strings
# ---------------------------------------------------------------------------
def _doc_nav_sx(items: list[tuple[str, str]], current: str) -> str:
async def _doc_nav_sx(items: list[tuple[str, str]], current: str) -> str:
"""Build the in-page doc navigation pills."""
items_sx = " ".join(
f'(list "{label}" "{href}")'
for label, href in items
)
return sx_call("doc-nav", items=SxExpr(f"(list {items_sx})"), current=current)
return await render_to_sx("doc-nav", items=SxExpr(f"(list {items_sx})"), current=current)
def _attr_table_sx(title: str, attrs: list[tuple[str, str, bool]]) -> str:
async def _attr_table_sx(title: str, attrs: list[tuple[str, str, bool]]) -> str:
"""Build an attribute reference table."""
from content.pages import ATTR_DETAILS
rows = []
for attr, desc, exists in attrs:
href = f"/reference/attributes/{attr}" if exists and attr in ATTR_DETAILS else None
rows.append(sx_call("doc-attr-row", attr=attr, description=desc,
rows.append(await render_to_sx("doc-attr-row", attr=attr, description=desc,
exists="true" if exists else None,
href=href))
return (
@@ -209,13 +209,13 @@ def _attr_table_sx(title: str, attrs: list[tuple[str, str, bool]]) -> str:
)
def _primitives_section_sx() -> str:
async def _primitives_section_sx() -> str:
"""Build the primitives section."""
from content.pages import PRIMITIVES
parts = []
for category, prims in PRIMITIVES.items():
prims_sx = " ".join(f'"{p}"' for p in prims)
parts.append(sx_call("doc-primitives-table",
parts.append(await render_to_sx("doc-primitives-table",
category=category,
primitives=SxExpr(f"(list {prims_sx})")))
return " ".join(parts)
@@ -245,8 +245,9 @@ def _headers_table_sx(title: str, headers: list[tuple[str, str, str]]) -> str:
def _docs_content_sx(slug: str) -> str:
async def _docs_content_sx(slug: str) -> str:
"""Route to the right docs content builder."""
import inspect
builders = {
"introduction": _docs_introduction_sx,
"getting-started": _docs_getting_started_sx,
@@ -257,7 +258,8 @@ def _docs_content_sx(slug: str) -> str:
"server-rendering": _docs_server_rendering_sx,
}
builder = builders.get(slug, _docs_introduction_sx)
return builder()
result = builder()
return await result if inspect.isawaitable(result) else result
def _docs_introduction_sx() -> str:
@@ -379,8 +381,8 @@ def _docs_evaluator_sx() -> str:
)
def _docs_primitives_sx() -> str:
prims = _primitives_section_sx()
async def _docs_primitives_sx() -> str:
prims = await _primitives_section_sx()
return (
f'(~doc-page :title "Primitives"'
f' (~doc-section :title "Built-in functions" :id "builtins"'
@@ -471,14 +473,16 @@ def _docs_server_rendering_sx() -> str:
# Reference pages
# ---------------------------------------------------------------------------
def _reference_content_sx(slug: str) -> str:
async def _reference_content_sx(slug: str) -> str:
import inspect
builders = {
"attributes": _reference_attrs_sx,
"headers": _reference_headers_sx,
"events": _reference_events_sx,
"js-api": _reference_js_api_sx,
}
return builders.get(slug or "", _reference_attrs_sx)()
result = builders.get(slug or "", _reference_attrs_sx)()
return await result if inspect.isawaitable(result) else result
def _reference_index_sx() -> str:
@@ -573,18 +577,22 @@ def _reference_attr_detail_sx(slug: str) -> str:
)
def _reference_attrs_sx() -> str:
async def _reference_attrs_sx() -> str:
from content.pages import REQUEST_ATTRS, BEHAVIOR_ATTRS, SX_UNIQUE_ATTRS, HTMX_MISSING_ATTRS
req = await _attr_table_sx("Request Attributes", REQUEST_ATTRS)
beh = await _attr_table_sx("Behavior Attributes", BEHAVIOR_ATTRS)
uniq = await _attr_table_sx("Unique to sx", SX_UNIQUE_ATTRS)
missing = await _attr_table_sx("htmx features not yet in sx", HTMX_MISSING_ATTRS)
return (
f'(~doc-page :title "Attribute Reference"'
f' (p :class "text-stone-600 mb-6"'
f' "sx attributes mirror htmx where possible. This table shows what exists, '
f'what\'s unique to sx, and what\'s not yet implemented.")'
f' (div :class "space-y-8"'
f' {_attr_table_sx("Request Attributes", REQUEST_ATTRS)}'
f' {_attr_table_sx("Behavior Attributes", BEHAVIOR_ATTRS)}'
f' {_attr_table_sx("Unique to sx", SX_UNIQUE_ATTRS)}'
f' {_attr_table_sx("htmx features not yet in sx", HTMX_MISSING_ATTRS)}))'
f' {req}'
f' {beh}'
f' {uniq}'
f' {missing}))'
)
@@ -2066,9 +2074,9 @@ def home_content_sx() -> str:
)
def docs_content_partial_sx(slug: str) -> str:
async def docs_content_partial_sx(slug: str) -> str:
"""Docs content as sx wire format."""
inner = _docs_content_sx(slug)
inner = await _docs_content_sx(slug)
return (
f'(section :id "main-panel"'
f' :class "flex-1 md:h-full md:min-h-0 overflow-y-auto overscroll-contain js-grid-viewport"'
@@ -2076,8 +2084,8 @@ def docs_content_partial_sx(slug: str) -> str:
)
def reference_content_partial_sx(slug: str) -> str:
inner = _reference_content_sx(slug)
async def reference_content_partial_sx(slug: str) -> str:
inner = await _reference_content_sx(slug)
return (
f'(section :id "main-panel"'
f' :class "flex-1 md:h-full md:min-h-0 overflow-y-auto overscroll-contain js-grid-viewport"'
@@ -2085,8 +2093,8 @@ def reference_content_partial_sx(slug: str) -> str:
)
def protocol_content_partial_sx(slug: str) -> str:
inner = _protocol_content_sx(slug)
async def protocol_content_partial_sx(slug: str) -> str:
inner = await _protocol_content_sx(slug)
return (
f'(section :id "main-panel"'
f' :class "flex-1 md:h-full md:min-h-0 overflow-y-auto overscroll-contain js-grid-viewport"'
@@ -2094,8 +2102,8 @@ def protocol_content_partial_sx(slug: str) -> str:
)
def examples_content_partial_sx(slug: str) -> str:
inner = _examples_content_sx(slug)
async def examples_content_partial_sx(slug: str) -> str:
inner = await _examples_content_sx(slug)
return (
f'(section :id "main-panel"'
f' :class "flex-1 md:h-full md:min-h-0 overflow-y-auto overscroll-contain js-grid-viewport"'
@@ -2103,8 +2111,8 @@ def examples_content_partial_sx(slug: str) -> str:
)
def essay_content_partial_sx(slug: str) -> str:
inner = _essay_content_sx(slug)
async def essay_content_partial_sx(slug: str) -> str:
inner = await _essay_content_sx(slug)
return (
f'(section :id "main-panel"'
f' :class "flex-1 md:h-full md:min-h-0 overflow-y-auto overscroll-contain js-grid-viewport"'

View File

@@ -64,7 +64,7 @@ def register(url_prefix: str = "/") -> Blueprint:
# S-expression wire format — sx.js renders client-side
from shared.sx.helpers import sx_response
from sx.sx_components import test_detail_sx
return sx_response(test_detail_sx(test))
return sx_response(await test_detail_sx(test))
# Full page render (direct navigation / refresh)
from shared.sx.page import get_template_context

View File

@@ -6,7 +6,7 @@ from datetime import datetime
from shared.sx.jinja_bridge import load_service_components
from shared.sx.helpers import (
sx_call, SxExpr,
render_to_sx, SxExpr,
root_header_sx, full_page_sx, header_child_sx,
)
@@ -50,9 +50,9 @@ def _filter_tests(tests: list[dict], active_filter: str | None,
# Results partial
# ---------------------------------------------------------------------------
def test_detail_sx(test: dict) -> str:
async def test_detail_sx(test: dict) -> str:
"""Return s-expression wire format for a test detail view."""
inner = sx_call(
inner = await render_to_sx(
"test-detail",
nodeid=test["nodeid"],
outcome=test["outcome"],
@@ -70,10 +70,10 @@ def test_detail_sx(test: dict) -> str:
# Sx-native versions — return sx source (not HTML)
# ---------------------------------------------------------------------------
def _test_header_sx(ctx: dict, active_service: str | None = None) -> str:
async def _test_header_sx(ctx: dict, active_service: str | None = None) -> str:
"""Build the Tests menu-row as sx call."""
nav = _service_nav_sx(ctx, active_service)
return sx_call("menu-row-sx",
nav = await _service_nav_sx(ctx, active_service)
return await render_to_sx("menu-row-sx",
id="test-row", level=1, colour="sky",
link_href="/", link_label="Tests", icon="fa fa-flask",
nav=SxExpr(nav),
@@ -81,17 +81,17 @@ def _test_header_sx(ctx: dict, active_service: str | None = None) -> str:
)
def _service_nav_sx(ctx: dict, active_service: str | None = None) -> str:
async def _service_nav_sx(ctx: dict, active_service: str | None = None) -> str:
"""Service filter nav as sx."""
from runner import _SERVICE_ORDER
parts = []
parts.append(sx_call("nav-link",
parts.append(await render_to_sx("nav-link",
href="/", label="all",
is_selected="true" if not active_service else None,
select_colours="aria-selected:bg-sky-200 aria-selected:text-sky-900",
))
for svc in _SERVICE_ORDER:
parts.append(sx_call("nav-link",
parts.append(await render_to_sx("nav-link",
href=f"/?service={svc}", label=svc,
is_selected="true" if active_service == svc else None,
select_colours="aria-selected:bg-sky-200 aria-selected:text-sky-900",
@@ -99,19 +99,19 @@ def _service_nav_sx(ctx: dict, active_service: str | None = None) -> str:
return "(<> " + " ".join(parts) + ")"
def _header_stack_sx(ctx: dict, active_service: str | None = None) -> str:
async def _header_stack_sx(ctx: dict, active_service: str | None = None) -> str:
"""Full header stack as sx."""
hdr = root_header_sx(ctx)
inner = _test_header_sx(ctx, active_service)
child = header_child_sx(inner)
hdr = await root_header_sx(ctx)
inner = await _test_header_sx(ctx, active_service)
child = await header_child_sx(inner)
return "(<> " + hdr + " " + child + ")"
def _test_rows_sx(tests: list[dict]) -> str:
async def _test_rows_sx(tests: list[dict]) -> str:
"""Render all test result rows as sx."""
parts = []
for t in tests:
parts.append(sx_call("test-row",
parts.append(await render_to_sx("test-row",
nodeid=t["nodeid"],
outcome=t["outcome"],
duration=str(t["duration"]),
@@ -120,46 +120,46 @@ def _test_rows_sx(tests: list[dict]) -> str:
return "(<> " + " ".join(parts) + ")"
def _grouped_rows_sx(tests: list[dict]) -> str:
async def _grouped_rows_sx(tests: list[dict]) -> str:
"""Test rows grouped by service as sx."""
from runner import group_tests_by_service
sections = group_tests_by_service(tests)
parts = []
for sec in sections:
parts.append(sx_call("test-service-header",
parts.append(await render_to_sx("test-service-header",
service=sec["service"],
total=str(sec["total"]),
passed=str(sec["passed"]),
failed=str(sec["failed"]),
))
parts.append(_test_rows_sx(sec["tests"]))
parts.append(await _test_rows_sx(sec["tests"]))
return "(<> " + " ".join(parts) + ")"
def _results_partial_sx(result: dict | None, running: bool, csrf: str,
async def _results_partial_sx(result: dict | None, running: bool, csrf: str,
active_filter: str | None = None,
active_service: str | None = None) -> str:
"""Results section as sx."""
if running and not result:
summary = sx_call("test-summary",
summary = await render_to_sx("test-summary",
status="running", passed="0", failed="0", errors="0",
skipped="0", total="0", duration="...",
last_run="in progress", running=True, csrf=csrf,
active_filter=active_filter,
)
return "(<> " + summary + " " + sx_call("test-running-indicator") + ")"
return "(<> " + summary + " " + await render_to_sx("test-running-indicator") + ")"
if not result:
summary = sx_call("test-summary",
summary = await render_to_sx("test-summary",
status=None, passed="0", failed="0", errors="0",
skipped="0", total="0", duration="0",
last_run="never", running=running, csrf=csrf,
active_filter=active_filter,
)
return "(<> " + summary + " " + sx_call("test-no-results") + ")"
return "(<> " + summary + " " + await render_to_sx("test-no-results") + ")"
status = "running" if running else result["status"]
summary = sx_call("test-summary",
summary = await render_to_sx("test-summary",
status=status,
passed=str(result["passed"]),
failed=str(result["failed"]),
@@ -174,16 +174,16 @@ def _results_partial_sx(result: dict | None, running: bool, csrf: str,
)
if running:
return "(<> " + summary + " " + sx_call("test-running-indicator") + ")"
return "(<> " + summary + " " + await render_to_sx("test-running-indicator") + ")"
tests = result.get("tests", [])
tests = _filter_tests(tests, active_filter, active_service)
if not tests:
return "(<> " + summary + " " + sx_call("test-no-results") + ")"
return "(<> " + summary + " " + await render_to_sx("test-no-results") + ")"
has_failures = result["failed"] > 0 or result["errors"] > 0
rows = _grouped_rows_sx(tests)
table = sx_call("test-results-table",
rows = await _grouped_rows_sx(tests)
table = await render_to_sx("test-results-table",
rows=SxExpr(rows),
has_failures=str(has_failures).lower(),
)
@@ -203,10 +203,10 @@ async def render_dashboard_page_sx(ctx: dict, result: dict | None,
active_filter: str | None = None,
active_service: str | None = None) -> str:
"""Full page: test dashboard (sx wire format)."""
hdr = _header_stack_sx(ctx, active_service)
inner = _results_partial_sx(result, running, csrf, active_filter, active_service)
hdr = await _header_stack_sx(ctx, active_service)
inner = await _results_partial_sx(result, running, csrf, active_filter, active_service)
content = _wrap_results_div_sx(inner, running)
return full_page_sx(ctx, header_rows=hdr, content=content)
return await full_page_sx(ctx, header_rows=hdr, content=content)
async def render_results_partial_sx(result: dict | None, running: bool,
@@ -214,25 +214,27 @@ async def render_results_partial_sx(result: dict | None, running: bool,
active_filter: str | None = None,
active_service: str | None = None) -> str:
"""HTMX partial: results section (sx wire format)."""
inner = _results_partial_sx(result, running, csrf, active_filter, active_service)
inner = await _results_partial_sx(result, running, csrf, active_filter, active_service)
return _wrap_results_div_sx(inner, running)
async def render_test_detail_page_sx(ctx: dict, test: dict) -> str:
"""Full page: test detail (sx wire format)."""
root_hdr = root_header_sx(ctx)
test_row = _test_header_sx(ctx)
detail_row = sx_call("menu-row-sx",
root_hdr = await root_header_sx(ctx)
test_row = await _test_header_sx(ctx)
detail_row = await render_to_sx("menu-row-sx",
id="test-detail-row", level=2, colour="sky",
link_href=f"/test/{test['nodeid']}",
link_label=test["nodeid"].rsplit("::", 1)[-1],
)
inner = "(<> " + test_row + " " + header_child_sx(detail_row, id="test-header-child") + ")"
hdr = "(<> " + root_hdr + " " + header_child_sx(inner) + ")"
content = sx_call("test-detail",
hdr_child_detail = await header_child_sx(detail_row, id="test-header-child")
inner = "(<> " + test_row + " " + hdr_child_detail + ")"
hdr_child_inner = await header_child_sx(inner)
hdr = "(<> " + root_hdr + " " + hdr_child_inner + ")"
content = await render_to_sx("test-detail",
nodeid=test["nodeid"],
outcome=test["outcome"],
duration=str(test["duration"]),
longrepr=test.get("longrepr", ""),
)
return full_page_sx(ctx, header_rows=hdr, content=content)
return await full_page_sx(ctx, header_rows=hdr, content=content)