Merge branch 'worktree-macros-essays' into macros
# Conflicts: # sx/sxc/pages/__init__.py
This commit is contained in:
@@ -67,7 +67,7 @@ def register() -> Blueprint:
|
||||
entries, has_more, pending_tickets, page_info = await _load_entries(page)
|
||||
|
||||
from shared.sx.page import get_template_context
|
||||
from sxc.pages import render_all_events_page, render_all_events_oob
|
||||
from sxc.pages.renders import render_all_events_page, render_all_events_oob
|
||||
|
||||
ctx = await get_template_context()
|
||||
if is_htmx_request():
|
||||
@@ -84,7 +84,7 @@ def register() -> Blueprint:
|
||||
|
||||
entries, has_more, pending_tickets, page_info = await _load_entries(page)
|
||||
|
||||
from sxc.pages import render_all_events_cards
|
||||
from sxc.pages.renders import render_all_events_cards
|
||||
sx_src = await render_all_events_cards(entries, has_more, pending_tickets, page_info, page, view)
|
||||
return sx_response(sx_src)
|
||||
|
||||
@@ -125,7 +125,7 @@ def register() -> Blueprint:
|
||||
if ident["session_id"] is not None:
|
||||
frag_params["session_id"] = ident["session_id"]
|
||||
|
||||
from sxc.pages import render_ticket_widget
|
||||
from sxc.pages.renders import render_ticket_widget
|
||||
widget_html = await render_ticket_widget(entry, qty, "/all-tickets/adjust")
|
||||
mini_html = await fetch_fragment("cart", "cart-mini", params=frag_params, required=False)
|
||||
return sx_response(widget_html + (mini_html or ""))
|
||||
|
||||
@@ -18,7 +18,7 @@ def register():
|
||||
@bp.get("/description/")
|
||||
@require_admin
|
||||
async def calendar_description_edit(calendar_slug: str, **kwargs):
|
||||
from sxc.pages import render_calendar_description_edit
|
||||
from sxc.pages.renders import render_calendar_description_edit
|
||||
html = await render_calendar_description_edit(g.calendar)
|
||||
return sx_response(html)
|
||||
|
||||
@@ -34,7 +34,7 @@ def register():
|
||||
g.calendar.description = description
|
||||
await g.s.flush()
|
||||
|
||||
from sxc.pages import render_calendar_description
|
||||
from sxc.pages.renders import render_calendar_description
|
||||
html = await render_calendar_description(g.calendar, oob=True)
|
||||
return sx_response(html)
|
||||
|
||||
@@ -42,7 +42,7 @@ def register():
|
||||
@bp.get("/description/view/")
|
||||
@require_admin
|
||||
async def calendar_description_view(calendar_slug: str, **kwargs):
|
||||
from sxc.pages import render_calendar_description
|
||||
from sxc.pages.renders import render_calendar_description
|
||||
html = await render_calendar_description(g.calendar)
|
||||
return sx_response(html)
|
||||
|
||||
|
||||
@@ -158,7 +158,7 @@ def register():
|
||||
confirmed_entries = visible.confirmed_entries
|
||||
|
||||
from shared.sx.page import get_template_context
|
||||
from sxc.pages import render_calendar_page, render_calendar_oob
|
||||
from sxc.pages.renders import render_calendar_page, render_calendar_oob
|
||||
|
||||
tctx = await get_template_context()
|
||||
tctx.update(dict(
|
||||
@@ -199,7 +199,7 @@ def register():
|
||||
|
||||
await update_calendar_description(g.calendar, description)
|
||||
from shared.sx.page import get_template_context
|
||||
from sxc.pages import _calendar_admin_main_panel_html
|
||||
from sxc.pages.calendar import _calendar_admin_main_panel_html
|
||||
ctx = await get_template_context()
|
||||
html = await _calendar_admin_main_panel_html(ctx)
|
||||
return sx_response(html)
|
||||
@@ -218,13 +218,13 @@ def register():
|
||||
# If we have post context (blog-embedded mode), update nav
|
||||
post_data = getattr(g, "post_data", None)
|
||||
from shared.sx.page import get_template_context
|
||||
from sxc.pages import render_calendars_list_panel
|
||||
from sxc.pages.renders import render_calendars_list_panel
|
||||
ctx = await get_template_context()
|
||||
html = await render_calendars_list_panel(ctx)
|
||||
|
||||
if post_data:
|
||||
from shared.services.entry_associations import get_associated_entries
|
||||
from sxc.pages import render_post_nav_entries_oob
|
||||
from sxc.pages.renders import render_post_nav_entries_oob
|
||||
|
||||
post_id = (post_data.get("post") or {}).get("id")
|
||||
cals = (
|
||||
|
||||
@@ -258,7 +258,7 @@ def register():
|
||||
"styles": styles,
|
||||
}
|
||||
|
||||
from sxc.pages import render_day_main_panel
|
||||
from sxc.pages.renders import render_day_main_panel
|
||||
html = await render_day_main_panel(ctx)
|
||||
mini_html = await fetch_fragment("cart", "cart-mini", params=frag_params, required=False)
|
||||
return sx_response(html + (mini_html or ""))
|
||||
@@ -279,12 +279,12 @@ def register():
|
||||
result = await g.s.execute(stmt)
|
||||
day_slots = list(result.scalars())
|
||||
|
||||
from sxc.pages import render_entry_add_form
|
||||
from sxc.pages.renders import render_entry_add_form
|
||||
return sx_response(await render_entry_add_form(g.calendar, day, month, year, day_slots))
|
||||
|
||||
@bp.get("/add-button/")
|
||||
async def add_button(day: int, month: int, year: int, **kwargs):
|
||||
from sxc.pages import render_entry_add_button
|
||||
from sxc.pages.renders import render_entry_add_button
|
||||
return sx_response(await render_entry_add_button(g.calendar, day, month, year))
|
||||
|
||||
|
||||
|
||||
@@ -111,7 +111,7 @@ def register():
|
||||
)
|
||||
|
||||
# Render OOB nav
|
||||
from sxc.pages import render_day_entries_nav_oob
|
||||
from sxc.pages.renders import render_day_entries_nav_oob
|
||||
return await render_day_entries_nav_oob(visible.confirmed_entries, calendar, day_date)
|
||||
|
||||
async def get_post_nav_oob(entry_id: int):
|
||||
@@ -148,7 +148,7 @@ def register():
|
||||
).scalars().all()
|
||||
|
||||
# Render OOB nav for this post
|
||||
from sxc.pages import render_post_nav_entries_oob
|
||||
from sxc.pages.renders import render_post_nav_entries_oob
|
||||
nav_oob = await render_post_nav_entries_oob(associated_entries, calendars, post)
|
||||
nav_oobs.append(nav_oob)
|
||||
|
||||
@@ -256,7 +256,7 @@ def register():
|
||||
result = await g.s.execute(stmt)
|
||||
day_slots = list(result.scalars())
|
||||
|
||||
from sxc.pages import render_entry_edit_form
|
||||
from sxc.pages.renders import render_entry_edit_form
|
||||
return sx_response(await render_entry_edit_form(g.entry, g.calendar, day, month, year, day_slots))
|
||||
|
||||
@bp.put("/")
|
||||
@@ -420,7 +420,7 @@ def register():
|
||||
nav_oob = await get_day_nav_oob(year, month, day)
|
||||
|
||||
from shared.sx.page import get_template_context
|
||||
from sxc.pages import _entry_main_panel_html
|
||||
from sxc.pages.entries import _entry_main_panel_html
|
||||
|
||||
tctx = await get_template_context()
|
||||
html = await _entry_main_panel_html(tctx)
|
||||
@@ -448,7 +448,7 @@ def register():
|
||||
|
||||
# Re-read entry to get updated state
|
||||
await g.s.refresh(g.entry)
|
||||
from sxc.pages import render_entry_optioned
|
||||
from sxc.pages.renders import render_entry_optioned
|
||||
html = await render_entry_optioned(g.entry, g.calendar, day, month, year)
|
||||
return sx_response(html + day_nav_oob + post_nav_oob)
|
||||
|
||||
@@ -473,7 +473,7 @@ def register():
|
||||
|
||||
# Re-read entry to get updated state
|
||||
await g.s.refresh(g.entry)
|
||||
from sxc.pages import render_entry_optioned
|
||||
from sxc.pages.renders import render_entry_optioned
|
||||
html = await render_entry_optioned(g.entry, g.calendar, day, month, year)
|
||||
return sx_response(html + day_nav_oob + post_nav_oob)
|
||||
|
||||
@@ -498,7 +498,7 @@ def register():
|
||||
|
||||
# Re-read entry to get updated state
|
||||
await g.s.refresh(g.entry)
|
||||
from sxc.pages import render_entry_optioned
|
||||
from sxc.pages.renders import render_entry_optioned
|
||||
html = await render_entry_optioned(g.entry, g.calendar, day, month, year)
|
||||
return sx_response(html + day_nav_oob + post_nav_oob)
|
||||
|
||||
@@ -542,7 +542,7 @@ def register():
|
||||
|
||||
# Return just the tickets fragment (targeted by hx-target="#entry-tickets-...")
|
||||
await g.s.refresh(g.entry)
|
||||
from sxc.pages import render_entry_tickets_config
|
||||
from sxc.pages.renders import render_entry_tickets_config
|
||||
html = await render_entry_tickets_config(g.entry, g.calendar, request.view_args.get("day"), request.view_args.get("month"), request.view_args.get("year"))
|
||||
return sx_response(html)
|
||||
|
||||
@@ -558,7 +558,7 @@ def register():
|
||||
total_pages = math.ceil(total / per_page) if total > 0 else 0
|
||||
|
||||
va = request.view_args or {}
|
||||
from sxc.pages import render_post_search_results
|
||||
from sxc.pages.renders import render_post_search_results
|
||||
return sx_response(await render_post_search_results(
|
||||
search_posts, query, page, total_pages,
|
||||
g.entry, g.calendar,
|
||||
@@ -592,7 +592,7 @@ def register():
|
||||
entry_posts = await get_entry_posts(g.s, entry_id)
|
||||
|
||||
# Return updated posts list + OOB nav update
|
||||
from sxc.pages import render_entry_posts_panel, render_entry_posts_nav_oob
|
||||
from sxc.pages.renders import render_entry_posts_panel, render_entry_posts_nav_oob
|
||||
va = request.view_args or {}
|
||||
html = await render_entry_posts_panel(entry_posts, g.entry, g.calendar, va.get("day"), va.get("month"), va.get("year"))
|
||||
nav_oob = await render_entry_posts_nav_oob(entry_posts)
|
||||
@@ -614,7 +614,7 @@ def register():
|
||||
entry_posts = await get_entry_posts(g.s, entry_id)
|
||||
|
||||
# Return updated posts list + OOB nav update
|
||||
from sxc.pages import render_entry_posts_panel, render_entry_posts_nav_oob
|
||||
from sxc.pages.renders import render_entry_posts_panel, render_entry_posts_nav_oob
|
||||
va = request.view_args or {}
|
||||
html = await render_entry_posts_panel(entry_posts, g.entry, g.calendar, va.get("day"), va.get("month"), va.get("year"))
|
||||
nav_oob = await render_entry_posts_nav_oob(entry_posts)
|
||||
|
||||
@@ -32,7 +32,7 @@ def register():
|
||||
@cache_page(tag="calendars")
|
||||
async def home(**kwargs):
|
||||
from shared.sx.page import get_template_context
|
||||
from sxc.pages import render_calendars_page, render_calendars_oob
|
||||
from sxc.pages.renders import render_calendars_page, render_calendars_oob
|
||||
|
||||
ctx = await get_template_context()
|
||||
if not is_htmx_request():
|
||||
@@ -67,14 +67,14 @@ def register():
|
||||
return await make_response(render_comp("error-inline", message=str(e)), 422)
|
||||
|
||||
from shared.sx.page import get_template_context
|
||||
from sxc.pages import render_calendars_list_panel
|
||||
from sxc.pages.renders import render_calendars_list_panel
|
||||
ctx = await get_template_context()
|
||||
html = await render_calendars_list_panel(ctx)
|
||||
|
||||
# Blog-embedded mode: also update post nav
|
||||
if post_data:
|
||||
from shared.services.entry_associations import get_associated_entries
|
||||
from sxc.pages import render_post_nav_entries_oob
|
||||
from sxc.pages.renders import render_post_nav_entries_oob
|
||||
|
||||
cals = (
|
||||
await g.s.execute(
|
||||
|
||||
@@ -123,7 +123,7 @@ def register():
|
||||
- pending only for current user/session
|
||||
"""
|
||||
from shared.sx.page import get_template_context
|
||||
from sxc.pages import render_day_page, render_day_oob
|
||||
from sxc.pages.renders import render_day_page, render_day_oob
|
||||
|
||||
tctx = await get_template_context()
|
||||
if not is_htmx_request():
|
||||
|
||||
@@ -42,7 +42,7 @@ def register():
|
||||
return await make_response(render_comp("error-inline", message=str(e)), 422)
|
||||
|
||||
from shared.sx.page import get_template_context
|
||||
from sxc.pages import render_markets_list_panel
|
||||
from sxc.pages.renders import render_markets_list_panel
|
||||
ctx = await get_template_context()
|
||||
return sx_response(await render_markets_list_panel(ctx))
|
||||
|
||||
@@ -55,7 +55,7 @@ def register():
|
||||
return await make_response("Market not found", 404)
|
||||
|
||||
from shared.sx.page import get_template_context
|
||||
from sxc.pages import render_markets_list_panel
|
||||
from sxc.pages.renders import render_markets_list_panel
|
||||
ctx = await get_template_context()
|
||||
return sx_response(await render_markets_list_panel(ctx))
|
||||
|
||||
|
||||
@@ -47,7 +47,7 @@ def register() -> Blueprint:
|
||||
entries, has_more, pending_tickets = await _load_entries(post["id"], page)
|
||||
|
||||
from shared.sx.page import get_template_context
|
||||
from sxc.pages import render_page_summary_page, render_page_summary_oob
|
||||
from sxc.pages.renders import render_page_summary_page, render_page_summary_oob
|
||||
|
||||
ctx = await get_template_context()
|
||||
if is_htmx_request():
|
||||
@@ -65,7 +65,7 @@ def register() -> Blueprint:
|
||||
|
||||
entries, has_more, pending_tickets = await _load_entries(post["id"], page)
|
||||
|
||||
from sxc.pages import render_page_summary_cards
|
||||
from sxc.pages.renders import render_page_summary_cards
|
||||
sx_src = await render_page_summary_cards(entries, has_more, pending_tickets, {}, page, view, post)
|
||||
return sx_response(sx_src)
|
||||
|
||||
@@ -106,7 +106,7 @@ def register() -> Blueprint:
|
||||
if ident["session_id"] is not None:
|
||||
frag_params["session_id"] = ident["session_id"]
|
||||
|
||||
from sxc.pages import render_ticket_widget
|
||||
from sxc.pages.renders import render_ticket_widget
|
||||
widget_html = await render_ticket_widget(entry, qty, f"/{g.post_slug}/tickets/adjust")
|
||||
mini_html = await fetch_fragment("cart", "cart-mini", params=frag_params, required=False)
|
||||
return sx_response(widget_html + (mini_html or ""))
|
||||
|
||||
@@ -35,7 +35,7 @@ def register():
|
||||
slot = await svc_get_slot(g.s, slot_id)
|
||||
if not slot:
|
||||
return await make_response("Not found", 404)
|
||||
from sxc.pages import render_slot_edit_form
|
||||
from sxc.pages.renders import render_slot_edit_form
|
||||
return sx_response(await render_slot_edit_form(slot, g.calendar))
|
||||
|
||||
@bp.get("/view/")
|
||||
@@ -44,7 +44,7 @@ def register():
|
||||
slot = await svc_get_slot(g.s, slot_id)
|
||||
if not slot:
|
||||
return await make_response("Not found", 404)
|
||||
from sxc.pages import render_slot_main_panel
|
||||
from sxc.pages.renders import render_slot_main_panel
|
||||
return sx_response(await render_slot_main_panel(slot, g.calendar))
|
||||
|
||||
@bp.delete("/")
|
||||
@@ -53,7 +53,7 @@ def register():
|
||||
async def slot_delete(slot_id: int, **kwargs):
|
||||
await svc_delete_slot(g.s, slot_id)
|
||||
slots = await svc_list_slots(g.s, g.calendar.id)
|
||||
from sxc.pages import render_slots_table
|
||||
from sxc.pages.renders import render_slots_table
|
||||
return sx_response(await render_slots_table(slots, g.calendar))
|
||||
|
||||
@bp.put("/")
|
||||
@@ -135,7 +135,7 @@ def register():
|
||||
}
|
||||
), 422
|
||||
|
||||
from sxc.pages import render_slot_main_panel
|
||||
from sxc.pages.renders import render_slot_main_panel
|
||||
return sx_response(await render_slot_main_panel(slot, g.calendar, oob=True))
|
||||
|
||||
|
||||
|
||||
@@ -110,20 +110,20 @@ def register():
|
||||
|
||||
# Success → re-render the slots table
|
||||
slots = await svc_list_slots(g.s, g.calendar.id)
|
||||
from sxc.pages import render_slots_table
|
||||
from sxc.pages.renders import render_slots_table
|
||||
return sx_response(await render_slots_table(slots, g.calendar))
|
||||
|
||||
|
||||
@bp.get("/add")
|
||||
@require_admin
|
||||
async def add_form(**kwargs):
|
||||
from sxc.pages import render_slot_add_form
|
||||
from sxc.pages.renders import render_slot_add_form
|
||||
return sx_response(await render_slot_add_form(g.calendar))
|
||||
|
||||
@bp.get("/add-button")
|
||||
@require_admin
|
||||
async def add_button(**kwargs):
|
||||
from sxc.pages import render_slot_add_button
|
||||
from sxc.pages.renders import render_slot_add_button
|
||||
return sx_response(await render_slot_add_button(g.calendar))
|
||||
|
||||
return bp
|
||||
|
||||
@@ -53,7 +53,7 @@ def register() -> Blueprint:
|
||||
|
||||
tickets = await get_tickets_for_entry(g.s, entry_id)
|
||||
|
||||
from sxc.pages import render_entry_tickets_admin
|
||||
from sxc.pages.renders import render_entry_tickets_admin
|
||||
html = await render_entry_tickets_admin(entry, tickets)
|
||||
return sx_response(html)
|
||||
|
||||
@@ -69,7 +69,7 @@ def register() -> Blueprint:
|
||||
)
|
||||
|
||||
ticket = await get_ticket_by_code(g.s, code)
|
||||
from sxc.pages import render_lookup_result
|
||||
from sxc.pages.renders import render_lookup_result
|
||||
if not ticket:
|
||||
return sx_response(await render_lookup_result(None, "Ticket not found"))
|
||||
|
||||
@@ -82,7 +82,7 @@ def register() -> Blueprint:
|
||||
"""Check in a ticket by its code."""
|
||||
success, error = await checkin_ticket(g.s, code)
|
||||
|
||||
from sxc.pages import render_checkin_result
|
||||
from sxc.pages.renders import render_checkin_result
|
||||
if not success:
|
||||
return sx_response(await render_checkin_result(False, error, None))
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@ def register():
|
||||
if not ticket_type:
|
||||
return await make_response("Not found", 404)
|
||||
|
||||
from sxc.pages import render_ticket_type_edit_form
|
||||
from sxc.pages.renders import render_ticket_type_edit_form
|
||||
va = request.view_args or {}
|
||||
return sx_response(await render_ticket_type_edit_form(
|
||||
ticket_type, g.entry, g.calendar,
|
||||
@@ -45,7 +45,7 @@ def register():
|
||||
if not ticket_type:
|
||||
return await make_response("Not found", 404)
|
||||
|
||||
from sxc.pages import render_ticket_type_main_panel
|
||||
from sxc.pages.renders import render_ticket_type_main_panel
|
||||
va = request.view_args or {}
|
||||
return sx_response(await render_ticket_type_main_panel(
|
||||
ticket_type, g.entry, g.calendar,
|
||||
@@ -112,7 +112,7 @@ def register():
|
||||
return await make_response("Not found", 404)
|
||||
|
||||
# Return updated view with OOB flag
|
||||
from sxc.pages import render_ticket_type_main_panel
|
||||
from sxc.pages.renders import render_ticket_type_main_panel
|
||||
va = request.view_args or {}
|
||||
return sx_response(await render_ticket_type_main_panel(
|
||||
ticket_type, g.entry, g.calendar,
|
||||
@@ -131,7 +131,7 @@ def register():
|
||||
|
||||
# Re-render the ticket types list
|
||||
ticket_types = await svc_list_ticket_types(g.s, g.entry.id)
|
||||
from sxc.pages import render_ticket_types_table
|
||||
from sxc.pages.renders import render_ticket_types_table
|
||||
va = request.view_args or {}
|
||||
return sx_response(await render_ticket_types_table(
|
||||
ticket_types, g.entry, g.calendar,
|
||||
|
||||
@@ -93,7 +93,7 @@ def register():
|
||||
|
||||
# Success → re-render the ticket types table
|
||||
ticket_types = await svc_list_ticket_types(g.s, g.entry.id)
|
||||
from sxc.pages import render_ticket_types_table
|
||||
from sxc.pages.renders import render_ticket_types_table
|
||||
va = request.view_args or {}
|
||||
return sx_response(await render_ticket_types_table(
|
||||
ticket_types, g.entry, g.calendar,
|
||||
@@ -104,7 +104,7 @@ def register():
|
||||
@require_admin
|
||||
async def add_form(**kwargs):
|
||||
"""Show the add ticket type form."""
|
||||
from sxc.pages import render_ticket_type_add_form
|
||||
from sxc.pages.renders import render_ticket_type_add_form
|
||||
va = request.view_args or {}
|
||||
return sx_response(await render_ticket_type_add_form(
|
||||
g.entry, g.calendar,
|
||||
@@ -115,7 +115,7 @@ def register():
|
||||
@require_admin
|
||||
async def add_button(**kwargs):
|
||||
"""Show the add ticket type button."""
|
||||
from sxc.pages import render_ticket_type_add_button
|
||||
from sxc.pages.renders import render_ticket_type_add_button
|
||||
va = request.view_args or {}
|
||||
return sx_response(await render_ticket_type_add_button(
|
||||
g.entry, g.calendar,
|
||||
|
||||
@@ -126,7 +126,7 @@ def register() -> Blueprint:
|
||||
summary = dto_from_dict(CartSummaryDTO, raw_summary) if raw_summary else CartSummaryDTO()
|
||||
cart_count = summary.count + summary.calendar_count + summary.ticket_count
|
||||
|
||||
from sxc.pages import render_buy_result
|
||||
from sxc.pages.renders import render_buy_result
|
||||
return sx_response(await render_buy_result(entry, created, remaining, cart_count))
|
||||
|
||||
@bp.post("/adjust/")
|
||||
@@ -249,7 +249,7 @@ def register() -> Blueprint:
|
||||
summary = dto_from_dict(CartSummaryDTO, raw_summary) if raw_summary else CartSummaryDTO()
|
||||
cart_count = summary.count + summary.calendar_count + summary.ticket_count
|
||||
|
||||
from sxc.pages import render_adjust_response
|
||||
from sxc.pages.renders import render_adjust_response
|
||||
return sx_response(await render_adjust_response(
|
||||
entry, ticket_remaining, ticket_sold_count,
|
||||
user_ticket_count, user_ticket_counts_by_type, cart_count,
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
678
events/sxc/pages/calendar.py
Normal file
678
events/sxc/pages/calendar.py
Normal file
@@ -0,0 +1,678 @@
|
||||
"""Calendar grid, day panels, month navigation, calendar-specific helpers."""
|
||||
from __future__ import annotations
|
||||
|
||||
from shared.sx.helpers import (
|
||||
call_url, render_to_sx, render_to_sx_with_env, _ctx_to_env,
|
||||
post_admin_header_sx,
|
||||
)
|
||||
from shared.sx.parser import SxExpr
|
||||
|
||||
from .utils import (
|
||||
_clear_deeper_oob, _ensure_container_nav,
|
||||
_entry_state_badge_html, _list_container,
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Post header helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
async def _post_header_sx(ctx: dict, *, oob: bool = False) -> str:
|
||||
"""Build the post-level header row — delegates to shared sx helper."""
|
||||
from shared.sx.helpers import post_header_sx
|
||||
return await post_header_sx(ctx, oob=oob)
|
||||
|
||||
|
||||
async def _post_nav_sx(ctx: dict) -> str:
|
||||
"""Post desktop nav: calendar links + container nav (markets, etc.)."""
|
||||
from quart import url_for, g
|
||||
|
||||
calendars = ctx.get("calendars") or []
|
||||
select_colours = ctx.get("select_colours", "")
|
||||
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("calendar.get", calendar_slug=cal_slug)
|
||||
is_sel = (cal_slug == current_cal_slug)
|
||||
parts.append(await render_to_sx("nav-link", href=href, icon="fa fa-calendar",
|
||||
label=cal_name, select_colours=select_colours,
|
||||
is_selected=is_sel))
|
||||
# Container nav fragments (markets, etc.)
|
||||
container_nav = ctx.get("container_nav", "")
|
||||
if container_nav:
|
||||
parts.append(container_nav)
|
||||
|
||||
# Admin cog → blog admin for this post (cross-domain, no HTMX)
|
||||
rights = ctx.get("rights") or {}
|
||||
has_admin = rights.get("admin") if isinstance(rights, dict) else getattr(rights, "admin", False)
|
||||
if has_admin:
|
||||
post = ctx.get("post") or {}
|
||||
slug = post.get("slug", "")
|
||||
styles = ctx.get("styles") or {}
|
||||
nav_btn = styles.get("nav_button", "") if isinstance(styles, dict) else getattr(styles, "nav_button", "")
|
||||
select_colours = ctx.get("select_colours", "")
|
||||
admin_href = call_url(ctx, "blog_url", f"/{slug}/admin/")
|
||||
aclass = f"{nav_btn} {select_colours}".strip() or (
|
||||
"justify-center cursor-pointer flex flex-row items-center gap-2 "
|
||||
"rounded bg-stone-200 text-black p-3"
|
||||
)
|
||||
parts.append(
|
||||
f'<div class="relative nav-group">'
|
||||
f'<a href="{admin_href}" class="{aclass}">'
|
||||
f'<i class="fa fa-cog" aria-hidden="true"></i></a></div>'
|
||||
)
|
||||
|
||||
return "".join(parts)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Calendars header
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
async def _calendars_header_sx(ctx: dict, *, oob: bool = False) -> str:
|
||||
"""Build the calendars section header row."""
|
||||
from quart import url_for
|
||||
link_href = url_for("calendars.home")
|
||||
return await render_to_sx("menu-row-sx", id="calendars-row", level=3,
|
||||
link_href=link_href,
|
||||
link_label_content=SxExpr(await render_to_sx("events-calendars-label")),
|
||||
child_id="calendars-header-child", oob=oob)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Calendar header
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
async def _calendar_header_sx(ctx: dict, *, oob: bool = False) -> str:
|
||||
"""Build a single calendar's header row."""
|
||||
from quart import url_for
|
||||
calendar = ctx.get("calendar")
|
||||
if not calendar:
|
||||
return ""
|
||||
cal_slug = getattr(calendar, "slug", "")
|
||||
cal_name = getattr(calendar, "name", "")
|
||||
cal_desc = getattr(calendar, "description", "") or ""
|
||||
|
||||
link_href = url_for("calendar.get", calendar_slug=cal_slug)
|
||||
label_html = await render_to_sx("events-calendar-label",
|
||||
name=cal_name, description=cal_desc)
|
||||
|
||||
# Desktop nav: slots + admin
|
||||
nav_html = await _calendar_nav_sx(ctx)
|
||||
|
||||
return await render_to_sx("menu-row-sx", id="calendar-row", level=3,
|
||||
link_href=link_href, link_label_content=SxExpr(label_html),
|
||||
nav=SxExpr(nav_html) if nav_html else None, child_id="calendar-header-child", oob=oob)
|
||||
|
||||
|
||||
async def _calendar_nav_sx(ctx: dict) -> str:
|
||||
"""Calendar desktop nav: Slots + admin link."""
|
||||
from quart import url_for
|
||||
calendar = ctx.get("calendar")
|
||||
if not calendar:
|
||||
return ""
|
||||
cal_slug = getattr(calendar, "slug", "")
|
||||
rights = ctx.get("rights") or {}
|
||||
is_admin = getattr(rights, "admin", False) if hasattr(rights, "admin") else rights.get("admin", False)
|
||||
select_colours = ctx.get("select_colours", "")
|
||||
|
||||
parts = []
|
||||
slots_href = url_for("defpage_slots_listing", calendar_slug=cal_slug)
|
||||
parts.append(await render_to_sx("nav-link", href=slots_href, icon="fa fa-clock",
|
||||
label="Slots", select_colours=select_colours))
|
||||
if is_admin:
|
||||
admin_href = url_for("defpage_calendar_admin", calendar_slug=cal_slug)
|
||||
parts.append(await render_to_sx("nav-link", href=admin_href, icon="fa fa-cog",
|
||||
select_colours=select_colours))
|
||||
return "(<> " + " ".join(parts) + ")" if parts else ""
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Day header
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
async def _day_header_sx(ctx: dict, *, oob: bool = False) -> str:
|
||||
"""Build day detail header row."""
|
||||
from quart import url_for
|
||||
calendar = ctx.get("calendar")
|
||||
if not calendar:
|
||||
return ""
|
||||
cal_slug = getattr(calendar, "slug", "")
|
||||
day_date = ctx.get("day_date")
|
||||
if not day_date:
|
||||
return ""
|
||||
|
||||
link_href = url_for(
|
||||
"calendar.day.show_day",
|
||||
calendar_slug=cal_slug,
|
||||
year=day_date.year,
|
||||
month=day_date.month,
|
||||
day=day_date.day,
|
||||
)
|
||||
label_html = await render_to_sx("events-day-label",
|
||||
date_str=day_date.strftime("%A %d %B %Y"))
|
||||
|
||||
nav_html = await _day_nav_sx(ctx)
|
||||
|
||||
return await render_to_sx("menu-row-sx", id="day-row", level=4,
|
||||
link_href=link_href, link_label_content=SxExpr(label_html),
|
||||
nav=SxExpr(nav_html) if nav_html else None, child_id="day-header-child", oob=oob)
|
||||
|
||||
|
||||
async def _day_nav_sx(ctx: dict) -> str:
|
||||
"""Day desktop nav: confirmed entries scrolling menu + admin link."""
|
||||
from quart import url_for
|
||||
calendar = ctx.get("calendar")
|
||||
if not calendar:
|
||||
return ""
|
||||
cal_slug = getattr(calendar, "slug", "")
|
||||
day_date = ctx.get("day_date")
|
||||
confirmed_entries = ctx.get("confirmed_entries") or []
|
||||
rights = ctx.get("rights") or {}
|
||||
is_admin = getattr(rights, "admin", False) if hasattr(rights, "admin") else rights.get("admin", False)
|
||||
|
||||
parts = []
|
||||
# Confirmed entries nav (scrolling menu)
|
||||
if confirmed_entries:
|
||||
entry_links = []
|
||||
for entry in confirmed_entries:
|
||||
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(await render_to_sx("events-day-entry-link",
|
||||
href=href, name=entry.name,
|
||||
time_str=f"{start}{end}"))
|
||||
inner = "".join(entry_links)
|
||||
parts.append(await render_to_sx("events-day-entries-nav", inner=SxExpr(inner)))
|
||||
|
||||
if is_admin and day_date:
|
||||
admin_href = url_for(
|
||||
"calendar.day.admin.admin",
|
||||
calendar_slug=cal_slug,
|
||||
year=day_date.year,
|
||||
month=day_date.month,
|
||||
day=day_date.day,
|
||||
)
|
||||
parts.append(await render_to_sx("nav-link", href=admin_href, icon="fa fa-cog"))
|
||||
return "".join(parts)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Day admin header
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
async def _day_admin_header_sx(ctx: dict, *, oob: bool = False) -> str:
|
||||
"""Build day admin header row."""
|
||||
from quart import url_for
|
||||
calendar = ctx.get("calendar")
|
||||
if not calendar:
|
||||
return ""
|
||||
cal_slug = getattr(calendar, "slug", "")
|
||||
day_date = ctx.get("day_date")
|
||||
if not day_date:
|
||||
return ""
|
||||
|
||||
link_href = url_for(
|
||||
"calendar.day.admin.admin",
|
||||
calendar_slug=cal_slug,
|
||||
year=day_date.year,
|
||||
month=day_date.month,
|
||||
day=day_date.day,
|
||||
)
|
||||
return await render_to_sx("menu-row-sx", id="day-admin-row", level=5,
|
||||
link_href=link_href, link_label="admin", icon="fa fa-cog",
|
||||
child_id="day-admin-header-child", oob=oob)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Calendar admin header
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
async def _calendar_admin_header_sx(ctx: dict, *, oob: bool = False) -> str:
|
||||
"""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 [
|
||||
("defpage_slots_listing", "slots"),
|
||||
("calendar.admin.calendar_description_edit", "description"),
|
||||
]:
|
||||
href = url_for(endpoint, calendar_slug=cal_slug)
|
||||
nav_parts.append(await render_to_sx("nav-link", href=href, label=label,
|
||||
select_colours=select_colours))
|
||||
|
||||
nav_html = "".join(nav_parts)
|
||||
return await render_to_sx("menu-row-sx", id="calendar-admin-row", level=4,
|
||||
link_label="admin", icon="fa fa-cog",
|
||||
nav=SxExpr(nav_html) if nav_html else None, child_id="calendar-admin-header-child", oob=oob)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Markets header
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
async def _markets_header_sx(ctx: dict, *, oob: bool = False) -> str:
|
||||
"""Build the markets section header row."""
|
||||
from quart import url_for
|
||||
link_href = url_for("defpage_events_markets")
|
||||
return await render_to_sx("menu-row-sx", id="markets-row", level=3,
|
||||
link_href=link_href,
|
||||
link_label_content=SxExpr(await render_to_sx("events-markets-label")),
|
||||
child_id="markets-header-child", oob=oob)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Calendars main panel
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
async def _calendars_main_panel_sx(ctx: dict) -> str:
|
||||
"""Render the calendars list + create form panel."""
|
||||
from quart import url_for
|
||||
rights = ctx.get("rights") or {}
|
||||
is_admin = getattr(rights, "admin", False) if hasattr(rights, "admin") else rights.get("admin", False)
|
||||
has_access = ctx.get("has_access")
|
||||
can_create = has_access("calendars.create_calendar") if callable(has_access) else is_admin
|
||||
csrf_token = ctx.get("csrf_token")
|
||||
csrf = csrf_token() if callable(csrf_token) else (csrf_token or "")
|
||||
|
||||
calendars = ctx.get("calendars") or []
|
||||
|
||||
form_html = ""
|
||||
if can_create:
|
||||
create_url = url_for("calendars.create_calendar")
|
||||
form_html = await render_to_sx("crud-create-form",
|
||||
create_url=create_url, csrf=csrf,
|
||||
errors_id="cal-create-errors", list_id="calendars-list",
|
||||
placeholder="e.g. Events, Gigs, Meetings", btn_label="Add calendar")
|
||||
|
||||
list_html = await _calendars_list_sx(ctx, calendars)
|
||||
return await render_to_sx("crud-panel",
|
||||
form=SxExpr(form_html), list=SxExpr(list_html),
|
||||
list_id="calendars-list")
|
||||
|
||||
|
||||
async def _calendars_list_sx(ctx: dict, calendars: list) -> str:
|
||||
"""Render the calendars list items."""
|
||||
from quart import url_for
|
||||
from shared.utils import route_prefix
|
||||
csrf_token = ctx.get("csrf_token")
|
||||
csrf = csrf_token() if callable(csrf_token) else (csrf_token or "")
|
||||
prefix = route_prefix()
|
||||
|
||||
if not calendars:
|
||||
return await render_to_sx("empty-state", message="No calendars yet. Create one above.",
|
||||
cls="text-gray-500 mt-4")
|
||||
|
||||
parts = []
|
||||
for cal in calendars:
|
||||
cal_slug = getattr(cal, "slug", "")
|
||||
cal_name = getattr(cal, "name", "")
|
||||
href = prefix + url_for("calendar.get", calendar_slug=cal_slug)
|
||||
del_url = url_for("calendar.delete", calendar_slug=cal_slug)
|
||||
csrf_hdr = f'{{"X-CSRFToken":"{csrf}"}}'
|
||||
parts.append(await render_to_sx("crud-item",
|
||||
href=href, name=cal_name, slug=cal_slug,
|
||||
del_url=del_url, csrf_hdr=csrf_hdr,
|
||||
list_id="calendars-list",
|
||||
confirm_title="Delete calendar?",
|
||||
confirm_text="Entries will be hidden (soft delete)"))
|
||||
return "".join(parts)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Calendar month grid
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
async def _calendar_main_panel_html(ctx: dict) -> str:
|
||||
"""Render the calendar month grid."""
|
||||
from quart import url_for
|
||||
from quart import session as qsession
|
||||
|
||||
calendar = ctx.get("calendar")
|
||||
if not calendar:
|
||||
return ""
|
||||
cal_slug = getattr(calendar, "slug", "")
|
||||
hx_select = ctx.get("hx_select_search", "#main-panel")
|
||||
styles = ctx.get("styles") or {}
|
||||
pill_cls = getattr(styles, "pill", "") if hasattr(styles, "pill") else styles.get("pill", "")
|
||||
|
||||
year = ctx.get("year", 2024)
|
||||
month = ctx.get("month", 1)
|
||||
month_name = ctx.get("month_name", "")
|
||||
weekday_names = ctx.get("weekday_names", [])
|
||||
weeks = ctx.get("weeks", [])
|
||||
prev_month = ctx.get("prev_month", 1)
|
||||
prev_month_year = ctx.get("prev_month_year", year)
|
||||
next_month = ctx.get("next_month", 1)
|
||||
next_month_year = ctx.get("next_month_year", year)
|
||||
prev_year = ctx.get("prev_year", year - 1)
|
||||
next_year = ctx.get("next_year", year + 1)
|
||||
month_entries = ctx.get("month_entries") or []
|
||||
user = ctx.get("user")
|
||||
qs = qsession if "qsession" not in ctx else ctx["qsession"]
|
||||
|
||||
def nav_link(y, m):
|
||||
return url_for("calendar.get", calendar_slug=cal_slug, year=y, month=m)
|
||||
|
||||
# Month navigation arrows
|
||||
nav_arrows = []
|
||||
for label, yr, mn in [
|
||||
("\u00ab", prev_year, month),
|
||||
("\u2039", prev_month_year, prev_month),
|
||||
]:
|
||||
href = nav_link(yr, mn)
|
||||
nav_arrows.append(await render_to_sx("events-calendar-nav-arrow",
|
||||
pill_cls=pill_cls, href=href, label=label))
|
||||
|
||||
nav_arrows.append(await render_to_sx("events-calendar-month-label",
|
||||
month_name=month_name, year=str(year)))
|
||||
|
||||
for label, yr, mn in [
|
||||
("\u203a", next_month_year, next_month),
|
||||
("\u00bb", next_year, month),
|
||||
]:
|
||||
href = nav_link(yr, mn)
|
||||
nav_arrows.append(await render_to_sx("events-calendar-nav-arrow",
|
||||
pill_cls=pill_cls, href=href, label=label))
|
||||
|
||||
# Weekday headers
|
||||
wd_parts = []
|
||||
for wd in weekday_names:
|
||||
wd_parts.append(await render_to_sx("events-calendar-weekday", name=wd))
|
||||
wd_html = "".join(wd_parts)
|
||||
|
||||
# Day cells
|
||||
cells = []
|
||||
for week in weeks:
|
||||
for day_cell in week:
|
||||
if isinstance(day_cell, dict):
|
||||
in_month = day_cell.get("in_month", True)
|
||||
is_today = day_cell.get("is_today", False)
|
||||
day_date = day_cell.get("date")
|
||||
else:
|
||||
in_month = getattr(day_cell, "in_month", True)
|
||||
is_today = getattr(day_cell, "is_today", False)
|
||||
day_date = getattr(day_cell, "date", None)
|
||||
|
||||
cell_cls = "min-h-20 sm:min-h-24 bg-white px-3 py-2 text-xs"
|
||||
if not in_month:
|
||||
cell_cls += " bg-stone-50 text-stone-400"
|
||||
if is_today:
|
||||
cell_cls += " ring-2 ring-blue-500 z-10 relative"
|
||||
|
||||
# Day number link
|
||||
day_num_html = ""
|
||||
day_short_html = ""
|
||||
if day_date:
|
||||
day_href = url_for(
|
||||
"calendar.day.show_day",
|
||||
calendar_slug=cal_slug,
|
||||
year=day_date.year, month=day_date.month, day=day_date.day,
|
||||
)
|
||||
day_short_html = await render_to_sx("events-calendar-day-short",
|
||||
day_str=day_date.strftime("%a"))
|
||||
day_num_html = await render_to_sx("events-calendar-day-num",
|
||||
pill_cls=pill_cls, href=day_href,
|
||||
num=str(day_date.day))
|
||||
|
||||
# Entry badges for this day
|
||||
entry_badges = []
|
||||
if day_date:
|
||||
for e in month_entries:
|
||||
if e.start_at and e.start_at.date() == day_date:
|
||||
is_mine = (
|
||||
(user and e.user_id == user.id)
|
||||
or (not user and e.session_id == qs.get("calendar_sid"))
|
||||
)
|
||||
if e.state == "confirmed":
|
||||
bg_cls = "bg-emerald-200 text-emerald-900" if is_mine else "bg-emerald-100 text-emerald-800"
|
||||
else:
|
||||
bg_cls = "bg-sky-100 text-sky-800" if is_mine else "bg-stone-100 text-stone-700"
|
||||
state_label = (e.state or "pending").replace("_", " ")
|
||||
entry_badges.append(await render_to_sx("events-calendar-entry-badge",
|
||||
bg_cls=bg_cls, name=e.name,
|
||||
state_label=state_label))
|
||||
|
||||
badges_html = "(<> " + "".join(entry_badges) + ")" if entry_badges else ""
|
||||
cells.append(await render_to_sx("events-calendar-cell",
|
||||
cell_cls=cell_cls, day_short=SxExpr(day_short_html),
|
||||
day_num=SxExpr(day_num_html),
|
||||
badges=SxExpr(badges_html) if badges_html else None))
|
||||
|
||||
cells_html = "(<> " + "".join(cells) + ")"
|
||||
arrows_html = "(<> " + "".join(nav_arrows) + ")"
|
||||
wd_html = "(<> " + wd_html + ")"
|
||||
return await render_to_sx("events-calendar-grid",
|
||||
arrows=SxExpr(arrows_html), weekdays=SxExpr(wd_html),
|
||||
cells=SxExpr(cells_html))
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Day main panel
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
async def _day_main_panel_html(ctx: dict) -> str:
|
||||
"""Render the day entries table + add button."""
|
||||
from quart import url_for
|
||||
|
||||
calendar = ctx.get("calendar")
|
||||
if not calendar:
|
||||
return ""
|
||||
cal_slug = getattr(calendar, "slug", "")
|
||||
day_entries = ctx.get("day_entries") or []
|
||||
day = ctx.get("day")
|
||||
month = ctx.get("month")
|
||||
year = ctx.get("year")
|
||||
hx_select = ctx.get("hx_select_search", "#main-panel")
|
||||
styles = ctx.get("styles") or {}
|
||||
list_container = getattr(styles, "list_container", "") if hasattr(styles, "list_container") else styles.get("list_container", "")
|
||||
pill_cls = getattr(styles, "pill", "") if hasattr(styles, "pill") else styles.get("pill", "")
|
||||
tr_cls = getattr(styles, "tr", "") if hasattr(styles, "tr") else styles.get("tr", "")
|
||||
pre_action = getattr(styles, "pre_action_button", "") if hasattr(styles, "pre_action_button") else styles.get("pre_action_button", "")
|
||||
|
||||
rows_html = ""
|
||||
if day_entries:
|
||||
row_parts = []
|
||||
for entry in day_entries:
|
||||
row_parts.append(await _day_row_html(ctx, entry))
|
||||
rows_html = "".join(row_parts)
|
||||
else:
|
||||
rows_html = await render_to_sx("events-day-empty-row")
|
||||
|
||||
add_url = url_for(
|
||||
"calendar.day.calendar_entries.add_form",
|
||||
calendar_slug=cal_slug,
|
||||
day=day, month=month, year=year,
|
||||
)
|
||||
|
||||
return await render_to_sx("events-day-table",
|
||||
list_container=list_container, rows=SxExpr(rows_html),
|
||||
pre_action=pre_action, add_url=add_url)
|
||||
|
||||
|
||||
async def _day_row_html(ctx: dict, entry) -> str:
|
||||
"""Render a single day table row."""
|
||||
from quart import url_for
|
||||
calendar = ctx.get("calendar")
|
||||
cal_slug = getattr(calendar, "slug", "")
|
||||
day = ctx.get("day")
|
||||
month = ctx.get("month")
|
||||
year = ctx.get("year")
|
||||
hx_select = ctx.get("hx_select_search", "#main-panel")
|
||||
styles = ctx.get("styles") or {}
|
||||
pill_cls = getattr(styles, "pill", "") if hasattr(styles, "pill") else styles.get("pill", "")
|
||||
tr_cls = getattr(styles, "tr", "") if hasattr(styles, "tr") else styles.get("tr", "")
|
||||
|
||||
entry_href = url_for(
|
||||
"calendar.day.calendar_entries.calendar_entry.get",
|
||||
calendar_slug=cal_slug, day=day, month=month, year=year, entry_id=entry.id,
|
||||
)
|
||||
|
||||
# Name
|
||||
name_html = await render_to_sx("events-day-row-name",
|
||||
href=entry_href, pill_cls=pill_cls, name=entry.name)
|
||||
|
||||
# Slot/Time
|
||||
slot = getattr(entry, "slot", None)
|
||||
if slot:
|
||||
slot_href = url_for("defpage_slot_detail", calendar_slug=cal_slug, slot_id=slot.id)
|
||||
time_start = slot.time_start.strftime("%H:%M") if slot.time_start else ""
|
||||
time_end = f" \u2192 {slot.time_end.strftime('%H:%M')}" if slot.time_end else ""
|
||||
slot_html = await render_to_sx("events-day-row-slot",
|
||||
href=slot_href, pill_cls=pill_cls, slot_name=slot.name,
|
||||
time_str=f"({time_start}{time_end})")
|
||||
else:
|
||||
start = entry.start_at.strftime("%H:%M") if entry.start_at else ""
|
||||
end = f" \u2192 {entry.end_at.strftime('%H:%M')}" if entry.end_at else ""
|
||||
slot_html = await render_to_sx("events-day-row-time", start=start, end=end)
|
||||
|
||||
# State
|
||||
state = getattr(entry, "state", "pending") or "pending"
|
||||
state_badge = await _entry_state_badge_html(state)
|
||||
state_td = await render_to_sx("events-day-row-state",
|
||||
state_id=f"entry-state-{entry.id}", badge=SxExpr(state_badge))
|
||||
|
||||
# Cost
|
||||
cost = getattr(entry, "cost", None)
|
||||
cost_str = f"\u00a3{cost:.2f}" if cost is not None else "\u00a30.00"
|
||||
cost_td = await render_to_sx("events-day-row-cost", cost_str=cost_str)
|
||||
|
||||
# Tickets
|
||||
tp = getattr(entry, "ticket_price", None)
|
||||
if tp is not None:
|
||||
tc = getattr(entry, "ticket_count", None)
|
||||
tc_str = f"{tc} tickets" if tc is not None else "Unlimited"
|
||||
tickets_td = await render_to_sx("events-day-row-tickets",
|
||||
price_str=f"\u00a3{tp:.2f}", count_str=tc_str)
|
||||
else:
|
||||
tickets_td = await render_to_sx("events-day-row-no-tickets")
|
||||
|
||||
actions_td = await render_to_sx("events-day-row-actions")
|
||||
|
||||
return await render_to_sx("events-day-row",
|
||||
tr_cls=tr_cls, name=SxExpr(name_html), slot=SxExpr(slot_html),
|
||||
state=SxExpr(state_td), cost=SxExpr(cost_td),
|
||||
tickets=SxExpr(tickets_td), actions=SxExpr(actions_td))
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Day admin main panel
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
async def _day_admin_main_panel_html(ctx: dict) -> str:
|
||||
"""Render day admin panel (placeholder nav)."""
|
||||
return await render_to_sx("events-day-admin-panel")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Calendar admin main panel
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
async def _calendar_admin_main_panel_html(ctx: dict) -> str:
|
||||
"""Render calendar admin config panel with description editor."""
|
||||
from quart import url_for
|
||||
calendar = ctx.get("calendar")
|
||||
if not calendar:
|
||||
return ""
|
||||
csrf_token = ctx.get("csrf_token")
|
||||
csrf = csrf_token() if callable(csrf_token) else (csrf_token or "")
|
||||
cal_slug = getattr(calendar, "slug", "")
|
||||
desc = getattr(calendar, "description", "") or ""
|
||||
hx_select = ctx.get("hx_select_search", "#main-panel")
|
||||
|
||||
desc_edit_url = url_for("calendar.admin.calendar_description_edit", calendar_slug=cal_slug)
|
||||
description_html = await _calendar_description_display_html(calendar, desc_edit_url)
|
||||
|
||||
return await render_to_sx("events-calendar-admin-panel",
|
||||
description_content=SxExpr(description_html), csrf=csrf,
|
||||
description=desc)
|
||||
|
||||
|
||||
async def _calendar_description_display_html(calendar, edit_url: str) -> str:
|
||||
"""Render calendar description display with edit button."""
|
||||
desc = getattr(calendar, "description", "") or ""
|
||||
return await render_to_sx("events-calendar-description-display",
|
||||
description=desc, edit_url=edit_url)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Markets main panel
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
async def _markets_main_panel_html(ctx: dict) -> str:
|
||||
"""Render markets list + create form panel."""
|
||||
from quart import url_for
|
||||
rights = ctx.get("rights") or {}
|
||||
is_admin = getattr(rights, "admin", False) if hasattr(rights, "admin") else rights.get("admin", False)
|
||||
has_access = ctx.get("has_access")
|
||||
can_create = has_access("markets.create_market") if callable(has_access) else is_admin
|
||||
csrf_token = ctx.get("csrf_token")
|
||||
csrf = csrf_token() if callable(csrf_token) else (csrf_token or "")
|
||||
markets = ctx.get("markets") or []
|
||||
|
||||
form_html = ""
|
||||
if can_create:
|
||||
create_url = url_for("markets.create_market")
|
||||
form_html = await render_to_sx("crud-create-form",
|
||||
create_url=create_url, csrf=csrf,
|
||||
errors_id="market-create-errors", list_id="markets-list",
|
||||
placeholder="e.g. Farm Shop, Bakery", btn_label="Add market")
|
||||
|
||||
list_html = await _markets_list_html(ctx, markets)
|
||||
return await render_to_sx("crud-panel",
|
||||
form=SxExpr(form_html), list=SxExpr(list_html),
|
||||
list_id="markets-list")
|
||||
|
||||
|
||||
async def _markets_list_html(ctx: dict, markets: list) -> str:
|
||||
"""Render markets list items."""
|
||||
from quart import url_for
|
||||
csrf_token = ctx.get("csrf_token")
|
||||
csrf = csrf_token() if callable(csrf_token) else (csrf_token or "")
|
||||
post = ctx.get("post") or {}
|
||||
slug = post.get("slug", "")
|
||||
|
||||
if not markets:
|
||||
return await render_to_sx("empty-state", message="No markets yet. Create one above.",
|
||||
cls="text-gray-500 mt-4")
|
||||
|
||||
parts = []
|
||||
for m in markets:
|
||||
m_slug = getattr(m, "slug", "") if hasattr(m, "slug") else m.get("slug", "")
|
||||
m_name = getattr(m, "name", "") if hasattr(m, "name") else m.get("name", "")
|
||||
market_href = call_url(ctx, "market_url", f"/{slug}/{m_slug}/")
|
||||
del_url = url_for("markets.delete_market", market_slug=m_slug)
|
||||
csrf_hdr = f'{{"X-CSRFToken":"{csrf}"}}'
|
||||
parts.append(await render_to_sx("crud-item",
|
||||
href=market_href, name=m_name,
|
||||
slug=m_slug, del_url=del_url,
|
||||
csrf_hdr=csrf_hdr,
|
||||
list_id="markets-list",
|
||||
confirm_title="Delete market?",
|
||||
confirm_text="Products will be hidden (soft delete)"))
|
||||
return "".join(parts)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Calendar admin helper
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
async def _events_post_admin_header_sx(ctx: dict, *, oob: bool = False,
|
||||
selected: str = "") -> str:
|
||||
"""Post-level admin row for events — delegates to shared helper."""
|
||||
slug = (ctx.get("post") or {}).get("slug", "")
|
||||
return await post_admin_header_sx(ctx, slug, oob=oob, selected=selected)
|
||||
1042
events/sxc/pages/entries.py
Normal file
1042
events/sxc/pages/entries.py
Normal file
File diff suppressed because it is too large
Load Diff
396
events/sxc/pages/helpers.py
Normal file
396
events/sxc/pages/helpers.py
Normal file
@@ -0,0 +1,396 @@
|
||||
"""Page helpers — _h_* helper functions + _register_events_helpers + _ensure_* context hydration."""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Shared hydration helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _add_to_defpage_ctx(**kwargs: Any) -> None:
|
||||
"""Add data to g._defpage_ctx for the app-level context_processor."""
|
||||
from quart import g
|
||||
if not hasattr(g, '_defpage_ctx'):
|
||||
g._defpage_ctx = {}
|
||||
g._defpage_ctx.update(kwargs)
|
||||
|
||||
|
||||
async def _ensure_calendar(calendar_slug: str | None) -> None:
|
||||
"""Load calendar into g.calendar if not already present."""
|
||||
from quart import g, abort
|
||||
if hasattr(g, 'calendar'):
|
||||
_add_to_defpage_ctx(calendar=g.calendar)
|
||||
return
|
||||
from bp.calendar.services.calendar_view import (
|
||||
get_calendar_by_post_and_slug, get_calendar_by_slug,
|
||||
)
|
||||
post_data = getattr(g, "post_data", None)
|
||||
if post_data:
|
||||
post_id = (post_data.get("post") or {}).get("id")
|
||||
cal = await get_calendar_by_post_and_slug(g.s, post_id, calendar_slug)
|
||||
else:
|
||||
cal = await get_calendar_by_slug(g.s, calendar_slug)
|
||||
if not cal:
|
||||
abort(404)
|
||||
g.calendar = cal
|
||||
g.calendar_slug = calendar_slug
|
||||
_add_to_defpage_ctx(calendar=cal)
|
||||
|
||||
|
||||
async def _ensure_entry(entry_id: int | None) -> None:
|
||||
"""Load calendar entry into g.entry if not already present."""
|
||||
from quart import g, abort
|
||||
if hasattr(g, 'entry'):
|
||||
_add_to_defpage_ctx(entry=g.entry)
|
||||
return
|
||||
from sqlalchemy import select
|
||||
from models.calendars import CalendarEntry
|
||||
result = await g.s.execute(
|
||||
select(CalendarEntry).where(
|
||||
CalendarEntry.id == entry_id,
|
||||
CalendarEntry.deleted_at.is_(None),
|
||||
)
|
||||
)
|
||||
entry = result.scalar_one_or_none()
|
||||
if entry is None:
|
||||
abort(404)
|
||||
g.entry = entry
|
||||
_add_to_defpage_ctx(entry=entry)
|
||||
|
||||
|
||||
async def _ensure_entry_context(entry_id: int | None) -> None:
|
||||
"""Load full entry context (ticket data, posts) into g.* and _defpage_ctx."""
|
||||
from quart import g
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.orm import selectinload
|
||||
from models.calendars import CalendarEntry
|
||||
from bp.tickets.services.tickets import (
|
||||
get_available_ticket_count,
|
||||
get_sold_ticket_count,
|
||||
get_user_reserved_count,
|
||||
)
|
||||
from shared.infrastructure.cart_identity import current_cart_identity
|
||||
from bp.calendar_entry.services.post_associations import get_entry_posts
|
||||
|
||||
await _ensure_entry(entry_id)
|
||||
|
||||
# Reload with ticket_types eagerly loaded
|
||||
stmt = (
|
||||
select(CalendarEntry)
|
||||
.where(CalendarEntry.id == entry_id, CalendarEntry.deleted_at.is_(None))
|
||||
.options(selectinload(CalendarEntry.ticket_types))
|
||||
)
|
||||
result = await g.s.execute(stmt)
|
||||
calendar_entry = result.scalar_one_or_none()
|
||||
|
||||
if calendar_entry and getattr(g, "calendar", None):
|
||||
if calendar_entry.calendar_id != g.calendar.id:
|
||||
calendar_entry = None
|
||||
|
||||
if calendar_entry:
|
||||
await g.s.refresh(calendar_entry, ['slot'])
|
||||
g.entry = calendar_entry
|
||||
entry_posts = await get_entry_posts(g.s, calendar_entry.id)
|
||||
ticket_remaining = await get_available_ticket_count(g.s, calendar_entry.id)
|
||||
ticket_sold_count = await get_sold_ticket_count(g.s, calendar_entry.id)
|
||||
ident = current_cart_identity()
|
||||
user_ticket_count = await get_user_reserved_count(
|
||||
g.s, calendar_entry.id,
|
||||
user_id=ident["user_id"],
|
||||
session_id=ident["session_id"],
|
||||
)
|
||||
user_ticket_counts_by_type = {}
|
||||
if calendar_entry.ticket_types:
|
||||
for tt in calendar_entry.ticket_types:
|
||||
if tt.deleted_at is None:
|
||||
user_ticket_counts_by_type[tt.id] = await get_user_reserved_count(
|
||||
g.s, calendar_entry.id,
|
||||
user_id=ident["user_id"],
|
||||
session_id=ident["session_id"],
|
||||
ticket_type_id=tt.id,
|
||||
)
|
||||
_add_to_defpage_ctx(
|
||||
entry=calendar_entry,
|
||||
entry_posts=entry_posts,
|
||||
ticket_remaining=ticket_remaining,
|
||||
ticket_sold_count=ticket_sold_count,
|
||||
user_ticket_count=user_ticket_count,
|
||||
user_ticket_counts_by_type=user_ticket_counts_by_type,
|
||||
)
|
||||
|
||||
|
||||
async def _ensure_day_data(year: int, month: int, day: int) -> None:
|
||||
"""Load day-specific data for layout header functions."""
|
||||
from quart import g, session as qsession
|
||||
if hasattr(g, 'day_date'):
|
||||
return
|
||||
from datetime import date as date_cls, datetime, timezone, timedelta
|
||||
from sqlalchemy import select
|
||||
from bp.calendar.services import get_visible_entries_for_period
|
||||
from models.calendars import CalendarSlot
|
||||
|
||||
calendar = getattr(g, "calendar", None)
|
||||
if not calendar:
|
||||
return
|
||||
|
||||
try:
|
||||
day_date = date_cls(year, month, day)
|
||||
except (ValueError, TypeError):
|
||||
return
|
||||
|
||||
period_start = datetime(year, month, day, tzinfo=timezone.utc)
|
||||
period_end = period_start + timedelta(days=1)
|
||||
|
||||
user = getattr(g, "user", None)
|
||||
session_id = qsession.get("calendar_sid")
|
||||
|
||||
visible = await get_visible_entries_for_period(
|
||||
sess=g.s,
|
||||
calendar_id=calendar.id,
|
||||
period_start=period_start,
|
||||
period_end=period_end,
|
||||
user=user,
|
||||
session_id=session_id,
|
||||
)
|
||||
|
||||
weekday_attr = ["mon", "tue", "wed", "thu", "fri", "sat", "sun"][day_date.weekday()]
|
||||
stmt = (
|
||||
select(CalendarSlot)
|
||||
.where(
|
||||
CalendarSlot.calendar_id == calendar.id,
|
||||
getattr(CalendarSlot, weekday_attr) == True, # noqa: E712
|
||||
CalendarSlot.deleted_at.is_(None),
|
||||
)
|
||||
.order_by(CalendarSlot.time_start.asc(), CalendarSlot.id.asc())
|
||||
)
|
||||
result = await g.s.execute(stmt)
|
||||
day_slots = list(result.scalars())
|
||||
|
||||
g.day_date = day_date
|
||||
_add_to_defpage_ctx(
|
||||
qsession=qsession,
|
||||
day_date=day_date,
|
||||
day=day,
|
||||
year=year,
|
||||
month=month,
|
||||
day_entries=visible.merged_entries,
|
||||
user_entries=visible.user_entries,
|
||||
confirmed_entries=visible.confirmed_entries,
|
||||
day_slots=day_slots,
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Page helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _register_events_helpers() -> None:
|
||||
from shared.sx.pages import register_page_helpers
|
||||
|
||||
register_page_helpers("events", {
|
||||
"calendar-admin-content": _h_calendar_admin_content,
|
||||
"day-admin-content": _h_day_admin_content,
|
||||
"slots-content": _h_slots_content,
|
||||
"slot-content": _h_slot_content,
|
||||
"entry-content": _h_entry_content,
|
||||
"entry-menu": _h_entry_menu,
|
||||
"entry-admin-content": _h_entry_admin_content,
|
||||
"admin-menu": _h_admin_menu,
|
||||
"ticket-types-content": _h_ticket_types_content,
|
||||
"ticket-type-content": _h_ticket_type_content,
|
||||
"tickets-content": _h_tickets_content,
|
||||
"ticket-detail-content": _h_ticket_detail_content,
|
||||
"ticket-admin-content": _h_ticket_admin_content,
|
||||
"markets-content": _h_markets_content,
|
||||
})
|
||||
|
||||
|
||||
async def _h_calendar_admin_content(calendar_slug=None, **kw):
|
||||
await _ensure_calendar(calendar_slug)
|
||||
from shared.sx.page import get_template_context
|
||||
from .calendar import _calendar_admin_main_panel_html
|
||||
ctx = await get_template_context()
|
||||
return await _calendar_admin_main_panel_html(ctx)
|
||||
|
||||
|
||||
async def _h_day_admin_content(calendar_slug=None, year=None, month=None, day=None, **kw):
|
||||
await _ensure_calendar(calendar_slug)
|
||||
if year is not None:
|
||||
await _ensure_day_data(int(year), int(month), int(day))
|
||||
from .calendar import _day_admin_main_panel_html
|
||||
return await _day_admin_main_panel_html({})
|
||||
|
||||
|
||||
async def _h_slots_content(calendar_slug=None, **kw):
|
||||
from quart import g
|
||||
await _ensure_calendar(calendar_slug)
|
||||
calendar = getattr(g, "calendar", None)
|
||||
from bp.slots.services.slots import list_slots as svc_list_slots
|
||||
from .slots import render_slots_table
|
||||
slots = await svc_list_slots(g.s, calendar.id) if calendar else []
|
||||
_add_to_defpage_ctx(slots=slots)
|
||||
return await render_slots_table(slots, calendar)
|
||||
|
||||
|
||||
async def _h_slot_content(calendar_slug=None, slot_id=None, **kw):
|
||||
from quart import g, abort
|
||||
await _ensure_calendar(calendar_slug)
|
||||
from bp.slot.services.slot import get_slot as svc_get_slot
|
||||
from .slots import render_slot_main_panel
|
||||
slot = await svc_get_slot(g.s, slot_id) if slot_id else None
|
||||
if not slot:
|
||||
abort(404)
|
||||
g.slot = slot
|
||||
_add_to_defpage_ctx(slot=slot)
|
||||
calendar = getattr(g, "calendar", None)
|
||||
return await render_slot_main_panel(slot, calendar)
|
||||
|
||||
|
||||
async def _h_entry_content(calendar_slug=None, entry_id=None, **kw):
|
||||
await _ensure_calendar(calendar_slug)
|
||||
await _ensure_entry_context(entry_id)
|
||||
from shared.sx.page import get_template_context
|
||||
from .entries import _entry_main_panel_html
|
||||
ctx = await get_template_context()
|
||||
return await _entry_main_panel_html(ctx)
|
||||
|
||||
|
||||
async def _h_entry_menu(calendar_slug=None, entry_id=None, **kw):
|
||||
await _ensure_calendar(calendar_slug)
|
||||
await _ensure_entry_context(entry_id)
|
||||
from shared.sx.page import get_template_context
|
||||
from .entries import _entry_nav_html
|
||||
ctx = await get_template_context()
|
||||
return await _entry_nav_html(ctx)
|
||||
|
||||
|
||||
async def _h_entry_admin_content(calendar_slug=None, entry_id=None, **kw):
|
||||
await _ensure_calendar(calendar_slug)
|
||||
await _ensure_entry_context(entry_id)
|
||||
from shared.sx.page import get_template_context
|
||||
from .entries import _entry_admin_main_panel_html
|
||||
ctx = await get_template_context()
|
||||
return await _entry_admin_main_panel_html(ctx)
|
||||
|
||||
|
||||
async def _h_admin_menu():
|
||||
from shared.sx.helpers import render_to_sx
|
||||
return await render_to_sx("events-admin-placeholder-nav")
|
||||
|
||||
|
||||
async def _h_ticket_types_content(calendar_slug=None, entry_id=None,
|
||||
year=None, month=None, day=None, **kw):
|
||||
from quart import g
|
||||
await _ensure_calendar(calendar_slug)
|
||||
await _ensure_entry(entry_id)
|
||||
entry = getattr(g, "entry", None)
|
||||
calendar = getattr(g, "calendar", None)
|
||||
from bp.ticket_types.services.tickets import list_ticket_types as svc_list_ticket_types
|
||||
from .tickets import render_ticket_types_table
|
||||
ticket_types = await svc_list_ticket_types(g.s, entry.id) if entry else []
|
||||
_add_to_defpage_ctx(ticket_types=ticket_types)
|
||||
return await render_ticket_types_table(ticket_types, entry, calendar, day, month, year)
|
||||
|
||||
|
||||
async def _h_ticket_type_content(calendar_slug=None, entry_id=None,
|
||||
ticket_type_id=None, year=None, month=None, day=None, **kw):
|
||||
from quart import g, abort
|
||||
await _ensure_calendar(calendar_slug)
|
||||
await _ensure_entry(entry_id)
|
||||
from bp.ticket_type.services.ticket import get_ticket_type as svc_get_ticket_type
|
||||
from .tickets import render_ticket_type_main_panel
|
||||
ticket_type = await svc_get_ticket_type(g.s, ticket_type_id) if ticket_type_id else None
|
||||
if not ticket_type:
|
||||
abort(404)
|
||||
g.ticket_type = ticket_type
|
||||
_add_to_defpage_ctx(ticket_type=ticket_type)
|
||||
entry = getattr(g, "entry", None)
|
||||
calendar = getattr(g, "calendar", None)
|
||||
return await render_ticket_type_main_panel(ticket_type, entry, calendar, day, month, year)
|
||||
|
||||
|
||||
async def _h_tickets_content(**kw):
|
||||
from quart import g
|
||||
from shared.infrastructure.cart_identity import current_cart_identity
|
||||
from bp.tickets.services.tickets import get_user_tickets
|
||||
from .tickets import _tickets_main_panel_html
|
||||
ident = current_cart_identity()
|
||||
tickets = await get_user_tickets(
|
||||
g.s,
|
||||
user_id=ident["user_id"],
|
||||
session_id=ident["session_id"],
|
||||
)
|
||||
from shared.sx.page import get_template_context
|
||||
ctx = await get_template_context()
|
||||
return await _tickets_main_panel_html(ctx, tickets)
|
||||
|
||||
|
||||
async def _h_ticket_detail_content(code=None, **kw):
|
||||
from quart import g, abort
|
||||
from shared.infrastructure.cart_identity import current_cart_identity
|
||||
from bp.tickets.services.tickets import get_ticket_by_code
|
||||
from .tickets import _ticket_detail_panel_html
|
||||
ticket = await get_ticket_by_code(g.s, code) if code else None
|
||||
if not ticket:
|
||||
abort(404)
|
||||
# Verify ownership
|
||||
ident = current_cart_identity()
|
||||
if ident["user_id"] is not None:
|
||||
if ticket.user_id != ident["user_id"]:
|
||||
abort(404)
|
||||
elif ident["session_id"] is not None:
|
||||
if ticket.session_id != ident["session_id"]:
|
||||
abort(404)
|
||||
else:
|
||||
abort(404)
|
||||
from shared.sx.page import get_template_context
|
||||
ctx = await get_template_context()
|
||||
return await _ticket_detail_panel_html(ctx, ticket)
|
||||
|
||||
|
||||
async def _h_ticket_admin_content(**kw):
|
||||
from quart import g
|
||||
from sqlalchemy import select, func
|
||||
from sqlalchemy.orm import selectinload
|
||||
from models.calendars import CalendarEntry, Ticket
|
||||
from .tickets import _ticket_admin_main_panel_html
|
||||
|
||||
result = await g.s.execute(
|
||||
select(Ticket)
|
||||
.options(
|
||||
selectinload(Ticket.entry).selectinload(CalendarEntry.calendar),
|
||||
selectinload(Ticket.ticket_type),
|
||||
)
|
||||
.order_by(Ticket.created_at.desc())
|
||||
.limit(50)
|
||||
)
|
||||
tickets = result.scalars().all()
|
||||
|
||||
total = await g.s.scalar(select(func.count(Ticket.id)))
|
||||
confirmed = await g.s.scalar(
|
||||
select(func.count(Ticket.id)).where(Ticket.state == "confirmed")
|
||||
)
|
||||
checked_in = await g.s.scalar(
|
||||
select(func.count(Ticket.id)).where(Ticket.state == "checked_in")
|
||||
)
|
||||
reserved = await g.s.scalar(
|
||||
select(func.count(Ticket.id)).where(Ticket.state == "reserved")
|
||||
)
|
||||
stats = {
|
||||
"total": total or 0,
|
||||
"confirmed": confirmed or 0,
|
||||
"checked_in": checked_in or 0,
|
||||
"reserved": reserved or 0,
|
||||
}
|
||||
|
||||
from shared.sx.page import get_template_context
|
||||
ctx = await get_template_context()
|
||||
return await _ticket_admin_main_panel_html(ctx, tickets, stats)
|
||||
|
||||
|
||||
async def _h_markets_content(**kw):
|
||||
from shared.sx.page import get_template_context
|
||||
from .calendar import _markets_main_panel_html
|
||||
ctx = await get_template_context()
|
||||
return await _markets_main_panel_html(ctx)
|
||||
288
events/sxc/pages/layouts.py
Normal file
288
events/sxc/pages/layouts.py
Normal file
@@ -0,0 +1,288 @@
|
||||
"""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, _ctx_to_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", _ctx_to_env(ctx),
|
||||
post_header=SxExpr(await _post_header_sx(ctx)),
|
||||
admin_header=SxExpr(await post_admin_header_sx(ctx, slug, selected="calendars")),
|
||||
calendar_header=SxExpr(await _calendar_header_sx(ctx)),
|
||||
calendar_admin_header=SxExpr(await _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, _ctx_to_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", _ctx_to_env(ctx, oob=True),
|
||||
admin_oob=SxExpr(await post_admin_header_sx(ctx, slug, oob=True, selected="calendars")),
|
||||
cal_oob=SxExpr(await _calendar_header_sx(ctx, oob=True)),
|
||||
cal_admin_oob_wrap=SxExpr(await oob_header_sx("calendar-header-child",
|
||||
"calendar-admin-header-child", await _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, _ctx_to_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", _ctx_to_env(ctx, oob=True),
|
||||
admin_oob=SxExpr(await post_admin_header_sx(ctx, slug, oob=True, selected="calendars")),
|
||||
cal_admin_oob=SxExpr(await _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, _ctx_to_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", _ctx_to_env(ctx),
|
||||
post_header=SxExpr(await _post_header_sx(ctx)),
|
||||
admin_header=SxExpr(await post_admin_header_sx(ctx, slug, selected="calendars")),
|
||||
calendar_header=SxExpr(await _calendar_header_sx(ctx)),
|
||||
calendar_admin_header=SxExpr(await _calendar_admin_header_sx(ctx)),
|
||||
slot_header=SxExpr(await _slot_header_html(ctx)),
|
||||
)
|
||||
|
||||
|
||||
async def _slot_oob(ctx: dict, **kw: Any) -> str:
|
||||
from shared.sx.helpers import render_to_sx_with_env, _ctx_to_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", _ctx_to_env(ctx, oob=True),
|
||||
admin_oob=SxExpr(await post_admin_header_sx(ctx, slug, oob=True, selected="calendars")),
|
||||
cal_admin_oob=SxExpr(await _calendar_admin_header_sx(ctx, oob=True)),
|
||||
slot_oob_wrap=SxExpr(await oob_header_sx("calendar-admin-header-child",
|
||||
"slot-header-child", await _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, _ctx_to_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", _ctx_to_env(ctx),
|
||||
post_header=SxExpr(await _post_header_sx(ctx)),
|
||||
admin_header=SxExpr(await post_admin_header_sx(ctx, slug, selected="calendars")),
|
||||
calendar_header=SxExpr(await _calendar_header_sx(ctx)),
|
||||
day_header=SxExpr(await _day_header_sx(ctx)),
|
||||
day_admin_header=SxExpr(await _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, _ctx_to_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", _ctx_to_env(ctx, oob=True),
|
||||
admin_oob=SxExpr(await post_admin_header_sx(ctx, slug, oob=True, selected="calendars")),
|
||||
cal_oob=SxExpr(await _calendar_header_sx(ctx, oob=True)),
|
||||
day_admin_oob_wrap=SxExpr(await oob_header_sx("day-header-child",
|
||||
"day-admin-header-child", await _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, _ctx_to_env
|
||||
from shared.sx.parser import SxExpr
|
||||
return await render_to_sx_with_env("events-entry-layout-full", _ctx_to_env(ctx),
|
||||
post_header=SxExpr(await _post_header_sx(ctx)),
|
||||
calendar_header=SxExpr(await _calendar_header_sx(ctx)),
|
||||
day_header=SxExpr(await _day_header_sx(ctx)),
|
||||
entry_header=SxExpr(await _entry_header_html(ctx)),
|
||||
)
|
||||
|
||||
|
||||
async def _entry_oob(ctx: dict, **kw: Any) -> str:
|
||||
from shared.sx.helpers import render_to_sx_with_env, _ctx_to_env, oob_header_sx
|
||||
from shared.sx.parser import SxExpr
|
||||
return await render_to_sx_with_env("events-entry-layout-oob", _ctx_to_env(ctx, oob=True),
|
||||
day_oob=SxExpr(await _day_header_sx(ctx, oob=True)),
|
||||
entry_oob_wrap=SxExpr(await oob_header_sx("day-header-child",
|
||||
"entry-header-child", await _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, _ctx_to_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", _ctx_to_env(ctx),
|
||||
post_header=SxExpr(await _post_header_sx(ctx)),
|
||||
admin_header=SxExpr(await post_admin_header_sx(ctx, slug, selected="calendars")),
|
||||
calendar_header=SxExpr(await _calendar_header_sx(ctx)),
|
||||
day_header=SxExpr(await _day_header_sx(ctx)),
|
||||
entry_header=SxExpr(await _entry_header_html(ctx)),
|
||||
entry_admin_header=SxExpr(await _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, _ctx_to_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", _ctx_to_env(ctx, oob=True),
|
||||
admin_oob=SxExpr(await post_admin_header_sx(ctx, slug, oob=True, selected="calendars")),
|
||||
entry_oob=SxExpr(await _entry_header_html(ctx, oob=True)),
|
||||
entry_admin_oob_wrap=SxExpr(await oob_header_sx("entry-header-child",
|
||||
"entry-admin-header-child", await _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, _ctx_to_env
|
||||
from shared.sx.parser import SxExpr
|
||||
return await render_to_sx_with_env("events-ticket-types-layout-full", _ctx_to_env(ctx),
|
||||
post_header=SxExpr(await _post_header_sx(ctx)),
|
||||
calendar_header=SxExpr(await _calendar_header_sx(ctx)),
|
||||
day_header=SxExpr(await _day_header_sx(ctx)),
|
||||
entry_header=SxExpr(await _entry_header_html(ctx)),
|
||||
entry_admin_header=SxExpr(await _entry_admin_header_html(ctx)),
|
||||
ticket_types_header=SxExpr(await _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, _ctx_to_env, oob_header_sx
|
||||
from shared.sx.parser import SxExpr
|
||||
return await render_to_sx_with_env("events-ticket-types-layout-oob", _ctx_to_env(ctx, oob=True),
|
||||
entry_admin_oob=SxExpr(await _entry_admin_header_html(ctx, oob=True)),
|
||||
ticket_types_oob_wrap=SxExpr(await oob_header_sx("entry-admin-header-child",
|
||||
"ticket_types-header-child", await _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, _ctx_to_env
|
||||
from shared.sx.parser import SxExpr
|
||||
return await render_to_sx_with_env("events-ticket-type-layout-full", _ctx_to_env(ctx),
|
||||
post_header=SxExpr(await _post_header_sx(ctx)),
|
||||
calendar_header=SxExpr(await _calendar_header_sx(ctx)),
|
||||
day_header=SxExpr(await _day_header_sx(ctx)),
|
||||
entry_header=SxExpr(await _entry_header_html(ctx)),
|
||||
entry_admin_header=SxExpr(await _entry_admin_header_html(ctx)),
|
||||
ticket_types_header=SxExpr(await _ticket_types_header_html(ctx)),
|
||||
ticket_type_header=SxExpr(await _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, _ctx_to_env, oob_header_sx
|
||||
from shared.sx.parser import SxExpr
|
||||
return await render_to_sx_with_env("events-ticket-type-layout-oob", _ctx_to_env(ctx, oob=True),
|
||||
ticket_types_oob=SxExpr(await _ticket_types_header_html(ctx, oob=True)),
|
||||
ticket_type_oob_wrap=SxExpr(await oob_header_sx("ticket_types-header-child",
|
||||
"ticket_type-header-child", await _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, _ctx_to_env
|
||||
from shared.sx.parser import SxExpr
|
||||
return await render_to_sx_with_env("events-markets-layout-full", _ctx_to_env(ctx),
|
||||
post_header=SxExpr(await _post_header_sx(ctx)),
|
||||
markets_header=SxExpr(await _markets_header_sx(ctx)),
|
||||
)
|
||||
|
||||
|
||||
async def _markets_oob(ctx: dict, **kw: Any) -> str:
|
||||
from shared.sx.helpers import render_to_sx_with_env, _ctx_to_env, oob_header_sx
|
||||
from shared.sx.parser import SxExpr
|
||||
return await render_to_sx_with_env("events-markets-layout-oob", _ctx_to_env(ctx, oob=True),
|
||||
post_oob=SxExpr(await _post_header_sx(ctx, oob=True)),
|
||||
markets_oob_wrap=SxExpr(await oob_header_sx("post-header-child",
|
||||
"markets-header-child", await _markets_header_sx(ctx))),
|
||||
)
|
||||
287
events/sxc/pages/renders.py
Normal file
287
events/sxc/pages/renders.py
Normal file
@@ -0,0 +1,287 @@
|
||||
"""Top-level render_* functions — public API called from route handlers."""
|
||||
from __future__ import annotations
|
||||
|
||||
from shared.sx.helpers import (
|
||||
render_to_sx_with_env, _ctx_to_env,
|
||||
post_admin_header_sx, oob_header_sx,
|
||||
header_child_sx, full_page_sx, oob_page_sx,
|
||||
)
|
||||
|
||||
from .utils import _clear_deeper_oob, _ensure_container_nav
|
||||
from .calendar import (
|
||||
_post_header_sx, _calendars_header_sx,
|
||||
_calendar_header_sx, _day_header_sx,
|
||||
_calendars_main_panel_sx,
|
||||
_calendar_main_panel_html, _day_main_panel_html,
|
||||
_calendar_admin_main_panel_html,
|
||||
_calendar_description_display_html,
|
||||
_markets_main_panel_html,
|
||||
)
|
||||
from .entries import (
|
||||
_events_main_panel_html, _entry_cards_html,
|
||||
_entry_main_panel_html,
|
||||
)
|
||||
from .tickets import render_buy_form
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# All events
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
async def render_all_events_page(ctx: dict, entries, has_more, pending_tickets,
|
||||
page_info, page, view) -> str:
|
||||
"""Full page: all events listing."""
|
||||
from quart import url_for
|
||||
from shared.utils import route_prefix
|
||||
from shared.infrastructure.urls import events_url
|
||||
|
||||
prefix = route_prefix()
|
||||
view_param = f"&view={view}" if view != "list" else ""
|
||||
ticket_url = url_for("all_events.adjust_ticket")
|
||||
next_url = prefix + url_for("all_events.entries_fragment", page=page + 1) + (f"?view={view}" if view != "list" else "")
|
||||
|
||||
content = await _events_main_panel_html(
|
||||
ctx, entries, has_more, pending_tickets, page_info,
|
||||
page, view, ticket_url, next_url, events_url,
|
||||
)
|
||||
hdr = await render_to_sx_with_env("layout-root-full", _ctx_to_env(ctx))
|
||||
return await full_page_sx(ctx, header_rows=hdr, content=content)
|
||||
|
||||
|
||||
async def render_all_events_oob(ctx: dict, entries, has_more, pending_tickets,
|
||||
page_info, page, view) -> str:
|
||||
"""OOB response: all events listing (htmx nav)."""
|
||||
from quart import url_for
|
||||
from shared.utils import route_prefix
|
||||
from shared.infrastructure.urls import events_url
|
||||
|
||||
prefix = route_prefix()
|
||||
ticket_url = url_for("all_events.adjust_ticket")
|
||||
next_url = prefix + url_for("all_events.entries_fragment", page=page + 1) + (f"?view={view}" if view != "list" else "")
|
||||
|
||||
content = await _events_main_panel_html(
|
||||
ctx, entries, has_more, pending_tickets, page_info,
|
||||
page, view, ticket_url, next_url, events_url,
|
||||
)
|
||||
return await oob_page_sx(content=content)
|
||||
|
||||
|
||||
async def render_all_events_cards(entries, has_more, pending_tickets,
|
||||
page_info, page, view) -> str:
|
||||
"""Pagination fragment: all events cards only."""
|
||||
from quart import url_for
|
||||
from shared.utils import route_prefix
|
||||
from shared.infrastructure.urls import events_url
|
||||
|
||||
prefix = route_prefix()
|
||||
ticket_url = url_for("all_events.adjust_ticket")
|
||||
next_url = prefix + url_for("all_events.entries_fragment", page=page + 1) + (f"?view={view}" if view != "list" else "")
|
||||
|
||||
return await _entry_cards_html(
|
||||
entries, page_info, pending_tickets, ticket_url, events_url,
|
||||
view, page, has_more, next_url,
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Page summary
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
async def render_page_summary_page(ctx: dict, entries, has_more, pending_tickets,
|
||||
page_info, page, view) -> str:
|
||||
"""Full page: page-scoped events listing."""
|
||||
from quart import url_for
|
||||
from shared.utils import route_prefix
|
||||
from shared.infrastructure.urls import events_url
|
||||
|
||||
prefix = route_prefix()
|
||||
post = ctx.get("post") or {}
|
||||
ticket_url = url_for("page_summary.adjust_ticket")
|
||||
next_url = prefix + url_for("page_summary.entries_fragment", page=page + 1) + (f"?view={view}" if view != "list" else "")
|
||||
|
||||
content = await _events_main_panel_html(
|
||||
ctx, entries, has_more, pending_tickets, page_info,
|
||||
page, view, ticket_url, next_url, events_url,
|
||||
is_page_scoped=True, post=post,
|
||||
)
|
||||
|
||||
hdr = await render_to_sx_with_env("layout-root-full", _ctx_to_env(ctx))
|
||||
hdr += await header_child_sx(await _post_header_sx(ctx))
|
||||
return await full_page_sx(ctx, header_rows=hdr, content=content)
|
||||
|
||||
|
||||
async def render_page_summary_oob(ctx: dict, entries, has_more, pending_tickets,
|
||||
page_info, page, view) -> str:
|
||||
"""OOB response: page-scoped events (htmx nav)."""
|
||||
from quart import url_for
|
||||
from shared.utils import route_prefix
|
||||
from shared.infrastructure.urls import events_url
|
||||
|
||||
prefix = route_prefix()
|
||||
post = ctx.get("post") or {}
|
||||
ticket_url = url_for("page_summary.adjust_ticket")
|
||||
next_url = prefix + url_for("page_summary.entries_fragment", page=page + 1) + (f"?view={view}" if view != "list" else "")
|
||||
|
||||
content = await _events_main_panel_html(
|
||||
ctx, entries, has_more, pending_tickets, page_info,
|
||||
page, view, ticket_url, next_url, events_url,
|
||||
is_page_scoped=True, post=post,
|
||||
)
|
||||
|
||||
oobs = await _post_header_sx(ctx, oob=True)
|
||||
oobs += _clear_deeper_oob("post-row", "post-header-child")
|
||||
return await oob_page_sx(oobs=oobs, content=content)
|
||||
|
||||
|
||||
async def render_page_summary_cards(entries, has_more, pending_tickets,
|
||||
page_info, page, view, post) -> str:
|
||||
"""Pagination fragment: page-scoped events cards only."""
|
||||
from quart import url_for
|
||||
from shared.utils import route_prefix
|
||||
from shared.infrastructure.urls import events_url
|
||||
|
||||
prefix = route_prefix()
|
||||
ticket_url = url_for("page_summary.adjust_ticket")
|
||||
next_url = prefix + url_for("page_summary.entries_fragment", page=page + 1) + (f"?view={view}" if view != "list" else "")
|
||||
|
||||
return await _entry_cards_html(
|
||||
entries, page_info, pending_tickets, ticket_url, events_url,
|
||||
view, page, has_more, next_url,
|
||||
is_page_scoped=True, post=post,
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Calendars home
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
async def render_calendars_page(ctx: dict) -> str:
|
||||
"""Full page: calendars listing."""
|
||||
content = await _calendars_main_panel_sx(ctx)
|
||||
ctx = await _ensure_container_nav(ctx)
|
||||
slug = (ctx.get("post") or {}).get("slug", "")
|
||||
root_hdr = await render_to_sx_with_env("layout-root-full", _ctx_to_env(ctx))
|
||||
post_hdr = await _post_header_sx(ctx)
|
||||
admin_hdr = await post_admin_header_sx(ctx, slug, selected="calendars")
|
||||
return await full_page_sx(ctx, header_rows=root_hdr + post_hdr + admin_hdr, content=content)
|
||||
|
||||
|
||||
async def render_calendars_oob(ctx: dict) -> str:
|
||||
"""OOB response: calendars listing."""
|
||||
content = await _calendars_main_panel_sx(ctx)
|
||||
ctx = await _ensure_container_nav(ctx)
|
||||
slug = (ctx.get("post") or {}).get("slug", "")
|
||||
oobs = await post_admin_header_sx(ctx, slug, oob=True, selected="calendars")
|
||||
oobs += _clear_deeper_oob("post-row", "post-header-child",
|
||||
"post-admin-row", "post-admin-header-child")
|
||||
return await oob_page_sx(oobs=oobs, content=content)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Calendar month view
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
async def render_calendar_page(ctx: dict) -> str:
|
||||
"""Full page: calendar month view."""
|
||||
content = await _calendar_main_panel_html(ctx)
|
||||
hdr = await render_to_sx_with_env("layout-root-full", _ctx_to_env(ctx))
|
||||
child = await _post_header_sx(ctx) + await _calendar_header_sx(ctx)
|
||||
hdr += await header_child_sx(child)
|
||||
return await full_page_sx(ctx, header_rows=hdr, content=content)
|
||||
|
||||
|
||||
async def render_calendar_oob(ctx: dict) -> str:
|
||||
"""OOB response: calendar month view."""
|
||||
content = await _calendar_main_panel_html(ctx)
|
||||
oobs = await _post_header_sx(ctx, oob=True)
|
||||
oobs += await oob_header_sx("post-header-child", "calendar-header-child",
|
||||
await _calendar_header_sx(ctx))
|
||||
oobs += _clear_deeper_oob("post-row", "post-header-child",
|
||||
"calendar-row", "calendar-header-child")
|
||||
return await oob_page_sx(oobs=oobs, content=content)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Day detail
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
async def render_day_page(ctx: dict) -> str:
|
||||
"""Full page: day detail."""
|
||||
content = await _day_main_panel_html(ctx)
|
||||
hdr = await render_to_sx_with_env("layout-root-full", _ctx_to_env(ctx))
|
||||
child = (await _post_header_sx(ctx)
|
||||
+ await _calendar_header_sx(ctx) + await _day_header_sx(ctx))
|
||||
hdr += await header_child_sx(child)
|
||||
return await full_page_sx(ctx, header_rows=hdr, content=content)
|
||||
|
||||
|
||||
async def render_day_oob(ctx: dict) -> str:
|
||||
"""OOB response: day detail."""
|
||||
content = await _day_main_panel_html(ctx)
|
||||
oobs = await _calendar_header_sx(ctx, oob=True)
|
||||
oobs += await oob_header_sx("calendar-header-child", "day-header-child",
|
||||
await _day_header_sx(ctx))
|
||||
oobs += _clear_deeper_oob("post-row", "post-header-child",
|
||||
"calendar-row", "calendar-header-child",
|
||||
"day-row", "day-header-child")
|
||||
return await oob_page_sx(oobs=oobs, content=content)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Day main panel -- public API
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
async def render_day_main_panel(ctx: dict) -> str:
|
||||
"""Public wrapper for day main panel rendering."""
|
||||
return await _day_main_panel_html(ctx)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Calendar description display + edit form
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
async def render_calendar_description(calendar, *, oob: bool = False) -> str:
|
||||
"""Render calendar description display with edit button, optionally with OOB title."""
|
||||
from quart import url_for
|
||||
from shared.sx.helpers import render_to_sx
|
||||
|
||||
cal_slug = getattr(calendar, "slug", "")
|
||||
edit_url = url_for("calendar.admin.calendar_description_edit", calendar_slug=cal_slug)
|
||||
html = await _calendar_description_display_html(calendar, edit_url)
|
||||
|
||||
if oob:
|
||||
desc = getattr(calendar, "description", "") or ""
|
||||
html += await render_to_sx("events-calendar-description-title-oob",
|
||||
description=desc)
|
||||
return html
|
||||
|
||||
|
||||
async def render_calendar_description_edit(calendar) -> str:
|
||||
"""Render calendar description edit form."""
|
||||
from quart import url_for
|
||||
from shared.browser.app.csrf import generate_csrf_token
|
||||
from shared.sx.helpers import render_to_sx
|
||||
csrf = generate_csrf_token()
|
||||
cal_slug = getattr(calendar, "slug", "")
|
||||
desc = getattr(calendar, "description", "") or ""
|
||||
|
||||
save_url = url_for("calendar.admin.calendar_description_save", calendar_slug=cal_slug)
|
||||
cancel_url = url_for("calendar.admin.calendar_description_view", calendar_slug=cal_slug)
|
||||
|
||||
return await render_to_sx("events-calendar-description-edit-form",
|
||||
save_url=save_url, cancel_url=cancel_url,
|
||||
csrf=csrf, description=desc)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Calendars / Markets list panels (for POST create / DELETE)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
async def render_calendars_list_panel(ctx: dict) -> str:
|
||||
"""Render the calendars main panel HTML for POST/DELETE response."""
|
||||
return await _calendars_main_panel_sx(ctx)
|
||||
|
||||
|
||||
async def render_markets_list_panel(ctx: dict) -> str:
|
||||
"""Render the markets main panel HTML for POST/DELETE response."""
|
||||
return await _markets_main_panel_html(ctx)
|
||||
381
events/sxc/pages/slots.py
Normal file
381
events/sxc/pages/slots.py
Normal file
@@ -0,0 +1,381 @@
|
||||
"""Slot panels, forms, edit/add, slot picker JS."""
|
||||
from __future__ import annotations
|
||||
|
||||
from markupsafe import escape
|
||||
|
||||
from shared.sx.helpers import render_to_sx
|
||||
from shared.sx.parser import SxExpr
|
||||
|
||||
|
||||
# ===========================================================================
|
||||
# SLOT PICKER JS — shared by entry edit + entry add forms
|
||||
# ===========================================================================
|
||||
|
||||
_SLOT_PICKER_JS = """\
|
||||
<script>
|
||||
(function () {
|
||||
function timeToMinutes(timeStr) {
|
||||
if (!timeStr) return 0;
|
||||
const [hours, minutes] = timeStr.split(':').map(Number);
|
||||
return hours * 60 + minutes;
|
||||
}
|
||||
|
||||
function calculateCost(slotCost, slotStart, slotEnd, actualStart, actualEnd, flexible) {
|
||||
if (!flexible) {
|
||||
return parseFloat(slotCost);
|
||||
}
|
||||
if (!actualStart || !actualEnd) return 0;
|
||||
const slotStartMin = timeToMinutes(slotStart);
|
||||
const slotEndMin = timeToMinutes(slotEnd);
|
||||
const actualStartMin = timeToMinutes(actualStart);
|
||||
const actualEndMin = timeToMinutes(actualEnd);
|
||||
const slotDuration = slotEndMin - slotStartMin;
|
||||
const actualDuration = actualEndMin - actualStartMin;
|
||||
if (slotDuration <= 0 || actualDuration <= 0) return 0;
|
||||
const ratio = actualDuration / slotDuration;
|
||||
return parseFloat(slotCost) * ratio;
|
||||
}
|
||||
|
||||
function initEntrySlotPicker(root, applyInitial) {
|
||||
if (applyInitial === undefined) applyInitial = false;
|
||||
const select = root.querySelector('[data-slot-picker]');
|
||||
if (!select) return;
|
||||
const timeFields = root.querySelector('[data-time-fields]');
|
||||
const startInput = root.querySelector('[data-entry-start]');
|
||||
const endInput = root.querySelector('[data-entry-end]');
|
||||
const helper = root.querySelector('[data-slot-boundary]');
|
||||
const costDisplay = root.querySelector('[data-cost-display]');
|
||||
const costRow = root.querySelector('[data-cost-row]');
|
||||
const fixedSummary = root.querySelector('[data-fixed-summary]');
|
||||
if (!startInput || !endInput) return;
|
||||
|
||||
function updateCost() {
|
||||
const opt = select.selectedOptions[0];
|
||||
if (!opt || !opt.value) {
|
||||
if (costDisplay) costDisplay.textContent = '\\u00a30.00';
|
||||
return;
|
||||
}
|
||||
const cost = opt.dataset.cost || '0';
|
||||
const s = opt.dataset.start || '';
|
||||
const e = opt.dataset.end || '';
|
||||
const flexible = opt.dataset.flexible === '1';
|
||||
const calculatedCost = calculateCost(cost, s, e, startInput.value, endInput.value, flexible);
|
||||
if (costDisplay) costDisplay.textContent = '\\u00a3' + calculatedCost.toFixed(2);
|
||||
}
|
||||
|
||||
function applyFromOption(opt) {
|
||||
if (!opt || !opt.value) {
|
||||
if (timeFields) timeFields.classList.add('hidden');
|
||||
if (costRow) costRow.classList.add('hidden');
|
||||
if (fixedSummary) fixedSummary.classList.add('hidden');
|
||||
return;
|
||||
}
|
||||
const s = opt.dataset.start || '';
|
||||
const e = opt.dataset.end || '';
|
||||
const flexible = opt.dataset.flexible === '1';
|
||||
if (!flexible) {
|
||||
if (s) startInput.value = s;
|
||||
if (e) endInput.value = e;
|
||||
if (timeFields) timeFields.classList.add('hidden');
|
||||
if (fixedSummary) {
|
||||
fixedSummary.classList.remove('hidden');
|
||||
fixedSummary.textContent = e ? s + ' \\u2013 ' + e : 'From ' + s + ' (open-ended)';
|
||||
}
|
||||
if (costRow) costRow.classList.remove('hidden');
|
||||
} else {
|
||||
if (timeFields) timeFields.classList.remove('hidden');
|
||||
if (fixedSummary) fixedSummary.classList.add('hidden');
|
||||
if (costRow) costRow.classList.remove('hidden');
|
||||
if (helper) {
|
||||
helper.textContent = e ? 'Times must be between ' + s + ' and ' + e + '.' : 'Start at or after ' + s + '.';
|
||||
}
|
||||
}
|
||||
updateCost();
|
||||
}
|
||||
|
||||
if (applyInitial) applyFromOption(select.selectedOptions[0]);
|
||||
|
||||
if (select._slotChangeHandler) select.removeEventListener('change', select._slotChangeHandler);
|
||||
select._slotChangeHandler = () => applyFromOption(select.selectedOptions[0]);
|
||||
select.addEventListener('change', select._slotChangeHandler);
|
||||
startInput.addEventListener('input', updateCost);
|
||||
endInput.addEventListener('input', updateCost);
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => initEntrySlotPicker(document, true));
|
||||
if (window.htmx) htmx.onLoad((content) => initEntrySlotPicker(content, true));
|
||||
})();
|
||||
</script>"""
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Slot options (shared by entry edit + add forms)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
async def _slot_options_html(day_slots, selected_slot_id=None) -> str:
|
||||
"""Build slot <option> elements."""
|
||||
parts = []
|
||||
for slot in day_slots:
|
||||
start = slot.time_start.strftime("%H:%M") if slot.time_start else ""
|
||||
end = slot.time_end.strftime("%H:%M") if slot.time_end else ""
|
||||
flexible = getattr(slot, "flexible", False)
|
||||
cost = getattr(slot, "cost", None)
|
||||
cost_str = str(cost) if cost is not None else "0"
|
||||
label_parts = [slot.name, f"({start}"]
|
||||
if end:
|
||||
label_parts.append(f"\u2013{end})")
|
||||
else:
|
||||
label_parts.append("\u2013open-ended)")
|
||||
if flexible:
|
||||
label_parts.append("[flexible]")
|
||||
label = " ".join(label_parts)
|
||||
|
||||
parts.append(await render_to_sx("events-slot-option",
|
||||
value=str(slot.id),
|
||||
data_start=start, data_end=end,
|
||||
data_flexible="1" if flexible else "0",
|
||||
data_cost=cost_str,
|
||||
selected="selected" if selected_slot_id == slot.id else None,
|
||||
label=label))
|
||||
return "".join(parts)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Slot header row
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
async def _slot_header_html(ctx: dict, *, oob: bool = False) -> str:
|
||||
"""Build the slot detail header row."""
|
||||
from quart import url_for
|
||||
|
||||
calendar = ctx.get("calendar")
|
||||
if not calendar:
|
||||
return ""
|
||||
cal_slug = getattr(calendar, "slug", "")
|
||||
slot = ctx.get("slot")
|
||||
if not slot:
|
||||
return ""
|
||||
|
||||
# Label: icon + name + description
|
||||
desc = getattr(slot, "description", "") or ""
|
||||
label_inner = (
|
||||
f'<div class="flex flex-col md:flex-row md:gap-2 items-center">'
|
||||
f'<div class="flex flex-row items-center gap-2">'
|
||||
f'<i class="fa fa-clock"></i>'
|
||||
f'<div class="shrink-0">{escape(slot.name)}</div>'
|
||||
f'</div>'
|
||||
f'<p class="text-stone-500 whitespace-pre-line break-all w-full">{escape(desc)}</p>'
|
||||
f'</div>'
|
||||
)
|
||||
|
||||
return await render_to_sx("menu-row-sx", id="slot-row", level=5,
|
||||
link_label_content=SxExpr(label_inner),
|
||||
child_id="slot-header-child", oob=oob)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Slot main panel
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
async def render_slot_main_panel(slot, calendar, *, oob: bool = False) -> str:
|
||||
"""Render slot detail view."""
|
||||
from quart import url_for, g
|
||||
|
||||
styles = getattr(g, "styles", None) or {}
|
||||
list_container = getattr(styles, "list_container", "") if hasattr(styles, "list_container") else styles.get("list_container", "")
|
||||
pre_action = getattr(styles, "pre_action_button", "") if hasattr(styles, "pre_action_button") else styles.get("pre_action_button", "")
|
||||
cal_slug = getattr(calendar, "slug", "")
|
||||
|
||||
days_display = getattr(slot, "days_display", "\u2014")
|
||||
days = days_display.split(", ")
|
||||
flexible = getattr(slot, "flexible", False)
|
||||
time_start = slot.time_start.strftime("%H:%M") if slot.time_start else ""
|
||||
time_end = slot.time_end.strftime("%H:%M") if slot.time_end else ""
|
||||
cost = getattr(slot, "cost", None)
|
||||
cost_str = f"{cost:.2f}" if cost is not None else ""
|
||||
desc = getattr(slot, "description", "") or ""
|
||||
|
||||
edit_url = url_for("calendar.slots.slot.get_edit", slot_id=slot.id, calendar_slug=cal_slug)
|
||||
|
||||
# Days pills
|
||||
if days and days[0] != "\u2014":
|
||||
days_inner = "".join(
|
||||
await render_to_sx("events-slot-day-pill", day=d) for d in days
|
||||
)
|
||||
days_html = await render_to_sx("events-slot-days-pills", days_inner=SxExpr(days_inner))
|
||||
else:
|
||||
days_html = await render_to_sx("events-slot-no-days")
|
||||
|
||||
sid = str(slot.id)
|
||||
|
||||
result = await render_to_sx("events-slot-panel",
|
||||
slot_id=sid, list_container=list_container,
|
||||
days=SxExpr(days_html),
|
||||
flexible="yes" if flexible else "no",
|
||||
time_str=f"{time_start} \u2014 {time_end}",
|
||||
cost_str=cost_str,
|
||||
pre_action=pre_action, edit_url=edit_url)
|
||||
|
||||
if oob:
|
||||
result += await render_to_sx("events-slot-description-oob", description=desc)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Slots table
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
async def render_slots_table(slots, calendar) -> str:
|
||||
"""Render slots table with rows and add button."""
|
||||
from quart import url_for, g
|
||||
from shared.browser.app.csrf import generate_csrf_token
|
||||
csrf = generate_csrf_token()
|
||||
|
||||
styles = getattr(g, "styles", None) or {}
|
||||
list_container = getattr(styles, "list_container", "") if hasattr(styles, "list_container") else styles.get("list_container", "")
|
||||
tr_cls = getattr(styles, "tr", "") if hasattr(styles, "tr") else styles.get("tr", "")
|
||||
pill_cls = getattr(styles, "pill", "") if hasattr(styles, "pill") else styles.get("pill", "")
|
||||
action_btn = getattr(styles, "action_button", "") if hasattr(styles, "action_button") else styles.get("action_button", "")
|
||||
pre_action = getattr(styles, "pre_action_button", "") if hasattr(styles, "pre_action_button") else styles.get("pre_action_button", "")
|
||||
hx_select = getattr(g, "hx_select_search", "#main-panel")
|
||||
cal_slug = getattr(calendar, "slug", "")
|
||||
|
||||
rows_html = ""
|
||||
if slots:
|
||||
for s in slots:
|
||||
slot_href = url_for("defpage_slot_detail", calendar_slug=cal_slug, slot_id=s.id)
|
||||
del_url = url_for("calendar.slots.slot.slot_delete", calendar_slug=cal_slug, slot_id=s.id)
|
||||
desc = getattr(s, "description", "") or ""
|
||||
|
||||
days_display = getattr(s, "days_display", "\u2014")
|
||||
day_list = days_display.split(", ")
|
||||
if day_list and day_list[0] != "\u2014":
|
||||
days_inner = "".join(
|
||||
await render_to_sx("events-slot-day-pill", day=d) for d in day_list
|
||||
)
|
||||
days_html = await render_to_sx("events-slot-days-pills", days_inner=SxExpr(days_inner))
|
||||
else:
|
||||
days_html = await render_to_sx("events-slot-no-days")
|
||||
|
||||
time_start = s.time_start.strftime("%H:%M") if s.time_start else ""
|
||||
time_end = s.time_end.strftime("%H:%M") if s.time_end else ""
|
||||
cost = getattr(s, "cost", None)
|
||||
cost_str = f"{cost:.2f}" if cost is not None else ""
|
||||
|
||||
rows_html += await render_to_sx("events-slots-row",
|
||||
tr_cls=tr_cls, slot_href=slot_href,
|
||||
pill_cls=pill_cls, hx_select=hx_select,
|
||||
slot_name=s.name, description=desc,
|
||||
flexible="yes" if s.flexible else "no",
|
||||
days=SxExpr(days_html),
|
||||
time_str=f"{time_start} - {time_end}",
|
||||
cost_str=cost_str, action_btn=action_btn,
|
||||
del_url=del_url,
|
||||
csrf_hdr=f'{{"X-CSRFToken": "{csrf}"}}')
|
||||
else:
|
||||
rows_html = await render_to_sx("events-slots-empty-row")
|
||||
|
||||
add_url = url_for("calendar.slots.add_form", calendar_slug=cal_slug)
|
||||
|
||||
return await render_to_sx("events-slots-table",
|
||||
list_container=list_container, rows=SxExpr(rows_html),
|
||||
pre_action=pre_action, add_url=add_url)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Slot edit form
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
async def render_slot_edit_form(slot, calendar) -> str:
|
||||
"""Render slot edit form (replaces _types/slot/_edit.html)."""
|
||||
from quart import url_for, g
|
||||
from shared.browser.app.csrf import generate_csrf_token
|
||||
csrf = generate_csrf_token()
|
||||
|
||||
styles = getattr(g, "styles", None) or {}
|
||||
list_container = styles.get("list_container", "") if isinstance(styles, dict) else getattr(styles, "list_container", "")
|
||||
action_btn = styles.get("action_button", "") if isinstance(styles, dict) else getattr(styles, "action_button", "")
|
||||
cancel_btn = styles.get("cancel_button", "") if isinstance(styles, dict) else getattr(styles, "cancel_button", "")
|
||||
cal_slug = getattr(calendar, "slug", "")
|
||||
sid = slot.id
|
||||
|
||||
put_url = url_for("calendar.slots.slot.put", calendar_slug=cal_slug, slot_id=sid)
|
||||
cancel_url = url_for("calendar.slots.slot.get_view", calendar_slug=cal_slug, slot_id=sid)
|
||||
|
||||
cost = getattr(slot, "cost", None)
|
||||
cost_val = f"{cost:.2f}" if cost is not None else ""
|
||||
start_val = slot.time_start.strftime("%H:%M") if slot.time_start else ""
|
||||
end_val = slot.time_end.strftime("%H:%M") if slot.time_end else ""
|
||||
desc_val = getattr(slot, "description", "") or ""
|
||||
|
||||
# Days checkboxes
|
||||
day_keys = [("mon", "Mon"), ("tue", "Tue"), ("wed", "Wed"), ("thu", "Thu"),
|
||||
("fri", "Fri"), ("sat", "Sat"), ("sun", "Sun")]
|
||||
all_checked = all(getattr(slot, k, False) for k, _ in day_keys)
|
||||
|
||||
days_parts = [await render_to_sx("events-day-all-checkbox",
|
||||
checked="checked" if all_checked else None)]
|
||||
for key, label in day_keys:
|
||||
checked = getattr(slot, key, False)
|
||||
days_parts.append(await render_to_sx("events-day-checkbox",
|
||||
name=key, label=label,
|
||||
checked="checked" if checked else None))
|
||||
days_html = "".join(days_parts)
|
||||
|
||||
flexible = getattr(slot, "flexible", False)
|
||||
|
||||
return await render_to_sx("events-slot-edit-form",
|
||||
slot_id=str(sid), list_container=list_container,
|
||||
put_url=put_url, cancel_url=cancel_url, csrf=csrf,
|
||||
name_val=slot.name or "", cost_val=cost_val,
|
||||
start_val=start_val, end_val=end_val,
|
||||
desc_val=desc_val, days=SxExpr(days_html),
|
||||
flexible_checked="checked" if flexible else None,
|
||||
action_btn=action_btn, cancel_btn=cancel_btn)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Slot add form / button
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
async def render_slot_add_form(calendar) -> str:
|
||||
"""Render slot add form (replaces _types/slots/_add.html)."""
|
||||
from quart import url_for, g
|
||||
from shared.browser.app.csrf import generate_csrf_token
|
||||
csrf = generate_csrf_token()
|
||||
|
||||
styles = getattr(g, "styles", None) or {}
|
||||
action_btn = styles.get("action_button", "") if isinstance(styles, dict) else getattr(styles, "action_button", "")
|
||||
cancel_btn = styles.get("cancel_button", "") if isinstance(styles, dict) else getattr(styles, "cancel_button", "")
|
||||
cal_slug = getattr(calendar, "slug", "")
|
||||
|
||||
post_url = url_for("calendar.slots.post", calendar_slug=cal_slug)
|
||||
cancel_url = url_for("calendar.slots.add_button", calendar_slug=cal_slug)
|
||||
csrf_hdr = f'{{"X-CSRFToken": "{csrf}"}}'
|
||||
|
||||
# Days checkboxes (all unchecked for add)
|
||||
day_keys = [("mon", "Mon"), ("tue", "Tue"), ("wed", "Wed"), ("thu", "Thu"),
|
||||
("fri", "Fri"), ("sat", "Sat"), ("sun", "Sun")]
|
||||
days_parts = [await render_to_sx("events-day-all-checkbox", checked=None)]
|
||||
for key, label in day_keys:
|
||||
days_parts.append(await render_to_sx("events-day-checkbox", name=key, label=label, checked=None))
|
||||
days_html = "".join(days_parts)
|
||||
|
||||
return await render_to_sx("events-slot-add-form",
|
||||
post_url=post_url, csrf=csrf_hdr,
|
||||
days=SxExpr(days_html),
|
||||
action_btn=action_btn, cancel_btn=cancel_btn,
|
||||
cancel_url=cancel_url)
|
||||
|
||||
|
||||
async def render_slot_add_button(calendar) -> str:
|
||||
"""Render slot add button (replaces _types/slots/_add_button.html)."""
|
||||
from quart import url_for, g
|
||||
|
||||
styles = getattr(g, "styles", None) or {}
|
||||
pre_action = styles.get("pre_action_button", "") if isinstance(styles, dict) else getattr(styles, "pre_action_button", "")
|
||||
cal_slug = getattr(calendar, "slug", "")
|
||||
add_url = url_for("calendar.slots.add_form", calendar_slug=cal_slug)
|
||||
|
||||
return await render_to_sx("events-slot-add-button", pre_action=pre_action, add_url=add_url)
|
||||
723
events/sxc/pages/tickets.py
Normal file
723
events/sxc/pages/tickets.py
Normal file
@@ -0,0 +1,723 @@
|
||||
"""Ticket panels, forms, admin views, buy/adjust controls."""
|
||||
from __future__ import annotations
|
||||
|
||||
from markupsafe import escape
|
||||
|
||||
from shared.sx.helpers import render_to_sx
|
||||
from shared.sx.parser import SxExpr
|
||||
|
||||
from .utils import (
|
||||
_ticket_state_badge_html, _list_container, _cart_icon_oob,
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Ticket widget (inline +/- for entry cards)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
async def _ticket_widget_html(entry, qty: int, ticket_url: str, *, ctx: dict) -> str:
|
||||
"""Render the inline +/- ticket widget."""
|
||||
csrf_token_val = ""
|
||||
if ctx:
|
||||
ct = ctx.get("csrf_token")
|
||||
csrf_token_val = ct() if callable(ct) else (ct or "")
|
||||
else:
|
||||
try:
|
||||
from flask_wtf.csrf import generate_csrf
|
||||
csrf_token_val = generate_csrf()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
eid = entry.id
|
||||
tp = getattr(entry, "ticket_price", 0) or 0
|
||||
tgt = f"#page-ticket-{eid}"
|
||||
|
||||
async def _tw_form(count_val, btn_html):
|
||||
return await render_to_sx("events-tw-form",
|
||||
ticket_url=ticket_url, target=tgt,
|
||||
csrf=csrf_token_val, entry_id=str(eid),
|
||||
count_val=str(count_val), btn=SxExpr(btn_html))
|
||||
|
||||
if qty == 0:
|
||||
inner = await _tw_form(1, await render_to_sx("events-tw-cart-plus"))
|
||||
else:
|
||||
minus = await _tw_form(qty - 1, await render_to_sx("events-tw-minus"))
|
||||
cart_icon = await render_to_sx("events-tw-cart-icon", qty=str(qty))
|
||||
plus = await _tw_form(qty + 1, await render_to_sx("events-tw-plus"))
|
||||
inner = minus + cart_icon + plus
|
||||
|
||||
return await render_to_sx("events-tw-widget",
|
||||
entry_id=str(eid), price=f"£{tp:.2f}",
|
||||
inner=SxExpr(inner))
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Tickets main panel (my tickets)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
async def _tickets_main_panel_html(ctx: dict, tickets: list) -> str:
|
||||
"""Render my tickets list."""
|
||||
from quart import url_for
|
||||
|
||||
ticket_cards = []
|
||||
if tickets:
|
||||
for ticket in tickets:
|
||||
href = url_for("defpage_ticket_detail", code=ticket.code)
|
||||
entry = getattr(ticket, "entry", None)
|
||||
entry_name = entry.name if entry else "Unknown event"
|
||||
tt = getattr(ticket, "ticket_type", None)
|
||||
state = getattr(ticket, "state", "")
|
||||
cal = getattr(entry, "calendar", None) if entry else None
|
||||
|
||||
time_str = ""
|
||||
if entry and entry.start_at:
|
||||
time_str = entry.start_at.strftime("%A, %B %d, %Y at %H:%M")
|
||||
if entry.end_at:
|
||||
time_str += f" \u2013 {entry.end_at.strftime('%H:%M')}"
|
||||
|
||||
ticket_cards.append(await render_to_sx("events-ticket-card",
|
||||
href=href, entry_name=entry_name,
|
||||
type_name=tt.name if tt else None,
|
||||
time_str=time_str or None,
|
||||
cal_name=cal.name if cal else None,
|
||||
badge=SxExpr(await _ticket_state_badge_html(state)),
|
||||
code_prefix=ticket.code[:8]))
|
||||
|
||||
cards_html = "".join(ticket_cards)
|
||||
return await render_to_sx("events-tickets-panel",
|
||||
list_container=_list_container(ctx),
|
||||
has_tickets=bool(tickets), cards=SxExpr(cards_html))
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Ticket detail panel
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
async def _ticket_detail_panel_html(ctx: dict, ticket) -> str:
|
||||
"""Render a single ticket detail with QR code."""
|
||||
from quart import url_for
|
||||
|
||||
entry = getattr(ticket, "entry", None)
|
||||
tt = getattr(ticket, "ticket_type", None)
|
||||
state = getattr(ticket, "state", "")
|
||||
code = ticket.code
|
||||
cal = getattr(entry, "calendar", None) if entry else None
|
||||
checked_in_at = getattr(ticket, "checked_in_at", None)
|
||||
|
||||
bg_map = {"confirmed": "bg-emerald-50", "checked_in": "bg-blue-50", "reserved": "bg-amber-50"}
|
||||
header_bg = bg_map.get(state, "bg-stone-50")
|
||||
entry_name = entry.name if entry else "Ticket"
|
||||
back_href = url_for("defpage_my_tickets")
|
||||
|
||||
# Badge with larger sizing
|
||||
badge = (await _ticket_state_badge_html(state)).replace('px-2 py-0.5 text-xs', 'px-3 py-1 text-sm')
|
||||
|
||||
# Time info
|
||||
time_date = entry.start_at.strftime("%A, %B %d, %Y") if entry and entry.start_at else None
|
||||
time_range = entry.start_at.strftime("%H:%M") if entry and entry.start_at else None
|
||||
if time_range and entry.end_at:
|
||||
time_range += f" \u2013 {entry.end_at.strftime('%H:%M')}"
|
||||
|
||||
tt_desc = f"{tt.name} \u2014 \u00a3{tt.cost:.2f}" if tt and getattr(tt, "cost", None) else None
|
||||
checkin_str = checked_in_at.strftime("Checked in: %B %d, %Y at %H:%M") if checked_in_at else None
|
||||
|
||||
qr_script = (
|
||||
f"(function(){{var c=document.getElementById('ticket-qr-{code}');"
|
||||
"if(c&&typeof QRCode!=='undefined'){"
|
||||
"var cv=document.createElement('canvas');"
|
||||
f"QRCode.toCanvas(cv,'{code}',{{width:200,margin:2,color:{{dark:'#1c1917',light:'#ffffff'}}}},function(e){{if(!e)c.appendChild(cv)}});"
|
||||
"}})()"
|
||||
)
|
||||
|
||||
return await render_to_sx("events-ticket-detail",
|
||||
list_container=_list_container(ctx), back_href=back_href,
|
||||
header_bg=header_bg, entry_name=entry_name,
|
||||
badge=SxExpr(badge), type_name=tt.name if tt else None,
|
||||
code=code, time_date=time_date, time_range=time_range,
|
||||
cal_name=cal.name if cal else None,
|
||||
type_desc=tt_desc, checkin_str=checkin_str,
|
||||
qr_script=qr_script)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Ticket admin main panel
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
async def _ticket_admin_main_panel_html(ctx: dict, tickets: list, stats: dict) -> str:
|
||||
"""Render ticket admin dashboard."""
|
||||
from quart import url_for
|
||||
csrf_token = ctx.get("csrf_token")
|
||||
csrf = csrf_token() if callable(csrf_token) else (csrf_token or "")
|
||||
lookup_url = url_for("ticket_admin.lookup")
|
||||
|
||||
# Stats cards
|
||||
stats_html = ""
|
||||
for label, key, border, bg, text_cls in [
|
||||
("Total", "total", "border-stone-200", "", "text-stone-900"),
|
||||
("Confirmed", "confirmed", "border-emerald-200", "bg-emerald-50", "text-emerald-700"),
|
||||
("Checked In", "checked_in", "border-blue-200", "bg-blue-50", "text-blue-700"),
|
||||
("Reserved", "reserved", "border-amber-200", "bg-amber-50", "text-amber-700"),
|
||||
]:
|
||||
val = stats.get(key, 0)
|
||||
lbl_cls = text_cls.replace("700", "600").replace("900", "500") if "stone" not in text_cls else "text-stone-500"
|
||||
stats_html += await render_to_sx("events-ticket-admin-stat",
|
||||
border=border, bg=bg, text_cls=text_cls,
|
||||
label_cls=lbl_cls, value=str(val), label=label)
|
||||
|
||||
# Ticket rows
|
||||
rows_html = ""
|
||||
for ticket in tickets:
|
||||
entry = getattr(ticket, "entry", None)
|
||||
tt = getattr(ticket, "ticket_type", None)
|
||||
state = getattr(ticket, "state", "")
|
||||
code = ticket.code
|
||||
|
||||
date_html = ""
|
||||
if entry and entry.start_at:
|
||||
date_html = await render_to_sx("events-ticket-admin-date",
|
||||
date_str=entry.start_at.strftime("%d %b %Y, %H:%M"))
|
||||
|
||||
action_html = ""
|
||||
if state in ("confirmed", "reserved"):
|
||||
checkin_url = url_for("ticket_admin.do_checkin", code=code)
|
||||
action_html = await render_to_sx("events-ticket-admin-checkin-form",
|
||||
checkin_url=checkin_url, code=code, csrf=csrf)
|
||||
elif state == "checked_in":
|
||||
checked_in_at = getattr(ticket, "checked_in_at", None)
|
||||
t_str = checked_in_at.strftime("%H:%M") if checked_in_at else ""
|
||||
action_html = await render_to_sx("events-ticket-admin-checked-in",
|
||||
time_str=t_str)
|
||||
|
||||
rows_html += await render_to_sx("events-ticket-admin-row",
|
||||
code=code, code_short=code[:12] + "...",
|
||||
entry_name=entry.name if entry else "\u2014",
|
||||
date=SxExpr(date_html),
|
||||
type_name=tt.name if tt else "\u2014",
|
||||
badge=SxExpr(await _ticket_state_badge_html(state)),
|
||||
action=SxExpr(action_html))
|
||||
|
||||
return await render_to_sx("events-ticket-admin-panel",
|
||||
list_container=_list_container(ctx), stats=SxExpr(stats_html),
|
||||
lookup_url=lookup_url, has_tickets=bool(tickets),
|
||||
rows=SxExpr(rows_html))
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Public render: ticket widget
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
async def render_ticket_widget(entry, qty: int, ticket_url: str) -> str:
|
||||
"""Render the +/- ticket widget for page_summary / all_events adjust_ticket."""
|
||||
return await _ticket_widget_html(entry, qty, ticket_url, ctx={})
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Ticket admin: checkin result
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
async def render_checkin_result(success: bool, error: str | None, ticket) -> str:
|
||||
"""Render checkin result: table row on success, error div on failure."""
|
||||
if not success:
|
||||
return await render_to_sx("events-checkin-error",
|
||||
message=error or "Check-in failed")
|
||||
if not ticket:
|
||||
return ""
|
||||
code = ticket.code
|
||||
entry = getattr(ticket, "entry", None)
|
||||
tt = getattr(ticket, "ticket_type", None)
|
||||
checked_in_at = getattr(ticket, "checked_in_at", None)
|
||||
time_str = checked_in_at.strftime("%H:%M") if checked_in_at else "Just now"
|
||||
|
||||
date_html = ""
|
||||
if entry and entry.start_at:
|
||||
date_html = await render_to_sx("events-ticket-admin-date",
|
||||
date_str=entry.start_at.strftime("%d %b %Y, %H:%M"))
|
||||
|
||||
return await render_to_sx("events-checkin-success-row",
|
||||
code=code, code_short=code[:12] + "...",
|
||||
entry_name=entry.name if entry else "\u2014",
|
||||
date=SxExpr(date_html),
|
||||
type_name=tt.name if tt else "\u2014",
|
||||
badge=SxExpr(await _ticket_state_badge_html("checked_in")),
|
||||
time_str=time_str)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Ticket admin: lookup result
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
async def render_lookup_result(ticket, error: str | None) -> str:
|
||||
"""Render ticket lookup result: error div or ticket info card."""
|
||||
from quart import url_for
|
||||
from shared.browser.app.csrf import generate_csrf_token
|
||||
|
||||
if error:
|
||||
return await render_to_sx("events-lookup-error", message=error)
|
||||
if not ticket:
|
||||
return ""
|
||||
|
||||
entry = getattr(ticket, "entry", None)
|
||||
tt = getattr(ticket, "ticket_type", None)
|
||||
state = getattr(ticket, "state", "")
|
||||
code = ticket.code
|
||||
checked_in_at = getattr(ticket, "checked_in_at", None)
|
||||
csrf = generate_csrf_token()
|
||||
|
||||
# Info section
|
||||
info_html = await render_to_sx("events-lookup-info",
|
||||
entry_name=entry.name if entry else "Unknown event")
|
||||
if tt:
|
||||
info_html += await render_to_sx("events-lookup-type", type_name=tt.name)
|
||||
if entry and entry.start_at:
|
||||
info_html += await render_to_sx("events-lookup-date",
|
||||
date_str=entry.start_at.strftime("%A, %B %d, %Y at %H:%M"))
|
||||
cal = getattr(entry, "calendar", None) if entry else None
|
||||
if cal:
|
||||
info_html += await render_to_sx("events-lookup-cal", cal_name=cal.name)
|
||||
info_html += await render_to_sx("events-lookup-status",
|
||||
badge=SxExpr(await _ticket_state_badge_html(state)), code=code)
|
||||
if checked_in_at:
|
||||
info_html += await render_to_sx("events-lookup-checkin-time",
|
||||
date_str=checked_in_at.strftime("%B %d, %Y at %H:%M"))
|
||||
|
||||
# Action area
|
||||
action_html = ""
|
||||
if state in ("confirmed", "reserved"):
|
||||
checkin_url = url_for("ticket_admin.do_checkin", code=code)
|
||||
action_html = await render_to_sx("events-lookup-checkin-btn",
|
||||
checkin_url=checkin_url, code=code, csrf=csrf)
|
||||
elif state == "checked_in":
|
||||
action_html = await render_to_sx("events-lookup-checked-in")
|
||||
elif state == "cancelled":
|
||||
action_html = await render_to_sx("events-lookup-cancelled")
|
||||
|
||||
return await render_to_sx("events-lookup-card",
|
||||
info=SxExpr(info_html), code=code, action=SxExpr(action_html))
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Ticket admin: entry tickets table
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
async def render_entry_tickets_admin(entry, tickets: list) -> str:
|
||||
"""Render admin ticket table for a specific entry."""
|
||||
from quart import url_for
|
||||
from shared.browser.app.csrf import generate_csrf_token
|
||||
csrf = generate_csrf_token()
|
||||
|
||||
count = len(tickets)
|
||||
suffix = "s" if count != 1 else ""
|
||||
|
||||
rows_html = ""
|
||||
for ticket in tickets:
|
||||
tt = getattr(ticket, "ticket_type", None)
|
||||
state = getattr(ticket, "state", "")
|
||||
code = ticket.code
|
||||
checked_in_at = getattr(ticket, "checked_in_at", None)
|
||||
|
||||
action_html = ""
|
||||
if state in ("confirmed", "reserved"):
|
||||
checkin_url = url_for("ticket_admin.do_checkin", code=code)
|
||||
action_html = await render_to_sx("events-entry-tickets-admin-checkin",
|
||||
checkin_url=checkin_url, code=code, csrf=csrf)
|
||||
elif state == "checked_in":
|
||||
t_str = checked_in_at.strftime("%H:%M") if checked_in_at else ""
|
||||
action_html = await render_to_sx("events-ticket-admin-checked-in",
|
||||
time_str=t_str)
|
||||
|
||||
rows_html += await render_to_sx("events-entry-tickets-admin-row",
|
||||
code=code, code_short=code[:12] + "...",
|
||||
type_name=tt.name if tt else "\u2014",
|
||||
badge=SxExpr(await _ticket_state_badge_html(state)),
|
||||
action=SxExpr(action_html))
|
||||
|
||||
if tickets:
|
||||
body_html = await render_to_sx("events-entry-tickets-admin-table",
|
||||
rows=SxExpr(rows_html))
|
||||
else:
|
||||
body_html = await render_to_sx("events-entry-tickets-admin-empty")
|
||||
|
||||
return await render_to_sx("events-entry-tickets-admin-panel",
|
||||
entry_name=entry.name,
|
||||
count_label=f"{count} ticket{suffix}",
|
||||
body=SxExpr(body_html))
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Ticket type main panel
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
async def render_ticket_type_main_panel(ticket_type, entry, calendar, day, month, year, *, oob: bool = False) -> str:
|
||||
"""Render ticket type detail view."""
|
||||
from quart import url_for, g
|
||||
|
||||
styles = getattr(g, "styles", None) or {}
|
||||
list_container = getattr(styles, "list_container", "") if hasattr(styles, "list_container") else styles.get("list_container", "")
|
||||
pre_action = getattr(styles, "pre_action_button", "") if hasattr(styles, "pre_action_button") else styles.get("pre_action_button", "")
|
||||
cal_slug = getattr(calendar, "slug", "")
|
||||
|
||||
cost = getattr(ticket_type, "cost", None)
|
||||
cost_str = f"\u00a3{cost:.2f}" if cost is not None else "\u00a30.00"
|
||||
count = getattr(ticket_type, "count", 0)
|
||||
tid = str(ticket_type.id)
|
||||
|
||||
edit_url = url_for(
|
||||
"calendar.day.calendar_entries.calendar_entry.ticket_types.ticket_type.get_edit",
|
||||
ticket_type_id=ticket_type.id, calendar_slug=cal_slug,
|
||||
year=year, month=month, day=day, entry_id=entry.id,
|
||||
)
|
||||
|
||||
async def _col(label, val):
|
||||
return await render_to_sx("events-ticket-type-col", label=label, value=val)
|
||||
|
||||
return await render_to_sx("events-ticket-type-panel",
|
||||
ticket_id=tid, list_container=list_container,
|
||||
c1=await _col("Name", ticket_type.name),
|
||||
c2=await _col("Cost", cost_str),
|
||||
c3=await _col("Count", str(count)),
|
||||
pre_action=pre_action, edit_url=edit_url)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Ticket types table
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
async def render_ticket_types_table(ticket_types, entry, calendar, day, month, year) -> str:
|
||||
"""Render ticket types table with rows and add button."""
|
||||
from quart import url_for, g
|
||||
from shared.browser.app.csrf import generate_csrf_token
|
||||
csrf = generate_csrf_token()
|
||||
|
||||
styles = getattr(g, "styles", None) or {}
|
||||
list_container = getattr(styles, "list_container", "") if hasattr(styles, "list_container") else styles.get("list_container", "")
|
||||
tr_cls = getattr(styles, "tr", "") if hasattr(styles, "tr") else styles.get("tr", "")
|
||||
pill_cls = getattr(styles, "pill", "") if hasattr(styles, "pill") else styles.get("pill", "")
|
||||
action_btn = getattr(styles, "action_button", "") if hasattr(styles, "action_button") else styles.get("action_button", "")
|
||||
hx_select = getattr(g, "hx_select_search", "#main-panel")
|
||||
cal_slug = getattr(calendar, "slug", "")
|
||||
eid = entry.id
|
||||
|
||||
rows_html = ""
|
||||
if ticket_types:
|
||||
for tt in ticket_types:
|
||||
tt_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=eid, ticket_type_id=tt.id,
|
||||
)
|
||||
del_url = url_for(
|
||||
"calendar.day.calendar_entries.calendar_entry.ticket_types.ticket_type.delete",
|
||||
calendar_slug=cal_slug, year=year, month=month, day=day,
|
||||
entry_id=eid, ticket_type_id=tt.id,
|
||||
)
|
||||
cost = getattr(tt, "cost", None)
|
||||
cost_str = f"\u00a3{cost:.2f}" if cost is not None else "\u00a30.00"
|
||||
|
||||
rows_html += await render_to_sx("events-ticket-types-row",
|
||||
tr_cls=tr_cls, tt_href=tt_href,
|
||||
pill_cls=pill_cls, hx_select=hx_select,
|
||||
tt_name=tt.name, cost_str=cost_str,
|
||||
count=str(tt.count), action_btn=action_btn,
|
||||
del_url=del_url,
|
||||
csrf_hdr=f'{{"X-CSRFToken": "{csrf}"}}')
|
||||
else:
|
||||
rows_html = await render_to_sx("events-ticket-types-empty-row")
|
||||
|
||||
add_url = url_for(
|
||||
"calendar.day.calendar_entries.calendar_entry.ticket_types.add_form",
|
||||
calendar_slug=cal_slug, entry_id=eid, year=year, month=month, day=day,
|
||||
)
|
||||
|
||||
return await render_to_sx("events-ticket-types-table",
|
||||
list_container=list_container, rows=SxExpr(rows_html),
|
||||
action_btn=action_btn, add_url=add_url)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Buy result (ticket purchase confirmation)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
async def render_buy_result(entry, created_tickets, remaining, cart_count) -> str:
|
||||
"""Render buy result card with created tickets + OOB cart icon."""
|
||||
from quart import url_for
|
||||
|
||||
cart_html = await _cart_icon_oob(cart_count)
|
||||
|
||||
count = len(created_tickets)
|
||||
suffix = "s" if count != 1 else ""
|
||||
|
||||
tickets_html = ""
|
||||
for ticket in created_tickets:
|
||||
href = url_for("defpage_ticket_detail", code=ticket.code)
|
||||
tickets_html += await render_to_sx("events-buy-result-ticket",
|
||||
href=href, code_short=ticket.code[:12] + "...")
|
||||
|
||||
remaining_html = ""
|
||||
if remaining is not None:
|
||||
r_suffix = "s" if remaining != 1 else ""
|
||||
remaining_html = await render_to_sx("events-buy-result-remaining",
|
||||
text=f"{remaining} ticket{r_suffix} remaining")
|
||||
|
||||
my_href = url_for("defpage_my_tickets")
|
||||
|
||||
return cart_html + await render_to_sx("events-buy-result",
|
||||
entry_id=str(entry.id),
|
||||
count_label=f"{count} ticket{suffix} reserved",
|
||||
tickets=SxExpr(tickets_html),
|
||||
remaining=SxExpr(remaining_html),
|
||||
my_tickets_href=my_href)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Buy form (ticket +/- controls)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
async def render_buy_form(entry, ticket_remaining, ticket_sold_count,
|
||||
user_ticket_count, user_ticket_counts_by_type) -> str:
|
||||
"""Render the ticket buy/adjust form with +/- controls."""
|
||||
from quart import url_for
|
||||
from shared.browser.app.csrf import generate_csrf_token
|
||||
csrf = generate_csrf_token()
|
||||
|
||||
eid = entry.id
|
||||
eid_s = str(eid)
|
||||
tp = getattr(entry, "ticket_price", None)
|
||||
state = getattr(entry, "state", "")
|
||||
ticket_types = getattr(entry, "ticket_types", None) or []
|
||||
|
||||
if tp is None:
|
||||
return ""
|
||||
|
||||
if state != "confirmed":
|
||||
return await render_to_sx("events-buy-not-confirmed", entry_id=eid_s)
|
||||
|
||||
adjust_url = url_for("tickets.adjust_quantity")
|
||||
target = f"#ticket-buy-{eid}"
|
||||
|
||||
# Info line
|
||||
info_html = ""
|
||||
info_items = ""
|
||||
if ticket_sold_count:
|
||||
info_items += await render_to_sx("events-buy-info-sold",
|
||||
count=str(ticket_sold_count))
|
||||
if ticket_remaining is not None:
|
||||
info_items += await render_to_sx("events-buy-info-remaining",
|
||||
count=str(ticket_remaining))
|
||||
if user_ticket_count:
|
||||
info_items += await render_to_sx("events-buy-info-basket",
|
||||
count=str(user_ticket_count))
|
||||
if info_items:
|
||||
info_html = await render_to_sx("events-buy-info-bar", items=SxExpr(info_items))
|
||||
|
||||
active_types = [tt for tt in ticket_types if getattr(tt, "deleted_at", None) is None]
|
||||
|
||||
body_html = ""
|
||||
if active_types:
|
||||
type_items = ""
|
||||
for tt in active_types:
|
||||
type_count = user_ticket_counts_by_type.get(tt.id, 0) if user_ticket_counts_by_type else 0
|
||||
cost_str = f"\u00a3{tt.cost:.2f}" if tt.cost is not None else "\u00a30.00"
|
||||
type_items += await render_to_sx("events-buy-type-item",
|
||||
type_name=tt.name, cost_str=cost_str,
|
||||
adjust_controls=SxExpr(await _ticket_adjust_controls(csrf, adjust_url, target, eid, type_count, ticket_type_id=tt.id)))
|
||||
body_html = await render_to_sx("events-buy-types-wrapper", items=SxExpr(type_items))
|
||||
else:
|
||||
qty = user_ticket_count or 0
|
||||
body_html = await render_to_sx("events-buy-default",
|
||||
price_str=f"\u00a3{tp:.2f}",
|
||||
adjust_controls=SxExpr(await _ticket_adjust_controls(csrf, adjust_url, target, eid, qty)))
|
||||
|
||||
return await render_to_sx("events-buy-panel",
|
||||
entry_id=eid_s, info=SxExpr(info_html), body=SxExpr(body_html))
|
||||
|
||||
|
||||
async def _ticket_adjust_controls(csrf, adjust_url, target, entry_id, count, *, ticket_type_id=None):
|
||||
"""Render +/- ticket controls for buy form."""
|
||||
from quart import url_for
|
||||
|
||||
tt_html = await render_to_sx("events-adjust-tt-hidden",
|
||||
ticket_type_id=str(ticket_type_id)) if ticket_type_id else ""
|
||||
eid_s = str(entry_id)
|
||||
|
||||
async def _adj_form(count_val, btn_html, *, extra_cls=""):
|
||||
return await render_to_sx("events-adjust-form",
|
||||
adjust_url=adjust_url, target=target,
|
||||
extra_cls=extra_cls, csrf=csrf,
|
||||
entry_id=eid_s, tt=SxExpr(tt_html) if tt_html else None,
|
||||
count_val=str(count_val), btn=SxExpr(btn_html))
|
||||
|
||||
if count == 0:
|
||||
return await _adj_form(1, await render_to_sx("events-adjust-cart-plus"),
|
||||
extra_cls="flex items-center")
|
||||
|
||||
my_tickets_href = url_for("defpage_my_tickets")
|
||||
minus = await _adj_form(count - 1, await render_to_sx("events-adjust-minus"))
|
||||
cart_icon = await render_to_sx("events-adjust-cart-icon",
|
||||
href=my_tickets_href, count=str(count))
|
||||
plus = await _adj_form(count + 1, await render_to_sx("events-adjust-plus"))
|
||||
|
||||
return await render_to_sx("events-adjust-controls",
|
||||
minus=SxExpr(minus), cart_icon=SxExpr(cart_icon), plus=SxExpr(plus))
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Adjust response (OOB cart icon + buy form)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
async def render_adjust_response(entry, ticket_remaining, ticket_sold_count,
|
||||
user_ticket_count, user_ticket_counts_by_type,
|
||||
cart_count) -> str:
|
||||
"""Render ticket adjust response: OOB cart icon + buy form."""
|
||||
cart_html = await _cart_icon_oob(cart_count)
|
||||
form_html = await render_buy_form(
|
||||
entry, ticket_remaining, ticket_sold_count,
|
||||
user_ticket_count, user_ticket_counts_by_type,
|
||||
)
|
||||
return cart_html + form_html
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Ticket types header rows
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
async def _ticket_types_header_html(ctx: dict, *, oob: bool = False) -> str:
|
||||
"""Build the ticket types header row."""
|
||||
from quart import url_for
|
||||
|
||||
calendar = ctx.get("calendar")
|
||||
entry = ctx.get("entry")
|
||||
if not calendar or not entry:
|
||||
return ""
|
||||
cal_slug = getattr(calendar, "slug", "")
|
||||
day = ctx.get("day")
|
||||
month = ctx.get("month")
|
||||
year = ctx.get("year")
|
||||
|
||||
link_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)
|
||||
|
||||
label_html = '<i class="fa fa-ticket"></i><div class="shrink-0">ticket types</div>'
|
||||
|
||||
nav_html = await render_to_sx("events-admin-placeholder-nav")
|
||||
|
||||
return await render_to_sx("menu-row-sx", id="ticket_types-row", level=7,
|
||||
link_href=link_href, link_label_content=SxExpr(label_html),
|
||||
nav=SxExpr(nav_html) if nav_html else None, child_id="ticket_type-header-child", oob=oob)
|
||||
|
||||
|
||||
|
||||
async def _ticket_type_header_html(ctx: dict, *, oob: bool = False) -> str:
|
||||
"""Build the single ticket type header row."""
|
||||
from quart import url_for
|
||||
|
||||
calendar = ctx.get("calendar")
|
||||
entry = ctx.get("entry")
|
||||
ticket_type = ctx.get("ticket_type")
|
||||
if not calendar or not entry or not ticket_type:
|
||||
return ""
|
||||
cal_slug = getattr(calendar, "slug", "")
|
||||
day = ctx.get("day")
|
||||
month = ctx.get("month")
|
||||
year = ctx.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,
|
||||
)
|
||||
|
||||
label_html = (
|
||||
f'<div class="flex flex-col md:flex-row md:gap-2 items-center">'
|
||||
f'<div class="flex flex-row items-center gap-2">'
|
||||
f'<i class="fa fa-ticket"></i>'
|
||||
f'<div class="shrink-0">{escape(ticket_type.name)}</div>'
|
||||
f'</div></div>'
|
||||
)
|
||||
|
||||
nav_html = await render_to_sx("events-admin-placeholder-nav")
|
||||
|
||||
return await render_to_sx("menu-row-sx", id="ticket_type-row", level=8,
|
||||
link_href=link_href, link_label_content=SxExpr(label_html),
|
||||
nav=SxExpr(nav_html) if nav_html else None, child_id="ticket_type-header-child-inner", oob=oob)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Ticket type edit form
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
async def render_ticket_type_edit_form(ticket_type, entry, calendar, day, month, year) -> str:
|
||||
"""Render ticket type edit form (replaces _types/ticket_type/_edit.html)."""
|
||||
from quart import url_for, g
|
||||
from shared.browser.app.csrf import generate_csrf_token
|
||||
csrf = generate_csrf_token()
|
||||
|
||||
styles = getattr(g, "styles", None) or {}
|
||||
list_container = styles.get("list_container", "") if isinstance(styles, dict) else getattr(styles, "list_container", "")
|
||||
action_btn = styles.get("action_button", "") if isinstance(styles, dict) else getattr(styles, "action_button", "")
|
||||
cancel_btn = styles.get("cancel_button", "") if isinstance(styles, dict) else getattr(styles, "cancel_button", "")
|
||||
cal_slug = getattr(calendar, "slug", "")
|
||||
tid = ticket_type.id
|
||||
|
||||
put_url = url_for("calendar.day.calendar_entries.calendar_entry.ticket_types.ticket_type.put",
|
||||
calendar_slug=cal_slug, year=year, month=month, day=day,
|
||||
entry_id=entry.id, ticket_type_id=tid)
|
||||
cancel_url = url_for("calendar.day.calendar_entries.calendar_entry.ticket_types.ticket_type.get_view",
|
||||
calendar_slug=cal_slug, entry_id=entry.id,
|
||||
year=year, month=month, day=day, ticket_type_id=tid)
|
||||
|
||||
cost = getattr(ticket_type, "cost", None)
|
||||
cost_val = f"{cost:.2f}" if cost is not None else ""
|
||||
count = getattr(ticket_type, "count", 0)
|
||||
|
||||
return await render_to_sx("events-ticket-type-edit-form",
|
||||
ticket_id=str(tid), list_container=list_container,
|
||||
put_url=put_url, cancel_url=cancel_url, csrf=csrf,
|
||||
name_val=ticket_type.name or "",
|
||||
cost_val=cost_val, count_val=str(count),
|
||||
action_btn=action_btn, cancel_btn=cancel_btn)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Ticket type add form / button
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
async def render_ticket_type_add_form(entry, calendar, day, month, year) -> str:
|
||||
"""Render ticket type add form (replaces _types/ticket_types/_add.html)."""
|
||||
from quart import url_for, g
|
||||
from shared.browser.app.csrf import generate_csrf_token
|
||||
csrf = generate_csrf_token()
|
||||
|
||||
styles = getattr(g, "styles", None) or {}
|
||||
action_btn = styles.get("action_button", "") if isinstance(styles, dict) else getattr(styles, "action_button", "")
|
||||
cancel_btn = styles.get("cancel_button", "") if isinstance(styles, dict) else getattr(styles, "cancel_button", "")
|
||||
cal_slug = getattr(calendar, "slug", "")
|
||||
|
||||
post_url = url_for("calendar.day.calendar_entries.calendar_entry.ticket_types.post",
|
||||
calendar_slug=cal_slug, entry_id=entry.id,
|
||||
year=year, month=month, day=day)
|
||||
cancel_url = url_for("calendar.day.calendar_entries.calendar_entry.ticket_types.add_button",
|
||||
calendar_slug=cal_slug, entry_id=entry.id,
|
||||
year=year, month=month, day=day)
|
||||
csrf_hdr = f'{{"X-CSRFToken": "{csrf}"}}'
|
||||
|
||||
return await render_to_sx("events-ticket-type-add-form",
|
||||
post_url=post_url, csrf=csrf_hdr,
|
||||
action_btn=action_btn, cancel_btn=cancel_btn,
|
||||
cancel_url=cancel_url)
|
||||
|
||||
|
||||
async def render_ticket_type_add_button(entry, calendar, day, month, year) -> str:
|
||||
"""Render ticket type add button (replaces _types/ticket_types/_add_button.html)."""
|
||||
from quart import url_for, g
|
||||
|
||||
styles = getattr(g, "styles", None) or {}
|
||||
action_btn = styles.get("action_button", "") if isinstance(styles, dict) else getattr(styles, "action_button", "")
|
||||
cal_slug = getattr(calendar, "slug", "")
|
||||
|
||||
add_url = url_for("calendar.day.calendar_entries.calendar_entry.ticket_types.add_form",
|
||||
calendar_slug=cal_slug, entry_id=entry.id,
|
||||
year=year, month=month, day=day)
|
||||
|
||||
return await render_to_sx("events-ticket-type-add-button",
|
||||
action_btn=action_btn, add_url=add_url)
|
||||
170
events/sxc/pages/utils.py
Normal file
170
events/sxc/pages/utils.py
Normal file
@@ -0,0 +1,170 @@
|
||||
"""Badge helpers, OOB helpers, formatting utilities, list containers."""
|
||||
from __future__ import annotations
|
||||
|
||||
from shared.sx.helpers import render_to_sx
|
||||
from shared.sx.parser import SxExpr
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# OOB header helper — delegates to shared
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Post header helpers — thin wrapper over shared post_header_sx
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _clear_oob(*ids: str) -> str:
|
||||
"""Generate OOB swaps to remove orphaned header rows/children."""
|
||||
return "".join(f'(div :id "{i}" :hx-swap-oob "outerHTML")' for i in ids)
|
||||
|
||||
|
||||
# All possible header row/child IDs at each depth (deepest first)
|
||||
_EVENTS_DEEP_IDS = [
|
||||
"entry-admin-row", "entry-admin-header-child",
|
||||
"entry-row", "entry-header-child",
|
||||
"day-admin-row", "day-admin-header-child",
|
||||
"day-row", "day-header-child",
|
||||
"calendar-admin-row", "calendar-admin-header-child",
|
||||
"calendar-row", "calendar-header-child",
|
||||
"calendars-row", "calendars-header-child",
|
||||
"post-admin-row", "post-admin-header-child",
|
||||
]
|
||||
|
||||
|
||||
def _clear_deeper_oob(*keep_ids: str) -> str:
|
||||
"""Clear all events header rows/children NOT in keep_ids."""
|
||||
to_clear = [i for i in _EVENTS_DEEP_IDS if i not in keep_ids]
|
||||
return _clear_oob(*to_clear)
|
||||
|
||||
|
||||
async def _ensure_container_nav(ctx: dict) -> dict:
|
||||
"""Fetch container_nav if not already present (for post header row)."""
|
||||
if ctx.get("container_nav"):
|
||||
return ctx
|
||||
post = ctx.get("post") or {}
|
||||
post_id = post.get("id")
|
||||
slug = post.get("slug", "")
|
||||
if not post_id:
|
||||
return ctx
|
||||
from quart import g
|
||||
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)
|
||||
return {**ctx, "container_nav": events_nav + market_nav}
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Utility
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _list_container(ctx: dict) -> str:
|
||||
styles = ctx.get("styles") or {}
|
||||
return getattr(styles, "list_container", "") if hasattr(styles, "list_container") else styles.get("list_container", "")
|
||||
|
||||
|
||||
async def _entry_state_badge_html(state: str) -> str:
|
||||
"""Render an entry state badge."""
|
||||
state_classes = {
|
||||
"confirmed": "bg-emerald-100 text-emerald-800",
|
||||
"provisional": "bg-amber-100 text-amber-800",
|
||||
"ordered": "bg-sky-100 text-sky-800",
|
||||
"pending": "bg-stone-100 text-stone-700",
|
||||
"declined": "bg-red-100 text-red-800",
|
||||
}
|
||||
cls = state_classes.get(state, "bg-stone-100 text-stone-700")
|
||||
label = state.replace("_", " ").capitalize()
|
||||
return await render_to_sx("badge", cls=cls, label=label)
|
||||
|
||||
|
||||
async def _ticket_state_badge_html(state: str) -> str:
|
||||
"""Render a ticket state badge."""
|
||||
cls_map = {
|
||||
"confirmed": "bg-emerald-100 text-emerald-800",
|
||||
"checked_in": "bg-blue-100 text-blue-800",
|
||||
"reserved": "bg-amber-100 text-amber-800",
|
||||
"cancelled": "bg-red-100 text-red-800",
|
||||
}
|
||||
cls = cls_map.get(state, "bg-stone-100 text-stone-700")
|
||||
label = (state or "").replace("_", " ").capitalize()
|
||||
return await render_to_sx("badge", cls=cls, label=label)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# View toggle + SVG caching
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
_LIST_SVG = None
|
||||
_TILE_SVG = None
|
||||
|
||||
|
||||
async def _get_list_svg():
|
||||
global _LIST_SVG
|
||||
if _LIST_SVG is None:
|
||||
_LIST_SVG = await render_to_sx("list-svg")
|
||||
return _LIST_SVG
|
||||
|
||||
|
||||
async def _get_tile_svg():
|
||||
global _TILE_SVG
|
||||
if _TILE_SVG is None:
|
||||
_TILE_SVG = await render_to_sx("tile-svg")
|
||||
return _TILE_SVG
|
||||
|
||||
|
||||
async def _view_toggle_html(ctx: dict, view: str) -> str:
|
||||
"""Render the list/tile view toggle bar."""
|
||||
from shared.utils import route_prefix
|
||||
prefix = route_prefix()
|
||||
clh = ctx.get("current_local_href", "/")
|
||||
hx_select = ctx.get("hx_select_search", "#main-panel")
|
||||
|
||||
list_href = prefix + str(clh)
|
||||
tile_href = prefix + str(clh)
|
||||
if "?" in list_href:
|
||||
list_href = list_href.split("?")[0]
|
||||
if "?" in tile_href:
|
||||
tile_href = tile_href.split("?")[0] + "?view=tile"
|
||||
else:
|
||||
tile_href = tile_href + "?view=tile"
|
||||
|
||||
list_active = 'bg-stone-200 text-stone-800' if view != 'tile' else 'text-stone-400 hover:text-stone-600'
|
||||
tile_active = 'bg-stone-200 text-stone-800' if view == 'tile' else 'text-stone-400 hover:text-stone-600'
|
||||
|
||||
return await render_to_sx("view-toggle",
|
||||
list_href=list_href, tile_href=tile_href,
|
||||
hx_select=hx_select, list_cls=list_active,
|
||||
tile_cls=tile_active, storage_key="events_view",
|
||||
list_svg=SxExpr(await _get_list_svg()), tile_svg=SxExpr(await _get_tile_svg()))
|
||||
|
||||
|
||||
async def _cart_icon_oob(count: int) -> str:
|
||||
"""Render the OOB cart icon/badge swap."""
|
||||
from quart import g
|
||||
|
||||
blog_url_fn = getattr(g, "blog_url", None)
|
||||
cart_url_fn = getattr(g, "cart_url", None)
|
||||
site_fn = getattr(g, "site", None)
|
||||
logo = ""
|
||||
if site_fn:
|
||||
site_obj = site_fn() if callable(site_fn) else site_fn
|
||||
logo = getattr(site_obj, "logo", "") if site_obj else ""
|
||||
|
||||
if count == 0:
|
||||
blog_href = blog_url_fn("/") if blog_url_fn else "/"
|
||||
return await render_to_sx("events-cart-icon-logo",
|
||||
blog_href=blog_href, logo=logo)
|
||||
|
||||
cart_href = cart_url_fn("/") if cart_url_fn else "/"
|
||||
return await render_to_sx("events-cart-icon-badge",
|
||||
cart_href=cart_href, count=str(count))
|
||||
@@ -25,7 +25,7 @@ def register(url_prefix: str = "/") -> Blueprint:
|
||||
@bp.get("/examples/api/click")
|
||||
async def api_click():
|
||||
from shared.sx.helpers import sx_response
|
||||
from sxc.pages import _oob_code, _component_source_text, _full_wire_text
|
||||
from sxc.pages.renders import _oob_code, _component_source_text, _full_wire_text
|
||||
now = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
sx_src = f'(~click-result :time "{now}")'
|
||||
comp_text = _component_source_text("click-result")
|
||||
@@ -38,7 +38,7 @@ def register(url_prefix: str = "/") -> Blueprint:
|
||||
@bp.post("/examples/api/form")
|
||||
async def api_form():
|
||||
from shared.sx.helpers import sx_response
|
||||
from sxc.pages import _oob_code, _component_source_text, _full_wire_text
|
||||
from sxc.pages.renders import _oob_code, _component_source_text, _full_wire_text
|
||||
form = await request.form
|
||||
name = form.get("name", "")
|
||||
escaped = name.replace('"', '\\"')
|
||||
@@ -54,7 +54,7 @@ def register(url_prefix: str = "/") -> Blueprint:
|
||||
@bp.get("/examples/api/poll")
|
||||
async def api_poll():
|
||||
from shared.sx.helpers import sx_response
|
||||
from sxc.pages import _oob_code, _component_source_text, _full_wire_text
|
||||
from sxc.pages.renders import _oob_code, _component_source_text, _full_wire_text
|
||||
_poll_count["n"] += 1
|
||||
now = datetime.now().strftime("%H:%M:%S")
|
||||
count = min(_poll_count["n"], 10)
|
||||
@@ -69,7 +69,7 @@ def register(url_prefix: str = "/") -> Blueprint:
|
||||
@bp.delete("/examples/api/delete/<item_id>")
|
||||
async def api_delete(item_id: str):
|
||||
from shared.sx.helpers import sx_response
|
||||
from sxc.pages import _oob_code, _component_source_text, _full_wire_text
|
||||
from sxc.pages.renders import _oob_code, _component_source_text, _full_wire_text
|
||||
# Empty primary response — outerHTML swap removes the row
|
||||
# But send OOB swaps to show what happened
|
||||
wire_text = _full_wire_text(f'(empty — row #{item_id} removed by outerHTML swap)')
|
||||
@@ -81,7 +81,7 @@ def register(url_prefix: str = "/") -> Blueprint:
|
||||
@bp.get("/examples/api/edit")
|
||||
async def api_edit_form():
|
||||
from shared.sx.helpers import sx_response
|
||||
from sxc.pages import _oob_code, _component_source_text, _full_wire_text
|
||||
from sxc.pages.renders import _oob_code, _component_source_text, _full_wire_text
|
||||
value = request.args.get("value", "")
|
||||
escaped = value.replace('"', '\\"')
|
||||
sx_src = f'(~inline-edit-form :value "{escaped}")'
|
||||
@@ -95,7 +95,7 @@ def register(url_prefix: str = "/") -> Blueprint:
|
||||
@bp.post("/examples/api/edit")
|
||||
async def api_edit_save():
|
||||
from shared.sx.helpers import sx_response
|
||||
from sxc.pages import _oob_code, _component_source_text, _full_wire_text
|
||||
from sxc.pages.renders import _oob_code, _component_source_text, _full_wire_text
|
||||
form = await request.form
|
||||
value = form.get("value", "")
|
||||
escaped = value.replace('"', '\\"')
|
||||
@@ -109,7 +109,7 @@ def register(url_prefix: str = "/") -> Blueprint:
|
||||
@bp.get("/examples/api/edit/cancel")
|
||||
async def api_edit_cancel():
|
||||
from shared.sx.helpers import sx_response
|
||||
from sxc.pages import _oob_code, _component_source_text, _full_wire_text
|
||||
from sxc.pages.renders import _oob_code, _component_source_text, _full_wire_text
|
||||
value = request.args.get("value", "")
|
||||
escaped = value.replace('"', '\\"')
|
||||
sx_src = f'(~inline-view :value "{escaped}")'
|
||||
@@ -122,7 +122,7 @@ def register(url_prefix: str = "/") -> Blueprint:
|
||||
@bp.get("/examples/api/oob")
|
||||
async def api_oob():
|
||||
from shared.sx.helpers import sx_response
|
||||
from sxc.pages import _oob_code, _full_wire_text
|
||||
from sxc.pages.renders import _oob_code, _full_wire_text
|
||||
now = datetime.now().strftime("%H:%M:%S")
|
||||
sx_src = (
|
||||
f'(<>'
|
||||
@@ -141,7 +141,7 @@ def register(url_prefix: str = "/") -> Blueprint:
|
||||
@bp.get("/examples/api/lazy")
|
||||
async def api_lazy():
|
||||
from shared.sx.helpers import sx_response
|
||||
from sxc.pages import _oob_code, _component_source_text, _full_wire_text
|
||||
from sxc.pages.renders import _oob_code, _component_source_text, _full_wire_text
|
||||
now = datetime.now().strftime("%H:%M:%S")
|
||||
sx_src = f'(~lazy-result :time "{now}")'
|
||||
comp_text = _component_source_text("lazy-result")
|
||||
@@ -155,7 +155,7 @@ def register(url_prefix: str = "/") -> Blueprint:
|
||||
@bp.get("/examples/api/scroll")
|
||||
async def api_scroll():
|
||||
from shared.sx.helpers import sx_response
|
||||
from sxc.pages import _oob_code, _full_wire_text
|
||||
from sxc.pages.renders import _oob_code, _full_wire_text
|
||||
page = int(request.args.get("page", 2))
|
||||
start = (page - 1) * 5 + 1
|
||||
next_page = page + 1
|
||||
@@ -191,7 +191,7 @@ def register(url_prefix: str = "/") -> Blueprint:
|
||||
@bp.post("/examples/api/progress/start")
|
||||
async def api_progress_start():
|
||||
from shared.sx.helpers import sx_response
|
||||
from sxc.pages import _oob_code, _component_source_text, _full_wire_text
|
||||
from sxc.pages.renders import _oob_code, _component_source_text, _full_wire_text
|
||||
job_id = str(uuid4())[:8]
|
||||
_jobs[job_id] = 0
|
||||
sx_src = f'(~progress-status :percent 0 :job-id "{job_id}")'
|
||||
@@ -204,7 +204,7 @@ def register(url_prefix: str = "/") -> Blueprint:
|
||||
@bp.get("/examples/api/progress/status")
|
||||
async def api_progress_status():
|
||||
from shared.sx.helpers import sx_response
|
||||
from sxc.pages import _oob_code, _component_source_text, _full_wire_text
|
||||
from sxc.pages.renders import _oob_code, _component_source_text, _full_wire_text
|
||||
job_id = request.args.get("job", "")
|
||||
current = _jobs.get(job_id, 0)
|
||||
current = min(current + random.randint(15, 30), 100)
|
||||
@@ -221,7 +221,7 @@ def register(url_prefix: str = "/") -> Blueprint:
|
||||
@bp.get("/examples/api/search")
|
||||
async def api_search():
|
||||
from shared.sx.helpers import sx_response
|
||||
from sxc.pages import _oob_code, _component_source_text, _full_wire_text
|
||||
from sxc.pages.renders import _oob_code, _component_source_text, _full_wire_text
|
||||
from content.pages import SEARCH_LANGUAGES
|
||||
q = request.args.get("q", "").strip().lower()
|
||||
if not q:
|
||||
@@ -244,7 +244,7 @@ def register(url_prefix: str = "/") -> Blueprint:
|
||||
@bp.get("/examples/api/validate")
|
||||
async def api_validate():
|
||||
from shared.sx.helpers import sx_response
|
||||
from sxc.pages import _oob_code, _component_source_text, _full_wire_text
|
||||
from sxc.pages.renders import _oob_code, _component_source_text, _full_wire_text
|
||||
email = request.args.get("email", "").strip()
|
||||
if not email:
|
||||
sx_src = '(~validation-error :message "Email is required")'
|
||||
@@ -282,7 +282,7 @@ def register(url_prefix: str = "/") -> Blueprint:
|
||||
@bp.get("/examples/api/values")
|
||||
async def api_values():
|
||||
from shared.sx.helpers import sx_response
|
||||
from sxc.pages import _oob_code, _full_wire_text
|
||||
from sxc.pages.renders import _oob_code, _full_wire_text
|
||||
from content.pages import VALUE_SELECT_DATA
|
||||
cat = request.args.get("category", "")
|
||||
items = VALUE_SELECT_DATA.get(cat, [])
|
||||
@@ -300,7 +300,7 @@ def register(url_prefix: str = "/") -> Blueprint:
|
||||
@bp.post("/examples/api/reset-submit")
|
||||
async def api_reset_submit():
|
||||
from shared.sx.helpers import sx_response
|
||||
from sxc.pages import _oob_code, _component_source_text, _full_wire_text
|
||||
from sxc.pages.renders import _oob_code, _component_source_text, _full_wire_text
|
||||
form = await request.form
|
||||
msg = form.get("message", "").strip() or "(empty)"
|
||||
escaped = msg.replace('"', '\\"')
|
||||
@@ -326,7 +326,7 @@ def register(url_prefix: str = "/") -> Blueprint:
|
||||
@bp.get("/examples/api/editrow/<row_id>")
|
||||
async def api_editrow_form(row_id: str):
|
||||
from shared.sx.helpers import sx_response
|
||||
from sxc.pages import _oob_code, _component_source_text, _full_wire_text
|
||||
from sxc.pages.renders import _oob_code, _component_source_text, _full_wire_text
|
||||
rows = _get_edit_rows()
|
||||
row = rows.get(row_id, {"id": row_id, "name": "", "price": "0", "stock": "0"})
|
||||
sx_src = (f'(~edit-row-form :id "{row["id"]}" :name "{row["name"]}"'
|
||||
@@ -341,7 +341,7 @@ def register(url_prefix: str = "/") -> Blueprint:
|
||||
@bp.post("/examples/api/editrow/<row_id>")
|
||||
async def api_editrow_save(row_id: str):
|
||||
from shared.sx.helpers import sx_response
|
||||
from sxc.pages import _oob_code, _component_source_text, _full_wire_text
|
||||
from sxc.pages.renders import _oob_code, _component_source_text, _full_wire_text
|
||||
form = await request.form
|
||||
rows = _get_edit_rows()
|
||||
rows[row_id] = {
|
||||
@@ -362,7 +362,7 @@ def register(url_prefix: str = "/") -> Blueprint:
|
||||
@bp.get("/examples/api/editrow/<row_id>/cancel")
|
||||
async def api_editrow_cancel(row_id: str):
|
||||
from shared.sx.helpers import sx_response
|
||||
from sxc.pages import _oob_code, _component_source_text, _full_wire_text
|
||||
from sxc.pages.renders import _oob_code, _component_source_text, _full_wire_text
|
||||
rows = _get_edit_rows()
|
||||
row = rows.get(row_id, {"id": row_id, "name": "", "price": "0", "stock": "0"})
|
||||
sx_src = (f'(~edit-row-view :id "{row["id"]}" :name "{row["name"]}"'
|
||||
@@ -388,7 +388,7 @@ def register(url_prefix: str = "/") -> Blueprint:
|
||||
@bp.post("/examples/api/bulk")
|
||||
async def api_bulk():
|
||||
from shared.sx.helpers import sx_response
|
||||
from sxc.pages import _oob_code, _component_source_text, _full_wire_text
|
||||
from sxc.pages.renders import _oob_code, _component_source_text, _full_wire_text
|
||||
action = request.args.get("action", "activate")
|
||||
form = await request.form
|
||||
ids = form.getlist("ids")
|
||||
@@ -418,7 +418,7 @@ def register(url_prefix: str = "/") -> Blueprint:
|
||||
@bp.post("/examples/api/swap-log")
|
||||
async def api_swap_log():
|
||||
from shared.sx.helpers import sx_response
|
||||
from sxc.pages import _oob_code, _full_wire_text
|
||||
from sxc.pages.renders import _oob_code, _full_wire_text
|
||||
mode = request.args.get("mode", "beforeend")
|
||||
_swap_count["n"] += 1
|
||||
now = datetime.now().strftime("%H:%M:%S")
|
||||
@@ -438,7 +438,7 @@ def register(url_prefix: str = "/") -> Blueprint:
|
||||
@bp.get("/examples/api/dashboard")
|
||||
async def api_dashboard():
|
||||
from shared.sx.helpers import sx_response
|
||||
from sxc.pages import _oob_code, _full_wire_text
|
||||
from sxc.pages.renders import _oob_code, _full_wire_text
|
||||
now = datetime.now().strftime("%H:%M:%S")
|
||||
sx_src = (
|
||||
f'(<>'
|
||||
@@ -483,7 +483,7 @@ def register(url_prefix: str = "/") -> Blueprint:
|
||||
@bp.get("/examples/api/tabs/<tab>")
|
||||
async def api_tabs(tab: str):
|
||||
from shared.sx.helpers import sx_response
|
||||
from sxc.pages import _oob_code, _full_wire_text
|
||||
from sxc.pages.renders import _oob_code, _full_wire_text
|
||||
sx_src = _TAB_CONTENT.get(tab, _TAB_CONTENT["tab1"])
|
||||
buttons = []
|
||||
for t, label in [("tab1", "Overview"), ("tab2", "Details"), ("tab3", "History")]:
|
||||
@@ -503,7 +503,7 @@ def register(url_prefix: str = "/") -> Blueprint:
|
||||
@bp.get("/examples/api/animate")
|
||||
async def api_animate():
|
||||
from shared.sx.helpers import sx_response
|
||||
from sxc.pages import _oob_code, _component_source_text, _full_wire_text
|
||||
from sxc.pages.renders import _oob_code, _component_source_text, _full_wire_text
|
||||
colors = ["bg-violet-100", "bg-emerald-100", "bg-blue-100", "bg-amber-100", "bg-rose-100"]
|
||||
color = random.choice(colors)
|
||||
now = datetime.now().strftime("%H:%M:%S")
|
||||
@@ -519,7 +519,7 @@ def register(url_prefix: str = "/") -> Blueprint:
|
||||
@bp.get("/examples/api/dialog")
|
||||
async def api_dialog():
|
||||
from shared.sx.helpers import sx_response
|
||||
from sxc.pages import _oob_code, _component_source_text, _full_wire_text
|
||||
from sxc.pages.renders import _oob_code, _component_source_text, _full_wire_text
|
||||
sx_src = '(~dialog-modal :title "Confirm Action" :message "Are you sure you want to proceed? This is a demo dialog rendered entirely with sx components.")'
|
||||
comp_text = _component_source_text("dialog-modal")
|
||||
wire_text = _full_wire_text(sx_src, "dialog-modal")
|
||||
@@ -530,7 +530,7 @@ def register(url_prefix: str = "/") -> Blueprint:
|
||||
@bp.get("/examples/api/dialog/close")
|
||||
async def api_dialog_close():
|
||||
from shared.sx.helpers import sx_response
|
||||
from sxc.pages import _oob_code, _full_wire_text
|
||||
from sxc.pages.renders import _oob_code, _full_wire_text
|
||||
wire_text = _full_wire_text("(empty — dialog closed)")
|
||||
oob_wire = _oob_code("dialog-wire", wire_text)
|
||||
return sx_response(f'(<> {oob_wire})')
|
||||
@@ -546,7 +546,7 @@ def register(url_prefix: str = "/") -> Blueprint:
|
||||
@bp.get("/examples/api/keyboard")
|
||||
async def api_keyboard():
|
||||
from shared.sx.helpers import sx_response
|
||||
from sxc.pages import _oob_code, _component_source_text, _full_wire_text
|
||||
from sxc.pages.renders import _oob_code, _component_source_text, _full_wire_text
|
||||
key = request.args.get("key", "")
|
||||
action = _KBD_ACTIONS.get(key, f"Unknown key: {key}")
|
||||
escaped_action = action.replace('"', '\\"')
|
||||
@@ -571,7 +571,7 @@ def register(url_prefix: str = "/") -> Blueprint:
|
||||
@bp.get("/examples/api/putpatch/edit-all")
|
||||
async def api_pp_edit_all():
|
||||
from shared.sx.helpers import sx_response
|
||||
from sxc.pages import _oob_code, _component_source_text, _full_wire_text
|
||||
from sxc.pages.renders import _oob_code, _component_source_text, _full_wire_text
|
||||
p = _get_profile()
|
||||
sx_src = f'(~pp-form-full :name "{p["name"]}" :email "{p["email"]}" :role "{p["role"]}")'
|
||||
comp_text = _component_source_text("pp-form-full")
|
||||
@@ -584,7 +584,7 @@ def register(url_prefix: str = "/") -> Blueprint:
|
||||
@bp.put("/examples/api/putpatch")
|
||||
async def api_pp_put():
|
||||
from shared.sx.helpers import sx_response
|
||||
from sxc.pages import _oob_code, _component_source_text, _full_wire_text
|
||||
from sxc.pages.renders import _oob_code, _component_source_text, _full_wire_text
|
||||
form = await request.form
|
||||
p = _get_profile()
|
||||
p["name"] = form.get("name", p["name"])
|
||||
@@ -600,7 +600,7 @@ def register(url_prefix: str = "/") -> Blueprint:
|
||||
@bp.get("/examples/api/putpatch/cancel")
|
||||
async def api_pp_cancel():
|
||||
from shared.sx.helpers import sx_response
|
||||
from sxc.pages import _oob_code, _component_source_text, _full_wire_text
|
||||
from sxc.pages.renders import _oob_code, _component_source_text, _full_wire_text
|
||||
p = _get_profile()
|
||||
sx_src = f'(~pp-view :name "{p["name"]}" :email "{p["email"]}" :role "{p["role"]}")'
|
||||
comp_text = _component_source_text("pp-view")
|
||||
@@ -615,7 +615,7 @@ def register(url_prefix: str = "/") -> Blueprint:
|
||||
@bp.post("/examples/api/json-echo")
|
||||
async def api_json_echo():
|
||||
from shared.sx.helpers import sx_response
|
||||
from sxc.pages import _oob_code, _component_source_text, _full_wire_text
|
||||
from sxc.pages.renders import _oob_code, _component_source_text, _full_wire_text
|
||||
data = await request.get_json(silent=True) or {}
|
||||
body = json.dumps(data, indent=2)
|
||||
ct = request.content_type or "unknown"
|
||||
@@ -633,7 +633,7 @@ def register(url_prefix: str = "/") -> Blueprint:
|
||||
@bp.get("/examples/api/echo-vals")
|
||||
async def api_echo_vals():
|
||||
from shared.sx.helpers import sx_response
|
||||
from sxc.pages import _oob_code, _component_source_text, _full_wire_text
|
||||
from sxc.pages.renders import _oob_code, _component_source_text, _full_wire_text
|
||||
vals = {k: v for k, v in request.args.items()
|
||||
if k not in ("_", "sx-request")}
|
||||
items_sx = " ".join(f'"{k}: {v}"' for k, v in vals.items())
|
||||
@@ -647,7 +647,7 @@ def register(url_prefix: str = "/") -> Blueprint:
|
||||
@bp.get("/examples/api/echo-headers")
|
||||
async def api_echo_headers():
|
||||
from shared.sx.helpers import sx_response
|
||||
from sxc.pages import _oob_code, _component_source_text, _full_wire_text
|
||||
from sxc.pages.renders import _oob_code, _component_source_text, _full_wire_text
|
||||
custom = {k: v for k, v in request.headers if k.lower().startswith("x-")}
|
||||
items_sx = " ".join(f'"{k}: {v}"' for k, v in custom.items())
|
||||
sx_src = f'(~echo-result :label "headers" :items (list {items_sx}))'
|
||||
@@ -662,7 +662,7 @@ def register(url_prefix: str = "/") -> Blueprint:
|
||||
@bp.get("/examples/api/slow")
|
||||
async def api_slow():
|
||||
from shared.sx.helpers import sx_response
|
||||
from sxc.pages import _oob_code, _component_source_text, _full_wire_text
|
||||
from sxc.pages.renders import _oob_code, _component_source_text, _full_wire_text
|
||||
await asyncio.sleep(2)
|
||||
now = datetime.now().strftime("%H:%M:%S")
|
||||
sx_src = f'(~loading-result :time "{now}")'
|
||||
@@ -677,7 +677,7 @@ def register(url_prefix: str = "/") -> Blueprint:
|
||||
@bp.get("/examples/api/slow-search")
|
||||
async def api_slow_search():
|
||||
from shared.sx.helpers import sx_response
|
||||
from sxc.pages import _oob_code, _component_source_text, _full_wire_text
|
||||
from sxc.pages.renders import _oob_code, _component_source_text, _full_wire_text
|
||||
delay = random.uniform(0.5, 2.0)
|
||||
await asyncio.sleep(delay)
|
||||
q = request.args.get("q", "").strip()
|
||||
@@ -697,7 +697,7 @@ def register(url_prefix: str = "/") -> Blueprint:
|
||||
@bp.get("/examples/api/flaky")
|
||||
async def api_flaky():
|
||||
from shared.sx.helpers import sx_response
|
||||
from sxc.pages import _oob_code, _component_source_text, _full_wire_text
|
||||
from sxc.pages.renders import _oob_code, _component_source_text, _full_wire_text
|
||||
_flaky["n"] += 1
|
||||
n = _flaky["n"]
|
||||
if n % 3 != 0:
|
||||
@@ -715,7 +715,7 @@ def register(url_prefix: str = "/") -> Blueprint:
|
||||
|
||||
def _ref_wire(wire_id: str, sx_src: str) -> str:
|
||||
"""Build OOB swap showing the wire response text."""
|
||||
from sxc.pages import _oob_code
|
||||
from sxc.pages.renders import _oob_code
|
||||
return _oob_code(f"ref-wire-{wire_id}", sx_src)
|
||||
|
||||
@bp.get("/reference/api/time")
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
2615
sx/sxc/pages/essays.py
Normal file
2615
sx/sxc/pages/essays.py
Normal file
File diff suppressed because it is too large
Load Diff
239
sx/sxc/pages/helpers.py
Normal file
239
sx/sxc/pages/helpers.py
Normal file
@@ -0,0 +1,239 @@
|
||||
"""Dispatcher functions, public partials, and page helper registration for sx docs."""
|
||||
from __future__ import annotations
|
||||
|
||||
from .essays import (
|
||||
_docs_introduction_sx, _docs_getting_started_sx, _docs_components_sx,
|
||||
_docs_evaluator_sx, _docs_primitives_sx, _docs_css_sx, _docs_server_rendering_sx,
|
||||
_reference_index_sx, _reference_attr_detail_sx, _reference_attrs_sx,
|
||||
_reference_headers_sx, _reference_events_sx, _reference_js_api_sx,
|
||||
_protocol_wire_format_sx, _protocol_fragments_sx, _protocol_resolver_io_sx,
|
||||
_protocol_internal_services_sx, _protocol_activitypub_sx, _protocol_future_sx,
|
||||
_example_click_to_load_sx, _example_form_submission_sx, _example_polling_sx,
|
||||
_example_delete_row_sx, _example_inline_edit_sx, _example_oob_swaps_sx,
|
||||
_example_lazy_loading_sx, _example_infinite_scroll_sx, _example_progress_bar_sx,
|
||||
_example_active_search_sx, _example_inline_validation_sx, _example_value_select_sx,
|
||||
_example_reset_on_submit_sx, _example_edit_row_sx, _example_bulk_update_sx,
|
||||
_example_swap_positions_sx, _example_select_filter_sx, _example_tabs_sx,
|
||||
_example_animations_sx, _example_dialogs_sx, _example_keyboard_shortcuts_sx,
|
||||
_example_put_patch_sx, _example_json_encoding_sx, _example_vals_and_headers_sx,
|
||||
_example_loading_states_sx, _example_sync_replace_sx, _example_retry_sx,
|
||||
_essay_sx_sucks, _essay_why_sexps, _essay_htmx_react_hybrid,
|
||||
_essay_on_demand_css, _essay_client_reactivity, _essay_sx_native,
|
||||
_essay_sx_manifesto, _essay_tail_call_optimization, _essay_continuations,
|
||||
)
|
||||
from .utils import _docs_nav_sx, _reference_nav_sx, _protocols_nav_sx, _examples_nav_sx, _essays_nav_sx
|
||||
from content.highlight import highlight
|
||||
|
||||
async def _docs_content_sx(slug: str) -> str:
|
||||
"""Route to the right docs content builder."""
|
||||
import inspect
|
||||
builders = {
|
||||
"introduction": _docs_introduction_sx,
|
||||
"getting-started": _docs_getting_started_sx,
|
||||
"components": _docs_components_sx,
|
||||
"evaluator": _docs_evaluator_sx,
|
||||
"primitives": _docs_primitives_sx,
|
||||
"css": _docs_css_sx,
|
||||
"server-rendering": _docs_server_rendering_sx,
|
||||
}
|
||||
builder = builders.get(slug, _docs_introduction_sx)
|
||||
result = builder()
|
||||
return await result if inspect.isawaitable(result) else result
|
||||
|
||||
|
||||
async def _reference_content_sx(slug: str) -> str:
|
||||
import inspect
|
||||
builders = {
|
||||
"attributes": _reference_attrs_sx,
|
||||
"headers": _reference_headers_sx,
|
||||
"events": _reference_events_sx,
|
||||
"js-api": _reference_js_api_sx,
|
||||
}
|
||||
result = builders.get(slug or "", _reference_attrs_sx)()
|
||||
return await result if inspect.isawaitable(result) else result
|
||||
|
||||
|
||||
def _protocol_content_sx(slug: str) -> str:
|
||||
builders = {
|
||||
"wire-format": _protocol_wire_format_sx,
|
||||
"fragments": _protocol_fragments_sx,
|
||||
"resolver-io": _protocol_resolver_io_sx,
|
||||
"internal-services": _protocol_internal_services_sx,
|
||||
"activitypub": _protocol_activitypub_sx,
|
||||
"future": _protocol_future_sx,
|
||||
}
|
||||
return builders.get(slug, _protocol_wire_format_sx)()
|
||||
|
||||
|
||||
def _examples_content_sx(slug: str) -> str:
|
||||
builders = {
|
||||
"click-to-load": _example_click_to_load_sx,
|
||||
"form-submission": _example_form_submission_sx,
|
||||
"polling": _example_polling_sx,
|
||||
"delete-row": _example_delete_row_sx,
|
||||
"inline-edit": _example_inline_edit_sx,
|
||||
"oob-swaps": _example_oob_swaps_sx,
|
||||
"lazy-loading": _example_lazy_loading_sx,
|
||||
"infinite-scroll": _example_infinite_scroll_sx,
|
||||
"progress-bar": _example_progress_bar_sx,
|
||||
"active-search": _example_active_search_sx,
|
||||
"inline-validation": _example_inline_validation_sx,
|
||||
"value-select": _example_value_select_sx,
|
||||
"reset-on-submit": _example_reset_on_submit_sx,
|
||||
"edit-row": _example_edit_row_sx,
|
||||
"bulk-update": _example_bulk_update_sx,
|
||||
"swap-positions": _example_swap_positions_sx,
|
||||
"select-filter": _example_select_filter_sx,
|
||||
"tabs": _example_tabs_sx,
|
||||
"animations": _example_animations_sx,
|
||||
"dialogs": _example_dialogs_sx,
|
||||
"keyboard-shortcuts": _example_keyboard_shortcuts_sx,
|
||||
"put-patch": _example_put_patch_sx,
|
||||
"json-encoding": _example_json_encoding_sx,
|
||||
"vals-and-headers": _example_vals_and_headers_sx,
|
||||
"loading-states": _example_loading_states_sx,
|
||||
"sync-replace": _example_sync_replace_sx,
|
||||
"retry": _example_retry_sx,
|
||||
}
|
||||
return builders.get(slug, _example_click_to_load_sx)()
|
||||
|
||||
|
||||
def _essay_content_sx(slug: str) -> str:
|
||||
builders = {
|
||||
"sx-sucks": _essay_sx_sucks,
|
||||
"why-sexps": _essay_why_sexps,
|
||||
"htmx-react-hybrid": _essay_htmx_react_hybrid,
|
||||
"on-demand-css": _essay_on_demand_css,
|
||||
"client-reactivity": _essay_client_reactivity,
|
||||
"sx-native": _essay_sx_native,
|
||||
"sx-manifesto": _essay_sx_manifesto,
|
||||
"tail-call-optimization": _essay_tail_call_optimization,
|
||||
"continuations": _essay_continuations,
|
||||
}
|
||||
return builders.get(slug, _essay_sx_sucks)()
|
||||
|
||||
|
||||
def home_content_sx() -> str:
|
||||
"""Home page content as sx wire format."""
|
||||
hero_code = highlight('(div :class "p-4 bg-white rounded shadow"\n'
|
||||
' (h1 :class "text-2xl font-bold" "Hello")\n'
|
||||
' (button :sx-get "/api/data"\n'
|
||||
' :sx-target "#result"\n'
|
||||
' "Load data"))', "lisp")
|
||||
return (
|
||||
f'(section :id "main-panel"'
|
||||
f' :class "flex-1 md:h-full md:min-h-0 overflow-y-auto overscroll-contain js-grid-viewport"'
|
||||
f' (div :id "main-content"'
|
||||
f' (~sx-hero {hero_code})'
|
||||
f' (~sx-philosophy)'
|
||||
f' (~sx-how-it-works)'
|
||||
f' (~sx-credits)))'
|
||||
)
|
||||
|
||||
|
||||
async def docs_content_partial_sx(slug: str) -> str:
|
||||
"""Docs content as sx wire format."""
|
||||
inner = await _docs_content_sx(slug)
|
||||
return (
|
||||
f'(section :id "main-panel"'
|
||||
f' :class "flex-1 md:h-full md:min-h-0 overflow-y-auto overscroll-contain js-grid-viewport"'
|
||||
f' {inner})'
|
||||
)
|
||||
|
||||
|
||||
async def reference_content_partial_sx(slug: str) -> str:
|
||||
inner = await _reference_content_sx(slug)
|
||||
return (
|
||||
f'(section :id "main-panel"'
|
||||
f' :class "flex-1 md:h-full md:min-h-0 overflow-y-auto overscroll-contain js-grid-viewport"'
|
||||
f' {inner})'
|
||||
)
|
||||
|
||||
|
||||
async def protocol_content_partial_sx(slug: str) -> str:
|
||||
inner = await _protocol_content_sx(slug)
|
||||
return (
|
||||
f'(section :id "main-panel"'
|
||||
f' :class "flex-1 md:h-full md:min-h-0 overflow-y-auto overscroll-contain js-grid-viewport"'
|
||||
f' {inner})'
|
||||
)
|
||||
|
||||
|
||||
async def examples_content_partial_sx(slug: str) -> str:
|
||||
inner = await _examples_content_sx(slug)
|
||||
return (
|
||||
f'(section :id "main-panel"'
|
||||
f' :class "flex-1 md:h-full md:min-h-0 overflow-y-auto overscroll-contain js-grid-viewport"'
|
||||
f' {inner})'
|
||||
)
|
||||
|
||||
|
||||
async def essay_content_partial_sx(slug: str) -> str:
|
||||
inner = await _essay_content_sx(slug)
|
||||
return (
|
||||
f'(section :id "main-panel"'
|
||||
f' :class "flex-1 md:h-full md:min-h-0 overflow-y-auto overscroll-contain js-grid-viewport"'
|
||||
f' {inner})'
|
||||
)
|
||||
|
||||
|
||||
def _register_sx_helpers() -> None:
|
||||
"""Register Python content builder functions as page helpers."""
|
||||
from shared.sx.pages import register_page_helpers
|
||||
from content.highlight import highlight as _highlight
|
||||
from content.pages import (
|
||||
DOCS_NAV, REFERENCE_NAV, PROTOCOLS_NAV,
|
||||
EXAMPLES_NAV, ESSAYS_NAV,
|
||||
)
|
||||
|
||||
def _find_current(nav_list, slug, match_fn=None):
|
||||
"""Find the current nav label for a slug."""
|
||||
if match_fn:
|
||||
return match_fn(nav_list, slug)
|
||||
for label, href in nav_list:
|
||||
if href.endswith(slug):
|
||||
return label
|
||||
return None
|
||||
|
||||
def _home_content():
|
||||
"""Build home page content (uses highlight for hero code block)."""
|
||||
hero_code = _highlight(
|
||||
'(div :class "p-4 bg-white rounded shadow"\n'
|
||||
' (h1 :class "text-2xl font-bold" "Hello")\n'
|
||||
' (button :sx-get "/api/data"\n'
|
||||
' :sx-target "#result"\n'
|
||||
' "Load data"))', "lisp")
|
||||
return (
|
||||
f'(div :id "main-content"'
|
||||
f' (~sx-hero {hero_code})'
|
||||
f' (~sx-philosophy)'
|
||||
f' (~sx-how-it-works)'
|
||||
f' (~sx-credits))'
|
||||
)
|
||||
|
||||
register_page_helpers("sx", {
|
||||
# Content builders
|
||||
"home-content": _home_content,
|
||||
"docs-content": _docs_content_sx,
|
||||
"reference-content": _reference_content_sx,
|
||||
"reference-index-content": _reference_index_sx,
|
||||
"reference-attr-detail": _reference_attr_detail_sx,
|
||||
"protocol-content": _protocol_content_sx,
|
||||
"examples-content": _examples_content_sx,
|
||||
"essay-content": _essay_content_sx,
|
||||
"highlight": _highlight,
|
||||
# Nav builders
|
||||
"docs-nav": _docs_nav_sx,
|
||||
"reference-nav": _reference_nav_sx,
|
||||
"protocols-nav": _protocols_nav_sx,
|
||||
"examples-nav": _examples_nav_sx,
|
||||
"essays-nav": _essays_nav_sx,
|
||||
# Nav data (for current label lookup)
|
||||
"DOCS_NAV": DOCS_NAV,
|
||||
"REFERENCE_NAV": REFERENCE_NAV,
|
||||
"PROTOCOLS_NAV": PROTOCOLS_NAV,
|
||||
"EXAMPLES_NAV": EXAMPLES_NAV,
|
||||
"ESSAYS_NAV": ESSAYS_NAV,
|
||||
# Utility
|
||||
"find-current": _find_current,
|
||||
})
|
||||
112
sx/sxc/pages/layouts.py
Normal file
112
sx/sxc/pages/layouts.py
Normal file
@@ -0,0 +1,112 @@
|
||||
"""Layout registration and header/mobile functions for sx docs."""
|
||||
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
|
||||
|
||||
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, _ctx_to_env
|
||||
from shared.sx.parser import SxExpr
|
||||
|
||||
main_nav = await _main_nav_sx(kw.get("section"))
|
||||
sx_row = await _sx_header_sx(main_nav)
|
||||
return await render_to_sx_with_env("sx-layout-full", _ctx_to_env(ctx),
|
||||
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, _ctx_to_env, oob_header_sx
|
||||
from shared.sx.parser import SxExpr
|
||||
|
||||
main_nav = await _main_nav_sx(kw.get("section"))
|
||||
sx_row = await _sx_header_sx(main_nav)
|
||||
rows = await render_to_sx_with_env("sx-layout-full", _ctx_to_env(ctx),
|
||||
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, _ctx_to_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 = await _main_nav_sx(section)
|
||||
sub_row = await _sub_row_sx(sub_label, sub_href, sub_nav, selected)
|
||||
sx_row = await _sx_header_sx(main_nav, child=sub_row)
|
||||
return await render_to_sx_with_env("sx-section-layout-full", _ctx_to_env(ctx),
|
||||
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, _ctx_to_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 = await _main_nav_sx(section)
|
||||
sub_row = await _sub_row_sx(sub_label, sub_href, sub_nav, selected)
|
||||
sx_row = await _sx_header_sx(main_nav, child=sub_row)
|
||||
rows = await render_to_sx_with_env("sx-section-layout-full", _ctx_to_env(ctx),
|
||||
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, render_to_sx, SxExpr,
|
||||
)
|
||||
|
||||
main_nav = await _main_nav_sx(kw.get("section"))
|
||||
return mobile_menu_sx(
|
||||
await render_to_sx("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, render_to_sx, 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 = await _main_nav_sx(section)
|
||||
|
||||
parts = []
|
||||
if sub_nav:
|
||||
parts.append(await render_to_sx("mobile-menu-section",
|
||||
label=sub_label, href=sub_href, level=2, colour="violet",
|
||||
items=SxExpr(sub_nav)))
|
||||
parts.append(await render_to_sx("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)
|
||||
92
sx/sxc/pages/renders.py
Normal file
92
sx/sxc/pages/renders.py
Normal file
@@ -0,0 +1,92 @@
|
||||
"""Public render/utility functions called from bp routes."""
|
||||
from __future__ import annotations
|
||||
|
||||
from content.highlight import highlight
|
||||
|
||||
|
||||
def _code(code: str, language: str = "lisp") -> str:
|
||||
"""Build a ~doc-code component with highlighted content."""
|
||||
highlighted = highlight(code, language)
|
||||
return f'(~doc-code :code {highlighted})'
|
||||
|
||||
|
||||
def _example_code(code: str, language: str = "lisp") -> str:
|
||||
"""Build an ~example-source component with highlighted content."""
|
||||
highlighted = highlight(code, language)
|
||||
return f'(~example-source :code {highlighted})'
|
||||
|
||||
|
||||
def _placeholder(div_id: str) -> str:
|
||||
"""Empty placeholder that will be filled by OOB swap on interaction."""
|
||||
return (f'(div :id "{div_id}"'
|
||||
f' (div :class "bg-stone-50 border border-stone-200 rounded p-4 mt-3"'
|
||||
f' (p :class "text-stone-400 italic text-sm"'
|
||||
f' "Trigger the demo to see the actual content.")))')
|
||||
|
||||
|
||||
def _component_source_text(*names: str) -> str:
|
||||
"""Get defcomp source text for named components."""
|
||||
from shared.sx.jinja_bridge import _COMPONENT_ENV
|
||||
from shared.sx.types import Component
|
||||
from shared.sx.parser import serialize
|
||||
parts = []
|
||||
for name in names:
|
||||
key = name if name.startswith("~") else f"~{name}"
|
||||
val = _COMPONENT_ENV.get(key)
|
||||
if isinstance(val, Component):
|
||||
param_strs = ["&key"] + list(val.params)
|
||||
if val.has_children:
|
||||
param_strs.extend(["&rest", "children"])
|
||||
params_sx = "(" + " ".join(param_strs) + ")"
|
||||
body_sx = serialize(val.body, pretty=True)
|
||||
parts.append(f"(defcomp ~{val.name} {params_sx}\n{body_sx})")
|
||||
return "\n\n".join(parts)
|
||||
|
||||
|
||||
def _oob_code(target_id: str, text: str) -> str:
|
||||
"""OOB swap that displays plain code in a styled block."""
|
||||
escaped = text.replace('\\', '\\\\').replace('"', '\\"')
|
||||
return (f'(div :id "{target_id}" :sx-swap-oob "innerHTML"'
|
||||
f' (div :class "bg-stone-50 border border-stone-200 rounded p-4 mt-3 overflow-x-auto"'
|
||||
f' (pre :class "text-sm whitespace-pre-wrap"'
|
||||
f' (code "{escaped}"))))')
|
||||
|
||||
|
||||
def _clear_components_btn() -> str:
|
||||
"""Button that clears the client-side component cache (localStorage + in-memory)."""
|
||||
js = ("localStorage.removeItem('sx-components-hash');"
|
||||
"localStorage.removeItem('sx-components-src');"
|
||||
"var e=Sx.getEnv();Object.keys(e).forEach(function(k){if(k.charAt(0)==='~')delete e[k]});"
|
||||
"var b=this;b.textContent='Cleared!';setTimeout(function(){b.textContent='Clear component cache'},2000)")
|
||||
return (f'(button :onclick "{js}"'
|
||||
f' :class "text-xs text-stone-400 hover:text-stone-600 border border-stone-200'
|
||||
f' rounded px-2 py-1 transition-colors"'
|
||||
f' "Clear component cache")')
|
||||
|
||||
|
||||
def _full_wire_text(sx_src: str, *comp_names: str) -> str:
|
||||
"""Build the full wire response text showing component defs + CSS note + sx source.
|
||||
|
||||
Only includes component definitions the client doesn't already have,
|
||||
matching the real behaviour of sx_response().
|
||||
"""
|
||||
from quart import request
|
||||
parts = []
|
||||
if comp_names:
|
||||
# Check which components the client already has
|
||||
loaded_raw = request.headers.get("SX-Components", "")
|
||||
loaded = set(loaded_raw.split(",")) if loaded_raw else set()
|
||||
missing = [n for n in comp_names
|
||||
if f"~{n}" not in loaded and n not in loaded]
|
||||
if missing:
|
||||
comp_text = _component_source_text(*missing)
|
||||
if comp_text:
|
||||
parts.append(f'<script type="text/sx" data-components>\n{comp_text}\n</script>')
|
||||
parts.append('<style data-sx-css>/* new CSS rules */</style>')
|
||||
# Pretty-print the sx source for readable display
|
||||
try:
|
||||
from shared.sx.parser import parse as _parse, serialize as _serialize
|
||||
parts.append(_serialize(_parse(sx_src), pretty=True))
|
||||
except Exception:
|
||||
parts.append(sx_src)
|
||||
return "\n\n".join(parts)
|
||||
137
sx/sxc/pages/utils.py
Normal file
137
sx/sxc/pages/utils.py
Normal file
@@ -0,0 +1,137 @@
|
||||
"""Shared utility functions for sx docs pages."""
|
||||
from __future__ import annotations
|
||||
|
||||
from shared.sx.helpers import (
|
||||
render_to_sx, SxExpr,
|
||||
)
|
||||
|
||||
|
||||
async def _nav_items_sx(items: list[tuple[str, str]], current: str | None = None) -> str:
|
||||
"""Build nav link items as sx."""
|
||||
parts = []
|
||||
for label, href in items:
|
||||
parts.append(await render_to_sx("nav-link",
|
||||
href=href, label=label,
|
||||
is_selected="true" if current == label else None,
|
||||
select_colours="aria-selected:bg-violet-200 aria-selected:text-violet-900",
|
||||
))
|
||||
return "(<> " + " ".join(parts) + ")"
|
||||
|
||||
|
||||
async def _doc_nav_sx(items: list[tuple[str, str]], current: str) -> str:
|
||||
"""Build the in-page doc navigation pills."""
|
||||
items_sx = " ".join(
|
||||
f'(list "{label}" "{href}")'
|
||||
for label, href in items
|
||||
)
|
||||
return await render_to_sx("doc-nav", items=SxExpr(f"(list {items_sx})"), current=current)
|
||||
|
||||
|
||||
async def _attr_table_sx(title: str, attrs: list[tuple[str, str, bool]]) -> str:
|
||||
"""Build an attribute reference table."""
|
||||
from content.pages import ATTR_DETAILS
|
||||
rows = []
|
||||
for attr, desc, exists in attrs:
|
||||
href = f"/reference/attributes/{attr}" if exists and attr in ATTR_DETAILS else None
|
||||
rows.append(await render_to_sx("doc-attr-row", attr=attr, description=desc,
|
||||
exists="true" if exists else None,
|
||||
href=href))
|
||||
return (
|
||||
f'(div :class "space-y-3"'
|
||||
f' (h3 :class "text-xl font-semibold text-stone-700" "{title}")'
|
||||
f' (div :class "overflow-x-auto rounded border border-stone-200"'
|
||||
f' (table :class "w-full text-left text-sm"'
|
||||
f' (thead (tr :class "border-b border-stone-200 bg-stone-50"'
|
||||
f' (th :class "px-3 py-2 font-medium text-stone-600" "Attribute")'
|
||||
f' (th :class "px-3 py-2 font-medium text-stone-600" "Description")'
|
||||
f' (th :class "px-3 py-2 font-medium text-stone-600 text-center w-20" "In sx?")))'
|
||||
f' (tbody {" ".join(rows)}))))'
|
||||
)
|
||||
|
||||
|
||||
def _headers_table_sx(title: str, headers: list[tuple[str, str, str]]) -> str:
|
||||
"""Build a headers reference table."""
|
||||
rows = []
|
||||
for name, value, desc in headers:
|
||||
rows.append(
|
||||
f'(tr :class "border-b border-stone-100"'
|
||||
f' (td :class "px-3 py-2 font-mono text-sm text-violet-700 whitespace-nowrap" "{name}")'
|
||||
f' (td :class "px-3 py-2 font-mono text-sm text-stone-500" "{value}")'
|
||||
f' (td :class "px-3 py-2 text-stone-700 text-sm" "{desc}"))'
|
||||
)
|
||||
return (
|
||||
f'(div :class "space-y-3"'
|
||||
f' (h3 :class "text-xl font-semibold text-stone-700" "{title}")'
|
||||
f' (div :class "overflow-x-auto rounded border border-stone-200"'
|
||||
f' (table :class "w-full text-left text-sm"'
|
||||
f' (thead (tr :class "border-b border-stone-200 bg-stone-50"'
|
||||
f' (th :class "px-3 py-2 font-medium text-stone-600" "Header")'
|
||||
f' (th :class "px-3 py-2 font-medium text-stone-600" "Value")'
|
||||
f' (th :class "px-3 py-2 font-medium text-stone-600" "Description")))'
|
||||
f' (tbody {" ".join(rows)}))))'
|
||||
)
|
||||
|
||||
|
||||
async def _primitives_section_sx() -> str:
|
||||
"""Build the primitives section."""
|
||||
from content.pages import PRIMITIVES
|
||||
parts = []
|
||||
for category, prims in PRIMITIVES.items():
|
||||
prims_sx = " ".join(f'"{p}"' for p in prims)
|
||||
parts.append(await render_to_sx("doc-primitives-table",
|
||||
category=category,
|
||||
primitives=SxExpr(f"(list {prims_sx})")))
|
||||
return " ".join(parts)
|
||||
|
||||
|
||||
async def _sx_header_sx(nav: str | None = None, *, child: str | None = None) -> str:
|
||||
"""Build the sx docs menu-row."""
|
||||
return await render_to_sx("menu-row-sx",
|
||||
id="sx-row", level=1, colour="violet",
|
||||
link_href="/", link_label="sx",
|
||||
link_label_content=SxExpr('(span :class "font-mono" "(<x>)")'),
|
||||
nav=SxExpr(nav) if nav else None,
|
||||
child_id="sx-header-child",
|
||||
child=SxExpr(child) if child else None,
|
||||
)
|
||||
|
||||
|
||||
async def _docs_nav_sx(current: str | None = None) -> str:
|
||||
from content.pages import DOCS_NAV
|
||||
return await _nav_items_sx(DOCS_NAV, current)
|
||||
|
||||
|
||||
async def _reference_nav_sx(current: str | None = None) -> str:
|
||||
from content.pages import REFERENCE_NAV
|
||||
return await _nav_items_sx(REFERENCE_NAV, current)
|
||||
|
||||
|
||||
async def _protocols_nav_sx(current: str | None = None) -> str:
|
||||
from content.pages import PROTOCOLS_NAV
|
||||
return await _nav_items_sx(PROTOCOLS_NAV, current)
|
||||
|
||||
|
||||
async def _examples_nav_sx(current: str | None = None) -> str:
|
||||
from content.pages import EXAMPLES_NAV
|
||||
return await _nav_items_sx(EXAMPLES_NAV, current)
|
||||
|
||||
|
||||
async def _essays_nav_sx(current: str | None = None) -> str:
|
||||
from content.pages import ESSAYS_NAV
|
||||
return await _nav_items_sx(ESSAYS_NAV, current)
|
||||
|
||||
|
||||
async def _main_nav_sx(current_section: str | None = None) -> str:
|
||||
from content.pages import MAIN_NAV
|
||||
return await _nav_items_sx(MAIN_NAV, current_section)
|
||||
|
||||
|
||||
async def _sub_row_sx(sub_label: str, sub_href: str, sub_nav: str,
|
||||
selected: str = "") -> str:
|
||||
"""Build the level-2 sub-section menu-row."""
|
||||
return await render_to_sx("menu-row-sx",
|
||||
id="sx-sub-row", level=2, colour="violet",
|
||||
link_href=sub_href, link_label=sub_label,
|
||||
selected=selected or None,
|
||||
nav=SxExpr(sub_nav),
|
||||
)
|
||||
Reference in New Issue
Block a user