Phase 6: Replace render_template() with s-expression rendering in all GET routes
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 1m15s

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

@@ -100,7 +100,10 @@ def register(url_prefix="/auth"):
# If there's a pending redirect (e.g. OAuth authorize), follow it
redirect_url = pop_login_redirect_target()
return redirect(redirect_url)
return await render_template("auth/login.html")
from shared.sexp.page import get_template_context
from sexp_components import render_login_page
ctx = await get_template_context()
return await render_login_page(ctx)
@auth_bp.post("/start/")
async def start_login():

View File

@@ -39,7 +39,11 @@ def register(url_prefix="/identity"):
if actor:
return redirect(url_for("activitypub.actor_profile", username=actor.preferred_username))
return await render_template("federation/choose_username.html")
from shared.sexp.page import get_template_context
from sexp_components import render_choose_username_page
ctx = await get_template_context()
ctx["actor"] = actor
return await render_choose_username_page(ctx)
@bp.post("/choose-username")
async def choose_username():

View File

@@ -39,12 +39,10 @@ def register(url_prefix="/social"):
return redirect(url_for("auth.login_form"))
actor = _require_actor()
items = await services.federation.get_home_timeline(g.s, actor.id)
return await render_template(
"federation/timeline.html",
items=items,
timeline_type="home",
actor=actor,
)
from shared.sexp.page import get_template_context
from sexp_components import render_timeline_page
ctx = await get_template_context()
return await render_timeline_page(ctx, items, "home", actor)
@bp.get("/timeline")
async def home_timeline_page():
@@ -59,23 +57,17 @@ def register(url_prefix="/social"):
items = await services.federation.get_home_timeline(
g.s, actor.id, before=before,
)
return await render_template(
"federation/_timeline_items.html",
items=items,
timeline_type="home",
actor=actor,
)
from sexp_components import render_timeline_items
return await render_timeline_items(items, "home", actor)
@bp.get("/public")
async def public_timeline():
items = await services.federation.get_public_timeline(g.s)
actor = getattr(g, "_social_actor", None)
return await render_template(
"federation/timeline.html",
items=items,
timeline_type="public",
actor=actor,
)
from shared.sexp.page import get_template_context
from sexp_components import render_timeline_page
ctx = await get_template_context()
return await render_timeline_page(ctx, items, "public", actor)
@bp.get("/public/timeline")
async def public_timeline_page():
@@ -88,12 +80,8 @@ def register(url_prefix="/social"):
pass
items = await services.federation.get_public_timeline(g.s, before=before)
actor = getattr(g, "_social_actor", None)
return await render_template(
"federation/_timeline_items.html",
items=items,
timeline_type="public",
actor=actor,
)
from sexp_components import render_timeline_items
return await render_timeline_items(items, "public", actor)
# -- Compose --------------------------------------------------------------
@@ -101,11 +89,10 @@ def register(url_prefix="/social"):
async def compose_form():
actor = _require_actor()
reply_to = request.args.get("reply_to")
return await render_template(
"federation/compose.html",
actor=actor,
reply_to=reply_to,
)
from shared.sexp.page import get_template_context
from sexp_components import render_compose_page
ctx = await get_template_context()
return await render_compose_page(ctx, actor, reply_to)
@bp.post("/compose")
async def compose_submit():
@@ -148,15 +135,10 @@ def register(url_prefix="/social"):
g.s, actor.preferred_username, page=1, per_page=1000,
)
followed_urls = {a.actor_url for a in following}
return await render_template(
"federation/search.html",
query=query,
actors=actors,
total=total,
page=1,
followed_urls=followed_urls,
actor=actor,
)
from shared.sexp.page import get_template_context
from sexp_components import render_search_page
ctx = await get_template_context()
return await render_search_page(ctx, query, actors, total, 1, followed_urls, actor)
@bp.get("/search/page")
async def search_page():
@@ -175,15 +157,8 @@ def register(url_prefix="/social"):
g.s, actor.preferred_username, page=1, per_page=1000,
)
followed_urls = {a.actor_url for a in following}
return await render_template(
"federation/_search_results.html",
actors=actors,
total=total,
page=page,
query=query,
followed_urls=followed_urls,
actor=actor,
)
from sexp_components import render_search_results
return await render_search_results(actors, query, page, followed_urls, actor)
@bp.post("/follow")
async def follow():
@@ -340,13 +315,10 @@ def register(url_prefix="/social"):
actors, total = await services.federation.get_following(
g.s, actor.preferred_username,
)
return await render_template(
"federation/following.html",
actors=actors,
total=total,
page=1,
actor=actor,
)
from shared.sexp.page import get_template_context
from sexp_components import render_following_page
ctx = await get_template_context()
return await render_following_page(ctx, actors, total, actor)
@bp.get("/following/page")
async def following_list_page():
@@ -355,15 +327,8 @@ def register(url_prefix="/social"):
actors, total = await services.federation.get_following(
g.s, actor.preferred_username, page=page,
)
return await render_template(
"federation/_actor_list_items.html",
actors=actors,
total=total,
page=page,
list_type="following",
followed_urls=set(),
actor=actor,
)
from sexp_components import render_following_items
return await render_following_items(actors, page, actor)
@bp.get("/followers")
async def followers_list():
@@ -376,14 +341,10 @@ def register(url_prefix="/social"):
g.s, actor.preferred_username, page=1, per_page=1000,
)
followed_urls = {a.actor_url for a in following}
return await render_template(
"federation/followers.html",
actors=actors,
total=total,
page=1,
followed_urls=followed_urls,
actor=actor,
)
from shared.sexp.page import get_template_context
from sexp_components import render_followers_page
ctx = await get_template_context()
return await render_followers_page(ctx, actors, total, followed_urls, actor)
@bp.get("/followers/page")
async def followers_list_page():
@@ -396,15 +357,8 @@ def register(url_prefix="/social"):
g.s, actor.preferred_username, page=1, per_page=1000,
)
followed_urls = {a.actor_url for a in following}
return await render_template(
"federation/_actor_list_items.html",
actors=actors,
total=total,
page=page,
list_type="followers",
followed_urls=followed_urls,
actor=actor,
)
from sexp_components import render_followers_items
return await render_followers_items(actors, page, followed_urls, actor)
@bp.get("/actor/<int:id>")
async def actor_timeline(id: int):
@@ -435,13 +389,10 @@ def register(url_prefix="/social"):
)
).scalar_one_or_none()
is_following = existing is not None
return await render_template(
"federation/actor_timeline.html",
remote_actor=remote_dto,
items=items,
is_following=is_following,
actor=actor,
)
from shared.sexp.page import get_template_context
from sexp_components import render_actor_timeline_page
ctx = await get_template_context()
return await render_actor_timeline_page(ctx, remote_dto, items, is_following, actor)
@bp.get("/actor/<int:id>/timeline")
async def actor_timeline_page(id: int):
@@ -456,13 +407,8 @@ def register(url_prefix="/social"):
items = await services.federation.get_actor_timeline(
g.s, id, before=before,
)
return await render_template(
"federation/_timeline_items.html",
items=items,
timeline_type="actor",
actor_id=id,
actor=actor,
)
from sexp_components import render_actor_timeline_items
return await render_actor_timeline_items(items, id, actor)
# -- Notifications --------------------------------------------------------
@@ -471,11 +417,10 @@ def register(url_prefix="/social"):
actor = _require_actor()
items = await services.federation.get_notifications(g.s, actor.id)
await services.federation.mark_notifications_read(g.s, actor.id)
return await render_template(
"federation/notifications.html",
notifications=items,
actor=actor,
)
from shared.sexp.page import get_template_context
from sexp_components import render_notifications_page
ctx = await get_template_context()
return await render_notifications_page(ctx, items, actor)
@bp.get("/notifications/count")
async def notification_count():