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

@@ -1,7 +1,7 @@
from __future__ import annotations
from quart import (
request, render_template, make_response, Blueprint, g
request, Blueprint, g
)
@@ -14,23 +14,18 @@ from shared.sx.helpers import sx_response
def register():
bp = Blueprint("admin", __name__, url_prefix='/admin')
# ---------- Pages ----------
@bp.get("/")
@require_admin
async def admin(calendar_slug: str, **kwargs):
from shared.browser.app.utils.htmx import is_htmx_request
@bp.before_request
async def _prepare_page_data():
if "defpage_" not in (request.endpoint or ""):
return
from shared.sx.page import get_template_context
from sx.sx_components import render_calendar_admin_page, render_calendar_admin_oob
from sx.sx_components import _calendar_admin_main_panel_html
ctx = await get_template_context()
g.calendar_admin_content = _calendar_admin_main_panel_html(ctx)
tctx = await get_template_context()
if not is_htmx_request():
html = await render_calendar_admin_page(tctx)
return await make_response(html)
else:
sx_src = await render_calendar_admin_oob(tctx)
return sx_response(sx_src)
from shared.sx.pages import mount_pages
mount_pages(bp, "events", names=["calendar-admin"])
@bp.get("/description/")
@require_admin

View File

@@ -1,29 +1,23 @@
from __future__ import annotations
from quart import (
make_response, Blueprint
request, Blueprint, g
)
from shared.browser.app.authz import require_admin
from shared.browser.app.utils.htmx import is_htmx_request
from shared.sx.helpers import sx_response
def register():
bp = Blueprint("admin", __name__, url_prefix='/admin')
# ---------- Pages ----------
@bp.get("/")
@require_admin
async def admin(entry_id: int, **kwargs):
@bp.before_request
async def _prepare_page_data():
if "defpage_" not in (request.endpoint or ""):
return
from shared.sx.page import get_template_context
from sx.sx_components import render_entry_admin_page, render_entry_admin_oob
from sx.sx_components import _entry_admin_main_panel_html
ctx = await get_template_context()
g.entry_admin_content = _entry_admin_main_panel_html(ctx)
from shared.sx.pages import mount_pages
mount_pages(bp, "events", names=["entry-admin"])
tctx = await get_template_context()
if not is_htmx_request():
html = await render_entry_admin_page(tctx)
return await make_response(html)
else:
sx_src = await render_entry_admin_oob(tctx)
return sx_response(sx_src)
return bp

View File

@@ -238,20 +238,18 @@ def register():
"user_ticket_counts_by_type": user_ticket_counts_by_type,
"container_nav": container_nav,
}
@bp.get("/")
@require_admin
async def get(entry_id: int, **rest):
from shared.browser.app.utils.htmx import is_htmx_request
@bp.before_request
async def _prepare_page_data():
if "defpage_" not in (request.endpoint or ""):
return
from shared.sx.page import get_template_context
from sx.sx_components import render_entry_page, render_entry_oob
from sx.sx_components import _entry_main_panel_html, _entry_nav_html
ctx = await get_template_context()
g.entry_content = _entry_main_panel_html(ctx)
g.entry_menu = _entry_nav_html(ctx)
tctx = await get_template_context()
if not is_htmx_request():
html = await render_entry_page(tctx)
return await make_response(html, 200)
else:
sx_src = await render_entry_oob(tctx)
return sx_response(sx_src)
from shared.sx.pages import mount_pages
mount_pages(bp, "events", names=["entry-detail"])
@bp.get("/edit/")
@require_admin
@@ -435,10 +433,10 @@ def register():
nav_oob = await get_day_nav_oob(year, month, day)
from shared.sx.page import get_template_context
from sx.sx_components import render_entry_page
from sx.sx_components import _entry_main_panel_html
tctx = await get_template_context()
html = await render_entry_page(tctx)
html = _entry_main_panel_html(tctx)
return sx_response(html + nav_oob)

View File

@@ -1,31 +1,21 @@
from __future__ import annotations
from quart import (
render_template, make_response, Blueprint
request, Blueprint, g
)
from shared.browser.app.authz import require_admin
from shared.sx.helpers import sx_response
def register():
bp = Blueprint("admin", __name__, url_prefix='/admin')
# ---------- Pages ----------
@bp.get("/")
@require_admin
async def admin(year: int, month: int, day: int, **kwargs):
from shared.browser.app.utils.htmx import is_htmx_request
from shared.sx.page import get_template_context
from sx.sx_components import render_day_admin_page, render_day_admin_oob
@bp.before_request
async def _prepare_page_data():
if "defpage_" not in (request.endpoint or ""):
return
from sx.sx_components import _day_admin_main_panel_html
g.day_admin_content = _day_admin_main_panel_html({})
from shared.sx.pages import mount_pages
mount_pages(bp, "events", names=["day-admin"])
tctx = await get_template_context()
if not is_htmx_request():
html = await render_day_admin_page(tctx)
return await make_response(html)
else:
sx_src = await render_day_admin_oob(tctx)
return sx_response(sx_src)
return bp

