Auto-mount defpages: eliminate Python route stubs across all 9 services
Defpages are now declared with absolute paths in .sx files and auto-mounted directly on the Quart app, removing ~850 lines of blueprint mount_pages calls, before_request hooks, and g.* wrapper boilerplate. A new page = one defpage declaration, nothing else. Infrastructure: - async_eval awaits coroutine results from callable dispatch - auto_mount_pages() mounts all registered defpages on the app - g._defpage_ctx pattern passes helper data to layout context Migrated: sx, account, orders, federation, cart, market, events, blog Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -94,10 +94,11 @@ def create_app() -> "Quart":
|
||||
app.register_blueprint(register_identity_bp())
|
||||
|
||||
social_bp = register_social_bp()
|
||||
from shared.sx.pages import mount_pages
|
||||
mount_pages(social_bp, "federation")
|
||||
app.register_blueprint(social_bp)
|
||||
|
||||
from shared.sx.pages import auto_mount_pages
|
||||
auto_mount_pages(app, "federation")
|
||||
|
||||
app.register_blueprint(register_fragments())
|
||||
|
||||
# --- home page ---
|
||||
|
||||
@@ -32,102 +32,6 @@ def register(url_prefix="/social"):
|
||||
actor = await services.federation.get_actor_by_user_id(g.s, g.user.id)
|
||||
g._social_actor = actor
|
||||
|
||||
@bp.before_request
|
||||
async def _prepare_page_data():
|
||||
"""Pre-render content for defpage routes."""
|
||||
endpoint = request.endpoint or ""
|
||||
|
||||
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")
|
||||
@@ -170,7 +74,7 @@ def register(url_prefix="/social"):
|
||||
form = await request.form
|
||||
content = form.get("content", "").strip()
|
||||
if not content:
|
||||
return redirect(url_for("social.defpage_compose_form"))
|
||||
return redirect(url_for("defpage_compose_form"))
|
||||
|
||||
visibility = form.get("visibility", "public")
|
||||
in_reply_to = form.get("in_reply_to") or None
|
||||
@@ -181,13 +85,13 @@ def register(url_prefix="/social"):
|
||||
visibility=visibility,
|
||||
in_reply_to=in_reply_to,
|
||||
)
|
||||
return redirect(url_for("social.defpage_home_timeline"))
|
||||
return redirect(url_for("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.defpage_home_timeline"))
|
||||
return redirect(url_for("defpage_home_timeline"))
|
||||
|
||||
# -- Search + Follow -------------------------------------------------------
|
||||
|
||||
@@ -223,7 +127,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.defpage_search"))
|
||||
return redirect(request.referrer or url_for("defpage_search"))
|
||||
|
||||
@bp.post("/unfollow")
|
||||
async def unfollow():
|
||||
@@ -236,7 +140,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.defpage_search"))
|
||||
return redirect(request.referrer or url_for("defpage_search"))
|
||||
|
||||
async def _actor_card_response(actor, remote_actor_url, is_followed):
|
||||
"""Re-render a single actor card after follow/unfollow via HTMX."""
|
||||
@@ -414,6 +318,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.defpage_notifications"))
|
||||
return redirect(url_for("defpage_notifications"))
|
||||
|
||||
return bp
|
||||
|
||||
@@ -69,41 +69,130 @@ def _register_federation_helpers() -> None:
|
||||
})
|
||||
|
||||
|
||||
def _h_home_timeline_content():
|
||||
def _get_actor():
|
||||
"""Return current user's actor or None."""
|
||||
from quart import g
|
||||
return getattr(g, "home_timeline_content", "")
|
||||
return getattr(g, "_social_actor", None)
|
||||
|
||||
|
||||
def _h_public_timeline_content():
|
||||
def _require_actor():
|
||||
"""Return current user's actor or abort 403."""
|
||||
from quart import abort
|
||||
actor = _get_actor()
|
||||
if not actor:
|
||||
abort(403, "You need to choose a federation username first")
|
||||
return actor
|
||||
|
||||
|
||||
async def _h_home_timeline_content(**kw):
|
||||
from quart import g
|
||||
return getattr(g, "public_timeline_content", "")
|
||||
from shared.services.registry import services
|
||||
actor = _require_actor()
|
||||
items = await services.federation.get_home_timeline(g.s, actor.id)
|
||||
from sx.sx_components import _timeline_content_sx
|
||||
return _timeline_content_sx(items, "home", actor)
|
||||
|
||||
|
||||
def _h_compose_content():
|
||||
async def _h_public_timeline_content(**kw):
|
||||
from quart import g
|
||||
return getattr(g, "compose_content", "")
|
||||
from shared.services.registry import services
|
||||
actor = _get_actor()
|
||||
items = await services.federation.get_public_timeline(g.s)
|
||||
from sx.sx_components import _timeline_content_sx
|
||||
return _timeline_content_sx(items, "public", actor)
|
||||
|
||||
|
||||
def _h_search_content():
|
||||
async def _h_compose_content(**kw):
|
||||
from quart import request
|
||||
actor = _require_actor()
|
||||
from sx.sx_components import _compose_content_sx
|
||||
reply_to = request.args.get("reply_to")
|
||||
return _compose_content_sx(actor, reply_to)
|
||||
|
||||
|
||||
async def _h_search_content(**kw):
|
||||
from quart import g, request
|
||||
from shared.services.registry import services
|
||||
actor = _get_actor()
|
||||
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
|
||||
return _search_content_sx(query, actors_list, total, 1, followed_urls, actor)
|
||||
|
||||
|
||||
async def _h_following_content(**kw):
|
||||
from quart import g
|
||||
return getattr(g, "search_content", "")
|
||||
from shared.services.registry import services
|
||||
actor = _require_actor()
|
||||
actors_list, total = await services.federation.get_following(
|
||||
g.s, actor.preferred_username,
|
||||
)
|
||||
from sx.sx_components import _following_content_sx
|
||||
return _following_content_sx(actors_list, total, actor)
|
||||
|
||||
|
||||
def _h_following_content():
|
||||
async def _h_followers_content(**kw):
|
||||
from quart import g
|
||||
return getattr(g, "following_content", "")
|
||||
from shared.services.registry import services
|
||||
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
|
||||
return _followers_content_sx(actors_list, total, followed_urls, actor)
|
||||
|
||||
|
||||
def _h_followers_content():
|
||||
async def _h_actor_timeline_content(id=None, **kw):
|
||||
from quart import g, abort
|
||||
from shared.services.registry import services
|
||||
actor = _get_actor()
|
||||
actor_id = 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
|
||||
return _actor_timeline_content_sx(remote_dto, items, is_following, actor)
|
||||
|
||||
|
||||
async def _h_notifications_content(**kw):
|
||||
from quart import g
|
||||
return getattr(g, "followers_content", "")
|
||||
|
||||
|
||||
def _h_actor_timeline_content():
|
||||
from quart import g
|
||||
return getattr(g, "actor_timeline_content", "")
|
||||
|
||||
|
||||
def _h_notifications_content():
|
||||
from quart import g
|
||||
return getattr(g, "notifications_content", "")
|
||||
from shared.services.registry import services
|
||||
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
|
||||
return _notifications_content_sx(items)
|
||||
|
||||
@@ -1,49 +1,49 @@
|
||||
;; Federation social pages
|
||||
|
||||
(defpage home-timeline
|
||||
:path "/"
|
||||
:path "/social/"
|
||||
:auth :login
|
||||
:layout :social
|
||||
:content (home-timeline-content))
|
||||
|
||||
(defpage public-timeline
|
||||
:path "/public"
|
||||
:path "/social/public"
|
||||
:auth :public
|
||||
:layout :social
|
||||
:content (public-timeline-content))
|
||||
|
||||
(defpage compose-form
|
||||
:path "/compose"
|
||||
:path "/social/compose"
|
||||
:auth :login
|
||||
:layout :social
|
||||
:content (compose-content))
|
||||
|
||||
(defpage search
|
||||
:path "/search"
|
||||
:path "/social/search"
|
||||
:auth :public
|
||||
:layout :social
|
||||
:content (search-content))
|
||||
|
||||
(defpage following-list
|
||||
:path "/following"
|
||||
:path "/social/following"
|
||||
:auth :login
|
||||
:layout :social
|
||||
:content (following-content))
|
||||
|
||||
(defpage followers-list
|
||||
:path "/followers"
|
||||
:path "/social/followers"
|
||||
:auth :login
|
||||
:layout :social
|
||||
:content (followers-content))
|
||||
|
||||
(defpage actor-timeline
|
||||
:path "/actor/<int:id>"
|
||||
:path "/social/actor/<int:id>"
|
||||
:auth :public
|
||||
:layout :social
|
||||
:content (actor-timeline-content))
|
||||
:content (actor-timeline-content id))
|
||||
|
||||
(defpage notifications
|
||||
:path "/notifications"
|
||||
:path "/social/notifications"
|
||||
:auth :login
|
||||
:layout :social
|
||||
:content (notifications-content))
|
||||
|
||||
Reference in New Issue
Block a user