diff --git a/blog/sx/layouts.sx b/blog/sx/layouts.sx index a1fe5e5..2521386 100644 --- a/blog/sx/layouts.sx +++ b/blog/sx/layouts.sx @@ -1,4 +1,5 @@ ;; 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) --- @@ -7,28 +8,161 @@ :link-label-content (div) :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 () (<> (~root-header-auto) (~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) - 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) - 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) - (<> settings-header-oob sub-header-oob)) +(defcomp ~blog-cache-layout-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 () (let* ((sc (select-colours)) diff --git a/blog/sxc/pages/layouts.py b/blog/sxc/pages/layouts.py index 002fbbd..2c9dd66 100644 --- a/blog/sxc/pages/layouts.py +++ b/blog/sxc/pages/layouts.py @@ -1,203 +1,19 @@ -"""Blog layout functions for defpage rendering.""" +"""Blog layout registration — all layouts delegate to .sx defcomps.""" 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: - from shared.sx.layouts import register_custom_layout - register_custom_layout("blog", _blog_full, _blog_oob) - register_custom_layout("blog-settings", _settings_full, _settings_oob, - mobile_fn=_settings_mobile) - register_custom_layout("blog-cache", _cache_full, _cache_oob) - register_custom_layout("blog-snippets", _snippets_full, _snippets_oob) - register_custom_layout("blog-menu-items", _menu_items_full, _menu_items_oob) - register_custom_layout("blog-tag-groups", _tag_groups_full, _tag_groups_oob) - register_custom_layout("blog-tag-group-edit", - _tag_group_edit_full, _tag_group_edit_oob) - - -# --- Blog layout (root + blog header) --- - -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)) + from shared.sx.layouts import register_sx_layout + register_sx_layout("blog", "blog-layout-full", "blog-layout-oob") + register_sx_layout("blog-settings", "blog-settings-layout-full", + "blog-settings-layout-oob", "blog-settings-layout-mobile") + register_sx_layout("blog-cache", "blog-cache-layout-full", + "blog-cache-layout-oob") + register_sx_layout("blog-snippets", "blog-snippets-layout-full", + "blog-snippets-layout-oob") + register_sx_layout("blog-menu-items", "blog-menu-items-layout-full", + "blog-menu-items-layout-oob") + register_sx_layout("blog-tag-groups", "blog-tag-groups-layout-full", + "blog-tag-groups-layout-oob") + register_sx_layout("blog-tag-group-edit", "blog-tag-group-edit-layout-full", + "blog-tag-group-edit-layout-oob") diff --git a/cart/services/cart_page.py b/cart/services/cart_page.py index a144108..1b6372e 100644 --- a/cart/services/cart_page.py +++ b/cart/services/cart_page.py @@ -172,6 +172,45 @@ class CartPageService: "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): from shared.sx.page import get_template_context diff --git a/cart/sx/layouts.sx b/cart/sx/layouts.sx index 1bdfaf9..93c2a23 100644 --- a/cart/sx/layouts.sx +++ b/cart/sx/layouts.sx @@ -1,25 +1,78 @@ ;; 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) (~header-child-sx - :inner (<> cart-row - (~header-child-sx :id "cart-header-child" :inner page-cart-row))))) + :inner (~cart-page-header-auto)))) -(defcomp ~cart-page-layout-oob (&key root-header-oob cart-row-oob page-cart-row) - (<> (~oob-header-sx :parent-id "cart-header-child" :row page-cart-row) - cart-row-oob - root-header-oob)) +(defcomp ~cart-page-layout-oob () + (<> (~cart-page-header-oob) + (~root-header-auto true))) -;; --- 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) - 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) (<> (~root-header-auto) @@ -35,7 +88,9 @@ :row (~orders-header-row :list-url list-url)) (~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) (<> (~root-header-auto) diff --git a/cart/sxc/pages/cart.sx b/cart/sxc/pages/cart.sx index e4e83c7..6b3ffd0 100644 --- a/cart/sxc/pages/cart.sx +++ b/cart/sxc/pages/cart.sx @@ -32,12 +32,13 @@ :path "//admin/" :auth :admin :layout :cart-admin + :data (service "cart-page" "admin-data") :content (~cart-admin-content)) (defpage cart-payments :path "//admin/payments/" :auth :admin :layout (:cart-admin :selected "payments") - :data (service "cart-page" "payments-data") + :data (service "cart-page" "payments-admin-data") :content (~cart-payments-content :page-config page-config)) diff --git a/cart/sxc/pages/layouts.py b/cart/sxc/pages/layouts.py index 536257f..53e20f7 100644 --- a/cart/sxc/pages/layouts.py +++ b/cart/sxc/pages/layouts.py @@ -1,135 +1,8 @@ -"""Cart layout registration and header builders.""" +"""Cart layout registration — all layouts delegate to .sx defcomps.""" from __future__ import annotations -from typing import Any - -from shared.sx.parser import SxExpr - def _register_cart_layouts() -> None: - from shared.sx.layouts import register_custom_layout - register_custom_layout("cart-page", _cart_page_full, _cart_page_oob) - register_custom_layout("cart-admin", _cart_admin_full, _cart_admin_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) + from shared.sx.layouts import register_sx_layout + register_sx_layout("cart-page", "cart-page-layout-full", "cart-page-layout-oob") + register_sx_layout("cart-admin", "cart-admin-layout-full", "cart-admin-layout-oob") diff --git a/events/sx/layouts.sx b/events/sx/layouts.sx index 81ff1a8..a97faf9 100644 --- a/events/sx/layouts.sx +++ b/events/sx/layouts.sx @@ -1,96 +1,405 @@ -;; Events layout defcomps — root header via ~root-header-auto, -;; events-specific headers passed as &key params. +;; Events layout defcomps — fully self-contained via IO primitives. +;; 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 - calendar-header calendar-admin-header) +(defmacro ~events-calendar-header-auto (oob) + "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) - post-header - (~header-child-sx :inner (<> admin-header calendar-header calendar-admin-header)))) + (~header-child-sx + :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) - (<> admin-oob cal-oob cal-admin-oob-wrap clear-oob)) +(defcomp ~events-cal-admin-layout-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) - (<> 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) +(defcomp ~events-slots-layout-full () (<> (~root-header-auto) - post-header - (~header-child-sx :inner (<> admin-header calendar-header calendar-admin-header slot-header)))) + (~header-child-sx + :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) - (<> admin-oob cal-admin-oob slot-oob-wrap clear-oob)) +(defcomp ~events-slots-layout-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 - calendar-header day-header day-admin-header) +(defcomp ~events-slot-layout-full () (<> (~root-header-auto) - post-header - (~header-child-sx :inner (<> admin-header calendar-header day-header day-admin-header)))) + (~header-child-sx + :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) - (<> admin-oob cal-oob day-admin-oob-wrap clear-oob)) +(defcomp ~events-slot-layout-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) - (~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) - (<> day-oob entry-oob-wrap clear-oob)) +(defcomp ~events-day-admin-layout-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 - calendar-header day-header - entry-header entry-admin-header) +(defcomp ~events-entry-layout-full () (<> (~root-header-auto) - post-header - (~header-child-sx :inner (<> admin-header calendar-header day-header - entry-header entry-admin-header)))) + (~header-child-sx + :inner (<> (~post-header-auto nil) + (~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) - (<> admin-oob entry-oob entry-admin-oob-wrap clear-oob)) +(defcomp ~events-entry-layout-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 - entry-header entry-admin-header - ticket-types-header) +(defcomp ~events-entry-admin-layout-full () (<> (~root-header-auto) - (~header-child-sx :inner (<> post-header calendar-header day-header - entry-header entry-admin-header ticket-types-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-entry-header-auto nil) + (~events-entry-admin-header-auto nil))))) -(defcomp ~events-ticket-types-layout-oob (&key entry-admin-oob ticket-types-oob-wrap) - (<> entry-admin-oob ticket-types-oob-wrap)) +(defcomp ~events-entry-admin-layout-oob () + (<> (~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 - entry-header entry-admin-header - ticket-types-header ticket-type-header) +(defcomp ~events-ticket-types-layout-full () (<> (~root-header-auto) - (~header-child-sx :inner (<> post-header calendar-header day-header - entry-header entry-admin-header - ticket-types-header ticket-type-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))))) -(defcomp ~events-ticket-type-layout-oob (&key ticket-types-oob ticket-type-oob-wrap) - (<> ticket-types-oob ticket-type-oob-wrap)) +(defcomp ~events-ticket-types-layout-oob () + (<> (~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) - (~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) - (<> post-oob markets-oob-wrap)) +(defcomp ~events-ticket-type-layout-oob () + (<> (~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))) diff --git a/events/sxc/pages/helpers.py b/events/sxc/pages/helpers.py index ddf8dab..2925ca8 100644 --- a/events/sxc/pages/helpers.py +++ b/events/sxc/pages/helpers.py @@ -5,30 +5,22 @@ from typing import Any from shared.sx.helpers import sx_call -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, - _calendars_main_panel_sx, _calendar_admin_main_panel_html, _day_admin_main_panel_html, _markets_main_panel_html, ) from .entries import ( - _entry_header_html, _entry_main_panel_html, + _entry_main_panel_html, _entry_nav_html, - _entry_admin_header_html, _entry_admin_main_panel_html, + _entry_admin_main_panel_html, ) from .tickets import ( _tickets_main_panel_html, _ticket_detail_panel_html, _ticket_admin_main_panel_html, - _ticket_types_header_html, _ticket_type_header_html, render_ticket_type_main_panel, render_ticket_types_table, ) -from .slots import ( - _slot_header_html, render_slot_main_panel, render_slots_table, -) +from .slots import render_slot_main_panel, render_slots_table # --------------------------------------------------------------------------- @@ -43,6 +35,40 @@ def _add_to_defpage_ctx(**kwargs: Any) -> None: 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: """Load calendar into g.calendar if not already present.""" from quart import g, abort @@ -63,6 +89,7 @@ async def _ensure_calendar(calendar_slug: str | None) -> None: g.calendar = cal g.calendar_slug = calendar_slug _add_to_defpage_ctx(calendar=cal) + _ensure_post_defpage_ctx() 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: - 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))), - ) + from shared.sx.layouts import register_sx_layout + register_sx_layout("events-calendar-admin", + "events-cal-admin-layout-full", "events-cal-admin-layout-oob") + register_sx_layout("events-slots", + "events-slots-layout-full", "events-slots-layout-oob") + register_sx_layout("events-slot", + "events-slot-layout-full", "events-slot-layout-oob") + register_sx_layout("events-day-admin", + "events-day-admin-layout-full", "events-day-admin-layout-oob") + register_sx_layout("events-entry", + "events-entry-layout-full", "events-entry-layout-oob") + register_sx_layout("events-entry-admin", + "events-entry-admin-layout-full", "events-entry-admin-layout-oob") + register_sx_layout("events-ticket-types", + "events-ticket-types-layout-full", "events-ticket-types-layout-oob") + register_sx_layout("events-ticket-type", + "events-ticket-type-layout-full", "events-ticket-type-layout-oob") + register_sx_layout("events-markets", + "events-markets-layout-full", "events-markets-layout-oob") # --------------------------------------------------------------------------- @@ -508,6 +288,7 @@ def _register_events_helpers() -> None: async def _h_calendar_admin_content(calendar_slug=None, **kw): await _ensure_calendar(calendar_slug) + await _ensure_container_nav_defpage_ctx() from shared.sx.page import get_template_context ctx = await get_template_context() 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): await _ensure_calendar(calendar_slug) + await _ensure_container_nav_defpage_ctx() if year is not None: await _ensure_day_data(int(year), int(month), int(day)) 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): from quart import g await _ensure_calendar(calendar_slug) + await _ensure_container_nav_defpage_ctx() calendar = getattr(g, "calendar", None) from bp.slots.services.slots import list_slots as svc_list_slots 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): from quart import g, abort await _ensure_calendar(calendar_slug) + await _ensure_container_nav_defpage_ctx() 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 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): await _ensure_calendar(calendar_slug) + await _ensure_container_nav_defpage_ctx() await _ensure_entry_context(entry_id) from shared.sx.page import 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): + _ensure_post_defpage_ctx() from shared.sx.page import get_template_context ctx = await get_template_context() return _markets_main_panel_html(ctx) diff --git a/events/sxc/pages/layouts.py b/events/sxc/pages/layouts.py deleted file mode 100644 index 5ebe088..0000000 --- a/events/sxc/pages/layouts.py +++ /dev/null @@ -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))), - ) diff --git a/federation/sx/layouts.sx b/federation/sx/layouts.sx index 03a3a0d..1549e03 100644 --- a/federation/sx/layouts.sx +++ b/federation/sx/layouts.sx @@ -1,17 +1,17 @@ -;; Federation layout defcomps — read ctx values from env free variables. -;; `actor` is injected into env by the layout registration in __init__.py. +;; Federation layout defcomps — fully self-contained via IO primitives. +;; Registered via register_sx_layout("social", ...) in __init__.py. ;; Full page: root header + social header in header-child (defcomp ~social-layout-full () (<> (~root-header-auto) (~header-child-sx :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 (defcomp ~social-layout-oob () (<> (~oob-header-sx :parent-id "root-header-child" :row (~federation-social-header - :nav (~federation-social-nav :actor actor))) + :nav (~federation-social-nav :actor (federation-actor-ctx)))) (~root-header-auto true))) diff --git a/federation/sxc/pages/__init__.py b/federation/sxc/pages/__init__.py index 983922b..4250df8 100644 --- a/federation/sxc/pages/__init__.py +++ b/federation/sxc/pages/__init__.py @@ -15,6 +15,5 @@ def _load_federation_page_files() -> None: def _register_federation_layouts() -> None: - from shared.sx.layouts import register_custom_layout - from .utils import _social_full, _social_oob - register_custom_layout("social", _social_full, _social_oob) + from shared.sx.layouts import register_sx_layout + register_sx_layout("social", "social-layout-full", "social-layout-oob") diff --git a/federation/sxc/pages/utils.py b/federation/sxc/pages/utils.py index 9e15547..685b378 100644 --- a/federation/sxc/pages/utils.py +++ b/federation/sxc/pages/utils.py @@ -1,8 +1,6 @@ """Federation page utilities — serializers, actor helpers, social page builder.""" from __future__ import annotations -from typing import Any - def _serialize_actor(actor) -> dict | None: """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, 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 markupsafe import escape @@ -47,22 +45,3 @@ def _require_actor(): if not actor: abort(403, "You need to choose a federation username first") 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) diff --git a/market/app.py b/market/app.py index a0c7868..28555a9 100644 --- a/market/app.py +++ b/market/app.py @@ -228,6 +228,11 @@ def create_app() -> "Quart": ("market", "container-nav", nav_params), ], required=False) 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 # --- oEmbed endpoint --- diff --git a/market/sx/layouts.sx b/market/sx/layouts.sx index 2893b51..a55828c 100644 --- a/market/sx/layouts.sx +++ b/market/sx/layouts.sx @@ -1,42 +1,105 @@ -;; Market layout defcomps — root header via ~root-header-auto, -;; market-specific headers passed as &key params. +;; Market layout defcomps — fully self-contained via IO primitives. +;; 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) - (~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) - (<> oob-header post-header-oob clear-oob)) +(defcomp ~market-browse-layout-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) (<> (~root-header-auto) (~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) (<> (~root-header-auto) (~header-child-sx :inner (<> post-header market-header product-header admin-header)))) -;; --- Market admin layout: root + post + market + market-admin --- - -(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 --- - +;; OOB wrappers (defcomp ~market-oob-wrap (&key parts) (<> parts)) -;; --- Content wrappers --- - +;; Content wrappers (defcomp ~market-content-padded (&key content) (<> content (div :class "pb-8"))) diff --git a/market/sxc/pages/layouts.py b/market/sxc/pages/layouts.py index e0d4c76..e1bf33e 100644 --- a/market/sxc/pages/layouts.py +++ b/market/sxc/pages/layouts.py @@ -4,15 +4,9 @@ from __future__ import annotations from typing import Any from shared.sx.parser import SxExpr -from shared.sx.helpers import ( - sx_call, - post_header_sx as _post_header_sx, - post_admin_header_sx, - oob_header_sx as _oob_header_sx, - header_child_sx, -) +from shared.sx.helpers import sx_call -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: - from shared.sx.layouts import register_custom_layout - register_custom_layout("market", _market_full, _market_oob, _market_mobile) - register_custom_layout("market-admin", _market_admin_full, _market_admin_oob) - - -async def _market_full(ctx: dict, **kw: Any) -> str: - 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"))) + from shared.sx.layouts import register_sx_layout + register_sx_layout("market", + "market-browse-layout-full", "market-browse-layout-oob", + "market-browse-layout-mobile") + register_sx_layout("market-admin", + "market-admin-layout-full", "market-admin-layout-oob") diff --git a/shared/sx/primitives_io.py b/shared/sx/primitives_io.py index eeab35d..63fe8e3 100644 --- a/shared/sx/primitives_io.py +++ b/shared/sx/primitives_io.py @@ -50,6 +50,15 @@ IO_PRIMITIVES: frozenset[str] = frozenset({ "select-colours", "account-nav-ctx", "app-rights", + "federation-actor-ctx", + "request-view-args", + "cart-page-ctx", + "events-calendar-ctx", + "events-day-ctx", + "events-entry-ctx", + "events-slot-ctx", + "events-ticket-type-ctx", + "market-header-ctx", }) @@ -557,6 +566,371 @@ async def _io_post_header_ctx( return result +async def _io_cart_page_ctx( + args: list[Any], kwargs: dict[str, Any], ctx: RequestContext +) -> dict[str, Any]: + """``(cart-page-ctx)`` → dict with cart page header values. + + Reads ``g.page_post`` (set by cart's before_request) and returns + slug, title, feature-image, and cart-url for the page cart header. + """ + from quart import g + from .types import NIL + from shared.infrastructure.urls import app_url + + page_post = getattr(g, "page_post", None) + if not page_post: + return {"slug": "", "title": "", "feature-image": NIL, "cart-url": "/"} + + slug = getattr(page_post, "slug", "") or "" + title = (getattr(page_post, "title", "") or "")[:160] + feature_image = getattr(page_post, "feature_image", None) or NIL + + return { + "slug": slug, + "title": title, + "feature-image": feature_image, + "page-cart-url": app_url("cart", f"/{slug}/"), + "cart-url": app_url("cart", "/"), + } + + +async def _io_federation_actor_ctx( + args: list[Any], kwargs: dict[str, Any], ctx: RequestContext +) -> dict[str, Any] | None: + """``(federation-actor-ctx)`` → serialized actor dict or None. + + Reads ``g._social_actor`` (set by federation social blueprint's + before_request hook) and serializes to a dict for .sx components. + """ + from quart import g + actor = getattr(g, "_social_actor", None) + if not actor: + return None + return { + "id": actor.id, + "preferred_username": actor.preferred_username, + "display_name": getattr(actor, "display_name", None), + "icon_url": getattr(actor, "icon_url", None), + "actor_url": getattr(actor, "actor_url", ""), + } + + +async def _io_request_view_args( + args: list[Any], kwargs: dict[str, Any], ctx: RequestContext +) -> Any: + """``(request-view-args "key")`` → request.view_args[key].""" + if not args: + raise ValueError("request-view-args requires a key") + from quart import request + key = str(args[0]) + return (request.view_args or {}).get(key) + + +async def _io_events_calendar_ctx( + args: list[Any], kwargs: dict[str, Any], ctx: RequestContext +) -> dict[str, Any]: + """``(events-calendar-ctx)`` → dict with events calendar header values. + + Reads ``g.calendar`` or ``g._defpage_ctx["calendar"]`` and returns + slug, name, description for the calendar header row. + """ + from quart import g + cal = getattr(g, "calendar", None) + if not cal: + dctx = getattr(g, "_defpage_ctx", None) or {} + cal = dctx.get("calendar") + if not cal: + return {"slug": ""} + return { + "slug": getattr(cal, "slug", "") or "", + "name": getattr(cal, "name", "") or "", + "description": getattr(cal, "description", "") or "", + } + + +async def _io_events_day_ctx( + args: list[Any], kwargs: dict[str, Any], ctx: RequestContext +) -> dict[str, Any]: + """``(events-day-ctx)`` → dict with events day header values. + + Reads ``g.day_date``, ``g.calendar``, confirmed entries from + ``g._defpage_ctx``. Pre-builds the confirmed entries nav as SxExpr. + """ + from quart import g, url_for + from .types import NIL + from .parser import SxExpr + + dctx = getattr(g, "_defpage_ctx", None) or {} + cal = getattr(g, "calendar", None) or dctx.get("calendar") + day_date = dctx.get("day_date") or getattr(g, "day_date", None) + if not cal or not day_date: + return {"date-str": ""} + + cal_slug = getattr(cal, "slug", "") or "" + + # Build confirmed entries nav + confirmed = dctx.get("confirmed_entries") or [] + rights = getattr(g, "rights", None) or {} + is_admin = ( + rights.get("admin", False) + if isinstance(rights, dict) + else getattr(rights, "admin", False) + ) + + from .helpers import sx_call + nav_parts: list[str] = [] + if confirmed: + entry_links = [] + for entry in confirmed: + href = url_for( + "calendar.day.calendar_entries.calendar_entry.get", + calendar_slug=cal_slug, + year=day_date.year, month=day_date.month, day=day_date.day, + entry_id=entry.id, + ) + start = entry.start_at.strftime("%H:%M") if entry.start_at else "" + end = ( + f" \u2013 {entry.end_at.strftime('%H:%M')}" + if entry.end_at else "" + ) + entry_links.append(sx_call( + "events-day-entry-link", + href=href, name=entry.name, time_str=f"{start}{end}", + )) + inner = "".join(entry_links) + nav_parts.append(sx_call( + "events-day-entries-nav", inner=SxExpr(inner), + )) + + if is_admin and day_date: + admin_href = url_for( + "defpage_day_admin", calendar_slug=cal_slug, + year=day_date.year, month=day_date.month, day=day_date.day, + ) + nav_parts.append(sx_call("nav-link", href=admin_href, icon="fa fa-cog")) + + return { + "date-str": day_date.strftime("%A %d %B %Y"), + "year": day_date.year, + "month": day_date.month, + "day": day_date.day, + "nav": SxExpr("".join(nav_parts)) if nav_parts else NIL, + } + + +async def _io_events_entry_ctx( + args: list[Any], kwargs: dict[str, Any], ctx: RequestContext +) -> dict[str, Any]: + """``(events-entry-ctx)`` → dict with events entry header values. + + Reads ``g.entry``, ``g.calendar``, and entry_posts from + ``g._defpage_ctx``. Pre-builds entry nav (posts + admin link) as SxExpr. + """ + from quart import g, url_for + from .types import NIL + from .parser import SxExpr + + dctx = getattr(g, "_defpage_ctx", None) or {} + cal = getattr(g, "calendar", None) or dctx.get("calendar") + entry = getattr(g, "entry", None) or dctx.get("entry") + if not cal or not entry: + return {"id": ""} + + cal_slug = getattr(cal, "slug", "") or "" + day = dctx.get("day") + month = dctx.get("month") + year = dctx.get("year") + + # Times + start = entry.start_at + end = entry.end_at + time_str = "" + if start: + time_str = start.strftime("%H:%M") + if end: + time_str += f" \u2192 {end.strftime('%H:%M')}" + + link_href = url_for( + "calendar.day.calendar_entries.calendar_entry.get", + calendar_slug=cal_slug, + year=year, month=month, day=day, entry_id=entry.id, + ) + + # Build nav: associated posts + admin link + entry_posts = dctx.get("entry_posts") or [] + rights = getattr(g, "rights", None) or {} + is_admin = ( + rights.get("admin", False) + if isinstance(rights, dict) + else getattr(rights, "admin", False) + ) + + from .helpers import sx_call + from shared.infrastructure.urls import app_url + + nav_parts: list[str] = [] + if entry_posts: + post_links = "" + for ep in entry_posts: + ep_slug = getattr(ep, "slug", "") + ep_title = getattr(ep, "title", "") + feat = getattr(ep, "feature_image", None) + href = app_url("blog", f"/{ep_slug}/") + if feat: + img_html = sx_call("events-post-img", src=feat, alt=ep_title) + else: + img_html = sx_call("events-post-img-placeholder") + post_links += sx_call( + "events-entry-nav-post-link", + href=href, img=SxExpr(img_html), title=ep_title, + ) + nav_parts.append( + sx_call("events-entry-posts-nav-oob", items=SxExpr(post_links)) + .replace(' :hx-swap-oob "true"', '') + ) + + if is_admin: + admin_url = url_for( + "calendar.day.calendar_entries.calendar_entry.admin.admin", + calendar_slug=cal_slug, + day=day, month=month, year=year, entry_id=entry.id, + ) + nav_parts.append(sx_call("events-entry-admin-link", href=admin_url)) + + # Entry admin nav (ticket_types link) + admin_href = url_for( + "calendar.day.calendar_entries.calendar_entry.admin.admin", + calendar_slug=cal_slug, + day=day, month=month, year=year, entry_id=entry.id, + ) if is_admin else "" + + ticket_types_href = url_for( + "calendar.day.calendar_entries.calendar_entry.ticket_types.get", + calendar_slug=cal_slug, entry_id=entry.id, + year=year, month=month, day=day, + ) + + from quart import current_app + select_colours = current_app.jinja_env.globals.get("select_colours", "") + + return { + "id": str(entry.id), + "name": entry.name or "", + "time-str": time_str, + "link-href": link_href, + "nav": SxExpr("".join(nav_parts)) if nav_parts else NIL, + "admin-href": admin_href, + "ticket-types-href": ticket_types_href, + "is-admin": is_admin, + "select-colours": select_colours, + } + + +async def _io_events_slot_ctx( + args: list[Any], kwargs: dict[str, Any], ctx: RequestContext +) -> dict[str, Any]: + """``(events-slot-ctx)`` → dict with events slot header values.""" + from quart import g + dctx = getattr(g, "_defpage_ctx", None) or {} + slot = getattr(g, "slot", None) or dctx.get("slot") + if not slot: + return {"name": ""} + return { + "name": getattr(slot, "name", "") or "", + "description": getattr(slot, "description", "") or "", + } + + +async def _io_events_ticket_type_ctx( + args: list[Any], kwargs: dict[str, Any], ctx: RequestContext +) -> dict[str, Any]: + """``(events-ticket-type-ctx)`` → dict with ticket type header values.""" + from quart import g, url_for + + dctx = getattr(g, "_defpage_ctx", None) or {} + cal = getattr(g, "calendar", None) or dctx.get("calendar") + entry = getattr(g, "entry", None) or dctx.get("entry") + ticket_type = getattr(g, "ticket_type", None) or dctx.get("ticket_type") + if not cal or not entry or not ticket_type: + return {"id": ""} + + cal_slug = getattr(cal, "slug", "") or "" + day = dctx.get("day") + month = dctx.get("month") + year = dctx.get("year") + + link_href = url_for( + "calendar.day.calendar_entries.calendar_entry.ticket_types.ticket_type.get", + calendar_slug=cal_slug, year=year, month=month, day=day, + entry_id=entry.id, ticket_type_id=ticket_type.id, + ) + + return { + "id": str(ticket_type.id), + "name": getattr(ticket_type, "name", "") or "", + "link-href": link_href, + } + + +async def _io_market_header_ctx( + args: list[Any], kwargs: dict[str, Any], ctx: RequestContext +) -> dict[str, Any]: + """``(market-header-ctx)`` → dict with market header values. + + Pre-builds desktop-nav and mobile-nav as SxExpr strings using + the existing Python helper functions in sxc.pages.layouts. + """ + from quart import g, url_for + from shared.config import config as get_config + from .parser import SxExpr + + cfg = get_config() + market_title = cfg.get("market_title", "") + link_href = url_for("defpage_market_home") + + # Get categories if market is loaded + market = getattr(g, "market", None) + categories = {} + if market: + from bp.browse.services.nav import get_nav + nav_data = await get_nav(g.s, market_id=market.id) + categories = nav_data.get("cats", {}) + + # Build minimal ctx for existing helper functions + select_colours = getattr(g, "select_colours", "") + if not select_colours: + from quart import current_app + select_colours = current_app.jinja_env.globals.get("select_colours", "") + rights = getattr(g, "rights", None) or {} + + mini_ctx: dict[str, Any] = { + "market_title": market_title, + "top_slug": "", + "sub_slug": "", + "categories": categories, + "qs": "", + "hx_select_search": "#main-panel", + "select_colours": select_colours, + "rights": rights, + "category_label": "", + } + + # Pre-build nav using existing helper functions (lazy import from market service) + from sxc.pages.layouts import _desktop_category_nav_sx, _mobile_nav_panel_sx + desktop_nav = _desktop_category_nav_sx(mini_ctx, categories, "", "#main-panel") + mobile_nav = _mobile_nav_panel_sx(mini_ctx) + + return { + "market-title": market_title, + "link-href": link_href, + "top-slug": "", + "sub-slug": "", + "desktop-nav": SxExpr(desktop_nav) if desktop_nav else "", + "mobile-nav": SxExpr(mobile_nav) if mobile_nav else "", + } + + _IO_HANDLERS: dict[str, Any] = { "frag": _io_frag, "query": _io_query, @@ -578,4 +952,13 @@ _IO_HANDLERS: dict[str, Any] = { "select-colours": _io_select_colours, "account-nav-ctx": _io_account_nav_ctx, "app-rights": _io_app_rights, + "federation-actor-ctx": _io_federation_actor_ctx, + "request-view-args": _io_request_view_args, + "cart-page-ctx": _io_cart_page_ctx, + "events-calendar-ctx": _io_events_calendar_ctx, + "events-day-ctx": _io_events_day_ctx, + "events-entry-ctx": _io_events_entry_ctx, + "events-slot-ctx": _io_events_slot_ctx, + "events-ticket-type-ctx": _io_events_ticket_type_ctx, + "market-header-ctx": _io_market_header_ctx, } diff --git a/sx/sx/layouts.sx b/sx/sx/layouts.sx index eeecb56..2f8a4a1 100644 --- a/sx/sx/layouts.sx +++ b/sx/sx/layouts.sx @@ -1,17 +1,92 @@ -;; SX docs layout defcomps — root header via ~root-header-auto, -;; sx-specific headers passed as &key params. +;; SX docs layout defcomps — fully self-contained via IO primitives. +;; 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) - sx-row)) + (~sx-header-row :nav (~sx-main-nav :section section)))) -(defcomp ~sx-layout-oob (&key root-header sx-row) - (<> root-header sx-row)) +(defcomp ~sx-layout-oob (&key section) + (<> (~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) - 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))) diff --git a/sx/sxc/pages/layouts.py b/sx/sxc/pages/layouts.py index ebd22db..aee86a9 100644 --- a/sx/sxc/pages/layouts.py +++ b/sx/sxc/pages/layouts.py @@ -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 typing import Any - -from .utils import _main_nav_sx, _sx_header_sx, _sub_row_sx def _register_sx_layouts() -> None: """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_custom_layout("sx-section", _sx_section_full_headers, _sx_section_oob_headers, _sx_section_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) + register_sx_layout("sx", "sx-layout-full", "sx-layout-oob", "sx-layout-mobile") + register_sx_layout("sx-section", "sx-section-layout-full", + "sx-section-layout-oob", "sx-section-layout-mobile")