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

@@ -2,16 +2,13 @@ from __future__ import annotations
from quart import Blueprint, g, redirect, url_for, make_response
from sqlalchemy import select
from sqlalchemy.orm import selectinload
from shared.models.order import Order
from shared.browser.app.payments.sumup import create_checkout as sumup_create_checkout
from shared.config import config
from shared.infrastructure.cart_identity import current_cart_identity
from shared.sx.page import get_template_context
from services.check_sumup_status import check_sumup_status
from shared.browser.app.utils.htmx import is_htmx_request
from .filters.qs import makeqs_factory, decode
@@ -33,34 +30,6 @@ def register() -> Blueprint:
def route():
g.makeqs_factory = makeqs_factory
@bp.get("/")
async def order_detail(order_id: int):
"""Show a single order + items."""
owner = _owner_filter()
if owner is None:
return await make_response("Order not found", 404)
result = await g.s.execute(
select(Order)
.options(selectinload(Order.items))
.where(Order.id == order_id, owner)
)
order = result.scalar_one_or_none()
if not order:
return await make_response("Order not found", 404)
from sx.sx_components import render_order_page, render_order_oob
ctx = await get_template_context()
calendar_entries = ctx.get("calendar_entries")
if not is_htmx_request():
html = await render_order_page(ctx, order, calendar_entries, url_for)
return await make_response(html)
else:
from shared.sx.helpers import sx_response
sx_src = await render_order_oob(ctx, order, calendar_entries, url_for)
return sx_response(sx_src)
@bp.get("/pay/")
async def order_pay(order_id: int):
"""Re-open the SumUp payment page for this order."""
@@ -73,7 +42,7 @@ def register() -> Blueprint:
return await make_response("Order not found", 404)
if order.status == "paid":
return redirect(url_for("orders.order.order_detail", order_id=order.id))
return redirect(url_for("orders.defpage_order_detail", order_id=order.id))
if order.sumup_hosted_url:
return redirect(order.sumup_hosted_url)
@@ -120,13 +89,13 @@ def register() -> Blueprint:
return await make_response("Order not found", 404)
if not order.sumup_checkout_id:
return redirect(url_for("orders.order.order_detail", order_id=order.id))
return redirect(url_for("orders.defpage_order_detail", order_id=order.id))
try:
await check_sumup_status(g.s, order)
except Exception:
pass
return redirect(url_for("orders.order.order_detail", order_id=order.id))
return redirect(url_for("orders.defpage_order_detail", order_id=order.id))
return bp

View File

