Delete orders + federation sx_components.py — rendering inlined to routes

Phase 2 (Orders):
- Checkout error/return renders moved directly into route handlers
- Removed orphaned test_sx_helpers.py

Phase 3 (Federation):
- Auth pages use _render_social_auth_page() helper in routes
- Choose-username render inlined into identity routes
- Timeline/search/follow/interaction renders inlined into social routes
  using serializers imported from sxc.pages
- Added _social_page() to sxc/pages/__init__.py for shared use
- Home page renders inline in app.py

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-04 01:22:33 +00:00
parent 400667b15a
commit dacb61b0ae
11 changed files with 326 additions and 589 deletions

View File

@@ -42,6 +42,16 @@ SESSION_USER_KEY = "uid"
ALLOWED_CLIENTS = {"blog", "market", "cart", "events", "account"}
async def _render_social_auth_page(component: str, title: str, **kwargs) -> str:
"""Render an auth page with social layout — replaces sx_components helpers."""
from shared.sx.helpers import render_to_sx
from shared.sx.page import get_template_context
from sxc.pages import _social_page
ctx = await get_template_context()
content = await render_to_sx(component, **{k: v for k, v in kwargs.items() if v})
return await _social_page(ctx, None, content=content, title=title)
def register(url_prefix="/auth"):
auth_bp = Blueprint("auth", __name__, url_prefix=url_prefix)
@@ -99,10 +109,7 @@ 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)
from shared.sx.page import get_template_context
from sx.sx_components import render_login_page
ctx = await get_template_context()
return await render_login_page(ctx)
return await _render_social_auth_page("account-login-content", "Login \u2014 Rose Ash")
@auth_bp.post("/start/")
async def start_login():
@@ -111,10 +118,10 @@ def register(url_prefix="/auth"):
is_valid, email = validate_email(email_input)
if not is_valid:
from shared.sx.page import get_template_context
from sx.sx_components import render_login_page
ctx = await get_template_context(error="Please enter a valid email address.", email=email_input)
return await render_login_page(ctx), 400
return await _render_social_auth_page(
"account-login-content", "Login \u2014 Rose Ash",
error="Please enter a valid email address.", email=email_input,
), 400
user = await find_or_create_user(g.s, email)
token, expires = await create_magic_link(g.s, user.id)
@@ -132,10 +139,10 @@ def register(url_prefix="/auth"):
"Please try again in a moment."
)
from shared.sx.page import get_template_context
from sx.sx_components import render_check_email_page
ctx = await get_template_context(email=email, email_error=email_error)
return await render_check_email_page(ctx)
return await _render_social_auth_page(
"account-check-email-content", "Check your email \u2014 Rose Ash",
email=email, email_error=email_error,
)
@auth_bp.get("/magic/<token>/")
async def magic(token: str):
@@ -148,17 +155,17 @@ def register(url_prefix="/auth"):
user, error = await validate_magic_link(s, token)
if error:
from shared.sx.page import get_template_context
from sx.sx_components import render_login_page
ctx = await get_template_context(error=error)
return await render_login_page(ctx), 400
return await _render_social_auth_page(
"account-login-content", "Login \u2014 Rose Ash",
error=error,
), 400
user_id = user.id
except Exception:
from shared.sx.page import get_template_context
from sx.sx_components import render_login_page
ctx = await get_template_context(error="Could not sign you in right now. Please try again.")
return await render_login_page(ctx), 502
return await _render_social_auth_page(
"account-login-content", "Login \u2014 Rose Ash",
error="Could not sign you in right now. Please try again.",
), 502
assert user_id is not None

View File

@@ -26,6 +26,33 @@ RESERVED = frozenset({
})
async def _render_choose_username(*, actor=None, error="", username=""):
"""Render choose-username page — replaces sx_components helper."""
from shared.browser.app.csrf import generate_csrf_token
from shared.config import config
from shared.sx.helpers import render_to_sx
from shared.sx.parser import SxExpr
from shared.sx.page import get_template_context
from sxc.pages import _social_page
from markupsafe import escape
ctx = await get_template_context()
csrf = generate_csrf_token()
ap_domain = config().get("ap_domain", "rose-ash.com")
check_url = url_for("identity.check_username")
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 await _social_page(ctx, actor, content=content,
title="Choose Username \u2014 Rose Ash")
def register(url_prefix="/identity"):
bp = Blueprint("identity", __name__, url_prefix=url_prefix)
@@ -39,11 +66,7 @@ def register(url_prefix="/identity"):
if actor:
return redirect(url_for("activitypub.actor_profile", username=actor.preferred_username))
from shared.sx.page import get_template_context
from sx.sx_components import render_choose_username_page
ctx = await get_template_context()
ctx["actor"] = actor
return await render_choose_username_page(ctx)
return await _render_choose_username(actor=actor)
@bp.post("/choose-username")
async def choose_username():
@@ -71,11 +94,7 @@ def register(url_prefix="/identity"):
error = "This username is already taken."
if error:
from shared.sx.page import get_template_context
from sx.sx_components import render_choose_username_page
ctx = await get_template_context(error=error, username=username)
ctx["actor"] = None
return await render_choose_username_page(ctx), 400
return await _render_choose_username(error=error, username=username), 400
# Create ActorProfile with RSA keys
display_name = g.user.name or username