View File

@@ -9,9 +9,8 @@ from .services.markets import (
soft_delete as svc_soft_delete,
)
from shared.browser.app.redis_cacher import cache_page, clear_cache
from shared.browser.app.redis_cacher import clear_cache
from shared.browser.app.authz import require_admin
from shared.browser.app.utils.htmx import is_htmx_request
from shared.sx.helpers import sx_response
@@ -22,18 +21,17 @@ def register():
async def inject_root():
return {}
@bp.get("/")
async def home(**kwargs):
@bp.before_request
async def _prepare_page_data():
if "defpage_" not in (request.endpoint or ""):
return
from shared.sx.page import get_template_context
from sx.sx_components import render_markets_page, render_markets_oob
from sx.sx_components import _markets_main_panel_html
ctx = await get_template_context()
if not is_htmx_request():
html = await render_markets_page(ctx)
return await make_response(html)
else:
sx_src = await render_markets_oob(ctx)
return sx_response(sx_src)
g.markets_content = _markets_main_panel_html(ctx)
from shared.sx.pages import mount_pages
mount_pages(bp, "events", names=["events-markets"])
@bp.post("/new/")
@require_admin

View File

@@ -1,7 +1,7 @@
from __future__ import annotations
from quart import (
request, render_template, make_response, Blueprint, g, jsonify
request, make_response, Blueprint, g, jsonify
)
from sqlalchemy.exc import IntegrityError
@@ -23,33 +23,32 @@ from shared.browser.app.utils import (
parse_time,
parse_cost
)
from shared.browser.app.utils.htmx import is_htmx_request
from shared.sx.helpers import sx_response
def register():
bp = Blueprint("slot", __name__, url_prefix='/<int:slot_id>')
# ---------- Pages ----------
@bp.get("/")
@require_admin
async def get(slot_id: int, **kwargs):
slot = await svc_get_slot(g.s, slot_id)
@bp.before_request
async def _prepare_page_data():
if "defpage_" not in (request.endpoint or ""):
return
slot_id = (request.view_args or {}).get("slot_id")
slot = await svc_get_slot(g.s, slot_id) if slot_id else None
if not slot:
return await make_response("Not found", 404)
from shared.sx.page import get_template_context
from sx.sx_components import render_slot_page, render_slot_oob
from quart import abort
abort(404)
g.slot = slot
calendar = getattr(g, "calendar", None)
from sx.sx_components import render_slot_main_panel
g.slot_content = render_slot_main_panel(slot, calendar)
tctx = await get_template_context()
if not is_htmx_request():
html = await render_slot_page(tctx)
return await make_response(html)
else:
sx_src = await render_slot_oob(tctx)
return sx_response(sx_src)
@bp.context_processor
async def _inject_slot():
return {"slot": getattr(g, "slot", None)}
from shared.sx.pages import mount_pages
mount_pages(bp, "events", names=["slot-detail"])
@bp.get("/edit/")
@require_admin

View File

@@ -1,7 +1,7 @@
from __future__ import annotations
from quart import (
request, render_template, make_response, Blueprint, g, jsonify
request, Blueprint, g, jsonify
)
from sqlalchemy.exc import IntegrityError
@@ -19,21 +19,16 @@ from shared.browser.app.utils import (
parse_time,
parse_cost
)
from shared.browser.app.utils.htmx import is_htmx_request
from shared.sx.helpers import sx_response
def register():
bp = Blueprint("slots", __name__, url_prefix='/slots')
# ---------- Pages ----------
bp.register_blueprint(
register_slot()
)
@bp.context_processor
async def get_slots():
calendar = getattr(g, "calendar", None)
@@ -43,19 +38,17 @@ def register():
}
return {"slots": []}
@bp.get("/")
async def get(**kwargs):
from shared.sx.page import get_template_context
from sx.sx_components import render_slots_page, render_slots_oob
tctx = await get_template_context()
if not is_htmx_request():
html = await render_slots_page(tctx)
return await make_response(html)
else:
sx_src = await render_slots_oob(tctx)
return sx_response(sx_src)
@bp.before_request
async def _prepare_page_data():
if "defpage_" not in (request.endpoint or ""):
return
calendar = getattr(g, "calendar", None)
slots = await svc_list_slots(g.s, calendar.id) if calendar else []
from sx.sx_components import render_slots_table
g.slots_content = render_slots_table(slots, calendar)
from shared.sx.pages import mount_pages
mount_pages(bp, "events", names=["slots-listing"])
@bp.post("/")
@require_admin

