Convert all 23 register_custom_layout calls to register_sx_layout across 6 services
Layout defcomps are now fully self-contained via IO-primitive auto-fetch macros, eliminating Python layout functions that manually threaded context values through SxExpr wrappers. Services converted: - Federation (1 layout): social - Blog (7 layouts): blog, blog-settings, blog-cache, blog-snippets, blog-menu-items, blog-tag-groups, blog-tag-group-edit - SX docs (2 layouts): sx, sx-section - Cart (2 layouts): cart-page, cart-admin + orders/order-detail - Events (9 layouts): calendar-admin, slots, slot, day-admin, entry, entry-admin, ticket-types, ticket-type, markets - Market (2 layouts): market, market-admin New IO primitives added to shared/sx/primitives_io.py: - federation-actor-ctx, cart-page-ctx, request-view-args - events-calendar-ctx, events-day-ctx, events-entry-ctx, events-slot-ctx, events-ticket-type-ctx - market-header-ctx (pre-builds desktop/mobile nav as SxExpr) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -50,6 +50,15 @@ IO_PRIMITIVES: frozenset[str] = frozenset({
|
||||
"select-colours",
|
||||
"account-nav-ctx",
|
||||
"app-rights",
|
||||
"federation-actor-ctx",
|
||||
"request-view-args",
|
||||
"cart-page-ctx",
|
||||
"events-calendar-ctx",
|
||||
"events-day-ctx",
|
||||
"events-entry-ctx",
|
||||
"events-slot-ctx",
|
||||
"events-ticket-type-ctx",
|
||||
"market-header-ctx",
|
||||
})
|
||||
|
||||
|
||||
@@ -557,6 +566,371 @@ async def _io_post_header_ctx(
|
||||
return result
|
||||
|
||||
|
||||
async def _io_cart_page_ctx(
|
||||
args: list[Any], kwargs: dict[str, Any], ctx: RequestContext
|
||||
) -> dict[str, Any]:
|
||||
"""``(cart-page-ctx)`` → dict with cart page header values.
|
||||
|
||||
Reads ``g.page_post`` (set by cart's before_request) and returns
|
||||
slug, title, feature-image, and cart-url for the page cart header.
|
||||
"""
|
||||
from quart import g
|
||||
from .types import NIL
|
||||
from shared.infrastructure.urls import app_url
|
||||
|
||||
page_post = getattr(g, "page_post", None)
|
||||
if not page_post:
|
||||
return {"slug": "", "title": "", "feature-image": NIL, "cart-url": "/"}
|
||||
|
||||
slug = getattr(page_post, "slug", "") or ""
|
||||
title = (getattr(page_post, "title", "") or "")[:160]
|
||||
feature_image = getattr(page_post, "feature_image", None) or NIL
|
||||
|
||||
return {
|
||||
"slug": slug,
|
||||
"title": title,
|
||||
"feature-image": feature_image,
|
||||
"page-cart-url": app_url("cart", f"/{slug}/"),
|
||||
"cart-url": app_url("cart", "/"),
|
||||
}
|
||||
|
||||
|
||||
async def _io_federation_actor_ctx(
|
||||
args: list[Any], kwargs: dict[str, Any], ctx: RequestContext
|
||||
) -> dict[str, Any] | None:
|
||||
"""``(federation-actor-ctx)`` → serialized actor dict or None.
|
||||
|
||||
Reads ``g._social_actor`` (set by federation social blueprint's
|
||||
before_request hook) and serializes to a dict for .sx components.
|
||||
"""
|
||||
from quart import g
|
||||
actor = getattr(g, "_social_actor", None)
|
||||
if not actor:
|
||||
return None
|
||||
return {
|
||||
"id": actor.id,
|
||||
"preferred_username": actor.preferred_username,
|
||||
"display_name": getattr(actor, "display_name", None),
|
||||
"icon_url": getattr(actor, "icon_url", None),
|
||||
"actor_url": getattr(actor, "actor_url", ""),
|
||||
}
|
||||
|
||||
|
||||
async def _io_request_view_args(
|
||||
args: list[Any], kwargs: dict[str, Any], ctx: RequestContext
|
||||
) -> Any:
|
||||
"""``(request-view-args "key")`` → request.view_args[key]."""
|
||||
if not args:
|
||||
raise ValueError("request-view-args requires a key")
|
||||
from quart import request
|
||||
key = str(args[0])
|
||||
return (request.view_args or {}).get(key)
|
||||
|
||||
|
||||
async def _io_events_calendar_ctx(
|
||||
args: list[Any], kwargs: dict[str, Any], ctx: RequestContext
|
||||
) -> dict[str, Any]:
|
||||
"""``(events-calendar-ctx)`` → dict with events calendar header values.
|
||||
|
||||
Reads ``g.calendar`` or ``g._defpage_ctx["calendar"]`` and returns
|
||||
slug, name, description for the calendar header row.
|
||||
"""
|
||||
from quart import g
|
||||
cal = getattr(g, "calendar", None)
|
||||
if not cal:
|
||||
dctx = getattr(g, "_defpage_ctx", None) or {}
|
||||
cal = dctx.get("calendar")
|
||||
if not cal:
|
||||
return {"slug": ""}
|
||||
return {
|
||||
"slug": getattr(cal, "slug", "") or "",
|
||||
"name": getattr(cal, "name", "") or "",
|
||||
"description": getattr(cal, "description", "") or "",
|
||||
}
|
||||
|
||||
|
||||
async def _io_events_day_ctx(
|
||||
args: list[Any], kwargs: dict[str, Any], ctx: RequestContext
|
||||
) -> dict[str, Any]:
|
||||
"""``(events-day-ctx)`` → dict with events day header values.
|
||||
|
||||
Reads ``g.day_date``, ``g.calendar``, confirmed entries from
|
||||
``g._defpage_ctx``. Pre-builds the confirmed entries nav as SxExpr.
|
||||
"""
|
||||
from quart import g, url_for
|
||||
from .types import NIL
|
||||
from .parser import SxExpr
|
||||
|
||||
dctx = getattr(g, "_defpage_ctx", None) or {}
|
||||
cal = getattr(g, "calendar", None) or dctx.get("calendar")
|
||||
day_date = dctx.get("day_date") or getattr(g, "day_date", None)
|
||||
if not cal or not day_date:
|
||||
return {"date-str": ""}
|
||||
|
||||
cal_slug = getattr(cal, "slug", "") or ""
|
||||
|
||||
# Build confirmed entries nav
|
||||
confirmed = dctx.get("confirmed_entries") or []
|
||||
rights = getattr(g, "rights", None) or {}
|
||||
is_admin = (
|
||||
rights.get("admin", False)
|
||||
if isinstance(rights, dict)
|
||||
else getattr(rights, "admin", False)
|
||||
)
|
||||
|
||||
from .helpers import sx_call
|
||||
nav_parts: list[str] = []
|
||||
if confirmed:
|
||||
entry_links = []
|
||||
for entry in confirmed:
|
||||
href = url_for(
|
||||
"calendar.day.calendar_entries.calendar_entry.get",
|
||||
calendar_slug=cal_slug,
|
||||
year=day_date.year, month=day_date.month, day=day_date.day,
|
||||
entry_id=entry.id,
|
||||
)
|
||||
start = entry.start_at.strftime("%H:%M") if entry.start_at else ""
|
||||
end = (
|
||||
f" \u2013 {entry.end_at.strftime('%H:%M')}"
|
||||
if entry.end_at else ""
|
||||
)
|
||||
entry_links.append(sx_call(
|
||||
"events-day-entry-link",
|
||||
href=href, name=entry.name, time_str=f"{start}{end}",
|
||||
))
|
||||
inner = "".join(entry_links)
|
||||
nav_parts.append(sx_call(
|
||||
"events-day-entries-nav", inner=SxExpr(inner),
|
||||
))
|
||||
|
||||
if is_admin and day_date:
|
||||
admin_href = url_for(
|
||||
"defpage_day_admin", calendar_slug=cal_slug,
|
||||
year=day_date.year, month=day_date.month, day=day_date.day,
|
||||
)
|
||||
nav_parts.append(sx_call("nav-link", href=admin_href, icon="fa fa-cog"))
|
||||
|
||||
return {
|
||||
"date-str": day_date.strftime("%A %d %B %Y"),
|
||||
"year": day_date.year,
|
||||
"month": day_date.month,
|
||||
"day": day_date.day,
|
||||
"nav": SxExpr("".join(nav_parts)) if nav_parts else NIL,
|
||||
}
|
||||
|
||||
|
||||
async def _io_events_entry_ctx(
|
||||
args: list[Any], kwargs: dict[str, Any], ctx: RequestContext
|
||||
) -> dict[str, Any]:
|
||||
"""``(events-entry-ctx)`` → dict with events entry header values.
|
||||
|
||||
Reads ``g.entry``, ``g.calendar``, and entry_posts from
|
||||
``g._defpage_ctx``. Pre-builds entry nav (posts + admin link) as SxExpr.
|
||||
"""
|
||||
from quart import g, url_for
|
||||
from .types import NIL
|
||||
from .parser import SxExpr
|
||||
|
||||
dctx = getattr(g, "_defpage_ctx", None) or {}
|
||||
cal = getattr(g, "calendar", None) or dctx.get("calendar")
|
||||
entry = getattr(g, "entry", None) or dctx.get("entry")
|
||||
if not cal or not entry:
|
||||
return {"id": ""}
|
||||
|
||||
cal_slug = getattr(cal, "slug", "") or ""
|
||||
day = dctx.get("day")
|
||||
month = dctx.get("month")
|
||||
year = dctx.get("year")
|
||||
|
||||
# Times
|
||||
start = entry.start_at
|
||||
end = entry.end_at
|
||||
time_str = ""
|
||||
if start:
|
||||
time_str = start.strftime("%H:%M")
|
||||
if end:
|
||||
time_str += f" \u2192 {end.strftime('%H:%M')}"
|
||||
|
||||
link_href = url_for(
|
||||
"calendar.day.calendar_entries.calendar_entry.get",
|
||||
calendar_slug=cal_slug,
|
||||
year=year, month=month, day=day, entry_id=entry.id,
|
||||
)
|
||||
|
||||
# Build nav: associated posts + admin link
|
||||
entry_posts = dctx.get("entry_posts") or []
|
||||
rights = getattr(g, "rights", None) or {}
|
||||
is_admin = (
|
||||
rights.get("admin", False)
|
||||
if isinstance(rights, dict)
|
||||
else getattr(rights, "admin", False)
|
||||
)
|
||||
|
||||
from .helpers import sx_call
|
||||
from shared.infrastructure.urls import app_url
|
||||
|
||||
nav_parts: list[str] = []
|
||||
if entry_posts:
|
||||
post_links = ""
|
||||
for ep in entry_posts:
|
||||
ep_slug = getattr(ep, "slug", "")
|
||||
ep_title = getattr(ep, "title", "")
|
||||
feat = getattr(ep, "feature_image", None)
|
||||
href = app_url("blog", f"/{ep_slug}/")
|
||||
if feat:
|
||||
img_html = sx_call("events-post-img", src=feat, alt=ep_title)
|
||||
else:
|
||||
img_html = sx_call("events-post-img-placeholder")
|
||||
post_links += sx_call(
|
||||
"events-entry-nav-post-link",
|
||||
href=href, img=SxExpr(img_html), title=ep_title,
|
||||
)
|
||||
nav_parts.append(
|
||||
sx_call("events-entry-posts-nav-oob", items=SxExpr(post_links))
|
||||
.replace(' :hx-swap-oob "true"', '')
|
||||
)
|
||||
|
||||
if is_admin:
|
||||
admin_url = url_for(
|
||||
"calendar.day.calendar_entries.calendar_entry.admin.admin",
|
||||
calendar_slug=cal_slug,
|
||||
day=day, month=month, year=year, entry_id=entry.id,
|
||||
)
|
||||
nav_parts.append(sx_call("events-entry-admin-link", href=admin_url))
|
||||
|
||||
# Entry admin nav (ticket_types link)
|
||||
admin_href = url_for(
|
||||
"calendar.day.calendar_entries.calendar_entry.admin.admin",
|
||||
calendar_slug=cal_slug,
|
||||
day=day, month=month, year=year, entry_id=entry.id,
|
||||
) if is_admin else ""
|
||||
|
||||
ticket_types_href = url_for(
|
||||
"calendar.day.calendar_entries.calendar_entry.ticket_types.get",
|
||||
calendar_slug=cal_slug, entry_id=entry.id,
|
||||
year=year, month=month, day=day,
|
||||
)
|
||||
|
||||
from quart import current_app
|
||||
select_colours = current_app.jinja_env.globals.get("select_colours", "")
|
||||
|
||||
return {
|
||||
"id": str(entry.id),
|
||||
"name": entry.name or "",
|
||||
"time-str": time_str,
|
||||
"link-href": link_href,
|
||||
"nav": SxExpr("".join(nav_parts)) if nav_parts else NIL,
|
||||
"admin-href": admin_href,
|
||||
"ticket-types-href": ticket_types_href,
|
||||
"is-admin": is_admin,
|
||||
"select-colours": select_colours,
|
||||
}
|
||||
|
||||
|
||||
async def _io_events_slot_ctx(
|
||||
args: list[Any], kwargs: dict[str, Any], ctx: RequestContext
|
||||
) -> dict[str, Any]:
|
||||
"""``(events-slot-ctx)`` → dict with events slot header values."""
|
||||
from quart import g
|
||||
dctx = getattr(g, "_defpage_ctx", None) or {}
|
||||
slot = getattr(g, "slot", None) or dctx.get("slot")
|
||||
if not slot:
|
||||
return {"name": ""}
|
||||
return {
|
||||
"name": getattr(slot, "name", "") or "",
|
||||
"description": getattr(slot, "description", "") or "",
|
||||
}
|
||||
|
||||
|
||||
async def _io_events_ticket_type_ctx(
|
||||
args: list[Any], kwargs: dict[str, Any], ctx: RequestContext
|
||||
) -> dict[str, Any]:
|
||||
"""``(events-ticket-type-ctx)`` → dict with ticket type header values."""
|
||||
from quart import g, url_for
|
||||
|
||||
dctx = getattr(g, "_defpage_ctx", None) or {}
|
||||
cal = getattr(g, "calendar", None) or dctx.get("calendar")
|
||||
entry = getattr(g, "entry", None) or dctx.get("entry")
|
||||
ticket_type = getattr(g, "ticket_type", None) or dctx.get("ticket_type")
|
||||
if not cal or not entry or not ticket_type:
|
||||
return {"id": ""}
|
||||
|
||||
cal_slug = getattr(cal, "slug", "") or ""
|
||||
day = dctx.get("day")
|
||||
month = dctx.get("month")
|
||||
year = dctx.get("year")
|
||||
|
||||
link_href = url_for(
|
||||
"calendar.day.calendar_entries.calendar_entry.ticket_types.ticket_type.get",
|
||||
calendar_slug=cal_slug, year=year, month=month, day=day,
|
||||
entry_id=entry.id, ticket_type_id=ticket_type.id,
|
||||
)
|
||||
|
||||
return {
|
||||
"id": str(ticket_type.id),
|
||||
"name": getattr(ticket_type, "name", "") or "",
|
||||
"link-href": link_href,
|
||||
}
|
||||
|
||||
|
||||
async def _io_market_header_ctx(
|
||||
args: list[Any], kwargs: dict[str, Any], ctx: RequestContext
|
||||
) -> dict[str, Any]:
|
||||
"""``(market-header-ctx)`` → dict with market header values.
|
||||
|
||||
Pre-builds desktop-nav and mobile-nav as SxExpr strings using
|
||||
the existing Python helper functions in sxc.pages.layouts.
|
||||
"""
|
||||
from quart import g, url_for
|
||||
from shared.config import config as get_config
|
||||
from .parser import SxExpr
|
||||
|
||||
cfg = get_config()
|
||||
market_title = cfg.get("market_title", "")
|
||||
link_href = url_for("defpage_market_home")
|
||||
|
||||
# Get categories if market is loaded
|
||||
market = getattr(g, "market", None)
|
||||
categories = {}
|
||||
if market:
|
||||
from bp.browse.services.nav import get_nav
|
||||
nav_data = await get_nav(g.s, market_id=market.id)
|
||||
categories = nav_data.get("cats", {})
|
||||
|
||||
# Build minimal ctx for existing helper functions
|
||||
select_colours = getattr(g, "select_colours", "")
|
||||
if not select_colours:
|
||||
from quart import current_app
|
||||
select_colours = current_app.jinja_env.globals.get("select_colours", "")
|
||||
rights = getattr(g, "rights", None) or {}
|
||||
|
||||
mini_ctx: dict[str, Any] = {
|
||||
"market_title": market_title,
|
||||
"top_slug": "",
|
||||
"sub_slug": "",
|
||||
"categories": categories,
|
||||
"qs": "",
|
||||
"hx_select_search": "#main-panel",
|
||||
"select_colours": select_colours,
|
||||
"rights": rights,
|
||||
"category_label": "",
|
||||
}
|
||||
|
||||
# Pre-build nav using existing helper functions (lazy import from market service)
|
||||
from sxc.pages.layouts import _desktop_category_nav_sx, _mobile_nav_panel_sx
|
||||
desktop_nav = _desktop_category_nav_sx(mini_ctx, categories, "", "#main-panel")
|
||||
mobile_nav = _mobile_nav_panel_sx(mini_ctx)
|
||||
|
||||
return {
|
||||
"market-title": market_title,
|
||||
"link-href": link_href,
|
||||
"top-slug": "",
|
||||
"sub-slug": "",
|
||||
"desktop-nav": SxExpr(desktop_nav) if desktop_nav else "",
|
||||
"mobile-nav": SxExpr(mobile_nav) if mobile_nav else "",
|
||||
}
|
||||
|
||||
|
||||
_IO_HANDLERS: dict[str, Any] = {
|
||||
"frag": _io_frag,
|
||||
"query": _io_query,
|
||||
@@ -578,4 +952,13 @@ _IO_HANDLERS: dict[str, Any] = {
|
||||
"select-colours": _io_select_colours,
|
||||
"account-nav-ctx": _io_account_nav_ctx,
|
||||
"app-rights": _io_app_rights,
|
||||
"federation-actor-ctx": _io_federation_actor_ctx,
|
||||
"request-view-args": _io_request_view_args,
|
||||
"cart-page-ctx": _io_cart_page_ctx,
|
||||
"events-calendar-ctx": _io_events_calendar_ctx,
|
||||
"events-day-ctx": _io_events_day_ctx,
|
||||
"events-entry-ctx": _io_events_entry_ctx,
|
||||
"events-slot-ctx": _io_events_slot_ctx,
|
||||
"events-ticket-type-ctx": _io_events_ticket_type_ctx,
|
||||
"market-header-ctx": _io_market_header_ctx,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user