Migrate all apps to defpage declarative page routes
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 3m41s

Replace Python GET page handlers with declarative defpage definitions in .sx
files across all 8 apps (sx docs, orders, account, market, cart, federation,
events, blog). Each app now has sxc/pages/ with setup functions, layout
registrations, page helpers, and .sx defpage declarations.

Core infrastructure: add g I/O primitive, PageDef support for auth/layout/
data/content/filter/aside/menu slots, post_author auth level, and custom
layout registration. Remove ~1400 lines of render_*_page/render_*_oob
boilerplate. Update all endpoint references in routes, sx_components, and
templates to defpage_* naming.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-03 14:52:34 +00:00
parent 5b4cacaf19
commit c243d17eeb
108 changed files with 3598 additions and 2851 deletions

View File

@@ -32,18 +32,103 @@ def register(url_prefix="/social"):
actor = await services.federation.get_actor_by_user_id(g.s, g.user.id)
g._social_actor = actor
# -- Timeline -------------------------------------------------------------
@bp.before_request
async def _prepare_page_data():
"""Pre-render content for defpage routes."""
endpoint = request.endpoint or ""
@bp.get("/")
async def home_timeline():
if not g.get("user"):
return redirect(url_for("auth.login_form"))
actor = _require_actor()
items = await services.federation.get_home_timeline(g.s, actor.id)
from shared.sx.page import get_template_context
from sx.sx_components import render_timeline_page
ctx = await get_template_context()
return await render_timeline_page(ctx, items, "home", actor)
if endpoint.endswith("defpage_home_timeline"):
actor = _require_actor()
items = await services.federation.get_home_timeline(g.s, actor.id)
from sx.sx_components import _timeline_content_sx
g.home_timeline_content = _timeline_content_sx(items, "home", actor)
elif endpoint.endswith("defpage_public_timeline"):
actor = getattr(g, "_social_actor", None)
items = await services.federation.get_public_timeline(g.s)
from sx.sx_components import _timeline_content_sx
g.public_timeline_content = _timeline_content_sx(items, "public", actor)
elif endpoint.endswith("defpage_compose_form"):
actor = _require_actor()
from sx.sx_components import _compose_content_sx
reply_to = request.args.get("reply_to")
g.compose_content = _compose_content_sx(actor, reply_to)
elif endpoint.endswith("defpage_search"):
actor = getattr(g, "_social_actor", None)
query = request.args.get("q", "").strip()
actors_list = []
total = 0
followed_urls: set[str] = set()
if query:
actors_list, total = await services.federation.search_actors(g.s, query)
if actor:
following, _ = await services.federation.get_following(
g.s, actor.preferred_username, page=1, per_page=1000,
)
followed_urls = {a.actor_url for a in following}
from sx.sx_components import _search_content_sx
g.search_content = _search_content_sx(query, actors_list, total, 1, followed_urls, actor)
elif endpoint.endswith("defpage_following_list"):
actor = _require_actor()
actors_list, total = await services.federation.get_following(
g.s, actor.preferred_username,
)
from sx.sx_components import _following_content_sx
g.following_content = _following_content_sx(actors_list, total, actor)
elif endpoint.endswith("defpage_followers_list"):
actor = _require_actor()
actors_list, total = await services.federation.get_followers_paginated(
g.s, actor.preferred_username,
)
following, _ = await services.federation.get_following(
g.s, actor.preferred_username, page=1, per_page=1000,
)
followed_urls = {a.actor_url for a in following}
from sx.sx_components import _followers_content_sx
g.followers_content = _followers_content_sx(actors_list, total, followed_urls, actor)
elif endpoint.endswith("defpage_actor_timeline"):
actor = getattr(g, "_social_actor", None)
actor_id = request.view_args.get("id")
from shared.models.federation import RemoteActor
from sqlalchemy import select as sa_select
remote = (
await g.s.execute(
sa_select(RemoteActor).where(RemoteActor.id == actor_id)
)
).scalar_one_or_none()
if not remote:
abort(404)
from shared.services.federation_impl import _remote_actor_to_dto
remote_dto = _remote_actor_to_dto(remote)
items = await services.federation.get_actor_timeline(g.s, actor_id)
is_following = False
if actor:
from shared.models.federation import APFollowing
existing = (
await g.s.execute(
sa_select(APFollowing).where(
APFollowing.actor_profile_id == actor.id,
APFollowing.remote_actor_id == actor_id,
)
)
).scalar_one_or_none()
is_following = existing is not None
from sx.sx_components import _actor_timeline_content_sx
g.actor_timeline_content = _actor_timeline_content_sx(remote_dto, items, is_following, actor)
elif endpoint.endswith("defpage_notifications"):
actor = _require_actor()
items = await services.federation.get_notifications(g.s, actor.id)
await services.federation.mark_notifications_read(g.s, actor.id)
from sx.sx_components import _notifications_content_sx
g.notifications_content = _notifications_content_sx(items)
# -- Timeline pagination ---------------------------------------------------
@bp.get("/timeline")
async def home_timeline_page():
@@ -62,15 +147,6 @@ def register(url_prefix="/social"):
sx_src = await render_timeline_items(items, "home", actor)
return sx_response(sx_src)
@bp.get("/public")
async def public_timeline():
items = await services.federation.get_public_timeline(g.s)
actor = getattr(g, "_social_actor", None)
from shared.sx.page import get_template_context
from sx.sx_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():
before_str = request.args.get("before")
@@ -86,16 +162,7 @@ def register(url_prefix="/social"):
sx_src = await render_timeline_items(items, "public", actor)
return sx_response(sx_src)
# -- Compose --------------------------------------------------------------
@bp.get("/compose")
async def compose_form():
actor = _require_actor()
reply_to = request.args.get("reply_to")
from shared.sx.page import get_template_context
from sx.sx_components import render_compose_page
ctx = await get_template_context()
return await render_compose_page(ctx, actor, reply_to)
# -- Compose ---------------------------------------------------------------
@bp.post("/compose")
async def compose_submit():
@@ -103,7 +170,7 @@ def register(url_prefix="/social"):
form = await request.form
content = form.get("content", "").strip()
if not content:
return redirect(url_for("social.compose_form"))
return redirect(url_for("social.defpage_compose_form"))
visibility = form.get("visibility", "public")
in_reply_to = form.get("in_reply_to") or None
@@ -114,45 +181,26 @@ def register(url_prefix="/social"):
visibility=visibility,
in_reply_to=in_reply_to,
)
return redirect(url_for("social.home_timeline"))
return redirect(url_for("social.defpage_home_timeline"))
@bp.post("/delete/<int:post_id>")
async def delete_post(post_id: int):
actor = _require_actor()
await services.federation.delete_local_post(g.s, actor.id, post_id)
return redirect(url_for("social.home_timeline"))
return redirect(url_for("social.defpage_home_timeline"))
# -- Search + Follow ------------------------------------------------------
@bp.get("/search")
async def search():
actor = getattr(g, "_social_actor", None)
query = request.args.get("q", "").strip()
actors = []
total = 0
followed_urls: set[str] = set()
if query:
actors, total = await services.federation.search_actors(g.s, query)
if actor:
following, _ = await services.federation.get_following(
g.s, actor.preferred_username, page=1, per_page=1000,
)
followed_urls = {a.actor_url for a in following}
from shared.sx.page import get_template_context
from sx.sx_components import render_search_page
ctx = await get_template_context()
return await render_search_page(ctx, query, actors, total, 1, followed_urls, actor)
# -- Search + Follow -------------------------------------------------------
@bp.get("/search/page")
async def search_page():
actor = getattr(g, "_social_actor", None)
query = request.args.get("q", "").strip()
page = request.args.get("page", 1, type=int)
actors = []
actors_list = []
total = 0
followed_urls: set[str] = set()
if query:
actors, total = await services.federation.search_actors(
actors_list, total = await services.federation.search_actors(
g.s, query, page=page,
)
if actor:
@@ -161,7 +209,7 @@ def register(url_prefix="/social"):
)
followed_urls = {a.actor_url for a in following}
from sx.sx_components import render_search_results
sx_src = await render_search_results(actors, query, page, followed_urls, actor)
sx_src = await render_search_results(actors_list, query, page, followed_urls, actor)
return sx_response(sx_src)
@bp.post("/follow")
@@ -175,7 +223,7 @@ def register(url_prefix="/social"):
)
if request.headers.get("SX-Request") or request.headers.get("HX-Request"):
return await _actor_card_response(actor, remote_actor_url, is_followed=True)
return redirect(request.referrer or url_for("social.search"))
return redirect(request.referrer or url_for("social.defpage_search"))
@bp.post("/unfollow")
async def unfollow():
@@ -188,7 +236,7 @@ def register(url_prefix="/social"):
)
if request.headers.get("SX-Request") or request.headers.get("HX-Request"):
return await _actor_card_response(actor, remote_actor_url, is_followed=False)
return redirect(request.referrer or url_for("social.search"))
return redirect(request.referrer or url_for("social.defpage_search"))
async def _actor_card_response(actor, remote_actor_url, is_followed):
"""Re-render a single actor card after follow/unfollow via HTMX."""
@@ -198,7 +246,6 @@ def register(url_prefix="/social"):
if not remote_dto:
return Response("", status=200)
followed_urls = {remote_actor_url} if is_followed else set()
# Detect list context from referer
referer = request.referrer or ""
if "/followers" in referer:
list_type = "followers"
@@ -207,7 +254,7 @@ def register(url_prefix="/social"):
from sx.sx_components import render_actor_card
return sx_response(render_actor_card(remote_dto, actor, followed_urls, list_type=list_type))
# -- Interactions ---------------------------------------------------------
# -- Interactions ----------------------------------------------------------
@bp.post("/like")
async def like():
@@ -216,7 +263,6 @@ def register(url_prefix="/social"):
object_id = form.get("object_id", "")
author_inbox = form.get("author_inbox", "")
await services.federation.like_post(g.s, actor.id, object_id, author_inbox)
# Return updated buttons for HTMX
return await _interaction_buttons_response(actor, object_id, author_inbox)
@bp.post("/unlike")
@@ -250,7 +296,6 @@ def register(url_prefix="/social"):
"""Re-render interaction buttons after a like/boost action."""
from shared.models.federation import APInteraction, APRemotePost, APActivity
from sqlalchemy import select
from shared.services.federation_impl import SqlFederationService
svc = services.federation
post_type, post_id = await svc._resolve_post(g.s, object_id)
@@ -304,51 +349,24 @@ def register(url_prefix="/social"):
actor=actor,
))
# -- Following / Followers ------------------------------------------------
@bp.get("/following")
async def following_list():
actor = _require_actor()
actors, total = await services.federation.get_following(
g.s, actor.preferred_username,
)
from shared.sx.page import get_template_context
from sx.sx_components import render_following_page
ctx = await get_template_context()
return await render_following_page(ctx, actors, total, actor)
# -- Following / Followers pagination --------------------------------------
@bp.get("/following/page")
async def following_list_page():
actor = _require_actor()
page = request.args.get("page", 1, type=int)
actors, total = await services.federation.get_following(
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, page, actor)
sx_src = await render_following_items(actors_list, page, actor)
return sx_response(sx_src)
@bp.get("/followers")
async def followers_list():
actor = _require_actor()
actors, total = await services.federation.get_followers_paginated(
g.s, actor.preferred_username,
)
# Build set of followed actor URLs to show Follow Back vs Unfollow
following, _ = await services.federation.get_following(
g.s, actor.preferred_username, page=1, per_page=1000,
)
followed_urls = {a.actor_url for a in following}
from shared.sx.page import get_template_context
from sx.sx_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():
actor = _require_actor()
page = request.args.get("page", 1, type=int)
actors, total = await services.federation.get_followers_paginated(
actors_list, total = await services.federation.get_followers_paginated(
g.s, actor.preferred_username, page=page,
)
following, _ = await services.federation.get_following(
@@ -356,43 +374,9 @@ def register(url_prefix="/social"):
)
followed_urls = {a.actor_url for a in following}
from sx.sx_components import render_followers_items
sx_src = await render_followers_items(actors, page, followed_urls, actor)
sx_src = await render_followers_items(actors_list, page, followed_urls, actor)
return sx_response(sx_src)
@bp.get("/actor/<int:id>")
async def actor_timeline(id: int):
actor = getattr(g, "_social_actor", None)
# Get remote actor info
from shared.models.federation import RemoteActor
from sqlalchemy import select as sa_select
remote = (
await g.s.execute(
sa_select(RemoteActor).where(RemoteActor.id == id)
)
).scalar_one_or_none()
if not remote:
abort(404)
from shared.services.federation_impl import _remote_actor_to_dto
remote_dto = _remote_actor_to_dto(remote)
items = await services.federation.get_actor_timeline(g.s, id)
# Check if we follow this actor
is_following = False
if actor:
from shared.models.federation import APFollowing
existing = (
await g.s.execute(
sa_select(APFollowing).where(
APFollowing.actor_profile_id == actor.id,
APFollowing.remote_actor_id == id,
)
)
).scalar_one_or_none()
is_following = existing is not None
from shared.sx.page import get_template_context
from sx.sx_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):
actor = getattr(g, "_social_actor", None)
@@ -410,17 +394,7 @@ def register(url_prefix="/social"):
sx_src = await render_actor_timeline_items(items, id, actor)
return sx_response(sx_src)
# -- Notifications --------------------------------------------------------
@bp.get("/notifications")
async def notifications():
actor = _require_actor()
items = await services.federation.get_notifications(g.s, actor.id)
await services.federation.mark_notifications_read(g.s, actor.id)
from shared.sx.page import get_template_context
from sx.sx_components import render_notifications_page
ctx = await get_template_context()
return await render_notifications_page(ctx, items, actor)
# -- Notifications ---------------------------------------------------------
@bp.get("/notifications/count")
async def notification_count():
@@ -440,6 +414,6 @@ def register(url_prefix="/social"):
async def mark_read():
actor = _require_actor()
await services.federation.mark_notifications_read(g.s, actor.id)
return redirect(url_for("social.notifications"))
return redirect(url_for("social.defpage_notifications"))
return bp