View File

@@ -12,7 +12,7 @@ from __future__ import annotations
import logging
from quart import (
Blueprint, g, request, render_template, make_response, jsonify,
Blueprint, g, request, make_response,
)
from sqlalchemy import select, func
from sqlalchemy.orm import selectinload
@@ -34,12 +34,10 @@ logger = logging.getLogger(__name__)
def register() -> Blueprint:
bp = Blueprint("ticket_admin", __name__, url_prefix="/admin/tickets")
@bp.get("/")
@require_admin
async def dashboard():
"""Ticket admin dashboard with QR scanner and recent tickets."""
from shared.browser.app.utils.htmx import is_htmx_request
@bp.before_request
async def _prepare_page_data():
if "defpage_" not in (request.endpoint or ""):
return
# Get recent tickets
result = await g.s.execute(
select(Ticket)
@@ -72,15 +70,9 @@ def register() -> Blueprint:
}
from shared.sx.page import get_template_context
from sx.sx_components import render_ticket_admin_page, render_ticket_admin_oob
from sx.sx_components import _ticket_admin_main_panel_html
ctx = await get_template_context()
if not is_htmx_request():
html = await render_ticket_admin_page(ctx, tickets, stats)
return await make_response(html, 200)
else:
sx_src = await render_ticket_admin_oob(ctx, tickets, stats)
return sx_response(sx_src)
g.ticket_admin_content = _ticket_admin_main_panel_html(ctx, tickets, stats)
@bp.get("/entry/<int:entry_id>/")
@require_admin

View File