@@ -1,6 +1,6 @@
from __future__ import annotations
from quart import Blueprint, g, render_template, redirect, url_for, make_response
from quart import Blueprint, g, redirect, url_for, make_response, request
from sqlalchemy import select, func, or_, cast, String, exists
from sqlalchemy.orm import selectinload
@@ -20,18 +20,6 @@ def register(url_prefix: str) -> Blueprint:
ORDERS_PER_PAGE = 10
oob = {
"extends": "_types/root/_index.html",
"child_id": "auth-header-child",
"header": "_types/auth/header/_header.html",
"nav": "_types/auth/_nav.html",
"main": "_types/auth/_main_panel.html",
}
@bp.context_processor
def inject_oob():
return {"oob": oob}
@bp.before_request
def route():
g.makeqs_factory = makeqs_factory
@@ -43,8 +31,115 @@ def register(url_prefix: str) -> Blueprint:
if not ident["user_id"] and not ident["session_id"]:
return redirect(url_for("auth.login_form"))
@bp.get("/")
async def list_orders():
@bp.before_request
async def _prepare_page_data():
"""Load data for defpage routes into g.*."""
if request.method != "GET":
return
endpoint = request.endpoint or ""
# Orders list page
if endpoint.endswith("defpage_orders_list"):
ident = current_cart_identity()
if ident["user_id"]:
owner_clause = Order.user_id == ident["user_id"]
elif ident["session_id"]:
owner_clause = Order.session_id == ident["session_id"]
else:
return
q = decode()
page, search = q.page, q.search
if page < 1:
page = 1
where_clause = _search_clause(search) if search else None
count_stmt = select(func.count()).select_from(Order).where(owner_clause)
if where_clause is not None:
count_stmt = count_stmt.where(where_clause)
total_count_result = await g.s.execute(count_stmt)
total_count = total_count_result.scalar_one() or 0
total_pages = max(1, (total_count + ORDERS_PER_PAGE - 1) // ORDERS_PER_PAGE)
if page > total_pages:
page = total_pages
offset = (page - 1) * ORDERS_PER_PAGE
stmt = (
select(Order)
.where(owner_clause)
.order_by(Order.created_at.desc())
.offset(offset)
.limit(ORDERS_PER_PAGE)
)
if where_clause is not None:
stmt = stmt.where(where_clause)
result = await g.s.execute(stmt)
orders = result.scalars().all()
from shared.utils import route_prefix
pfx = route_prefix()
qs_fn = makeqs_factory()
g.orders_page_data = {
"orders": orders,
"page": page,
"total_pages": total_pages,
"search": search,
"search_count": total_count,
"url_for_fn": url_for,
"qs_fn": qs_fn,
"list_url": pfx + url_for("orders.defpage_orders_list"),
}
# Order detail page
elif endpoint.endswith("defpage_order_detail"):
order_id = request.view_args.get("order_id")
if order_id is None:
return
ident = current_cart_identity()
if ident["user_id"]:
owner = Order.user_id == ident["user_id"]
elif ident["session_id"]:
owner = Order.session_id == ident["session_id"]
else:
from quart import abort
abort(404)
return
result = await g.s.execute(
select(Order)
.options(selectinload(Order.items))
.where(Order.id == order_id, owner)
)
order = result.scalar_one_or_none()
if not order:
from quart import abort
abort(404)
return
from shared.utils import route_prefix
from shared.browser.app.csrf import generate_csrf_token
pfx = route_prefix()
g.order_detail_data = {
"order": order,
"calendar_entries": None,
"detail_url": pfx + url_for("orders.defpage_order_detail", order_id=order.id),
"list_url": pfx + url_for("orders.defpage_orders_list"),
"recheck_url": pfx + url_for("orders.order.order_recheck", order_id=order.id),
"pay_url": pfx + url_for("orders.order.order_pay", order_id=order.id),
"csrf_token": generate_csrf_token(),
}
@bp.get("/rows")
async def orders_rows():
"""Pagination endpoint — returns order rows for page > 1."""
ident = current_cart_identity()
if ident["user_id"]:
owner_clause = Order.user_id == ident["user_id"]
@@ -58,38 +153,7 @@ def register(url_prefix: str) -> Blueprint:
if page < 1:
page = 1
where_clause = None
if search:
term = f"%{search.strip()}%"
conditions = [
Order.status.ilike(term),
Order.currency.ilike(term),
Order.sumup_checkout_id.ilike(term),
Order.sumup_status.ilike(term),
Order.description.ilike(term),
]
conditions.append(
exists(
select(1)
.select_from(OrderItem)
.where(
OrderItem.order_id == Order.id,
or_(
OrderItem.product_title.ilike(term),
OrderItem.product_slug.ilike(term),
),
)
)
)
try:
search_id = int(search)
except (TypeError, ValueError):
search_id = None
if search_id is not None:
conditions.append(Order.id == search_id)
else:
conditions.append(cast(Order.id, String).ilike(term))
where_clause = or_(*conditions)
where_clause = _search_clause(search) if search else None
count_stmt = select(func.count()).select_from(Order).where(owner_clause)
if where_clause is not None:
@@ -116,38 +180,47 @@ def register(url_prefix: str) -> Blueprint:
result = await g.s.execute(stmt)
orders = result.scalars().all()
from shared.sx.page import get_template_context
from sx.sx_components import (
render_orders_page,
render_orders_rows,
render_orders_oob,
)
from sx.sx_components import _orders_rows_sx
from shared.sx.helpers import sx_response
ctx = await get_template_context()
qs_fn = makeqs_factory()
if not is_htmx_request():
html = await render_orders_page(
ctx, orders, page, total_pages, search, total_count,
url_for, qs_fn,
)
resp = await make_response(html)
elif page > 1:
# Sx wire format — client renders order rows
from shared.sx.helpers import sx_response
sx_src = await render_orders_rows(
ctx, orders, page, total_pages, url_for, qs_fn,
)
resp = sx_response(sx_src)
else:
from shared.sx.helpers import sx_response
sx_src = await render_orders_oob(
ctx, orders, page, total_pages, search, total_count,
url_for, qs_fn,
)
resp = sx_response(sx_src)
sx_src = _orders_rows_sx(orders, page, total_pages, url_for, qs_fn)
resp = sx_response(sx_src)
resp.headers["Hx-Push-Url"] = _current_url_without_page()
return _vary(resp)
return bp
def _search_clause(search: str):
"""Build an OR search clause across order fields."""
term = f"%{search.strip()}%"
conditions = [
Order.status.ilike(term),
Order.currency.ilike(term),
Order.sumup_checkout_id.ilike(term),
Order.sumup_status.ilike(term),
Order.description.ilike(term),
]
conditions.append(
exists(
select(1)
.select_from(OrderItem)
.where(
OrderItem.order_id == Order.id,
or_(
OrderItem.product_title.ilike(term),
OrderItem.product_slug.ilike(term),
),
)
)
)
try:
search_id = int(search)
except (TypeError, ValueError):
search_id = None
if search_id is not None:
conditions.append(Order.id == search_id)
else:
conditions.append(cast(Order.id, String).ilike(term))
return or_(*conditions)