Phase 6: Replace render_template() with s-expression rendering in all GET routes

Migrate ~52 GET route handlers across all 7 services from Jinja
render_template() to s-expression component rendering. Each service
gets a sexp_components.py with page/oob/cards render functions.

- Add per-service sexp_components.py (account, blog, cart, events,
  federation, market, orders) with full page, OOB, and pagination
  card rendering
- Add shared/sexp/helpers.py with call_url, root_header_html,
  full_page, oob_page utilities
- Update all GET routes to use get_template_context() + render fns
- Fix get_template_context() to inject Jinja globals (URL helpers)
- Add qs_filter to base_context for sexp filter URL building
- Mount sexp_components.py in docker-compose.dev.yml for all services
- Import sexp_components in app.py for Hypercorn --reload watching
- Fix route_prefix import (shared.utils not shared.infrastructure.urls)
- Fix federation choose-username missing actor in context
- Fix market page_markets missing post in context

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-27 23:19:33 +00:00
parent 8013317b41
commit d53b9648a9
53 changed files with 8690 additions and 463 deletions

View File

@@ -16,6 +16,51 @@ from shared.utils import host_url
from shared.browser.app.utils import current_route_relative_path
def _qs_filter_fn():
"""Build a qs_filter(dict) wrapper for sexp components, or None.
Sexp components call ``qs_fn({"page": 2})``, ``qs_fn({"sort": "az"})``,
``qs_fn({"labels": ["organic", "local"]})``, etc.
Simple keys (page, sort, search, liked, clear_filters) are forwarded
to ``makeqs(**kwargs)``. List-valued keys (labels, stickers, brands)
represent *replacement* sets, so we rebuild the querystring from the
current base with those overridden.
"""
factory = getattr(g, "makeqs_factory", None)
if not factory:
return None
makeqs = factory()
def _qs(d: dict) -> str:
from shared.browser.app.filters.qs_base import build_qs
# Collect list-valued overrides
list_overrides = {}
for plural, singular in (("labels", "label"), ("stickers", "sticker"), ("brands", "brand")):
if plural in d:
list_overrides[singular] = list(d[plural] or [])
simple = {k: v for k, v in d.items()
if k in ("page", "sort", "search", "liked", "clear_filters")}
if not list_overrides:
return makeqs(**simple)
# For list overrides: get the base qs, parse out the overridden keys,
# then rebuild with the new values.
base_qs = makeqs(**simple)
from urllib.parse import parse_qsl, urlencode
params = [(k, v) for k, v in parse_qsl(base_qs.lstrip("?"))
if k not in list_overrides]
for singular, vals in list_overrides.items():
for v in vals:
params.append((singular, v))
return ("?" + urlencode(params)) if params else ""
return _qs
async def base_context() -> dict:
"""
Common template variables available in every app.
@@ -50,6 +95,7 @@ async def base_context() -> dict:
("price-desc", "\u00a3 high\u2192low", "order/h-l.svg"),
],
"zap_filter": zap_filter,
"qs_filter": _qs_filter_fn(),
"print": print,
"base_url": base_url,
"base_title": config()["title"],