@@ -1,7 +1,7 @@
from __future__ import annotations
from quart import (
request, render_template, make_response, Blueprint, g, jsonify
request, make_response, Blueprint, g, jsonify
)
from shared.browser.app.authz import require_admin
@@ -16,30 +16,37 @@ from .services.ticket import (
from ..ticket_types.services.tickets import (
list_ticket_types as svc_list_ticket_types,
)
from shared.browser.app.utils.htmx import is_htmx_request
from shared.sx.helpers import sx_response
def register():
bp = Blueprint("ticket_type", __name__, url_prefix='/<int:ticket_type_id>')
@bp.get("/")
@require_admin
async def get(ticket_type_id: int, **kwargs):
"""View a single ticket type."""
ticket_type = await svc_get_ticket_type(g.s, ticket_type_id)
@bp.before_request
async def _prepare_page_data():
if "defpage_" not in (request.endpoint or ""):
return
ticket_type_id = (request.view_args or {}).get("ticket_type_id")
ticket_type = await svc_get_ticket_type(g.s, ticket_type_id) if ticket_type_id else None
if not ticket_type:
return await make_response("Not found", 404)
from shared.sx.page import get_template_context
from sx.sx_components import render_ticket_type_page, render_ticket_type_oob
from quart import abort
abort(404)
g.ticket_type = ticket_type
entry = getattr(g, "entry", None)
calendar = getattr(g, "calendar", None)
va = request.view_args or {}
from sx.sx_components import render_ticket_type_main_panel
g.ticket_type_content = render_ticket_type_main_panel(
ticket_type, entry, calendar,
va.get("day"), va.get("month"), va.get("year"),
)
tctx = await get_template_context()
if not is_htmx_request():
html = await render_ticket_type_page(tctx)
return await make_response(html)
else:
sx_src = await render_ticket_type_oob(tctx)
return sx_response(sx_src)
@bp.context_processor
async def _inject_ticket_type():
return {"ticket_type": getattr(g, "ticket_type", None)}
from shared.sx.pages import mount_pages
mount_pages(bp, "events", names=["ticket-type-detail"])
@bp.get("/edit/")
@require_admin

View File

@@ -1,7 +1,7 @@
from __future__ import annotations
from quart import (
request, render_template, make_response, Blueprint, g, jsonify
request, Blueprint, g, jsonify
)
from shared.browser.app.authz import require_admin
@@ -14,7 +14,6 @@ from .services.tickets import (
from ..ticket_type.routes import register as register_ticket_type
from shared.browser.app.utils.htmx import is_htmx_request
from shared.sx.helpers import sx_response
@@ -36,19 +35,22 @@ def register():
}
return {"ticket_types": []}
@bp.get("/")
async def get(**kwargs):
"""List all ticket types for the current entry."""
from shared.sx.page import get_template_context
from sx.sx_components import render_ticket_types_page, render_ticket_types_oob
@bp.before_request
async def _prepare_page_data():
if "defpage_" not in (request.endpoint or ""):
return
entry = getattr(g, "entry", None)
calendar = getattr(g, "calendar", None)
ticket_types = await svc_list_ticket_types(g.s, entry.id) if entry else []
va = request.view_args or {}
from sx.sx_components import render_ticket_types_table
g.ticket_types_content = render_ticket_types_table(
ticket_types, entry, calendar,
va.get("day"), va.get("month"), va.get("year"),
)
tctx = await get_template_context()
if not is_htmx_request():
html = await render_ticket_types_page(tctx)
return await make_response(html)
else:
sx_src = await render_ticket_types_oob(tctx)
return sx_response(sx_src)
from shared.sx.pages import mount_pages
mount_pages(bp, "events", names=["ticket-types-listing"])
@bp.post("/")
@require_admin

View File

@@ -12,7 +12,7 @@ from __future__ import annotations
import logging
from quart import (
Blueprint, g, request, render_template, make_response,
Blueprint, g, request, make_response,
)
from sqlalchemy import select
from sqlalchemy.orm import selectinload
@@ -39,59 +39,43 @@ logger = logging.getLogger(__name__)
def register() -> Blueprint:
bp = Blueprint("tickets", __name__, url_prefix="/tickets")
@bp.get("/")
async def my_tickets():
"""List all tickets for the current user/session."""
from shared.browser.app.utils.htmx import is_htmx_request
ident = current_cart_identity()
tickets = await get_user_tickets(
g.s,
user_id=ident["user_id"],
session_id=ident["session_id"],
)
from shared.sx.page import get_template_context
from sx.sx_components import render_tickets_page, render_tickets_oob
ctx = await get_template_context()
if not is_htmx_request():
html = await render_tickets_page(ctx, tickets)
return await make_response(html, 200)
else:
sx_src = await render_tickets_oob(ctx, tickets)
return sx_response(sx_src)
@bp.get("/<code>/")
async def ticket_detail(code: str):
"""View a single ticket with QR code."""
from shared.browser.app.utils.htmx import is_htmx_request
ticket = await get_ticket_by_code(g.s, code)
if not ticket:
return await make_response("Ticket not found", 404)
# Verify ownership
ident = current_cart_identity()
if ident["user_id"] is not None:
if ticket.user_id != ident["user_id"]:
return await make_response("Ticket not found", 404)
elif ident["session_id"] is not None:
if ticket.session_id != ident["session_id"]:
return await make_response("Ticket not found", 404)
else:
return await make_response("Ticket not found", 404)
from shared.sx.page import get_template_context
from sx.sx_components import render_ticket_detail_page, render_ticket_detail_oob
ctx = await get_template_context()
if not is_htmx_request():
html = await render_ticket_detail_page(ctx, ticket)
return await make_response(html, 200)
else:
sx_src = await render_ticket_detail_oob(ctx, ticket)
return sx_response(sx_src)
@bp.before_request
async def _prepare_page_data():
ep = request.endpoint or ""
if "defpage_my_tickets" in ep:
ident = current_cart_identity()
tickets = await get_user_tickets(
g.s,
user_id=ident["user_id"],
session_id=ident["session_id"],
)
from shared.sx.page import get_template_context
from sx.sx_components import _tickets_main_panel_html
ctx = await get_template_context()
g.tickets_content = _tickets_main_panel_html(ctx, tickets)
elif "defpage_ticket_detail" in ep:
code = (request.view_args or {}).get("code")
ticket = await get_ticket_by_code(g.s, code) if code else None
if not ticket:
from quart import abort
abort(404)
# Verify ownership
ident = current_cart_identity()
if ident["user_id"] is not None:
if ticket.user_id != ident["user_id"]:
from quart import abort
abort(404)
elif ident["session_id"] is not None:
if ticket.session_id != ident["session_id"]:
from quart import abort
abort(404)
else:
from quart import abort
abort(404)
from shared.sx.page import get_template_context
from sx.sx_components import _ticket_detail_panel_html
ctx = await get_template_context()
g.ticket_detail_content = _ticket_detail_panel_html(ctx, ticket)
@bp.post("/buy/")
@clear_cache(tag="calendars", tag_scope="all")