Merge branch 'worktree-sx-layout-conversion' into macros

# Conflicts:
#	blog/sxc/pages/layouts.py
#	cart/sxc/pages/layouts.py
#	events/sxc/pages/helpers.py
#	events/sxc/pages/layouts.py
#	market/sxc/pages/layouts.py
#	sx/sxc/pages/layouts.py
This commit is contained in:
2026-03-04 22:25:52 +00:00
18 changed files with 1289 additions and 1180 deletions

View File

@@ -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,
}