Files
mono/account/sx/sx_components.py
giles e085fe43b4 Replace sx_call() with render_to_sx() across all services
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>
2026-03-04 00:08:33 +00:00

108 lines
4.0 KiB
Python

"""
Account service s-expression page components.
Renders login, device auth, and check-email pages. Dashboard and newsletters
are now fully handled by .sx defcomps called from defpage expressions.
"""
from __future__ import annotations
import os
from typing import Any
from shared.sx.jinja_bridge import load_service_components
from shared.sx.helpers import (
render_to_sx,
root_header_sx, full_page_sx,
)
# Load account-specific .sx components + handlers at import time
load_service_components(os.path.dirname(os.path.dirname(__file__)),
service_name="account")
# ---------------------------------------------------------------------------
# Public API: Auth pages (login, device, check_email)
# ---------------------------------------------------------------------------
async def render_login_page(ctx: dict) -> str:
"""Full page: login form."""
error = ctx.get("error", "")
email = ctx.get("email", "")
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>')
async def render_device_page(ctx: dict) -> str:
"""Full page: device authorization form."""
error = ctx.get("error", "")
code = ctx.get("code", "")
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 = 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>')
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 = 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>')
# ---------------------------------------------------------------------------
# Public API: Fragment renderers for POST handlers
# ---------------------------------------------------------------------------
async def render_newsletter_toggle(un) -> str:
"""Render a newsletter toggle switch for POST response."""
from shared.browser.app.csrf import generate_csrf_token
nid = un.newsletter_id
from quart import g
account_url_fn = getattr(g, "_account_url", None)
if account_url_fn is None:
from shared.infrastructure.urls import account_url
account_url_fn = account_url
toggle_url = account_url_fn(f"/newsletter/{nid}/toggle/")
csrf = generate_csrf_token()
if un.subscribed:
bg = "bg-emerald-500"
translate = "translate-x-6"
checked = "true"
else:
bg = "bg-stone-300"
translate = "translate-x-1"
checked = "false"
return await render_to_sx(
"account-newsletter-toggle",
id=f"nl-{nid}", url=toggle_url,
hdrs=f'{{"X-CSRFToken": "{csrf}"}}',
target=f"#nl-{nid}",
cls=f"relative inline-flex h-6 w-11 items-center rounded-full transition-colors focus:outline-none focus:ring-2 focus:ring-emerald-500 focus:ring-offset-2 {bg}",
checked=checked,
knob_cls=f"inline-block h-4 w-4 rounded-full bg-white shadow transform transition-transform {translate}",
)