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:
2026-03-04 22:21:44 +00:00
parent ad75798ab7
commit 57a31a3b83
18 changed files with 1289 additions and 1215 deletions

View File

@@ -1,4 +1,5 @@
;; Blog layout defcomps — fully self-contained via IO primitives. ;; Blog layout defcomps — fully self-contained via IO primitives.
;; Registered via register_sx_layout in __init__.py.
;; --- Blog header (invisible row for blog-header-child swap target) --- ;; --- Blog header (invisible row for blog-header-child swap target) ---
@@ -7,28 +8,161 @@
:link-label-content (div) :link-label-content (div)
:child-id "blog-header-child" :oob oob)) :child-id "blog-header-child" :oob oob))
;; --- Blog layout (root + blog header) --- ;; --- Auto-fetching settings header macro ---
(defmacro ~blog-settings-header-auto (oob)
(quasiquote
(~menu-row-sx :id "root-settings-row" :level 1
:link-href (url-for "settings.defpage_settings_home")
:link-label-content (~blog-admin-label)
:nav (~blog-settings-nav)
:child-id "root-settings-header-child"
:oob (unquote oob))))
;; --- Auto-fetching sub-settings header macro ---
(defmacro ~blog-sub-settings-header-auto (row-id child-id endpoint icon label oob)
(quasiquote
(~menu-row-sx :id (unquote row-id) :level 2
:link-href (url-for (unquote endpoint))
:link-label-content (~blog-sub-settings-label
:icon (str "fa fa-" (unquote icon))
:label (unquote label))
:child-id (unquote child-id)
:oob (unquote oob))))
;; ---------------------------------------------------------------------------
;; Blog layout (root + blog header)
;; ---------------------------------------------------------------------------
(defcomp ~blog-layout-full () (defcomp ~blog-layout-full ()
(<> (~root-header-auto) (<> (~root-header-auto)
(~blog-header))) (~blog-header)))
;; --- Settings layout (root + settings header) --- (defcomp ~blog-layout-oob ()
(<> (~blog-header :oob true)
(~clear-oob-div :id "blog-header-child")
(~root-header-auto true)))
(defcomp ~settings-layout-full (&key settings-header) ;; ---------------------------------------------------------------------------
;; Settings layout (root + settings header)
;; ---------------------------------------------------------------------------
(defcomp ~blog-settings-layout-full ()
(<> (~root-header-auto) (<> (~root-header-auto)
settings-header)) (~blog-settings-header-auto)))
;; --- Sub-settings layout (root + settings + sub row) --- (defcomp ~blog-settings-layout-oob ()
(<> (~blog-settings-header-auto true)
(~clear-oob-div :id "root-settings-header-child")
(~root-header-auto true)))
(defcomp ~sub-settings-layout-full (&key settings-header sub-header) (defcomp ~blog-settings-layout-mobile ()
(~blog-settings-nav))
;; ---------------------------------------------------------------------------
;; Cache layout (root + settings + cache sub-header)
;; ---------------------------------------------------------------------------
(defcomp ~blog-cache-layout-full ()
(<> (~root-header-auto) (<> (~root-header-auto)
settings-header sub-header)) (~blog-settings-header-auto)
(~blog-sub-settings-header-auto
"cache-row" "cache-header-child"
"settings.defpage_cache_page" "refresh" "Cache")))
(defcomp ~sub-settings-layout-oob (&key settings-header-oob sub-header-oob) (defcomp ~blog-cache-layout-oob ()
(<> settings-header-oob sub-header-oob)) (<> (~blog-sub-settings-header-auto
"cache-row" "cache-header-child"
"settings.defpage_cache_page" "refresh" "Cache" true)
(~clear-oob-div :id "cache-header-child")
(~blog-settings-header-auto true)
(~root-header-auto true)))
;; --- Settings nav links — uses (select-colours) IO primitive --- ;; ---------------------------------------------------------------------------
;; Snippets layout (root + settings + snippets sub-header)
;; ---------------------------------------------------------------------------
(defcomp ~blog-snippets-layout-full ()
(<> (~root-header-auto)
(~blog-settings-header-auto)
(~blog-sub-settings-header-auto
"snippets-row" "snippets-header-child"
"snippets.defpage_snippets_page" "puzzle-piece" "Snippets")))
(defcomp ~blog-snippets-layout-oob ()
(<> (~blog-sub-settings-header-auto
"snippets-row" "snippets-header-child"
"snippets.defpage_snippets_page" "puzzle-piece" "Snippets" true)
(~clear-oob-div :id "snippets-header-child")
(~blog-settings-header-auto true)
(~root-header-auto true)))
;; ---------------------------------------------------------------------------
;; Menu Items layout (root + settings + menu-items sub-header)
;; ---------------------------------------------------------------------------
(defcomp ~blog-menu-items-layout-full ()
(<> (~root-header-auto)
(~blog-settings-header-auto)
(~blog-sub-settings-header-auto
"menu_items-row" "menu_items-header-child"
"menu_items.defpage_menu_items_page" "bars" "Menu Items")))
(defcomp ~blog-menu-items-layout-oob ()
(<> (~blog-sub-settings-header-auto
"menu_items-row" "menu_items-header-child"
"menu_items.defpage_menu_items_page" "bars" "Menu Items" true)
(~clear-oob-div :id "menu_items-header-child")
(~blog-settings-header-auto true)
(~root-header-auto true)))
;; ---------------------------------------------------------------------------
;; Tag Groups layout (root + settings + tag-groups sub-header)
;; ---------------------------------------------------------------------------
(defcomp ~blog-tag-groups-layout-full ()
(<> (~root-header-auto)
(~blog-settings-header-auto)
(~blog-sub-settings-header-auto
"tag-groups-row" "tag-groups-header-child"
"blog.tag_groups_admin.defpage_tag_groups_page" "tags" "Tag Groups")))
(defcomp ~blog-tag-groups-layout-oob ()
(<> (~blog-sub-settings-header-auto
"tag-groups-row" "tag-groups-header-child"
"blog.tag_groups_admin.defpage_tag_groups_page" "tags" "Tag Groups" true)
(~clear-oob-div :id "tag-groups-header-child")
(~blog-settings-header-auto true)
(~root-header-auto true)))
;; ---------------------------------------------------------------------------
;; Tag Group Edit layout (root + settings + tag-groups sub-header with id)
;; ---------------------------------------------------------------------------
(defcomp ~blog-tag-group-edit-layout-full ()
(<> (~root-header-auto)
(~blog-settings-header-auto)
(~menu-row-sx :id "tag-groups-row" :level 2
:link-href (url-for "blog.tag_groups_admin.defpage_tag_group_edit"
:id (request-view-args "id"))
:link-label-content (~blog-sub-settings-label
:icon "fa fa-tags" :label "Tag Groups")
:child-id "tag-groups-header-child")))
(defcomp ~blog-tag-group-edit-layout-oob ()
(<> (~menu-row-sx :id "tag-groups-row" :level 2
:link-href (url-for "blog.tag_groups_admin.defpage_tag_group_edit"
:id (request-view-args "id"))
:link-label-content (~blog-sub-settings-label
:icon "fa fa-tags" :label "Tag Groups")
:child-id "tag-groups-header-child"
:oob true)
(~clear-oob-div :id "tag-groups-header-child")
(~blog-settings-header-auto true)
(~root-header-auto true)))
;; --- Settings nav links — uses IO primitives ---
(defcomp ~blog-settings-nav () (defcomp ~blog-settings-nav ()
(let* ((sc (select-colours)) (let* ((sc (select-colours))

View File

@@ -1,203 +1,19 @@
"""Blog layout functions for defpage rendering.""" """Blog layout registration — all layouts delegate to .sx defcomps."""
from __future__ import annotations from __future__ import annotations
from typing import Any
# ---------------------------------------------------------------------------
# Header helpers (moved from sx_components — thin sx_call wrappers)
# ---------------------------------------------------------------------------
def _settings_header_sx(ctx: dict, *, oob: bool = False) -> str:
from shared.sx.helpers import sx_call
from shared.sx.parser import SxExpr
from quart import url_for as qurl
settings_href = qurl("settings.defpage_settings_home")
label_sx = sx_call("blog-admin-label")
nav_sx = _settings_nav_sx(ctx)
return sx_call("menu-row-sx",
id="root-settings-row", level=1,
link_href=settings_href,
link_label_content=SxExpr(label_sx),
nav=SxExpr(nav_sx) if nav_sx else None,
child_id="root-settings-header-child", oob=oob)
def _settings_nav_sx(ctx: dict) -> str:
from shared.sx.helpers import sx_call
return sx_call("blog-settings-nav")
def _sub_settings_header_sx(row_id: str, child_id: str, href: str,
icon: str, label: str, ctx: dict,
*, oob: bool = False, nav_sx: str = "") -> str:
from shared.sx.helpers import sx_call
from shared.sx.parser import SxExpr
label_sx = sx_call("blog-sub-settings-label",
icon=f"fa fa-{icon}", label=label)
return sx_call("menu-row-sx",
id=row_id, level=2,
link_href=href,
link_label_content=SxExpr(label_sx),
nav=SxExpr(nav_sx) if nav_sx else None,
child_id=child_id, oob=oob)
# ---------------------------------------------------------------------------
# Layouts
# ---------------------------------------------------------------------------
def _register_blog_layouts() -> None: def _register_blog_layouts() -> None:
from shared.sx.layouts import register_custom_layout from shared.sx.layouts import register_sx_layout
register_custom_layout("blog", _blog_full, _blog_oob) register_sx_layout("blog", "blog-layout-full", "blog-layout-oob")
register_custom_layout("blog-settings", _settings_full, _settings_oob, register_sx_layout("blog-settings", "blog-settings-layout-full",
mobile_fn=_settings_mobile) "blog-settings-layout-oob", "blog-settings-layout-mobile")
register_custom_layout("blog-cache", _cache_full, _cache_oob) register_sx_layout("blog-cache", "blog-cache-layout-full",
register_custom_layout("blog-snippets", _snippets_full, _snippets_oob) "blog-cache-layout-oob")
register_custom_layout("blog-menu-items", _menu_items_full, _menu_items_oob) register_sx_layout("blog-snippets", "blog-snippets-layout-full",
register_custom_layout("blog-tag-groups", _tag_groups_full, _tag_groups_oob) "blog-snippets-layout-oob")
register_custom_layout("blog-tag-group-edit", register_sx_layout("blog-menu-items", "blog-menu-items-layout-full",
_tag_group_edit_full, _tag_group_edit_oob) "blog-menu-items-layout-oob")
register_sx_layout("blog-tag-groups", "blog-tag-groups-layout-full",
"blog-tag-groups-layout-oob")
# --- Blog layout (root + blog header) --- register_sx_layout("blog-tag-group-edit", "blog-tag-group-edit-layout-full",
"blog-tag-group-edit-layout-oob")
async def _blog_full(ctx: dict, **kw: Any) -> str:
from shared.sx.helpers import render_to_sx_with_env
return await render_to_sx_with_env("blog-layout-full", {})
async def _blog_oob(ctx: dict, **kw: Any) -> str:
from shared.sx.helpers import render_to_sx_with_env, oob_header_sx
rows = await render_to_sx_with_env("blog-layout-full", {})
return await oob_header_sx("root-header-child", "blog-header-child", rows)
# --- Settings layout (root + settings header) ---
async def _settings_full(ctx: dict, **kw: Any) -> str:
from shared.sx.helpers import render_to_sx_with_env
from shared.sx.parser import SxExpr
return await render_to_sx_with_env("settings-layout-full", {},
settings_header=SxExpr(_settings_header_sx(ctx)))
async def _settings_oob(ctx: dict, **kw: Any) -> str:
from shared.sx.helpers import render_to_sx_with_env, oob_header_sx
from shared.sx.parser import SxExpr
rows = await render_to_sx_with_env("settings-layout-full", {},
settings_header=SxExpr(_settings_header_sx(ctx)))
return await oob_header_sx("root-header-child", "root-settings-header-child", rows)
def _settings_mobile(ctx: dict, **kw: Any) -> str:
return _settings_nav_sx(ctx)
# --- Sub-settings helpers ---
async def _sub_settings_full(ctx: dict, row_id: str, child_id: str,
endpoint: str, icon: str, label: str) -> str:
from shared.sx.helpers import render_to_sx_with_env
from shared.sx.parser import SxExpr
from quart import url_for as qurl
return await render_to_sx_with_env("sub-settings-layout-full", {},
settings_header=SxExpr(_settings_header_sx(ctx)),
sub_header=SxExpr(_sub_settings_header_sx(
row_id, child_id, qurl(endpoint), icon, label, ctx)))
async def _sub_settings_oob(ctx: dict, row_id: str, child_id: str,
endpoint: str, icon: str, label: str) -> str:
from shared.sx.helpers import oob_header_sx, sx_call
from shared.sx.parser import SxExpr
from quart import url_for as qurl
settings_hdr_oob = _settings_header_sx(ctx, oob=True)
sub_hdr = _sub_settings_header_sx(
row_id, child_id, qurl(endpoint), icon, label, ctx)
sub_oob = await oob_header_sx("root-settings-header-child", child_id, sub_hdr)
return sx_call("sub-settings-layout-oob",
settings_header_oob=SxExpr(settings_hdr_oob),
sub_header_oob=SxExpr(sub_oob))
# --- Cache ---
async def _cache_full(ctx: dict, **kw: Any) -> str:
return await _sub_settings_full(ctx, "cache-row", "cache-header-child",
"defpage_cache_page", "refresh", "Cache")
async def _cache_oob(ctx: dict, **kw: Any) -> str:
return await _sub_settings_oob(ctx, "cache-row", "cache-header-child",
"defpage_cache_page", "refresh", "Cache")
# --- Snippets ---
async def _snippets_full(ctx: dict, **kw: Any) -> str:
return await _sub_settings_full(ctx, "snippets-row", "snippets-header-child",
"defpage_snippets_page", "puzzle-piece", "Snippets")
async def _snippets_oob(ctx: dict, **kw: Any) -> str:
return await _sub_settings_oob(ctx, "snippets-row", "snippets-header-child",
"defpage_snippets_page", "puzzle-piece", "Snippets")
# --- Menu Items ---
async def _menu_items_full(ctx: dict, **kw: Any) -> str:
return await _sub_settings_full(ctx, "menu_items-row", "menu_items-header-child",
"defpage_menu_items_page", "bars", "Menu Items")
async def _menu_items_oob(ctx: dict, **kw: Any) -> str:
return await _sub_settings_oob(ctx, "menu_items-row", "menu_items-header-child",
"defpage_menu_items_page", "bars", "Menu Items")
# --- Tag Groups ---
async def _tag_groups_full(ctx: dict, **kw: Any) -> str:
return await _sub_settings_full(ctx, "tag-groups-row", "tag-groups-header-child",
"defpage_tag_groups_page", "tags", "Tag Groups")
async def _tag_groups_oob(ctx: dict, **kw: Any) -> str:
return await _sub_settings_oob(ctx, "tag-groups-row", "tag-groups-header-child",
"defpage_tag_groups_page", "tags", "Tag Groups")
# --- Tag Group Edit ---
async def _tag_group_edit_full(ctx: dict, **kw: Any) -> str:
from quart import request, url_for as qurl
from shared.sx.helpers import render_to_sx_with_env
from shared.sx.parser import SxExpr
g_id = (request.view_args or {}).get("id")
return await render_to_sx_with_env("sub-settings-layout-full", {},
settings_header=SxExpr(_settings_header_sx(ctx)),
sub_header=SxExpr(_sub_settings_header_sx(
"tag-groups-row", "tag-groups-header-child",
qurl("defpage_tag_group_edit", id=g_id),
"tags", "Tag Groups", ctx)))
async def _tag_group_edit_oob(ctx: dict, **kw: Any) -> str:
from quart import request, url_for as qurl
from shared.sx.helpers import oob_header_sx, sx_call
from shared.sx.parser import SxExpr
g_id = (request.view_args or {}).get("id")
settings_hdr_oob = _settings_header_sx(ctx, oob=True)
sub_hdr = _sub_settings_header_sx(
"tag-groups-row", "tag-groups-header-child",
qurl("defpage_tag_group_edit", id=g_id),
"tags", "Tag Groups", ctx)
sub_oob = await oob_header_sx("root-settings-header-child", "tag-groups-header-child", sub_hdr)
return sx_call("sub-settings-layout-oob",
settings_header_oob=SxExpr(settings_hdr_oob),
sub_header_oob=SxExpr(sub_oob))

View File

@@ -172,6 +172,45 @@ class CartPageService:
"summary": summary, "summary": summary,
} }
async def admin_data(self, session, **kw):
"""Populate post context for cart-admin layout headers."""
from quart import g
from shared.infrastructure.fragments import fetch_fragments
post = g.page_post
slug = post.slug if post else ""
post_id = post.id if post else None
# Fetch container_nav for post header
container_nav = ""
if post_id:
nav_params = {
"container_type": "page",
"container_id": str(post_id),
"post_slug": slug,
}
events_nav, market_nav = await fetch_fragments([
("events", "container-nav", nav_params),
("market", "container-nav", nav_params),
], required=False)
container_nav = events_nav + market_nav
return {
"post": {
"id": post_id,
"slug": slug,
"title": (post.title if post else "")[:160],
"feature_image": getattr(post, "feature_image", None),
},
"container_nav": container_nav,
}
async def payments_admin_data(self, session, **kw):
"""Admin data + payments data combined for cart-payments page."""
admin = await self.admin_data(session)
payments = await self.payments_data(session)
return {**admin, **payments}
async def payments_data(self, session, **kw): async def payments_data(self, session, **kw):
from shared.sx.page import get_template_context from shared.sx.page import get_template_context

View File

@@ -1,25 +1,78 @@
;; Cart layout defcomps — fully self-contained via IO primitives. ;; Cart layout defcomps — fully self-contained via IO primitives.
;; Registered via register_sx_layout in __init__.py.
;; --- cart-page layout: root + cart row + page-cart row --- ;; ---------------------------------------------------------------------------
;; Auto-fetching cart page header macros
;; ---------------------------------------------------------------------------
(defcomp ~cart-page-layout-full (&key cart-row page-cart-row) (defmacro ~cart-page-header-auto (oob)
"Cart page header: cart-row + page-cart-row using (cart-page-ctx)."
(quasiquote
(let ((__cpctx (cart-page-ctx)))
(<>
(~menu-row-sx :id "cart-row" :level 1 :colour "sky"
:link-href (get __cpctx "cart-url")
:link-label "cart" :icon "fa fa-shopping-cart"
:child-id "cart-header-child")
(~header-child-sx :id "cart-header-child"
:inner (~menu-row-sx :id "page-cart-row" :level 2 :colour "sky"
:link-href (get __cpctx "page-cart-url")
:link-label-content (~cart-page-label
:feature-image (get __cpctx "feature-image")
:title (get __cpctx "title"))
:nav (~cart-all-carts-link :href (get __cpctx "cart-url"))
:oob (unquote oob)))))))
(defmacro ~cart-page-header-oob ()
"Cart page OOB: individual oob rows."
(quasiquote
(let ((__cpctx (cart-page-ctx)))
(<>
(~menu-row-sx :id "page-cart-row" :level 2 :colour "sky"
:link-href (get __cpctx "page-cart-url")
:link-label-content (~cart-page-label
:feature-image (get __cpctx "feature-image")
:title (get __cpctx "title"))
:nav (~cart-all-carts-link :href (get __cpctx "cart-url"))
:oob true)
(~menu-row-sx :id "cart-row" :level 1 :colour "sky"
:link-href (get __cpctx "cart-url")
:link-label "cart" :icon "fa fa-shopping-cart"
:child-id "cart-header-child"
:oob true)))))
;; ---------------------------------------------------------------------------
;; cart-page layout: root + cart row + page-cart row
;; ---------------------------------------------------------------------------
(defcomp ~cart-page-layout-full ()
(<> (~root-header-auto) (<> (~root-header-auto)
(~header-child-sx (~header-child-sx
:inner (<> cart-row :inner (~cart-page-header-auto))))
(~header-child-sx :id "cart-header-child" :inner page-cart-row)))))
(defcomp ~cart-page-layout-oob (&key root-header-oob cart-row-oob page-cart-row) (defcomp ~cart-page-layout-oob ()
(<> (~oob-header-sx :parent-id "cart-header-child" :row page-cart-row) (<> (~cart-page-header-oob)
cart-row-oob (~root-header-auto true)))
root-header-oob))
;; --- cart-admin layout: root + post header + admin header --- ;; ---------------------------------------------------------------------------
;; cart-admin layout: root + post header + admin header
;; Uses (post-header-ctx) — requires :data handler to populate g._defpage_ctx
;; ---------------------------------------------------------------------------
(defcomp ~cart-admin-layout-full (&key post-header admin-header) (defcomp ~cart-admin-layout-full (&key selected)
(<> (~root-header-auto) (<> (~root-header-auto)
post-header admin-header)) (~header-child-sx
:inner (~post-header-auto nil))))
;; --- orders-within-cart: root + auth-simple + orders --- (defcomp ~cart-admin-layout-oob (&key selected)
(<> (~post-header-auto true)
(~oob-header-sx :parent-id "post-header-child"
:row (~post-admin-header-auto nil selected))
(~root-header-auto true)))
;; ---------------------------------------------------------------------------
;; orders-within-cart: root + auth-simple + orders
;; ---------------------------------------------------------------------------
(defcomp ~cart-orders-layout-full (&key list-url) (defcomp ~cart-orders-layout-full (&key list-url)
(<> (~root-header-auto) (<> (~root-header-auto)
@@ -35,7 +88,9 @@
:row (~orders-header-row :list-url list-url)) :row (~orders-header-row :list-url list-url))
(~root-header-auto true))) (~root-header-auto true)))
;; --- order-detail-within-cart: root + auth-simple + orders + order --- ;; ---------------------------------------------------------------------------
;; order-detail-within-cart: root + auth-simple + orders + order
;; ---------------------------------------------------------------------------
(defcomp ~cart-order-detail-layout-full (&key list-url detail-url order-label) (defcomp ~cart-order-detail-layout-full (&key list-url detail-url order-label)
(<> (~root-header-auto) (<> (~root-header-auto)

View File

@@ -32,12 +32,13 @@
:path "/<page_slug>/admin/" :path "/<page_slug>/admin/"
:auth :admin :auth :admin
:layout :cart-admin :layout :cart-admin
:data (service "cart-page" "admin-data")
:content (~cart-admin-content)) :content (~cart-admin-content))
(defpage cart-payments (defpage cart-payments
:path "/<page_slug>/admin/payments/" :path "/<page_slug>/admin/payments/"
:auth :admin :auth :admin
:layout (:cart-admin :selected "payments") :layout (:cart-admin :selected "payments")
:data (service "cart-page" "payments-data") :data (service "cart-page" "payments-admin-data")
:content (~cart-payments-content :content (~cart-payments-content
:page-config page-config)) :page-config page-config))

View File

@@ -1,135 +1,8 @@
"""Cart layout registration and header builders.""" """Cart layout registration — all layouts delegate to .sx defcomps."""
from __future__ import annotations from __future__ import annotations
from typing import Any
from shared.sx.parser import SxExpr
def _register_cart_layouts() -> None: def _register_cart_layouts() -> None:
from shared.sx.layouts import register_custom_layout from shared.sx.layouts import register_sx_layout
register_custom_layout("cart-page", _cart_page_full, _cart_page_oob) register_sx_layout("cart-page", "cart-page-layout-full", "cart-page-layout-oob")
register_custom_layout("cart-admin", _cart_admin_full, _cart_admin_oob) register_sx_layout("cart-admin", "cart-admin-layout-full", "cart-admin-layout-oob")
# ---------------------------------------------------------------------------
# Header helpers
# ---------------------------------------------------------------------------
def _ensure_post_ctx(ctx: dict, page_post: Any) -> dict:
"""Ensure ctx has a 'post' dict from page_post DTO."""
if ctx.get("post") or not page_post:
return ctx
return {**ctx, "post": {
"id": getattr(page_post, "id", None),
"slug": getattr(page_post, "slug", ""),
"title": getattr(page_post, "title", ""),
"feature_image": getattr(page_post, "feature_image", None),
}}
async def _ensure_container_nav(ctx: dict) -> dict:
"""Fetch container_nav if not already present."""
if ctx.get("container_nav"):
return ctx
post = ctx.get("post") or {}
post_id = post.get("id")
if not post_id:
return ctx
slug = post.get("slug", "")
from shared.infrastructure.fragments import fetch_fragments
nav_params = {
"container_type": "page",
"container_id": str(post_id),
"post_slug": slug,
}
events_nav, market_nav = await fetch_fragments([
("events", "container-nav", nav_params),
("market", "container-nav", nav_params),
], required=False)
return {**ctx, "container_nav": events_nav + market_nav}
async def _post_header_sx(ctx: dict, page_post: Any, *, oob: bool = False) -> str:
from shared.sx.helpers import post_header_sx as _shared_post_header_sx
ctx = _ensure_post_ctx(ctx, page_post)
ctx = await _ensure_container_nav(ctx)
return await _shared_post_header_sx(ctx, oob=oob)
def _cart_header_sx(ctx: dict, *, oob: bool = False) -> str:
from shared.sx.helpers import sx_call, call_url
return sx_call(
"menu-row-sx",
id="cart-row", level=1, colour="sky",
link_href=call_url(ctx, "cart_url", "/"),
link_label="cart", icon="fa fa-shopping-cart",
child_id="cart-header-child", oob=oob,
)
def _page_cart_header_sx(ctx: dict, page_post: Any, *, oob: bool = False) -> str:
from shared.sx.helpers import sx_call, call_url
slug = page_post.slug if page_post else ""
title = ((page_post.title if page_post else None) or "")[:160]
label_sx = sx_call("cart-page-label",
feature_image=page_post.feature_image if page_post else None,
title=title)
nav_sx = sx_call("cart-all-carts-link", href=call_url(ctx, "cart_url", "/"))
return sx_call(
"menu-row-sx",
id="page-cart-row", level=2, colour="sky",
link_href=call_url(ctx, "cart_url", f"/{slug}/"),
link_label_content=SxExpr(label_sx),
nav=SxExpr(nav_sx), oob=oob,
)
async def _cart_page_admin_header_sx(ctx: dict, page_post: Any, *, oob: bool = False,
selected: str = "") -> str:
from shared.sx.helpers import post_admin_header_sx
slug = page_post.slug if page_post else ""
ctx = _ensure_post_ctx(ctx, page_post)
return await post_admin_header_sx(ctx, slug, oob=oob, selected=selected)
# ---------------------------------------------------------------------------
# Layout functions
# ---------------------------------------------------------------------------
async def _cart_page_full(ctx: dict, **kw: Any) -> str:
from shared.sx.helpers import render_to_sx_with_env
page_post = ctx.get("page_post")
env = {}
return await render_to_sx_with_env("cart-page-layout-full", env,
cart_row=SxExpr(_cart_header_sx(ctx)),
page_cart_row=SxExpr(_page_cart_header_sx(ctx, page_post)),
)
async def _cart_page_oob(ctx: dict, **kw: Any) -> str:
from shared.sx.helpers import render_to_sx_with_env, root_header_sx
page_post = ctx.get("page_post")
env = {}
return await render_to_sx_with_env("cart-page-layout-oob", env,
root_header_oob=SxExpr(await root_header_sx(ctx, oob=True)),
cart_row_oob=SxExpr(_cart_header_sx(ctx, oob=True)),
page_cart_row=SxExpr(_page_cart_header_sx(ctx, page_post)),
)
async def _cart_admin_full(ctx: dict, **kw: Any) -> str:
from shared.sx.helpers import render_to_sx_with_env
page_post = ctx.get("page_post")
selected = kw.get("selected", "")
env = {}
return await render_to_sx_with_env("cart-admin-layout-full", env,
post_header=SxExpr(await _post_header_sx(ctx, page_post)),
admin_header=SxExpr(await _cart_page_admin_header_sx(ctx, page_post, selected=selected)),
)
async def _cart_admin_oob(ctx: dict, **kw: Any) -> str:
page_post = ctx.get("page_post")
selected = kw.get("selected", "")
return await _cart_page_admin_header_sx(ctx, page_post, oob=True, selected=selected)

View File

@@ -1,96 +1,405 @@
;; Events layout defcomps — root header via ~root-header-auto, ;; Events layout defcomps — fully self-contained via IO primitives.
;; events-specific headers passed as &key params. ;; Registered via register_sx_layout in helpers.py.
;; --- Calendar admin layout: root + post + child(admin + cal + cal-admin) --- ;; ---------------------------------------------------------------------------
;; Auto-fetching header macros — calendar, day, entry, slot, tickets
;; ---------------------------------------------------------------------------
(defcomp ~events-cal-admin-layout-full (&key post-header admin-header (defmacro ~events-calendar-header-auto (oob)
calendar-header calendar-admin-header) "Calendar header row using (events-calendar-ctx)."
(quasiquote
(let ((__cal (events-calendar-ctx))
(__sc (select-colours)))
(when (get __cal "slug")
(~menu-row-sx :id "calendar-row" :level 3
:link-href (url-for "calendar.get"
:calendar-slug (get __cal "slug"))
:link-label-content (~events-calendar-label
:name (get __cal "name")
:description (get __cal "description"))
:nav (<>
(~nav-link :href (url-for "defpage_slots_listing"
:calendar-slug (get __cal "slug"))
:icon "fa fa-clock" :label "Slots"
:select-colours __sc)
(let ((__rights (app-rights)))
(when (get __rights "admin")
(~nav-link :href (url-for "defpage_calendar_admin"
:calendar-slug (get __cal "slug"))
:icon "fa fa-cog"
:select-colours __sc))))
:child-id "calendar-header-child"
:oob (unquote oob))))))
(defmacro ~events-calendar-admin-header-auto (oob)
"Calendar admin header row."
(quasiquote
(let ((__cal (events-calendar-ctx))
(__sc (select-colours)))
(when (get __cal "slug")
(~menu-row-sx :id "calendar-admin-row" :level 4
:link-label "admin" :icon "fa fa-cog"
:nav (<>
(~nav-link :href (url-for "defpage_slots_listing"
:calendar-slug (get __cal "slug"))
:label "slots" :select-colours __sc)
(~nav-link :href (url-for "calendar.admin.calendar_description_edit"
:calendar-slug (get __cal "slug"))
:label "description" :select-colours __sc))
:child-id "calendar-admin-header-child"
:oob (unquote oob))))))
(defmacro ~events-day-header-auto (oob)
"Day header row using (events-day-ctx)."
(quasiquote
(let ((__day (events-day-ctx))
(__cal (events-calendar-ctx)))
(when (get __day "date-str")
(~menu-row-sx :id "day-row" :level 4
:link-href (url-for "calendar.day.show_day"
:calendar-slug (get __cal "slug")
:year (get __day "year")
:month (get __day "month")
:day (get __day "day"))
:link-label-content (~events-day-label
:date-str (get __day "date-str"))
:nav (get __day "nav")
:child-id "day-header-child"
:oob (unquote oob))))))
(defmacro ~events-day-admin-header-auto (oob)
"Day admin header row."
(quasiquote
(let ((__day (events-day-ctx))
(__cal (events-calendar-ctx)))
(when (get __day "date-str")
(~menu-row-sx :id "day-admin-row" :level 5
:link-href (url-for "defpage_day_admin"
:calendar-slug (get __cal "slug")
:year (get __day "year")
:month (get __day "month")
:day (get __day "day"))
:link-label "admin" :icon "fa fa-cog"
:child-id "day-admin-header-child"
:oob (unquote oob))))))
(defmacro ~events-entry-header-auto (oob)
"Entry header row using (events-entry-ctx)."
(quasiquote
(let ((__ectx (events-entry-ctx)))
(when (get __ectx "id")
(~menu-row-sx :id "entry-row" :level 5
:link-href (get __ectx "link-href")
:link-label-content (~events-entry-label
:entry-id (get __ectx "id")
:title (~events-entry-title :name (get __ectx "name"))
:times (~events-entry-times :time-str (get __ectx "time-str")))
:nav (get __ectx "nav")
:child-id "entry-header-child"
:oob (unquote oob))))))
(defmacro ~events-entry-admin-header-auto (oob)
"Entry admin header row."
(quasiquote
(let ((__ectx (events-entry-ctx)))
(when (get __ectx "id")
(~menu-row-sx :id "entry-admin-row" :level 6
:link-href (get __ectx "admin-href")
:link-label "admin" :icon "fa fa-cog"
:nav (when (get __ectx "is-admin")
(~nav-link :href (get __ectx "ticket-types-href")
:label "ticket_types"
:select-colours (get __ectx "select-colours")))
:child-id "entry-admin-header-child"
:oob (unquote oob))))))
(defmacro ~events-slot-header-auto (oob)
"Slot detail header row using (events-slot-ctx)."
(quasiquote
(let ((__slot (events-slot-ctx)))
(when (get __slot "name")
(~menu-row-sx :id "slot-row" :level 5
:link-label-content (~events-slot-label
:name (get __slot "name")
:description (get __slot "description"))
:child-id "slot-header-child"
:oob (unquote oob))))))
(defmacro ~events-ticket-types-header-auto (oob)
"Ticket types header row."
(quasiquote
(let ((__ectx (events-entry-ctx))
(__cal (events-calendar-ctx)))
(when (get __ectx "id")
(~menu-row-sx :id "ticket_types-row" :level 7
:link-href (get __ectx "ticket-types-href")
:link-label-content (<>
(i :class "fa fa-ticket")
(div :class "shrink-0" "ticket types"))
:nav (~events-admin-placeholder-nav)
:child-id "ticket_type-header-child"
:oob (unquote oob))))))
(defmacro ~events-ticket-type-header-auto (oob)
"Single ticket type header row using (events-ticket-type-ctx)."
(quasiquote
(let ((__tt (events-ticket-type-ctx)))
(when (get __tt "id")
(~menu-row-sx :id "ticket_type-row" :level 8
:link-href (get __tt "link-href")
:link-label-content (div :class "flex flex-col md:flex-row md:gap-2 items-center"
(div :class "flex flex-row items-center gap-2"
(i :class "fa fa-ticket")
(div :class "shrink-0" (get __tt "name"))))
:nav (~events-admin-placeholder-nav)
:child-id "ticket_type-header-child-inner"
:oob (unquote oob))))))
(defmacro ~events-markets-header-auto (oob)
"Markets section header row."
(quasiquote
(~menu-row-sx :id "markets-row" :level 3
:link-href (url-for "defpage_events_markets")
:link-label-content (~events-markets-label)
:child-id "markets-header-child"
:oob (unquote oob))))
;; ---------------------------------------------------------------------------
;; OOB clear helpers — clear deeper header rows not present at this level
;; ---------------------------------------------------------------------------
(defcomp ~events-clear-oob-cal-admin ()
"Clear OOB divs for cal-admin level (keeps down to calendar-admin)."
(<>
(~clear-oob-div :id "entry-admin-row")
(~clear-oob-div :id "entry-admin-header-child")
(~clear-oob-div :id "entry-row")
(~clear-oob-div :id "entry-header-child")
(~clear-oob-div :id "day-admin-row")
(~clear-oob-div :id "day-admin-header-child")
(~clear-oob-div :id "day-row")
(~clear-oob-div :id "day-header-child")
(~clear-oob-div :id "calendars-row")
(~clear-oob-div :id "calendars-header-child")))
(defcomp ~events-clear-oob-slot ()
"Clear OOB divs for slot level."
(<>
(~clear-oob-div :id "entry-admin-row")
(~clear-oob-div :id "entry-admin-header-child")
(~clear-oob-div :id "entry-row")
(~clear-oob-div :id "entry-header-child")
(~clear-oob-div :id "day-admin-row")
(~clear-oob-div :id "day-admin-header-child")
(~clear-oob-div :id "day-row")
(~clear-oob-div :id "day-header-child")
(~clear-oob-div :id "calendars-row")
(~clear-oob-div :id "calendars-header-child")))
(defcomp ~events-clear-oob-day-admin ()
"Clear OOB divs for day-admin level."
(<>
(~clear-oob-div :id "entry-admin-row")
(~clear-oob-div :id "entry-admin-header-child")
(~clear-oob-div :id "entry-row")
(~clear-oob-div :id "entry-header-child")
(~clear-oob-div :id "calendars-row")
(~clear-oob-div :id "calendars-header-child")))
(defcomp ~events-clear-oob-entry ()
"Clear OOB divs for entry level (public, no admin rows)."
(<>
(~clear-oob-div :id "entry-admin-row")
(~clear-oob-div :id "entry-admin-header-child")
(~clear-oob-div :id "day-admin-row")
(~clear-oob-div :id "day-admin-header-child")
(~clear-oob-div :id "calendar-admin-row")
(~clear-oob-div :id "calendar-admin-header-child")
(~clear-oob-div :id "calendars-row")
(~clear-oob-div :id "calendars-header-child")
(~clear-oob-div :id "post-admin-row")
(~clear-oob-div :id "post-admin-header-child")))
(defcomp ~events-clear-oob-entry-admin ()
"Clear OOB divs for entry-admin level."
(<>
(~clear-oob-div :id "calendars-row")
(~clear-oob-div :id "calendars-header-child")))
;; ---------------------------------------------------------------------------
;; Calendar admin layout: root + post + child(post-admin + cal + cal-admin)
;; ---------------------------------------------------------------------------
(defcomp ~events-cal-admin-layout-full ()
(<> (~root-header-auto) (<> (~root-header-auto)
post-header (~header-child-sx
(~header-child-sx :inner (<> admin-header calendar-header calendar-admin-header)))) :inner (<> (~post-header-auto nil)
(~post-admin-header-auto nil "calendars")
(~events-calendar-header-auto nil)
(~events-calendar-admin-header-auto nil)))))
(defcomp ~events-cal-admin-layout-oob (&key admin-oob cal-oob cal-admin-oob-wrap clear-oob) (defcomp ~events-cal-admin-layout-oob ()
(<> admin-oob cal-oob cal-admin-oob-wrap clear-oob)) (<> (~post-admin-header-auto true "calendars")
(~events-calendar-header-auto true)
(~oob-header-sx :parent-id "calendar-header-child"
:row (~events-calendar-admin-header-auto nil))
(~events-clear-oob-cal-admin)
(~root-header-auto true)))
;; --- Slots layout: same full as cal-admin --- ;; ---------------------------------------------------------------------------
;; Slots layout: same full as cal-admin
;; ---------------------------------------------------------------------------
(defcomp ~events-slots-layout-oob (&key admin-oob cal-admin-oob clear-oob) (defcomp ~events-slots-layout-full ()
(<> admin-oob cal-admin-oob clear-oob))
;; --- Slot detail layout: root + post + child(admin + cal + cal-admin + slot) ---
(defcomp ~events-slot-layout-full (&key post-header admin-header
calendar-header calendar-admin-header slot-header)
(<> (~root-header-auto) (<> (~root-header-auto)
post-header (~header-child-sx
(~header-child-sx :inner (<> admin-header calendar-header calendar-admin-header slot-header)))) :inner (<> (~post-header-auto nil)
(~post-admin-header-auto nil "calendars")
(~events-calendar-header-auto nil)
(~events-calendar-admin-header-auto nil)))))
(defcomp ~events-slot-layout-oob (&key admin-oob cal-admin-oob slot-oob-wrap clear-oob) (defcomp ~events-slots-layout-oob ()
(<> admin-oob cal-admin-oob slot-oob-wrap clear-oob)) (<> (~post-admin-header-auto true "calendars")
(~events-calendar-admin-header-auto true)
(~events-clear-oob-cal-admin)
(~root-header-auto true)))
;; --- Day admin layout: root + post + child(admin + cal + day + day-admin) --- ;; ---------------------------------------------------------------------------
;; Slot detail layout: root + post + child(admin + cal + cal-admin + slot)
;; ---------------------------------------------------------------------------
(defcomp ~events-day-admin-layout-full (&key post-header admin-header (defcomp ~events-slot-layout-full ()
calendar-header day-header day-admin-header)
(<> (~root-header-auto) (<> (~root-header-auto)
post-header (~header-child-sx
(~header-child-sx :inner (<> admin-header calendar-header day-header day-admin-header)))) :inner (<> (~post-header-auto nil)
(~post-admin-header-auto nil "calendars")
(~events-calendar-header-auto nil)
(~events-calendar-admin-header-auto nil)
(~events-slot-header-auto nil)))))
(defcomp ~events-day-admin-layout-oob (&key admin-oob cal-oob day-admin-oob-wrap clear-oob) (defcomp ~events-slot-layout-oob ()
(<> admin-oob cal-oob day-admin-oob-wrap clear-oob)) (<> (~post-admin-header-auto true "calendars")
(~events-calendar-admin-header-auto true)
(~oob-header-sx :parent-id "calendar-admin-header-child"
:row (~events-slot-header-auto nil))
(~events-clear-oob-slot)
(~root-header-auto true)))
;; --- Entry layout: root + child(post + cal + day + entry) --- ;; ---------------------------------------------------------------------------
;; Day admin layout: root + post + child(admin + cal + day + day-admin)
;; ---------------------------------------------------------------------------
(defcomp ~events-entry-layout-full (&key post-header calendar-header day-header entry-header) (defcomp ~events-day-admin-layout-full ()
(<> (~root-header-auto) (<> (~root-header-auto)
(~header-child-sx :inner (<> post-header calendar-header day-header entry-header)))) (~header-child-sx
:inner (<> (~post-header-auto nil)
(~post-admin-header-auto nil "calendars")
(~events-calendar-header-auto nil)
(~events-day-header-auto nil)
(~events-day-admin-header-auto nil)))))
(defcomp ~events-entry-layout-oob (&key day-oob entry-oob-wrap clear-oob) (defcomp ~events-day-admin-layout-oob ()
(<> day-oob entry-oob-wrap clear-oob)) (<> (~post-admin-header-auto true "calendars")
(~events-calendar-header-auto true)
(~oob-header-sx :parent-id "day-header-child"
:row (~events-day-admin-header-auto nil))
(~events-clear-oob-day-admin)
(~root-header-auto true)))
;; --- Entry admin layout: root + post + child(admin + cal + day + entry + entry-admin) --- ;; ---------------------------------------------------------------------------
;; Entry layout: root + child(post + cal + day + entry) — public, no admin
;; ---------------------------------------------------------------------------
(defcomp ~events-entry-admin-layout-full (&key post-header admin-header (defcomp ~events-entry-layout-full ()
calendar-header day-header
entry-header entry-admin-header)
(<> (~root-header-auto) (<> (~root-header-auto)
post-header (~header-child-sx
(~header-child-sx :inner (<> admin-header calendar-header day-header :inner (<> (~post-header-auto nil)
entry-header entry-admin-header)))) (~events-calendar-header-auto nil)
(~events-day-header-auto nil)
(~events-entry-header-auto nil)))))
(defcomp ~events-entry-admin-layout-oob (&key admin-oob entry-oob entry-admin-oob-wrap clear-oob) (defcomp ~events-entry-layout-oob ()
(<> admin-oob entry-oob entry-admin-oob-wrap clear-oob)) (<> (~events-day-header-auto true)
(~oob-header-sx :parent-id "day-header-child"
:row (~events-entry-header-auto nil))
(~events-clear-oob-entry)
(~root-header-auto true)))
;; --- Ticket types layout: root + child(post + cal + day + entry + entry-admin + ticket-types) --- ;; ---------------------------------------------------------------------------
;; Entry admin layout: root + post + child(admin + cal + day + entry + entry-admin)
;; ---------------------------------------------------------------------------
(defcomp ~events-ticket-types-layout-full (&key post-header calendar-header day-header (defcomp ~events-entry-admin-layout-full ()
entry-header entry-admin-header
ticket-types-header)
(<> (~root-header-auto) (<> (~root-header-auto)
(~header-child-sx :inner (<> post-header calendar-header day-header (~header-child-sx
entry-header entry-admin-header ticket-types-header)))) :inner (<> (~post-header-auto nil)
(~post-admin-header-auto nil "calendars")
(~events-calendar-header-auto nil)
(~events-day-header-auto nil)
(~events-entry-header-auto nil)
(~events-entry-admin-header-auto nil)))))
(defcomp ~events-ticket-types-layout-oob (&key entry-admin-oob ticket-types-oob-wrap) (defcomp ~events-entry-admin-layout-oob ()
(<> entry-admin-oob ticket-types-oob-wrap)) (<> (~post-admin-header-auto true "calendars")
(~events-entry-header-auto true)
(~oob-header-sx :parent-id "entry-header-child"
:row (~events-entry-admin-header-auto nil))
(~events-clear-oob-entry-admin)
(~root-header-auto true)))
;; --- Ticket type detail layout: root + child(post + cal + day + entry + entry-admin + types + type) --- ;; ---------------------------------------------------------------------------
;; Ticket types layout: root + child(post + cal + day + entry + entry-admin + ticket-types)
;; ---------------------------------------------------------------------------
(defcomp ~events-ticket-type-layout-full (&key post-header calendar-header day-header (defcomp ~events-ticket-types-layout-full ()
entry-header entry-admin-header
ticket-types-header ticket-type-header)
(<> (~root-header-auto) (<> (~root-header-auto)
(~header-child-sx :inner (<> post-header calendar-header day-header (~header-child-sx
entry-header entry-admin-header :inner (<> (~post-header-auto nil)
ticket-types-header ticket-type-header)))) (~events-calendar-header-auto nil)
(~events-day-header-auto nil)
(~events-entry-header-auto nil)
(~events-entry-admin-header-auto nil)
(~events-ticket-types-header-auto nil)))))
(defcomp ~events-ticket-type-layout-oob (&key ticket-types-oob ticket-type-oob-wrap) (defcomp ~events-ticket-types-layout-oob ()
(<> ticket-types-oob ticket-type-oob-wrap)) (<> (~events-entry-admin-header-auto true)
(~oob-header-sx :parent-id "entry-admin-header-child"
:row (~events-ticket-types-header-auto nil))
(~root-header-auto true)))
;; --- Markets layout: root + child(post + markets) --- ;; ---------------------------------------------------------------------------
;; Ticket type layout: all headers down to ticket-type
;; ---------------------------------------------------------------------------
(defcomp ~events-markets-layout-full (&key post-header markets-header) (defcomp ~events-ticket-type-layout-full ()
(<> (~root-header-auto) (<> (~root-header-auto)
(~header-child-sx :inner (<> post-header markets-header)))) (~header-child-sx
:inner (<> (~post-header-auto nil)
(~events-calendar-header-auto nil)
(~events-day-header-auto nil)
(~events-entry-header-auto nil)
(~events-entry-admin-header-auto nil)
(~events-ticket-types-header-auto nil)
(~events-ticket-type-header-auto nil)))))
(defcomp ~events-markets-layout-oob (&key post-oob markets-oob-wrap) (defcomp ~events-ticket-type-layout-oob ()
(<> post-oob markets-oob-wrap)) (<> (~events-ticket-types-header-auto true)
(~oob-header-sx :parent-id "ticket_types-header-child"
:row (~events-ticket-type-header-auto nil))
(~root-header-auto true)))
;; ---------------------------------------------------------------------------
;; Markets layout: root + child(post + markets)
;; ---------------------------------------------------------------------------
(defcomp ~events-markets-layout-full ()
(<> (~root-header-auto)
(~header-child-sx
:inner (<> (~post-header-auto nil)
(~events-markets-header-auto nil)))))
(defcomp ~events-markets-layout-oob ()
(<> (~post-header-auto true)
(~oob-header-sx :parent-id "post-header-child"
:row (~events-markets-header-auto nil))
(~root-header-auto true)))

View File

@@ -5,30 +5,22 @@ from typing import Any
from shared.sx.helpers import sx_call from shared.sx.helpers import sx_call
from .utils import _clear_deeper_oob, _ensure_container_nav
from .calendar import ( from .calendar import (
_post_header_sx, _calendar_header_sx,
_calendar_admin_header_sx, _day_header_sx,
_day_admin_header_sx, _markets_header_sx,
_calendars_main_panel_sx,
_calendar_admin_main_panel_html, _calendar_admin_main_panel_html,
_day_admin_main_panel_html, _day_admin_main_panel_html,
_markets_main_panel_html, _markets_main_panel_html,
) )
from .entries import ( from .entries import (
_entry_header_html, _entry_main_panel_html, _entry_main_panel_html,
_entry_nav_html, _entry_nav_html,
_entry_admin_header_html, _entry_admin_main_panel_html, _entry_admin_main_panel_html,
) )
from .tickets import ( from .tickets import (
_tickets_main_panel_html, _ticket_detail_panel_html, _tickets_main_panel_html, _ticket_detail_panel_html,
_ticket_admin_main_panel_html, _ticket_admin_main_panel_html,
_ticket_types_header_html, _ticket_type_header_html,
render_ticket_type_main_panel, render_ticket_types_table, render_ticket_type_main_panel, render_ticket_types_table,
) )
from .slots import ( from .slots import render_slot_main_panel, render_slots_table
_slot_header_html, render_slot_main_panel, render_slots_table,
)
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
@@ -43,6 +35,40 @@ def _add_to_defpage_ctx(**kwargs: Any) -> None:
g._defpage_ctx.update(kwargs) g._defpage_ctx.update(kwargs)
def _ensure_post_defpage_ctx() -> None:
"""Copy g.post_data["post"] into g._defpage_ctx for layout IO primitives."""
from quart import g
post_data = getattr(g, "post_data", None)
if post_data and post_data.get("post"):
_add_to_defpage_ctx(post=post_data["post"])
async def _ensure_container_nav_defpage_ctx() -> None:
"""Fetch container_nav and add to g._defpage_ctx for layout IO primitives."""
from quart import g
dctx = getattr(g, "_defpage_ctx", None) or {}
if dctx.get("container_nav"):
return
post = dctx.get("post") or {}
post_id = post.get("id")
slug = post.get("slug", "")
if not post_id:
return
from shared.infrastructure.fragments import fetch_fragments
current_cal = getattr(g, "calendar_slug", "") or ""
nav_params = {
"container_type": "page",
"container_id": str(post_id),
"post_slug": slug,
"current_calendar": current_cal,
}
events_nav, market_nav = await fetch_fragments([
("events", "container-nav", nav_params),
("market", "container-nav", nav_params),
], required=False)
_add_to_defpage_ctx(container_nav=events_nav + market_nav)
async def _ensure_calendar(calendar_slug: str | None) -> None: async def _ensure_calendar(calendar_slug: str | None) -> None:
"""Load calendar into g.calendar if not already present.""" """Load calendar into g.calendar if not already present."""
from quart import g, abort from quart import g, abort
@@ -63,6 +89,7 @@ async def _ensure_calendar(calendar_slug: str | None) -> None:
g.calendar = cal g.calendar = cal
g.calendar_slug = calendar_slug g.calendar_slug = calendar_slug
_add_to_defpage_ctx(calendar=cal) _add_to_defpage_ctx(calendar=cal)
_ensure_post_defpage_ctx()
async def _ensure_entry(entry_id: int | None) -> None: async def _ensure_entry(entry_id: int | None) -> None:
@@ -209,276 +236,29 @@ async def _ensure_day_data(year: int, month: int, day: int) -> None:
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# Layouts # Layouts — all layouts delegate to .sx defcomps via register_sx_layout
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
def _register_events_layouts() -> None: def _register_events_layouts() -> None:
from shared.sx.layouts import register_custom_layout from shared.sx.layouts import register_sx_layout
register_custom_layout("events-calendar-admin", _cal_admin_full, _cal_admin_oob) register_sx_layout("events-calendar-admin",
register_custom_layout("events-slots", _slots_full, _slots_oob) "events-cal-admin-layout-full", "events-cal-admin-layout-oob")
register_custom_layout("events-slot", _slot_full, _slot_oob) register_sx_layout("events-slots",
register_custom_layout("events-day-admin", _day_admin_full, _day_admin_oob) "events-slots-layout-full", "events-slots-layout-oob")
register_custom_layout("events-entry", _entry_full, _entry_oob) register_sx_layout("events-slot",
register_custom_layout("events-entry-admin", _entry_admin_full, _entry_admin_oob) "events-slot-layout-full", "events-slot-layout-oob")
register_custom_layout("events-ticket-types", _ticket_types_full, _ticket_types_oob) register_sx_layout("events-day-admin",
register_custom_layout("events-ticket-type", _ticket_type_full, _ticket_type_oob) "events-day-admin-layout-full", "events-day-admin-layout-oob")
register_custom_layout("events-markets", _markets_full, _markets_oob) register_sx_layout("events-entry",
"events-entry-layout-full", "events-entry-layout-oob")
register_sx_layout("events-entry-admin",
# --- Calendar admin layout (root + post + child(post-admin + calendar + cal-admin)) --- "events-entry-admin-layout-full", "events-entry-admin-layout-oob")
register_sx_layout("events-ticket-types",
async def _cal_admin_full(ctx: dict, **kw: Any) -> str: "events-ticket-types-layout-full", "events-ticket-types-layout-oob")
from shared.sx.helpers import render_to_sx_with_env, post_admin_header_sx register_sx_layout("events-ticket-type",
from shared.sx.parser import SxExpr "events-ticket-type-layout-full", "events-ticket-type-layout-oob")
ctx = await _ensure_container_nav(ctx) register_sx_layout("events-markets",
slug = (ctx.get("post") or {}).get("slug", "") "events-markets-layout-full", "events-markets-layout-oob")
return await render_to_sx_with_env("events-cal-admin-layout-full", {},
post_header=SxExpr(await _post_header_sx(ctx)),
admin_header=SxExpr(await post_admin_header_sx(ctx, slug, selected="calendars")),
calendar_header=SxExpr(_calendar_header_sx(ctx)),
calendar_admin_header=SxExpr(_calendar_admin_header_sx(ctx)),
)
async def _cal_admin_oob(ctx: dict, **kw: Any) -> str:
from shared.sx.helpers import render_to_sx_with_env, post_admin_header_sx, oob_header_sx
from shared.sx.parser import SxExpr
ctx = await _ensure_container_nav(ctx)
slug = (ctx.get("post") or {}).get("slug", "")
return await render_to_sx_with_env("events-cal-admin-layout-oob", {},
admin_oob=SxExpr(await post_admin_header_sx(ctx, slug, oob=True, selected="calendars")),
cal_oob=SxExpr(_calendar_header_sx(ctx, oob=True)),
cal_admin_oob_wrap=SxExpr(await oob_header_sx("calendar-header-child",
"calendar-admin-header-child", _calendar_admin_header_sx(ctx))),
clear_oob=SxExpr(_clear_deeper_oob("post-row", "post-header-child",
"post-admin-row", "post-admin-header-child",
"calendar-row", "calendar-header-child",
"calendar-admin-row", "calendar-admin-header-child")),
)
# --- Slots layout (same full as cal-admin but different OOB) ---
async def _slots_full(ctx: dict, **kw: Any) -> str:
return await _cal_admin_full({**ctx, "is_admin_section": True}, **kw)
async def _slots_oob(ctx: dict, **kw: Any) -> str:
from shared.sx.helpers import render_to_sx_with_env, post_admin_header_sx
from shared.sx.parser import SxExpr
ctx = await _ensure_container_nav({**ctx, "is_admin_section": True})
slug = (ctx.get("post") or {}).get("slug", "")
return await render_to_sx_with_env("events-slots-layout-oob", {},
admin_oob=SxExpr(await post_admin_header_sx(ctx, slug, oob=True, selected="calendars")),
cal_admin_oob=SxExpr(_calendar_admin_header_sx(ctx, oob=True)),
clear_oob=SxExpr(_clear_deeper_oob("post-row", "post-header-child",
"post-admin-row", "post-admin-header-child",
"calendar-row", "calendar-header-child",
"calendar-admin-row", "calendar-admin-header-child")),
)
# --- Slot detail layout (extends cal-admin with slot header) ---
async def _slot_full(ctx: dict, **kw: Any) -> str:
from shared.sx.helpers import render_to_sx_with_env, post_admin_header_sx
from shared.sx.parser import SxExpr
ctx = await _ensure_container_nav({**ctx, "is_admin_section": True})
slug = (ctx.get("post") or {}).get("slug", "")
return await render_to_sx_with_env("events-slot-layout-full", {},
post_header=SxExpr(await _post_header_sx(ctx)),
admin_header=SxExpr(await post_admin_header_sx(ctx, slug, selected="calendars")),
calendar_header=SxExpr(_calendar_header_sx(ctx)),
calendar_admin_header=SxExpr(_calendar_admin_header_sx(ctx)),
slot_header=SxExpr(_slot_header_html(ctx)),
)
async def _slot_oob(ctx: dict, **kw: Any) -> str:
from shared.sx.helpers import render_to_sx_with_env, post_admin_header_sx, oob_header_sx
from shared.sx.parser import SxExpr
ctx = await _ensure_container_nav({**ctx, "is_admin_section": True})
slug = (ctx.get("post") or {}).get("slug", "")
return await render_to_sx_with_env("events-slot-layout-oob", {},
admin_oob=SxExpr(await post_admin_header_sx(ctx, slug, oob=True, selected="calendars")),
cal_admin_oob=SxExpr(_calendar_admin_header_sx(ctx, oob=True)),
slot_oob_wrap=SxExpr(await oob_header_sx("calendar-admin-header-child",
"slot-header-child", _slot_header_html(ctx))),
clear_oob=SxExpr(_clear_deeper_oob("post-row", "post-header-child",
"post-admin-row", "post-admin-header-child",
"calendar-row", "calendar-header-child",
"calendar-admin-row", "calendar-admin-header-child",
"slot-row", "slot-header-child")),
)
# --- Day admin layout (root + post + post-admin + child(cal + day + day-admin)) ---
async def _day_admin_full(ctx: dict, **kw: Any) -> str:
from shared.sx.helpers import render_to_sx_with_env, post_admin_header_sx
from shared.sx.parser import SxExpr
ctx = await _ensure_container_nav(ctx)
slug = (ctx.get("post") or {}).get("slug", "")
return await render_to_sx_with_env("events-day-admin-layout-full", {},
post_header=SxExpr(await _post_header_sx(ctx)),
admin_header=SxExpr(await post_admin_header_sx(ctx, slug, selected="calendars")),
calendar_header=SxExpr(_calendar_header_sx(ctx)),
day_header=SxExpr(_day_header_sx(ctx)),
day_admin_header=SxExpr(_day_admin_header_sx(ctx)),
)
async def _day_admin_oob(ctx: dict, **kw: Any) -> str:
from shared.sx.helpers import render_to_sx_with_env, post_admin_header_sx, oob_header_sx
from shared.sx.parser import SxExpr
ctx = await _ensure_container_nav(ctx)
slug = (ctx.get("post") or {}).get("slug", "")
return await render_to_sx_with_env("events-day-admin-layout-oob", {},
admin_oob=SxExpr(await post_admin_header_sx(ctx, slug, oob=True, selected="calendars")),
cal_oob=SxExpr(_calendar_header_sx(ctx, oob=True)),
day_admin_oob_wrap=SxExpr(await oob_header_sx("day-header-child",
"day-admin-header-child", _day_admin_header_sx(ctx))),
clear_oob=SxExpr(_clear_deeper_oob("post-row", "post-header-child",
"post-admin-row", "post-admin-header-child",
"calendar-row", "calendar-header-child",
"day-row", "day-header-child",
"day-admin-row", "day-admin-header-child")),
)
# --- Entry layout (root + child(post + cal + day + entry), + menu) ---
async def _entry_full(ctx: dict, **kw: Any) -> str:
from shared.sx.helpers import render_to_sx_with_env
from shared.sx.parser import SxExpr
return await render_to_sx_with_env("events-entry-layout-full", {},
post_header=SxExpr(await _post_header_sx(ctx)),
calendar_header=SxExpr(_calendar_header_sx(ctx)),
day_header=SxExpr(_day_header_sx(ctx)),
entry_header=SxExpr(_entry_header_html(ctx)),
)
async def _entry_oob(ctx: dict, **kw: Any) -> str:
from shared.sx.helpers import render_to_sx_with_env, oob_header_sx
from shared.sx.parser import SxExpr
return await render_to_sx_with_env("events-entry-layout-oob", {},
day_oob=SxExpr(_day_header_sx(ctx, oob=True)),
entry_oob_wrap=SxExpr(await oob_header_sx("day-header-child",
"entry-header-child", _entry_header_html(ctx))),
clear_oob=SxExpr(_clear_deeper_oob("post-row", "post-header-child",
"calendar-row", "calendar-header-child",
"day-row", "day-header-child",
"entry-row", "entry-header-child")),
)
# --- Entry admin layout (root + post + child(post-admin + cal + day + entry + entry-admin), + menu) ---
async def _entry_admin_full(ctx: dict, **kw: Any) -> str:
from shared.sx.helpers import render_to_sx_with_env, post_admin_header_sx
from shared.sx.parser import SxExpr
ctx = await _ensure_container_nav(ctx)
slug = (ctx.get("post") or {}).get("slug", "")
return await render_to_sx_with_env("events-entry-admin-layout-full", {},
post_header=SxExpr(await _post_header_sx(ctx)),
admin_header=SxExpr(await post_admin_header_sx(ctx, slug, selected="calendars")),
calendar_header=SxExpr(_calendar_header_sx(ctx)),
day_header=SxExpr(_day_header_sx(ctx)),
entry_header=SxExpr(_entry_header_html(ctx)),
entry_admin_header=SxExpr(_entry_admin_header_html(ctx)),
)
async def _entry_admin_oob(ctx: dict, **kw: Any) -> str:
from shared.sx.helpers import render_to_sx_with_env, post_admin_header_sx, oob_header_sx
from shared.sx.parser import SxExpr
ctx = await _ensure_container_nav(ctx)
slug = (ctx.get("post") or {}).get("slug", "")
return await render_to_sx_with_env("events-entry-admin-layout-oob", {},
admin_oob=SxExpr(await post_admin_header_sx(ctx, slug, oob=True, selected="calendars")),
entry_oob=SxExpr(_entry_header_html(ctx, oob=True)),
entry_admin_oob_wrap=SxExpr(await oob_header_sx("entry-header-child",
"entry-admin-header-child", _entry_admin_header_html(ctx))),
clear_oob=SxExpr(_clear_deeper_oob("post-row", "post-header-child",
"post-admin-row", "post-admin-header-child",
"calendar-row", "calendar-header-child",
"day-row", "day-header-child",
"entry-row", "entry-header-child",
"entry-admin-row", "entry-admin-header-child")),
)
# --- Ticket types layout (extends entry admin with ticket-types header, + menu) ---
async def _ticket_types_full(ctx: dict, **kw: Any) -> str:
from shared.sx.helpers import render_to_sx_with_env
from shared.sx.parser import SxExpr
return await render_to_sx_with_env("events-ticket-types-layout-full", {},
post_header=SxExpr(await _post_header_sx(ctx)),
calendar_header=SxExpr(_calendar_header_sx(ctx)),
day_header=SxExpr(_day_header_sx(ctx)),
entry_header=SxExpr(_entry_header_html(ctx)),
entry_admin_header=SxExpr(_entry_admin_header_html(ctx)),
ticket_types_header=SxExpr(_ticket_types_header_html(ctx)),
)
async def _ticket_types_oob(ctx: dict, **kw: Any) -> str:
from shared.sx.helpers import render_to_sx_with_env, oob_header_sx
from shared.sx.parser import SxExpr
return await render_to_sx_with_env("events-ticket-types-layout-oob", {},
entry_admin_oob=SxExpr(_entry_admin_header_html(ctx, oob=True)),
ticket_types_oob_wrap=SxExpr(await oob_header_sx("entry-admin-header-child",
"ticket_types-header-child", _ticket_types_header_html(ctx))),
)
# --- Ticket type detail layout (extends ticket types with ticket-type header, + menu) ---
async def _ticket_type_full(ctx: dict, **kw: Any) -> str:
from shared.sx.helpers import render_to_sx_with_env
from shared.sx.parser import SxExpr
return await render_to_sx_with_env("events-ticket-type-layout-full", {},
post_header=SxExpr(await _post_header_sx(ctx)),
calendar_header=SxExpr(_calendar_header_sx(ctx)),
day_header=SxExpr(_day_header_sx(ctx)),
entry_header=SxExpr(_entry_header_html(ctx)),
entry_admin_header=SxExpr(_entry_admin_header_html(ctx)),
ticket_types_header=SxExpr(_ticket_types_header_html(ctx)),
ticket_type_header=SxExpr(_ticket_type_header_html(ctx)),
)
async def _ticket_type_oob(ctx: dict, **kw: Any) -> str:
from shared.sx.helpers import render_to_sx_with_env, oob_header_sx
from shared.sx.parser import SxExpr
return await render_to_sx_with_env("events-ticket-type-layout-oob", {},
ticket_types_oob=SxExpr(_ticket_types_header_html(ctx, oob=True)),
ticket_type_oob_wrap=SxExpr(await oob_header_sx("ticket_types-header-child",
"ticket_type-header-child", _ticket_type_header_html(ctx))),
)
# --- Markets layout (root + child(post + markets)) ---
async def _markets_full(ctx: dict, **kw: Any) -> str:
from shared.sx.helpers import render_to_sx_with_env
from shared.sx.parser import SxExpr
return await render_to_sx_with_env("events-markets-layout-full", {},
post_header=SxExpr(await _post_header_sx(ctx)),
markets_header=SxExpr(_markets_header_sx(ctx)),
)
async def _markets_oob(ctx: dict, **kw: Any) -> str:
from shared.sx.helpers import render_to_sx_with_env, oob_header_sx
from shared.sx.parser import SxExpr
return await render_to_sx_with_env("events-markets-layout-oob", {},
post_oob=SxExpr(await _post_header_sx(ctx, oob=True)),
markets_oob_wrap=SxExpr(await oob_header_sx("post-header-child",
"markets-header-child", _markets_header_sx(ctx))),
)
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
@@ -508,6 +288,7 @@ def _register_events_helpers() -> None:
async def _h_calendar_admin_content(calendar_slug=None, **kw): async def _h_calendar_admin_content(calendar_slug=None, **kw):
await _ensure_calendar(calendar_slug) await _ensure_calendar(calendar_slug)
await _ensure_container_nav_defpage_ctx()
from shared.sx.page import get_template_context from shared.sx.page import get_template_context
ctx = await get_template_context() ctx = await get_template_context()
return _calendar_admin_main_panel_html(ctx) return _calendar_admin_main_panel_html(ctx)
@@ -515,6 +296,7 @@ async def _h_calendar_admin_content(calendar_slug=None, **kw):
async def _h_day_admin_content(calendar_slug=None, year=None, month=None, day=None, **kw): async def _h_day_admin_content(calendar_slug=None, year=None, month=None, day=None, **kw):
await _ensure_calendar(calendar_slug) await _ensure_calendar(calendar_slug)
await _ensure_container_nav_defpage_ctx()
if year is not None: if year is not None:
await _ensure_day_data(int(year), int(month), int(day)) await _ensure_day_data(int(year), int(month), int(day))
return _day_admin_main_panel_html({}) return _day_admin_main_panel_html({})
@@ -523,6 +305,7 @@ async def _h_day_admin_content(calendar_slug=None, year=None, month=None, day=No
async def _h_slots_content(calendar_slug=None, **kw): async def _h_slots_content(calendar_slug=None, **kw):
from quart import g from quart import g
await _ensure_calendar(calendar_slug) await _ensure_calendar(calendar_slug)
await _ensure_container_nav_defpage_ctx()
calendar = getattr(g, "calendar", None) calendar = getattr(g, "calendar", None)
from bp.slots.services.slots import list_slots as svc_list_slots from bp.slots.services.slots import list_slots as svc_list_slots
slots = await svc_list_slots(g.s, calendar.id) if calendar else [] slots = await svc_list_slots(g.s, calendar.id) if calendar else []
@@ -533,6 +316,7 @@ async def _h_slots_content(calendar_slug=None, **kw):
async def _h_slot_content(calendar_slug=None, slot_id=None, **kw): async def _h_slot_content(calendar_slug=None, slot_id=None, **kw):
from quart import g, abort from quart import g, abort
await _ensure_calendar(calendar_slug) await _ensure_calendar(calendar_slug)
await _ensure_container_nav_defpage_ctx()
from bp.slot.services.slot import get_slot as svc_get_slot from bp.slot.services.slot import get_slot as svc_get_slot
slot = await svc_get_slot(g.s, slot_id) if slot_id else None slot = await svc_get_slot(g.s, slot_id) if slot_id else None
if not slot: if not slot:
@@ -561,6 +345,7 @@ async def _h_entry_menu(calendar_slug=None, entry_id=None, **kw):
async def _h_entry_admin_content(calendar_slug=None, entry_id=None, **kw): async def _h_entry_admin_content(calendar_slug=None, entry_id=None, **kw):
await _ensure_calendar(calendar_slug) await _ensure_calendar(calendar_slug)
await _ensure_container_nav_defpage_ctx()
await _ensure_entry_context(entry_id) await _ensure_entry_context(entry_id)
from shared.sx.page import get_template_context from shared.sx.page import get_template_context
ctx = await get_template_context() ctx = await get_template_context()
@@ -677,6 +462,7 @@ async def _h_ticket_admin_content(**kw):
async def _h_markets_content(**kw): async def _h_markets_content(**kw):
_ensure_post_defpage_ctx()
from shared.sx.page import get_template_context from shared.sx.page import get_template_context
ctx = await get_template_context() ctx = await get_template_context()
return _markets_main_panel_html(ctx) return _markets_main_panel_html(ctx)

View File

@@ -1,288 +0,0 @@
"""Layout registration + header builders."""
from __future__ import annotations
from typing import Any
from shared.sx.parser import SxExpr
from .utils import _clear_deeper_oob, _ensure_container_nav
from .calendar import (
_post_header_sx, _calendar_header_sx, _calendar_admin_header_sx,
_day_header_sx, _day_admin_header_sx, _markets_header_sx,
)
from .entries import _entry_header_html, _entry_admin_header_html
from .slots import _slot_header_html
from .tickets import _ticket_types_header_html, _ticket_type_header_html
# ---------------------------------------------------------------------------
# Layouts
# ---------------------------------------------------------------------------
def _register_events_layouts() -> None:
from shared.sx.layouts import register_custom_layout
register_custom_layout("events-calendar-admin", _cal_admin_full, _cal_admin_oob)
register_custom_layout("events-slots", _slots_full, _slots_oob)
register_custom_layout("events-slot", _slot_full, _slot_oob)
register_custom_layout("events-day-admin", _day_admin_full, _day_admin_oob)
register_custom_layout("events-entry", _entry_full, _entry_oob)
register_custom_layout("events-entry-admin", _entry_admin_full, _entry_admin_oob)
register_custom_layout("events-ticket-types", _ticket_types_full, _ticket_types_oob)
register_custom_layout("events-ticket-type", _ticket_type_full, _ticket_type_oob)
register_custom_layout("events-markets", _markets_full, _markets_oob)
# --- Calendar admin layout (root + post + child(post-admin + calendar + cal-admin)) ---
async def _cal_admin_full(ctx: dict, **kw: Any) -> str:
from shared.sx.helpers import render_to_sx_with_env, post_admin_header_sx
from shared.sx.parser import SxExpr
ctx = await _ensure_container_nav(ctx)
slug = (ctx.get("post") or {}).get("slug", "")
return await render_to_sx_with_env("events-cal-admin-layout-full", {},
post_header=SxExpr(await _post_header_sx(ctx)),
admin_header=SxExpr(await post_admin_header_sx(ctx, slug, selected="calendars")),
calendar_header=SxExpr(_calendar_header_sx(ctx)),
calendar_admin_header=SxExpr(_calendar_admin_header_sx(ctx)),
)
async def _cal_admin_oob(ctx: dict, **kw: Any) -> str:
from shared.sx.helpers import render_to_sx_with_env, post_admin_header_sx, oob_header_sx
from shared.sx.parser import SxExpr
ctx = await _ensure_container_nav(ctx)
slug = (ctx.get("post") or {}).get("slug", "")
return await render_to_sx_with_env("events-cal-admin-layout-oob", {},
admin_oob=SxExpr(await post_admin_header_sx(ctx, slug, oob=True, selected="calendars")),
cal_oob=SxExpr(_calendar_header_sx(ctx, oob=True)),
cal_admin_oob_wrap=SxExpr(await oob_header_sx("calendar-header-child",
"calendar-admin-header-child", _calendar_admin_header_sx(ctx))),
clear_oob=SxExpr(_clear_deeper_oob("post-row", "post-header-child",
"post-admin-row", "post-admin-header-child",
"calendar-row", "calendar-header-child",
"calendar-admin-row", "calendar-admin-header-child")),
)
# --- Slots layout (same full as cal-admin but different OOB) ---
async def _slots_full(ctx: dict, **kw: Any) -> str:
return await _cal_admin_full({**ctx, "is_admin_section": True}, **kw)
async def _slots_oob(ctx: dict, **kw: Any) -> str:
from shared.sx.helpers import render_to_sx_with_env, post_admin_header_sx
from shared.sx.parser import SxExpr
ctx = await _ensure_container_nav({**ctx, "is_admin_section": True})
slug = (ctx.get("post") or {}).get("slug", "")
return await render_to_sx_with_env("events-slots-layout-oob", {},
admin_oob=SxExpr(await post_admin_header_sx(ctx, slug, oob=True, selected="calendars")),
cal_admin_oob=SxExpr(_calendar_admin_header_sx(ctx, oob=True)),
clear_oob=SxExpr(_clear_deeper_oob("post-row", "post-header-child",
"post-admin-row", "post-admin-header-child",
"calendar-row", "calendar-header-child",
"calendar-admin-row", "calendar-admin-header-child")),
)
# --- Slot detail layout (extends cal-admin with slot header) ---
async def _slot_full(ctx: dict, **kw: Any) -> str:
from shared.sx.helpers import render_to_sx_with_env, post_admin_header_sx
from shared.sx.parser import SxExpr
ctx = await _ensure_container_nav({**ctx, "is_admin_section": True})
slug = (ctx.get("post") or {}).get("slug", "")
return await render_to_sx_with_env("events-slot-layout-full", {},
post_header=SxExpr(await _post_header_sx(ctx)),
admin_header=SxExpr(await post_admin_header_sx(ctx, slug, selected="calendars")),
calendar_header=SxExpr(_calendar_header_sx(ctx)),
calendar_admin_header=SxExpr(_calendar_admin_header_sx(ctx)),
slot_header=SxExpr(_slot_header_html(ctx)),
)
async def _slot_oob(ctx: dict, **kw: Any) -> str:
from shared.sx.helpers import render_to_sx_with_env, post_admin_header_sx, oob_header_sx
from shared.sx.parser import SxExpr
ctx = await _ensure_container_nav({**ctx, "is_admin_section": True})
slug = (ctx.get("post") or {}).get("slug", "")
return await render_to_sx_with_env("events-slot-layout-oob", {},
admin_oob=SxExpr(await post_admin_header_sx(ctx, slug, oob=True, selected="calendars")),
cal_admin_oob=SxExpr(_calendar_admin_header_sx(ctx, oob=True)),
slot_oob_wrap=SxExpr(await oob_header_sx("calendar-admin-header-child",
"slot-header-child", _slot_header_html(ctx))),
clear_oob=SxExpr(_clear_deeper_oob("post-row", "post-header-child",
"post-admin-row", "post-admin-header-child",
"calendar-row", "calendar-header-child",
"calendar-admin-row", "calendar-admin-header-child",
"slot-row", "slot-header-child")),
)
# --- Day admin layout (root + post + post-admin + child(cal + day + day-admin)) ---
async def _day_admin_full(ctx: dict, **kw: Any) -> str:
from shared.sx.helpers import render_to_sx_with_env, post_admin_header_sx
from shared.sx.parser import SxExpr
ctx = await _ensure_container_nav(ctx)
slug = (ctx.get("post") or {}).get("slug", "")
return await render_to_sx_with_env("events-day-admin-layout-full", {},
post_header=SxExpr(await _post_header_sx(ctx)),
admin_header=SxExpr(await post_admin_header_sx(ctx, slug, selected="calendars")),
calendar_header=SxExpr(_calendar_header_sx(ctx)),
day_header=SxExpr(_day_header_sx(ctx)),
day_admin_header=SxExpr(_day_admin_header_sx(ctx)),
)
async def _day_admin_oob(ctx: dict, **kw: Any) -> str:
from shared.sx.helpers import render_to_sx_with_env, post_admin_header_sx, oob_header_sx
from shared.sx.parser import SxExpr
ctx = await _ensure_container_nav(ctx)
slug = (ctx.get("post") or {}).get("slug", "")
return await render_to_sx_with_env("events-day-admin-layout-oob", {},
admin_oob=SxExpr(await post_admin_header_sx(ctx, slug, oob=True, selected="calendars")),
cal_oob=SxExpr(_calendar_header_sx(ctx, oob=True)),
day_admin_oob_wrap=SxExpr(await oob_header_sx("day-header-child",
"day-admin-header-child", _day_admin_header_sx(ctx))),
clear_oob=SxExpr(_clear_deeper_oob("post-row", "post-header-child",
"post-admin-row", "post-admin-header-child",
"calendar-row", "calendar-header-child",
"day-row", "day-header-child",
"day-admin-row", "day-admin-header-child")),
)
# --- Entry layout (root + child(post + cal + day + entry), + menu) ---
async def _entry_full(ctx: dict, **kw: Any) -> str:
from shared.sx.helpers import render_to_sx_with_env
from shared.sx.parser import SxExpr
return await render_to_sx_with_env("events-entry-layout-full", {},
post_header=SxExpr(await _post_header_sx(ctx)),
calendar_header=SxExpr(_calendar_header_sx(ctx)),
day_header=SxExpr(_day_header_sx(ctx)),
entry_header=SxExpr(_entry_header_html(ctx)),
)
async def _entry_oob(ctx: dict, **kw: Any) -> str:
from shared.sx.helpers import render_to_sx_with_env, oob_header_sx
from shared.sx.parser import SxExpr
return await render_to_sx_with_env("events-entry-layout-oob", {},
day_oob=SxExpr(_day_header_sx(ctx, oob=True)),
entry_oob_wrap=SxExpr(await oob_header_sx("day-header-child",
"entry-header-child", _entry_header_html(ctx))),
clear_oob=SxExpr(_clear_deeper_oob("post-row", "post-header-child",
"calendar-row", "calendar-header-child",
"day-row", "day-header-child",
"entry-row", "entry-header-child")),
)
# --- Entry admin layout (root + post + child(post-admin + cal + day + entry + entry-admin), + menu) ---
async def _entry_admin_full(ctx: dict, **kw: Any) -> str:
from shared.sx.helpers import render_to_sx_with_env, post_admin_header_sx
from shared.sx.parser import SxExpr
ctx = await _ensure_container_nav(ctx)
slug = (ctx.get("post") or {}).get("slug", "")
return await render_to_sx_with_env("events-entry-admin-layout-full", {},
post_header=SxExpr(await _post_header_sx(ctx)),
admin_header=SxExpr(await post_admin_header_sx(ctx, slug, selected="calendars")),
calendar_header=SxExpr(_calendar_header_sx(ctx)),
day_header=SxExpr(_day_header_sx(ctx)),
entry_header=SxExpr(_entry_header_html(ctx)),
entry_admin_header=SxExpr(_entry_admin_header_html(ctx)),
)
async def _entry_admin_oob(ctx: dict, **kw: Any) -> str:
from shared.sx.helpers import render_to_sx_with_env, post_admin_header_sx, oob_header_sx
from shared.sx.parser import SxExpr
ctx = await _ensure_container_nav(ctx)
slug = (ctx.get("post") or {}).get("slug", "")
return await render_to_sx_with_env("events-entry-admin-layout-oob", {},
admin_oob=SxExpr(await post_admin_header_sx(ctx, slug, oob=True, selected="calendars")),
entry_oob=SxExpr(_entry_header_html(ctx, oob=True)),
entry_admin_oob_wrap=SxExpr(await oob_header_sx("entry-header-child",
"entry-admin-header-child", _entry_admin_header_html(ctx))),
clear_oob=SxExpr(_clear_deeper_oob("post-row", "post-header-child",
"post-admin-row", "post-admin-header-child",
"calendar-row", "calendar-header-child",
"day-row", "day-header-child",
"entry-row", "entry-header-child",
"entry-admin-row", "entry-admin-header-child")),
)
# --- Ticket types layout (extends entry admin with ticket-types header, + menu) ---
async def _ticket_types_full(ctx: dict, **kw: Any) -> str:
from shared.sx.helpers import render_to_sx_with_env
from shared.sx.parser import SxExpr
return await render_to_sx_with_env("events-ticket-types-layout-full", {},
post_header=SxExpr(await _post_header_sx(ctx)),
calendar_header=SxExpr(_calendar_header_sx(ctx)),
day_header=SxExpr(_day_header_sx(ctx)),
entry_header=SxExpr(_entry_header_html(ctx)),
entry_admin_header=SxExpr(_entry_admin_header_html(ctx)),
ticket_types_header=SxExpr(_ticket_types_header_html(ctx)),
)
async def _ticket_types_oob(ctx: dict, **kw: Any) -> str:
from shared.sx.helpers import render_to_sx_with_env, oob_header_sx
from shared.sx.parser import SxExpr
return await render_to_sx_with_env("events-ticket-types-layout-oob", {},
entry_admin_oob=SxExpr(_entry_admin_header_html(ctx, oob=True)),
ticket_types_oob_wrap=SxExpr(await oob_header_sx("entry-admin-header-child",
"ticket_types-header-child", _ticket_types_header_html(ctx))),
)
# --- Ticket type detail layout (extends ticket types with ticket-type header, + menu) ---
async def _ticket_type_full(ctx: dict, **kw: Any) -> str:
from shared.sx.helpers import render_to_sx_with_env
from shared.sx.parser import SxExpr
return await render_to_sx_with_env("events-ticket-type-layout-full", {},
post_header=SxExpr(await _post_header_sx(ctx)),
calendar_header=SxExpr(_calendar_header_sx(ctx)),
day_header=SxExpr(_day_header_sx(ctx)),
entry_header=SxExpr(_entry_header_html(ctx)),
entry_admin_header=SxExpr(_entry_admin_header_html(ctx)),
ticket_types_header=SxExpr(_ticket_types_header_html(ctx)),
ticket_type_header=SxExpr(_ticket_type_header_html(ctx)),
)
async def _ticket_type_oob(ctx: dict, **kw: Any) -> str:
from shared.sx.helpers import render_to_sx_with_env, oob_header_sx
from shared.sx.parser import SxExpr
return await render_to_sx_with_env("events-ticket-type-layout-oob", {},
ticket_types_oob=SxExpr(_ticket_types_header_html(ctx, oob=True)),
ticket_type_oob_wrap=SxExpr(await oob_header_sx("ticket_types-header-child",
"ticket_type-header-child", _ticket_type_header_html(ctx))),
)
# --- Markets layout (root + child(post + markets)) ---
async def _markets_full(ctx: dict, **kw: Any) -> str:
from shared.sx.helpers import render_to_sx_with_env
from shared.sx.parser import SxExpr
return await render_to_sx_with_env("events-markets-layout-full", {},
post_header=SxExpr(await _post_header_sx(ctx)),
markets_header=SxExpr(_markets_header_sx(ctx)),
)
async def _markets_oob(ctx: dict, **kw: Any) -> str:
from shared.sx.helpers import render_to_sx_with_env, oob_header_sx
from shared.sx.parser import SxExpr
return await render_to_sx_with_env("events-markets-layout-oob", {},
post_oob=SxExpr(await _post_header_sx(ctx, oob=True)),
markets_oob_wrap=SxExpr(await oob_header_sx("post-header-child",
"markets-header-child", _markets_header_sx(ctx))),
)

View File

@@ -1,17 +1,17 @@
;; Federation layout defcomps — read ctx values from env free variables. ;; Federation layout defcomps — fully self-contained via IO primitives.
;; `actor` is injected into env by the layout registration in __init__.py. ;; Registered via register_sx_layout("social", ...) in __init__.py.
;; Full page: root header + social header in header-child ;; Full page: root header + social header in header-child
(defcomp ~social-layout-full () (defcomp ~social-layout-full ()
(<> (~root-header-auto) (<> (~root-header-auto)
(~header-child-sx (~header-child-sx
:inner (~federation-social-header :inner (~federation-social-header
:nav (~federation-social-nav :actor actor))))) :nav (~federation-social-nav :actor (federation-actor-ctx))))))
;; OOB (HTMX): social header oob + root header oob ;; OOB (HTMX): social header oob + root header oob
(defcomp ~social-layout-oob () (defcomp ~social-layout-oob ()
(<> (~oob-header-sx (<> (~oob-header-sx
:parent-id "root-header-child" :parent-id "root-header-child"
:row (~federation-social-header :row (~federation-social-header
:nav (~federation-social-nav :actor actor))) :nav (~federation-social-nav :actor (federation-actor-ctx))))
(~root-header-auto true))) (~root-header-auto true)))

View File

@@ -15,6 +15,5 @@ def _load_federation_page_files() -> None:
def _register_federation_layouts() -> None: def _register_federation_layouts() -> None:
from shared.sx.layouts import register_custom_layout from shared.sx.layouts import register_sx_layout
from .utils import _social_full, _social_oob register_sx_layout("social", "social-layout-full", "social-layout-oob")
register_custom_layout("social", _social_full, _social_oob)

View File

@@ -1,8 +1,6 @@
"""Federation page utilities — serializers, actor helpers, social page builder.""" """Federation page utilities — serializers, actor helpers, social page builder."""
from __future__ import annotations from __future__ import annotations
from typing import Any
def _serialize_actor(actor) -> dict | None: def _serialize_actor(actor) -> dict | None:
"""Serialize an actor profile to a dict for sx defcomps.""" """Serialize an actor profile to a dict for sx defcomps."""
@@ -24,7 +22,7 @@ def _serialize_remote_actor(a) -> dict:
async def _social_page(ctx: dict, actor, *, content: str, async def _social_page(ctx: dict, actor, *, content: str,
title: str = "Rose Ash", meta_html: str = "") -> str: title: str = "Rose Ash", meta_html: str = "") -> str:
"""Build a full social page with social header.""" """Build a full social page with social header (non-defpage routes)."""
from shared.sx.helpers import render_to_sx_with_env, full_page_sx from shared.sx.helpers import render_to_sx_with_env, full_page_sx
from markupsafe import escape from markupsafe import escape
@@ -47,22 +45,3 @@ def _require_actor():
if not actor: if not actor:
abort(403, "You need to choose a federation username first") abort(403, "You need to choose a federation username first")
return actor return actor
def _actor_data(ctx: dict) -> dict | None:
actor = ctx.get("actor")
if not actor:
return None
return _serialize_actor(actor)
async def _social_full(ctx: dict, **kw: Any) -> str:
from shared.sx.helpers import render_to_sx_with_env
env = {"actor": kw.get("actor") or _actor_data(ctx)}
return await render_to_sx_with_env("social-layout-full", env)
async def _social_oob(ctx: dict, **kw: Any) -> str:
from shared.sx.helpers import render_to_sx_with_env
env = {"actor": kw.get("actor") or _actor_data(ctx)}
return await render_to_sx_with_env("social-layout-oob", env)

View File

@@ -228,6 +228,11 @@ def create_app() -> "Quart":
("market", "container-nav", nav_params), ("market", "container-nav", nav_params),
], required=False) ], required=False)
ctx["container_nav"] = events_nav + market_nav ctx["container_nav"] = events_nav + market_nav
# Populate g._defpage_ctx for layout IO primitives
if not hasattr(g, '_defpage_ctx'):
g._defpage_ctx = {}
g._defpage_ctx.setdefault("post", post_data.get("post"))
g._defpage_ctx.setdefault("container_nav", ctx["container_nav"])
return ctx return ctx
# --- oEmbed endpoint --- # --- oEmbed endpoint ---

View File

@@ -1,42 +1,105 @@
;; Market layout defcomps — root header via ~root-header-auto, ;; Market layout defcomps — fully self-contained via IO primitives.
;; market-specific headers passed as &key params. ;; Registered via register_sx_layout in layouts.py.
;; --- Browse layout: root + post header + market header --- ;; ---------------------------------------------------------------------------
;; Auto-fetching market header macro
;; ---------------------------------------------------------------------------
(defcomp ~market-browse-layout-full (&key post-header market-header) (defmacro ~market-header-auto (oob)
"Market header row using (market-header-ctx)."
(quasiquote
(let ((__mctx (market-header-ctx)))
(~menu-row-sx :id "market-row" :level 2
:link-href (get __mctx "link-href")
:link-label-content (~market-shop-label
:title (get __mctx "market-title")
:top-slug (get __mctx "top-slug")
:sub-div (get __mctx "sub-slug"))
:nav (get __mctx "desktop-nav")
:child-id "market-header-child"
:oob (unquote oob)))))
;; ---------------------------------------------------------------------------
;; OOB clear helpers
;; ---------------------------------------------------------------------------
(defcomp ~market-clear-oob ()
"Clear OOB divs for browse level."
(<>
(~clear-oob-div :id "product-admin-row")
(~clear-oob-div :id "product-admin-header-child")
(~clear-oob-div :id "product-row")
(~clear-oob-div :id "product-header-child")
(~clear-oob-div :id "market-admin-row")
(~clear-oob-div :id "market-admin-header-child")
(~clear-oob-div :id "post-admin-row")
(~clear-oob-div :id "post-admin-header-child")))
(defcomp ~market-clear-oob-admin ()
"Clear OOB divs for admin level."
(<>
(~clear-oob-div :id "product-admin-row")
(~clear-oob-div :id "product-admin-header-child")
(~clear-oob-div :id "product-row")
(~clear-oob-div :id "product-header-child")))
;; ---------------------------------------------------------------------------
;; Browse layout: root + post + market (self-contained)
;; ---------------------------------------------------------------------------
(defcomp ~market-browse-layout-full ()
(<> (~root-header-auto) (<> (~root-header-auto)
(~header-child-sx :inner (<> post-header market-header)))) (~header-child-sx
:inner (<> (~post-header-auto nil)
(~market-header-auto nil)))))
(defcomp ~market-browse-layout-oob (&key oob-header post-header-oob clear-oob) (defcomp ~market-browse-layout-oob ()
(<> oob-header post-header-oob clear-oob)) (<> (~post-header-auto true)
(~oob-header-sx :parent-id "post-header-child"
:row (~market-header-auto nil))
(~market-clear-oob)
(~root-header-auto true)))
;; --- Product layout: root + post + market + product --- (defcomp ~market-browse-layout-mobile ()
(let ((__mctx (market-header-ctx)))
(get __mctx "mobile-nav")))
;; ---------------------------------------------------------------------------
;; Market admin layout: root + post + market + post-admin (self-contained)
;; ---------------------------------------------------------------------------
(defcomp ~market-admin-layout-full (&key selected)
(<> (~root-header-auto)
(~header-child-sx
:inner (<> (~post-header-auto nil)
(~market-header-auto nil)
(~post-admin-header-auto nil selected)))))
(defcomp ~market-admin-layout-oob (&key selected)
(<> (~market-header-auto true)
(~oob-header-sx :parent-id "market-header-child"
:row (~post-admin-header-auto nil selected))
(~market-clear-oob-admin)
(~root-header-auto true)))
;; ---------------------------------------------------------------------------
;; Parameterized defcomps — used by renders.py (non-defpage routes)
;; ---------------------------------------------------------------------------
;; Product layout: root + post + market + product
(defcomp ~market-product-layout-full (&key post-header market-header product-header) (defcomp ~market-product-layout-full (&key post-header market-header product-header)
(<> (~root-header-auto) (<> (~root-header-auto)
(~header-child-sx :inner (<> post-header market-header product-header)))) (~header-child-sx :inner (<> post-header market-header product-header))))
;; --- Product admin layout: root + post + market + product + admin --- ;; Product admin layout: root + post + market + product + admin
(defcomp ~market-product-admin-layout-full (&key post-header market-header product-header admin-header) (defcomp ~market-product-admin-layout-full (&key post-header market-header product-header admin-header)
(<> (~root-header-auto) (<> (~root-header-auto)
(~header-child-sx :inner (<> post-header market-header product-header admin-header)))) (~header-child-sx :inner (<> post-header market-header product-header admin-header))))
;; --- Market admin layout: root + post + market + market-admin --- ;; OOB wrappers
(defcomp ~market-admin-layout-full (&key post-header market-header admin-header)
(<> (~root-header-auto)
(~header-child-sx :inner (<> post-header market-header admin-header))))
(defcomp ~market-admin-layout-oob (&key market-header-oob admin-oob-header clear-oob)
(<> market-header-oob admin-oob-header clear-oob))
;; --- OOB wrappers ---
(defcomp ~market-oob-wrap (&key parts) (defcomp ~market-oob-wrap (&key parts)
(<> parts)) (<> parts))
;; --- Content wrappers --- ;; Content wrappers
(defcomp ~market-content-padded (&key content) (defcomp ~market-content-padded (&key content)
(<> content (div :class "pb-8"))) (<> content (div :class "pb-8")))

View File

@@ -4,15 +4,9 @@ from __future__ import annotations
from typing import Any from typing import Any
from shared.sx.parser import SxExpr from shared.sx.parser import SxExpr
from shared.sx.helpers import ( from shared.sx.helpers import sx_call
sx_call,
post_header_sx as _post_header_sx,
post_admin_header_sx,
oob_header_sx as _oob_header_sx,
header_child_sx,
)
from .utils import _set_prices, _price_str, _clear_deeper_oob from .utils import _set_prices, _price_str
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
@@ -272,62 +266,14 @@ def _product_admin_header_sx(ctx: dict, d: dict, *, oob: bool = False) -> str:
) )
# ---------------------------------------------------------------------------
# Market admin header
# ---------------------------------------------------------------------------
async def _market_admin_header_sx(ctx: dict, *, oob: bool = False, selected: str = "") -> str:
"""Build market admin header row -- delegates to shared helper."""
slug = (ctx.get("post") or {}).get("slug", "")
return await post_admin_header_sx(ctx, slug, oob=oob, selected=selected)
# =========================================================================== # ===========================================================================
# Layout registration # Layout registration — all layouts delegate to .sx defcomps
# =========================================================================== # ===========================================================================
def _register_market_layouts() -> None: def _register_market_layouts() -> None:
from shared.sx.layouts import register_custom_layout from shared.sx.layouts import register_sx_layout
register_custom_layout("market", _market_full, _market_oob, _market_mobile) register_sx_layout("market",
register_custom_layout("market-admin", _market_admin_full, _market_admin_oob) "market-browse-layout-full", "market-browse-layout-oob",
"market-browse-layout-mobile")
register_sx_layout("market-admin",
async def _market_full(ctx: dict, **kw: Any) -> str: "market-admin-layout-full", "market-admin-layout-oob")
from shared.sx.helpers import render_to_sx_with_env
return await render_to_sx_with_env("market-browse-layout-full", {},
post_header=SxExpr(await _post_header_sx(ctx)),
market_header=SxExpr(_market_header_sx(ctx)))
async def _market_oob(ctx: dict, **kw: Any) -> str:
oob_hdr = await _oob_header_sx("post-header-child", "market-header-child",
_market_header_sx(ctx))
return sx_call("market-browse-layout-oob",
oob_header=SxExpr(oob_hdr),
post_header_oob=SxExpr(await _post_header_sx(ctx, oob=True)),
clear_oob=SxExpr(_clear_deeper_oob("post-row", "post-header-child",
"market-row", "market-header-child")))
def _market_mobile(ctx: dict, **kw: Any) -> str:
return _mobile_nav_panel_sx(ctx)
async def _market_admin_full(ctx: dict, **kw: Any) -> str:
from shared.sx.helpers import render_to_sx_with_env
selected = kw.get("selected", "")
return await render_to_sx_with_env("market-admin-layout-full", {},
post_header=SxExpr(await _post_header_sx(ctx)),
market_header=SxExpr(_market_header_sx(ctx)),
admin_header=SxExpr(await _market_admin_header_sx(ctx, selected=selected)))
async def _market_admin_oob(ctx: dict, **kw: Any) -> str:
selected = kw.get("selected", "")
return sx_call("market-admin-layout-oob",
market_header_oob=SxExpr(_market_header_sx(ctx, oob=True)),
admin_oob_header=SxExpr(await _oob_header_sx("market-header-child", "market-admin-header-child",
await _market_admin_header_sx(ctx, selected=selected))),
clear_oob=SxExpr(_clear_deeper_oob("post-row", "post-header-child",
"market-row", "market-header-child",
"market-admin-row", "market-admin-header-child")))