View File

@@ -7,7 +7,7 @@ from datetime import datetime
from quart import Blueprint, request, g, redirect, url_for, abort, Response
from shared.services.registry import services
from shared.sx.helpers import sx_response
from shared.sx.helpers import sx_response, render_to_sx
log = logging.getLogger(__name__)
@@ -47,8 +47,7 @@ def register(url_prefix="/social"):
items = await services.federation.get_home_timeline(
g.s, actor.id, before=before,
)
from sx.sx_components import render_timeline_items
sx_src = await render_timeline_items(items, "home", actor)
sx_src = await _render_timeline_items(items, "home", actor)
return sx_response(sx_src)
@bp.get("/public/timeline")
@@ -62,8 +61,7 @@ def register(url_prefix="/social"):
pass
items = await services.federation.get_public_timeline(g.s, before=before)
actor = getattr(g, "_social_actor", None)
from sx.sx_components import render_timeline_items
sx_src = await render_timeline_items(items, "public", actor)
sx_src = await _render_timeline_items(items, "public", actor)
return sx_response(sx_src)
# -- Compose ---------------------------------------------------------------
@@ -97,6 +95,8 @@ def register(url_prefix="/social"):
@bp.get("/search/page")
async def search_page():
from sxc.pages import _serialize_remote_actor, _serialize_actor
actor = getattr(g, "_social_actor", None)
query = request.args.get("q", "").strip()
page = request.args.get("page", 1, type=int)
@@ -112,8 +112,18 @@ def register(url_prefix="/social"):
g.s, actor.preferred_username, page=1, per_page=1000,
)
followed_urls = {a.actor_url for a in following}
from sx.sx_components import render_search_results
sx_src = await render_search_results(actors_list, query, page, followed_urls, actor)
actor_dicts = [_serialize_remote_actor(a) for a in actors_list]
actor_data = _serialize_actor(actor)
parts = []
for ad in actor_dicts:
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_list) >= 20:
next_url = url_for("social.search_page", q=query, page=page + 1)
parts.append(await render_to_sx("federation-scroll-sentinel", url=next_url))
sx_src = "(<> " + " ".join(parts) + ")" if parts else ""
return sx_response(sx_src)
@bp.post("/follow")
@@ -144,6 +154,8 @@ def register(url_prefix="/social"):
async def _actor_card_response(actor, remote_actor_url, is_followed):
"""Re-render a single actor card after follow/unfollow via HTMX."""
from sxc.pages import _serialize_remote_actor, _serialize_actor
remote_dto = await services.federation.get_or_fetch_remote_actor(
g.s, remote_actor_url,
)
@@ -151,12 +163,12 @@ def register(url_prefix="/social"):
return Response("", status=200)
followed_urls = {remote_actor_url} if is_followed else set()
referer = request.referrer or ""
if "/followers" in referer:
list_type = "followers"
else:
list_type = "following"
from sx.sx_components import render_actor_card
return sx_response(await render_actor_card(remote_dto, actor, followed_urls, list_type=list_type))
list_type = "followers" if "/followers" in referer else "following"
actor_data = _serialize_actor(actor)
ad = _serialize_remote_actor(remote_dto)
return sx_response(await render_to_sx("federation-actor-card-from-data",
a=ad, actor=actor_data,
followed_urls=list(followed_urls), list_type=list_type))
# -- Interactions ----------------------------------------------------------
@@ -198,7 +210,9 @@ def register(url_prefix="/social"):
async def _interaction_buttons_response(actor, object_id, author_inbox):
"""Re-render interaction buttons after a like/boost action."""
from shared.models.federation import APInteraction, APRemotePost, APActivity
from shared.models.federation import APInteraction
from shared.browser.app.csrf import generate_csrf_token
from shared.sx.parser import SxExpr
from sqlalchemy import select
svc = services.federation
@@ -242,32 +256,72 @@ def register(url_prefix="/social"):
).limit(1)
)).scalar())
from sx.sx_components import render_interaction_buttons
return sx_response(await render_interaction_buttons(
object_id=object_id,
author_inbox=author_inbox,
like_count=like_count,
boost_count=boost_count,
liked_by_me=liked_by_me,
boosted_by_me=boosted_by_me,
actor=actor,
))
csrf = generate_csrf_token()
safe_id = object_id.replace("/", "_").replace(":", "_")
target = f"#interactions-{safe_id}"
if liked_by_me:
like_action = url_for("social.unlike")
like_cls = "text-red-500 hover:text-red-600"
like_icon = "\u2665"
else:
like_action = url_for("social.like")
like_cls = "hover:text-red-500"
like_icon = "\u2661"
if boosted_by_me:
boost_action = url_for("social.unboost")
boost_cls = "text-green-600 hover:text-green-700"
else:
boost_action = url_for("social.boost")
boost_cls = "hover:text-green-600"
reply_url = url_for("social.defpage_compose_form", reply_to=object_id) if object_id else ""
reply_sx = await render_to_sx("federation-reply-link", url=reply_url) if reply_url else ""
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 = 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_response(await render_to_sx("federation-interaction-buttons",
like=SxExpr(like_form),
boost=SxExpr(boost_form),
reply=SxExpr(reply_sx) if reply_sx else None))
# -- Following / Followers pagination --------------------------------------
@bp.get("/following/page")
async def following_list_page():
from sxc.pages import _serialize_remote_actor, _serialize_actor
actor = _require_actor()
page = request.args.get("page", 1, type=int)
actors_list, total = await services.federation.get_following(
g.s, actor.preferred_username, page=page,
)
from sx.sx_components import render_following_items
sx_src = await render_following_items(actors_list, page, actor)
actor_dicts = [_serialize_remote_actor(a) for a in actors_list]
actor_data = _serialize_actor(actor)
parts = []
for ad in actor_dicts:
parts.append(await render_to_sx("federation-actor-card-from-data",
a=ad, actor=actor_data,
followed_urls=[], list_type="following"))
if len(actors_list) >= 20:
next_url = url_for("social.following_list_page", page=page + 1)
parts.append(await render_to_sx("federation-scroll-sentinel", url=next_url))
sx_src = "(<> " + " ".join(parts) + ")" if parts else ""
return sx_response(sx_src)
@bp.get("/followers/page")
async def followers_list_page():
from sxc.pages import _serialize_remote_actor, _serialize_actor
actor = _require_actor()
page = request.args.get("page", 1, type=int)
actors_list, total = await services.federation.get_followers_paginated(
@@ -277,8 +331,17 @@ def register(url_prefix="/social"):
g.s, actor.preferred_username, page=1, per_page=1000,
)
followed_urls = {a.actor_url for a in following}
from sx.sx_components import render_followers_items
sx_src = await render_followers_items(actors_list, page, followed_urls, actor)
actor_dicts = [_serialize_remote_actor(a) for a in actors_list]
actor_data = _serialize_actor(actor)
parts = []
for ad in actor_dicts:
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_list) >= 20:
next_url = url_for("social.followers_list_page", page=page + 1)
parts.append(await render_to_sx("federation-scroll-sentinel", url=next_url))
sx_src = "(<> " + " ".join(parts) + ")" if parts else ""
return sx_response(sx_src)
@bp.get("/actor/<int:id>/timeline")
@@ -294,8 +357,7 @@ def register(url_prefix="/social"):
items = await services.federation.get_actor_timeline(
g.s, id, before=before,
)
from sx.sx_components import render_actor_timeline_items
sx_src = await render_actor_timeline_items(items, id, actor)
sx_src = await _render_timeline_items(items, "actor", actor, id)
return sx_response(sx_src)
# -- Notifications ---------------------------------------------------------
@@ -321,3 +383,26 @@ def register(url_prefix="/social"):
return redirect(url_for("defpage_notifications"))
return bp
async def _render_timeline_items(items, timeline_type, actor, actor_id=None):
"""Render timeline pagination items as SX fragment."""
from sxc.pages import _serialize_timeline_item, _serialize_actor
item_dicts = [_serialize_timeline_item(i) for i in items]
actor_data = _serialize_actor(actor)
next_url = None
if items:
last = items[-1]
before = last.published.isoformat() if last.published else ""
if timeline_type == "actor" and actor_id is not None:
next_url = url_for("social.actor_timeline_page", id=actor_id, before=before)
else:
next_url = url_for(f"social.{timeline_type}_timeline_page", before=before)
return await render_to_sx("federation-timeline-items",
items=item_dicts,
timeline_type=timeline_type,
actor=actor_data,
next_url=next_url)