Auto-mount defpages: eliminate Python route stubs across all 9 services
Some checks failed
Build and Deploy / build-and-deploy (push) Failing after 16s

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:
2026-03-03 19:03:15 +00:00
parent 4ba63bda17
commit 293f7713d6
63 changed files with 1340 additions and 1216 deletions

View File

@@ -10,7 +10,6 @@ from quart import (
url_for,
)
from shared.browser.app.authz import require_admin, require_post_author
from shared.browser.app.utils.htmx import is_htmx_request
from shared.sx.helpers import sx_response
from shared.utils import host_url
@@ -55,155 +54,6 @@ def _post_to_edit_dict(post) -> dict:
def register():
bp = Blueprint("admin", __name__, url_prefix='/admin')
@bp.before_request
async def _prepare_page_data():
ep = request.endpoint or ""
if "defpage_post_admin" in ep:
from sqlalchemy import select
from shared.models.page_config import PageConfig
post = (g.post_data or {}).get("post", {})
features = {}
sumup_configured = False
sumup_merchant_code = ""
sumup_checkout_prefix = ""
if post.get("is_page"):
pc = (await g.s.execute(
select(PageConfig).where(
PageConfig.container_type == "page",
PageConfig.container_id == post["id"],
)
)).scalar_one_or_none()
if pc:
features = pc.features or {}
sumup_configured = bool(pc.sumup_api_key)
sumup_merchant_code = pc.sumup_merchant_code or ""
sumup_checkout_prefix = pc.sumup_checkout_prefix or ""
from shared.sx.page import get_template_context
from sx.sx_components import _post_admin_main_panel_sx
tctx = await get_template_context()
tctx.update({
"features": features,
"sumup_configured": sumup_configured,
"sumup_merchant_code": sumup_merchant_code,
"sumup_checkout_prefix": sumup_checkout_prefix,
})
g.post_admin_content = _post_admin_main_panel_sx(tctx)
elif "defpage_post_data" in ep:
from shared.sx.page import get_template_context
from sx.sx_components import _post_data_content_sx
tctx = await get_template_context()
g.post_data_content = _post_data_content_sx(tctx)
elif "defpage_post_preview" in ep:
from models.ghost_content import Post
from sqlalchemy import select as sa_select
post_id = g.post_data["post"]["id"]
post = (await g.s.execute(
sa_select(Post).where(Post.id == post_id)
)).scalar_one_or_none()
preview_ctx = {}
sx_content = getattr(post, "sx_content", None) or ""
if sx_content:
from shared.sx.prettify import sx_to_pretty_sx
preview_ctx["sx_pretty"] = sx_to_pretty_sx(sx_content)
lexical_raw = getattr(post, "lexical", None) or ""
if lexical_raw:
from shared.sx.prettify import json_to_pretty_sx
preview_ctx["json_pretty"] = json_to_pretty_sx(lexical_raw)
if sx_content:
from shared.sx.parser import parse as sx_parse
from shared.sx.html import render as sx_html_render
from shared.sx.jinja_bridge import _COMPONENT_ENV
try:
parsed = sx_parse(sx_content)
preview_ctx["sx_rendered"] = sx_html_render(parsed, dict(_COMPONENT_ENV))
except Exception:
preview_ctx["sx_rendered"] = "<em>Error rendering sx</em>"
if lexical_raw:
from bp.blog.ghost.lexical_renderer import render_lexical
try:
preview_ctx["lex_rendered"] = render_lexical(lexical_raw)
except Exception:
preview_ctx["lex_rendered"] = "<em>Error rendering lexical</em>"
from shared.sx.page import get_template_context
from sx.sx_components import _preview_main_panel_sx
tctx = await get_template_context()
tctx.update(preview_ctx)
g.post_preview_content = _preview_main_panel_sx(tctx)
elif "defpage_post_entries" in ep:
from sqlalchemy import select
from shared.models.calendars import Calendar
from ..services.entry_associations import get_post_entry_ids
post_id = g.post_data["post"]["id"]
associated_entry_ids = await get_post_entry_ids(post_id)
result = await g.s.execute(
select(Calendar)
.where(Calendar.deleted_at.is_(None))
.order_by(Calendar.name.asc())
)
all_calendars = result.scalars().all()
for calendar in all_calendars:
await g.s.refresh(calendar, ["entries", "post"])
from shared.sx.page import get_template_context
from sx.sx_components import _post_entries_content_sx
tctx = await get_template_context()
tctx["all_calendars"] = all_calendars
tctx["associated_entry_ids"] = associated_entry_ids
g.post_entries_content = _post_entries_content_sx(tctx)
elif "defpage_post_settings" in ep:
from models.ghost_content import Post
from sqlalchemy import select as sa_select
from sqlalchemy.orm import selectinload
post_id = g.post_data["post"]["id"]
post = (await g.s.execute(
sa_select(Post)
.where(Post.id == post_id)
.options(selectinload(Post.tags))
)).scalar_one_or_none()
ghost_post = _post_to_edit_dict(post) if post else {}
save_success = request.args.get("saved") == "1"
from shared.sx.page import get_template_context
from sx.sx_components import _post_settings_content_sx
tctx = await get_template_context()
tctx["ghost_post"] = ghost_post
tctx["save_success"] = save_success
g.post_settings_content = _post_settings_content_sx(tctx)
elif "defpage_post_edit" in ep:
from models.ghost_content import Post
from sqlalchemy import select as sa_select
from sqlalchemy.orm import selectinload
from shared.infrastructure.data_client import fetch_data
post_id = g.post_data["post"]["id"]
post = (await g.s.execute(
sa_select(Post)
.where(Post.id == post_id)
.options(selectinload(Post.tags))
)).scalar_one_or_none()
ghost_post = _post_to_edit_dict(post) if post else {}
save_success = request.args.get("saved") == "1"
save_error = request.args.get("error", "")
raw_newsletters = await fetch_data("account", "newsletters", required=False) or []
from types import SimpleNamespace
newsletters = [SimpleNamespace(**nl) for nl in raw_newsletters]
from shared.sx.page import get_template_context
from sx.sx_components import _post_edit_content_sx
tctx = await get_template_context()
tctx["ghost_post"] = ghost_post
tctx["save_success"] = save_success
tctx["save_error"] = save_error
tctx["newsletters"] = newsletters
g.post_edit_content = _post_edit_content_sx(tctx)
from shared.sx.pages import mount_pages
mount_pages(bp, "blog", names=[
"post-admin", "post-data", "post-preview",
"post-entries", "post-settings", "post-edit",
])
@bp.put("/features/")
@require_admin
async def update_features(slug: str):
@@ -468,7 +318,7 @@ def register():
except OptimisticLockError:
from urllib.parse import quote
return redirect(
host_url(url_for("blog.post.admin.defpage_post_settings", slug=slug))
host_url(url_for("defpage_post_settings", slug=slug))
+ "?error=" + quote("Someone else edited this post. Please reload and try again.")
)
@@ -479,7 +329,7 @@ def register():
await invalidate_tag_cache("post.post_detail")
# Redirect using the (possibly new) slug
return redirect(host_url(url_for("blog.post.admin.defpage_post_settings", slug=post.slug)) + "?saved=1")
return redirect(host_url(url_for("defpage_post_settings", slug=post.slug)) + "?saved=1")
@bp.post("/edit/")
@require_post_author
@@ -504,11 +354,11 @@ def register():
try:
lexical_doc = json.loads(lexical_raw)
except (json.JSONDecodeError, TypeError):
return redirect(host_url(url_for("blog.post.admin.defpage_post_edit", slug=slug)) + "?error=" + quote("Invalid JSON in editor content."))
return redirect(host_url(url_for("defpage_post_edit", slug=slug)) + "?error=" + quote("Invalid JSON in editor content."))
ok, reason = validate_lexical(lexical_doc)
if not ok:
return redirect(host_url(url_for("blog.post.admin.defpage_post_edit", slug=slug)) + "?error=" + quote(reason))
return redirect(host_url(url_for("defpage_post_edit", slug=slug)) + "?error=" + quote(reason))
# Publish workflow
is_admin = bool((g.get("rights") or {}).get("admin"))
@@ -544,7 +394,7 @@ def register():
)
except OptimisticLockError:
return redirect(
host_url(url_for("blog.post.admin.defpage_post_edit", slug=slug))
host_url(url_for("defpage_post_edit", slug=slug))
+ "?error=" + quote("Someone else edited this post. Please reload and try again.")
)
@@ -560,7 +410,7 @@ def register():
await invalidate_tag_cache("post.post_detail")
# Redirect to GET (PRG pattern) — use post.slug in case it changed
redirect_url = host_url(url_for("blog.post.admin.defpage_post_edit", slug=post.slug)) + "?saved=1"
redirect_url = host_url(url_for("defpage_post_edit", slug=post.slug)) + "?saved=1"
if publish_requested_msg:
redirect_url += "&publish_requested=1"
return redirect(redirect_url)