View File

@@ -50,6 +50,15 @@ IO_PRIMITIVES: frozenset[str] = frozenset({
"select-colours", "select-colours",
"account-nav-ctx", "account-nav-ctx",
"app-rights", "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 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] = { _IO_HANDLERS: dict[str, Any] = {
"frag": _io_frag, "frag": _io_frag,
"query": _io_query, "query": _io_query,
@@ -578,4 +952,13 @@ _IO_HANDLERS: dict[str, Any] = {
"select-colours": _io_select_colours, "select-colours": _io_select_colours,
"account-nav-ctx": _io_account_nav_ctx, "account-nav-ctx": _io_account_nav_ctx,
"app-rights": _io_app_rights, "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,
} }

View File

@@ -1,17 +1,92 @@
;; SX docs layout defcomps — root header via ~root-header-auto, ;; SX docs layout defcomps — fully self-contained via IO primitives.
;; sx-specific headers passed as &key params. ;; Registered via register_sx_layout in __init__.py.
;; --- SX home layout: root + sx menu row --- ;; --- Main nav defcomp: static nav items from MAIN_NAV ---
;; @css aria-selected:bg-violet-200 aria-selected:text-violet-900
(defcomp ~sx-layout-full (&key sx-row) (defcomp ~sx-main-nav (&key section)
(let* ((sc "aria-selected:bg-violet-200 aria-selected:text-violet-900")
(items (list
(dict :label "Docs" :href "/docs/introduction")
(dict :label "Reference" :href "/reference/")
(dict :label "Protocols" :href "/protocols/wire-format")
(dict :label "Examples" :href "/examples/click-to-load")
(dict :label "Essays" :href "/essays/sx-sucks"))))
(<> (map (lambda (item)
(~nav-link
:href (get item "href")
:label (get item "label")
:is-selected (when (= (get item "label") section) "true")
:select-colours sc))
items))))
;; --- SX header row ---
(defcomp ~sx-header-row (&key nav child oob)
(~menu-row-sx :id "sx-row" :level 1 :colour "violet"
:link-href "/" :link-label "sx"
:link-label-content (~sx-docs-label)
:nav nav
:child-id "sx-header-child"
:child child
:oob oob))
;; --- Sub-row for section pages ---
(defcomp ~sx-sub-row (&key sub-label sub-href sub-nav selected oob)
(~menu-row-sx :id "sx-sub-row" :level 2 :colour "violet"
:link-href sub-href :link-label sub-label
:selected selected
:nav sub-nav
:oob oob))
;; ---------------------------------------------------------------------------
;; SX home layout (root + sx header)
;; ---------------------------------------------------------------------------
(defcomp ~sx-layout-full (&key section)
(<> (~root-header-auto) (<> (~root-header-auto)
sx-row)) (~sx-header-row :nav (~sx-main-nav :section section))))
(defcomp ~sx-layout-oob (&key root-header sx-row) (defcomp ~sx-layout-oob (&key section)
(<> root-header sx-row)) (<> (~sx-header-row
:nav (~sx-main-nav :section section)
:oob true)
(~clear-oob-div :id "sx-header-child")
(~root-header-auto true)))
;; --- SX section layout: root + sx row (with child sub-row) --- (defcomp ~sx-layout-mobile (&key section)
(<> (~mobile-menu-section
:label "sx" :href "/" :level 1 :colour "violet"
:items (~sx-main-nav :section section))
(~root-mobile-auto)))
(defcomp ~sx-section-layout-full (&key sx-row) ;; ---------------------------------------------------------------------------
;; SX section layout (root + sx header + sub-row)
;; ---------------------------------------------------------------------------
(defcomp ~sx-section-layout-full (&key section sub-label sub-href sub-nav selected)
(<> (~root-header-auto) (<> (~root-header-auto)
sx-row)) (~sx-header-row
:nav (~sx-main-nav :section section)
:child (~sx-sub-row :sub-label sub-label :sub-href sub-href
:sub-nav sub-nav :selected selected))))
(defcomp ~sx-section-layout-oob (&key section sub-label sub-href sub-nav selected)
(<> (~sx-sub-row :sub-label sub-label :sub-href sub-href
:sub-nav sub-nav :selected selected :oob true)
(~sx-header-row
:nav (~sx-main-nav :section section)
:oob true)
(~root-header-auto true)))
(defcomp ~sx-section-layout-mobile (&key section sub-label sub-href sub-nav)
(<>
(when sub-nav
(~mobile-menu-section
:label (or sub-label section) :href sub-href :level 2 :colour "violet"
:items sub-nav))
(~mobile-menu-section
:label "sx" :href "/" :level 1 :colour "violet"
:items (~sx-main-nav :section section))
(~root-mobile-auto)))

View File

@@ -1,112 +1,11 @@
"""Layout registration and header/mobile functions for sx docs.""" """SX docs layout registration — all layouts delegate to .sx defcomps."""
from __future__ import annotations from __future__ import annotations
from typing import Any
from .utils import _main_nav_sx, _sx_header_sx, _sub_row_sx
def _register_sx_layouts() -> None: def _register_sx_layouts() -> None:
"""Register the sx docs layout presets.""" """Register the sx docs layout presets."""
from shared.sx.layouts import register_custom_layout from shared.sx.layouts import register_sx_layout
register_custom_layout("sx", _sx_full_headers, _sx_oob_headers, _sx_mobile) register_sx_layout("sx", "sx-layout-full", "sx-layout-oob", "sx-layout-mobile")
register_custom_layout("sx-section", _sx_section_full_headers, _sx_section_oob_headers, _sx_section_mobile) register_sx_layout("sx-section", "sx-section-layout-full",
"sx-section-layout-oob", "sx-section-layout-mobile")
async def _sx_full_headers(ctx: dict, **kw: Any) -> str:
"""Full headers for sx home page: root + sx menu row."""
from shared.sx.helpers import render_to_sx_with_env
from shared.sx.parser import SxExpr
main_nav = _main_nav_sx(kw.get("section"))
sx_row = _sx_header_sx(main_nav)
return await render_to_sx_with_env("sx-layout-full", {},
sx_row=SxExpr(sx_row))
async def _sx_oob_headers(ctx: dict, **kw: Any) -> str:
"""OOB headers for sx home page."""
from shared.sx.helpers import render_to_sx_with_env, oob_header_sx
from shared.sx.parser import SxExpr
main_nav = _main_nav_sx(kw.get("section"))
sx_row = _sx_header_sx(main_nav)
rows = await render_to_sx_with_env("sx-layout-full", {},
sx_row=SxExpr(sx_row))
return await oob_header_sx("root-header-child", "sx-header-child", rows)
async def _sx_section_full_headers(ctx: dict, **kw: Any) -> str:
"""Full headers for sx section pages: root + sx row + sub row."""
from shared.sx.helpers import render_to_sx_with_env
from shared.sx.parser import SxExpr
section = kw.get("section", "")
sub_label = kw.get("sub_label", section)
sub_href = kw.get("sub_href", "/")
sub_nav = kw.get("sub_nav", "")
selected = kw.get("selected", "")
main_nav = _main_nav_sx(section)
sub_row = _sub_row_sx(sub_label, sub_href, sub_nav, selected)
sx_row = _sx_header_sx(main_nav, child=sub_row)
return await render_to_sx_with_env("sx-section-layout-full", {},
sx_row=SxExpr(sx_row))
async def _sx_section_oob_headers(ctx: dict, **kw: Any) -> str:
"""OOB headers for sx section pages."""
from shared.sx.helpers import render_to_sx_with_env, oob_header_sx
from shared.sx.parser import SxExpr
section = kw.get("section", "")
sub_label = kw.get("sub_label", section)
sub_href = kw.get("sub_href", "/")
sub_nav = kw.get("sub_nav", "")
selected = kw.get("selected", "")
main_nav = _main_nav_sx(section)
sub_row = _sub_row_sx(sub_label, sub_href, sub_nav, selected)
sx_row = _sx_header_sx(main_nav, child=sub_row)
rows = await render_to_sx_with_env("sx-section-layout-full", {},
sx_row=SxExpr(sx_row))
return await oob_header_sx("root-header-child", "sx-header-child", rows)
async def _sx_mobile(ctx: dict, **kw: Any) -> str:
"""Mobile menu for sx home page: main nav + root."""
from shared.sx.helpers import (
mobile_menu_sx, mobile_root_nav_sx, sx_call, SxExpr,
)
main_nav = _main_nav_sx(kw.get("section"))
return mobile_menu_sx(
sx_call("mobile-menu-section",
label="sx", href="/", level=1, colour="violet",
items=SxExpr(main_nav)),
await mobile_root_nav_sx(ctx),
)
async def _sx_section_mobile(ctx: dict, **kw: Any) -> str:
"""Mobile menu for sx section pages: sub nav + main nav + root."""
from shared.sx.helpers import (
mobile_menu_sx, mobile_root_nav_sx, sx_call, SxExpr,
)
section = kw.get("section", "")
sub_label = kw.get("sub_label", section)
sub_href = kw.get("sub_href", "/")
sub_nav = kw.get("sub_nav", "")
main_nav = _main_nav_sx(section)
parts = []
if sub_nav:
parts.append(sx_call("mobile-menu-section",
label=sub_label, href=sub_href, level=2, colour="violet",
items=SxExpr(sub_nav)))
parts.append(sx_call("mobile-menu-section",
label="sx", href="/", level=1, colour="violet",
items=SxExpr(main_nav)))
parts.append(await mobile_root_nav_sx(ctx))
return mobile_menu_sx(*parts)