Enable cross-subdomain htmx and purify layout to sexp
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 1m15s
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 1m15s
- Disable htmx selfRequestsOnly, add CORS headers for *.rose-ash.com - Remove same-origin guards from ~menu-row and ~nav-link htmx attrs - Convert ~app-layout from string-concatenated HTML to pure sexp tree - Extract ~app-head component, replace ~app-shell with inline structure - Convert hamburger SVG from Python HTML constant to ~hamburger sexp component - Fix cross-domain fragment URLs (events_url, market_url) - Fix starts-with? primitive to handle nil values - Fix duplicate admin menu rows on OOB swaps - Add calendar admin nav links (slots, description) - Convert slots page from Jinja to sexp rendering - Disable page caching in development mode - Backfill migration to clean orphaned container_relations Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -77,9 +77,23 @@ def register():
|
||||
|
||||
@bp.context_processor
|
||||
async def inject_root():
|
||||
from shared.infrastructure.fragments import fetch_fragment
|
||||
|
||||
container_nav_html = ""
|
||||
post_data = getattr(g, "post_data", None)
|
||||
if post_data:
|
||||
post_id = post_data["post"]["id"]
|
||||
post_slug = post_data["post"]["slug"]
|
||||
container_nav_html = await fetch_fragment("relations", "container-nav", params={
|
||||
"container_type": "page",
|
||||
"container_id": str(post_id),
|
||||
"post_slug": post_slug,
|
||||
"exclude": "page->calendar",
|
||||
})
|
||||
|
||||
return {
|
||||
"calendar": getattr(g, "calendar", None),
|
||||
"container_nav_html": container_nav_html,
|
||||
}
|
||||
|
||||
# ---------- Pages ----------
|
||||
|
||||
@@ -44,16 +44,14 @@ def register():
|
||||
|
||||
@bp.get("/")
|
||||
async def get(**kwargs):
|
||||
from shared.sexp.page import get_template_context
|
||||
from sexp.sexp_components import render_slots_page, render_slots_oob
|
||||
|
||||
tctx = await get_template_context()
|
||||
if not is_htmx_request():
|
||||
# Normal browser request: full page with layout
|
||||
html = await render_template(
|
||||
"_types/slots/index.html",
|
||||
)
|
||||
html = await render_slots_page(tctx)
|
||||
else:
|
||||
|
||||
html = await render_template(
|
||||
"_types/slots/_oob_elements.html",
|
||||
)
|
||||
html = await render_slots_oob(tctx)
|
||||
return await make_response(html)
|
||||
|
||||
|
||||
|
||||
@@ -79,34 +79,31 @@ def _post_header_html(ctx: dict, *, oob: bool = False) -> str:
|
||||
|
||||
|
||||
def _post_nav_html(ctx: dict) -> str:
|
||||
"""Post desktop nav: calendar links + admin gear."""
|
||||
from quart import url_for
|
||||
"""Post desktop nav: calendar links + container nav (markets, etc.)."""
|
||||
from quart import url_for, g
|
||||
|
||||
calendars = ctx.get("calendars") or []
|
||||
rights = ctx.get("rights") or {}
|
||||
is_admin = getattr(rights, "admin", False) if hasattr(rights, "admin") else rights.get("admin", False)
|
||||
hx_select = ctx.get("hx_select_search", "#main-panel")
|
||||
select_colours = ctx.get("select_colours", "")
|
||||
post = ctx.get("post") or {}
|
||||
slug = post.get("slug", "")
|
||||
current_cal_slug = getattr(g, "calendar_slug", None)
|
||||
|
||||
parts = []
|
||||
for cal in calendars:
|
||||
cal_slug = getattr(cal, "slug", "") if hasattr(cal, "slug") else cal.get("slug", "")
|
||||
cal_name = getattr(cal, "name", "") if hasattr(cal, "name") else cal.get("name", "")
|
||||
href = url_for("calendars.calendar.get", calendar_slug=cal_slug)
|
||||
is_sel = (cal_slug == current_cal_slug)
|
||||
parts.append(sexp(
|
||||
'(~nav-link :href h :icon "fa fa-calendar" :label l :select-colours sc)',
|
||||
'(~nav-link :href h :icon "fa fa-calendar" :label l :select-colours sc :is-selected sel)',
|
||||
h=href,
|
||||
l=cal_name,
|
||||
sc=select_colours,
|
||||
sel=is_sel,
|
||||
))
|
||||
if is_admin:
|
||||
admin_href = call_url(ctx, "blog_url", f"/{slug}/admin/")
|
||||
parts.append(
|
||||
f'<a href="{admin_href}" class="flex items-center gap-2 px-3 py-2 rounded">'
|
||||
f'<i class="fa fa-cog" aria-hidden="true"></i></a>'
|
||||
)
|
||||
# Container nav fragments (markets, etc.)
|
||||
container_nav = ctx.get("container_nav_html", "")
|
||||
if container_nav:
|
||||
parts.append(container_nav)
|
||||
|
||||
return "".join(parts)
|
||||
|
||||
|
||||
@@ -114,21 +111,6 @@ def _post_nav_html(ctx: dict) -> str:
|
||||
# Post admin header
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _post_admin_header_html(ctx: dict, *, oob: bool = False) -> str:
|
||||
"""Build the post-admin-level header row."""
|
||||
post = ctx.get("post") or {}
|
||||
slug = post.get("slug", "")
|
||||
link_href = call_url(ctx, "blog_url", f"/{slug}/admin/")
|
||||
|
||||
return sexp(
|
||||
'(~menu-row :id "post-admin-row" :level 2'
|
||||
' :link-href lh :link-label "admin" :icon "fa fa-cog"'
|
||||
' :child-id "post-admin-header-child" :oob oob)',
|
||||
lh=link_href,
|
||||
oob=oob,
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Calendars header
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -347,11 +329,30 @@ def _day_admin_header_html(ctx: dict, *, oob: bool = False) -> str:
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _calendar_admin_header_html(ctx: dict, *, oob: bool = False) -> str:
|
||||
"""Build calendar admin header row."""
|
||||
"""Build calendar admin header row with nav links."""
|
||||
from quart import url_for
|
||||
calendar = ctx.get("calendar")
|
||||
cal_slug = getattr(calendar, "slug", "") if calendar else ""
|
||||
select_colours = ctx.get("select_colours", "")
|
||||
|
||||
nav_parts = []
|
||||
if cal_slug:
|
||||
for endpoint, label in [
|
||||
("calendars.calendar.slots.get", "slots"),
|
||||
("calendars.calendar.admin.calendar_description_edit", "description"),
|
||||
]:
|
||||
href = url_for(endpoint, calendar_slug=cal_slug)
|
||||
nav_parts.append(sexp(
|
||||
'(~nav-link :href h :label l :select-colours sc)',
|
||||
h=href, l=label, sc=select_colours,
|
||||
))
|
||||
|
||||
nav_html = "".join(nav_parts)
|
||||
return sexp(
|
||||
'(~menu-row :id "calendar-admin-row" :level 4'
|
||||
' :link-label "admin" :icon "fa fa-cog"'
|
||||
' :child-id "calendar-admin-header-child" :oob oob)',
|
||||
' :nav-html nh :child-id "calendar-admin-header-child" :oob oob)',
|
||||
nh=nav_html,
|
||||
oob=oob,
|
||||
)
|
||||
|
||||
@@ -1659,7 +1660,7 @@ async def render_calendars_page(ctx: dict) -> str:
|
||||
"""Full page: calendars listing."""
|
||||
content = _calendars_main_panel_html(ctx)
|
||||
hdr = root_header_html(ctx)
|
||||
child = _post_header_html(ctx) + _post_admin_header_html(ctx) + _calendars_header_html(ctx)
|
||||
child = _post_header_html(ctx) + _calendars_header_html(ctx)
|
||||
hdr += sexp('(div :id "root-header-child" :class "w-full" (raw! h))', h=child)
|
||||
return full_page(ctx, header_rows_html=hdr, content_html=content)
|
||||
|
||||
@@ -1667,8 +1668,8 @@ async def render_calendars_page(ctx: dict) -> str:
|
||||
async def render_calendars_oob(ctx: dict) -> str:
|
||||
"""OOB response: calendars listing."""
|
||||
content = _calendars_main_panel_html(ctx)
|
||||
oobs = _post_admin_header_html(ctx, oob=True)
|
||||
oobs += _oob_header_html("post-admin-header-child", "calendars-header-child",
|
||||
oobs = _post_header_html(ctx, oob=True)
|
||||
oobs += _oob_header_html("post-header-child", "calendars-header-child",
|
||||
_calendars_header_html(ctx))
|
||||
return oob_page(ctx, oobs_html=oobs, content_html=content)
|
||||
|
||||
@@ -1681,7 +1682,7 @@ async def render_calendar_page(ctx: dict) -> str:
|
||||
"""Full page: calendar month view."""
|
||||
content = _calendar_main_panel_html(ctx)
|
||||
hdr = root_header_html(ctx)
|
||||
child = _post_header_html(ctx) + _post_admin_header_html(ctx) + _calendar_header_html(ctx)
|
||||
child = _post_header_html(ctx) + _calendar_header_html(ctx)
|
||||
hdr += sexp('(div :id "root-header-child" :class "w-full" (raw! h))', h=child)
|
||||
return full_page(ctx, header_rows_html=hdr, content_html=content)
|
||||
|
||||
@@ -1703,7 +1704,7 @@ async def render_day_page(ctx: dict) -> str:
|
||||
"""Full page: day detail."""
|
||||
content = _day_main_panel_html(ctx)
|
||||
hdr = root_header_html(ctx)
|
||||
child = (_post_header_html(ctx) + _post_admin_header_html(ctx)
|
||||
child = (_post_header_html(ctx)
|
||||
+ _calendar_header_html(ctx) + _day_header_html(ctx))
|
||||
hdr += sexp('(div :id "root-header-child" :class "w-full" (raw! h))', h=child)
|
||||
return full_page(ctx, header_rows_html=hdr, content_html=content)
|
||||
@@ -1726,7 +1727,7 @@ async def render_day_admin_page(ctx: dict) -> str:
|
||||
"""Full page: day admin."""
|
||||
content = _day_admin_main_panel_html(ctx)
|
||||
hdr = root_header_html(ctx)
|
||||
child = (_post_header_html(ctx) + _post_admin_header_html(ctx)
|
||||
child = (_post_header_html(ctx)
|
||||
+ _calendar_header_html(ctx) + _day_header_html(ctx)
|
||||
+ _day_admin_header_html(ctx))
|
||||
hdr += sexp('(div :id "root-header-child" :class "w-full" (raw! h))', h=child)
|
||||
@@ -1750,7 +1751,7 @@ async def render_calendar_admin_page(ctx: dict) -> str:
|
||||
"""Full page: calendar admin."""
|
||||
content = _calendar_admin_main_panel_html(ctx)
|
||||
hdr = root_header_html(ctx)
|
||||
child = (_post_header_html(ctx) + _post_admin_header_html(ctx)
|
||||
child = (_post_header_html(ctx)
|
||||
+ _calendar_header_html(ctx) + _calendar_admin_header_html(ctx))
|
||||
hdr += sexp('(div :id "root-header-child" :class "w-full" (raw! h))', h=child)
|
||||
return full_page(ctx, header_rows_html=hdr, content_html=content)
|
||||
@@ -1765,6 +1766,32 @@ async def render_calendar_admin_oob(ctx: dict) -> str:
|
||||
return oob_page(ctx, oobs_html=oobs, content_html=content)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Slots
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
async def render_slots_page(ctx: dict) -> str:
|
||||
"""Full page: slots listing."""
|
||||
from quart import g
|
||||
slots = ctx.get("slots") or []
|
||||
calendar = ctx.get("calendar")
|
||||
content = render_slots_table(slots, calendar)
|
||||
hdr = root_header_html(ctx)
|
||||
child = (_post_header_html(ctx)
|
||||
+ _calendar_header_html(ctx) + _calendar_admin_header_html(ctx))
|
||||
hdr += sexp('(div :id "root-header-child" :class "w-full" (raw! h))', h=child)
|
||||
return full_page(ctx, header_rows_html=hdr, content_html=content)
|
||||
|
||||
|
||||
async def render_slots_oob(ctx: dict) -> str:
|
||||
"""OOB response: slots listing."""
|
||||
slots = ctx.get("slots") or []
|
||||
calendar = ctx.get("calendar")
|
||||
content = render_slots_table(slots, calendar)
|
||||
oobs = _calendar_admin_header_html(ctx, oob=True)
|
||||
return oob_page(ctx, oobs_html=oobs, content_html=content)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Tickets
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -1820,7 +1847,7 @@ async def render_markets_page(ctx: dict) -> str:
|
||||
"""Full page: markets listing."""
|
||||
content = _markets_main_panel_html(ctx)
|
||||
hdr = root_header_html(ctx)
|
||||
child = _post_header_html(ctx) + _post_admin_header_html(ctx) + _markets_header_html(ctx)
|
||||
child = _post_header_html(ctx) + _markets_header_html(ctx)
|
||||
hdr += sexp('(div :id "root-header-child" :class "w-full" (raw! h))', h=child)
|
||||
return full_page(ctx, header_rows_html=hdr, content_html=content)
|
||||
|
||||
@@ -1828,8 +1855,8 @@ async def render_markets_page(ctx: dict) -> str:
|
||||
async def render_markets_oob(ctx: dict) -> str:
|
||||
"""OOB response: markets listing."""
|
||||
content = _markets_main_panel_html(ctx)
|
||||
oobs = _post_admin_header_html(ctx, oob=True)
|
||||
oobs += _oob_header_html("post-admin-header-child", "markets-header-child",
|
||||
oobs = _post_header_html(ctx, oob=True)
|
||||
oobs += _oob_header_html("post-header-child", "markets-header-child",
|
||||
_markets_header_html(ctx))
|
||||
return oob_page(ctx, oobs_html=oobs, content_html=content)
|
||||
|
||||
@@ -1842,7 +1869,7 @@ async def render_payments_page(ctx: dict) -> str:
|
||||
"""Full page: payments admin."""
|
||||
content = _payments_main_panel_html(ctx)
|
||||
hdr = root_header_html(ctx)
|
||||
child = _post_header_html(ctx) + _post_admin_header_html(ctx) + _payments_header_html(ctx)
|
||||
child = _post_header_html(ctx) + _payments_header_html(ctx)
|
||||
hdr += sexp('(div :id "root-header-child" :class "w-full" (raw! h))', h=child)
|
||||
return full_page(ctx, header_rows_html=hdr, content_html=content)
|
||||
|
||||
@@ -1850,8 +1877,8 @@ async def render_payments_page(ctx: dict) -> str:
|
||||
async def render_payments_oob(ctx: dict) -> str:
|
||||
"""OOB response: payments admin."""
|
||||
content = _payments_main_panel_html(ctx)
|
||||
oobs = _post_admin_header_html(ctx, oob=True)
|
||||
oobs += _oob_header_html("post-admin-header-child", "payments-header-child",
|
||||
oobs = _post_header_html(ctx, oob=True)
|
||||
oobs += _oob_header_html("post-header-child", "payments-header-child",
|
||||
_payments_header_html(ctx))
|
||||
return oob_page(ctx, oobs_html=oobs, content_html=content)
|
||||
|
||||
@@ -2322,7 +2349,7 @@ async def render_entry_page(ctx: dict) -> str:
|
||||
"""Full page: entry detail."""
|
||||
content = _entry_main_panel_html(ctx)
|
||||
hdr = root_header_html(ctx)
|
||||
child = (_post_header_html(ctx) + _post_admin_header_html(ctx)
|
||||
child = (_post_header_html(ctx)
|
||||
+ _calendar_header_html(ctx) + _day_header_html(ctx)
|
||||
+ _entry_header_html(ctx))
|
||||
hdr += sexp('(div :id "root-header-child" :class "w-full" (raw! h))', h=child)
|
||||
|
||||
Reference in New Issue
Block a user