Files
rose-ash/orders/sx/sx_components.py
giles c243d17eeb
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 3m41s
Migrate all apps to defpage declarative page routes
Replace Python GET page handlers with declarative defpage definitions in .sx
files across all 8 apps (sx docs, orders, account, market, cart, federation,
events, blog). Each app now has sxc/pages/ with setup functions, layout
registrations, page helpers, and .sx defpage declarations.

Core infrastructure: add g I/O primitive, PageDef support for auth/layout/
data/content/filter/aside/menu slots, post_author auth level, and custom
layout registration. Remove ~1400 lines of render_*_page/render_*_oob
boilerplate. Update all endpoint references in routes, sx_components, and
templates to defpage_* naming.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 14:52:34 +00:00

359 lines
13 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
Orders service s-expression page components.
Each function renders a complete page section (full page, OOB, or pagination)
using shared s-expression components. Called from route handlers in place
of ``render_template()``.
"""
from __future__ import annotations
import os
from typing import Any
from shared.sx.jinja_bridge import load_service_components
from shared.sx.helpers import (
call_url,
sx_call, SxExpr,
)
from shared.infrastructure.urls import market_product_url, cart_url
# Load orders-specific .sx components + handlers at import time
load_service_components(os.path.dirname(os.path.dirname(__file__)),
service_name="orders")
# ---------------------------------------------------------------------------
# Header helpers (shared auth + orders-specific) — sx-native
# ---------------------------------------------------------------------------
def _auth_nav_sx(ctx: dict) -> str:
"""Auth section desktop nav items as sx."""
parts = [
sx_call("nav-link",
href=call_url(ctx, "account_url", "/newsletters/"),
label="newsletters",
select_colours=ctx.get("select_colours", ""),
),
]
account_nav = ctx.get("account_nav")
if account_nav:
parts.append(str(account_nav))
return "(<> " + " ".join(parts) + ")"
def _auth_header_sx(ctx: dict, *, oob: bool = False) -> str:
"""Build the account section header row as sx."""
return sx_call(
"menu-row-sx",
id="auth-row", level=1, colour="sky",
link_href=call_url(ctx, "account_url", "/"),
link_label="account", icon="fa-solid fa-user",
nav=SxExpr(_auth_nav_sx(ctx)),
child_id="auth-header-child", oob=oob,
)
def _orders_header_sx(ctx: dict, list_url: str) -> str:
"""Build the orders section header row as sx."""
return sx_call(
"menu-row-sx",
id="orders-row", level=2, colour="sky",
link_href=list_url, link_label="Orders", icon="fa fa-gbp",
child_id="orders-header-child",
)
# ---------------------------------------------------------------------------
# Orders list rendering
# ---------------------------------------------------------------------------
def _status_pill_cls(status: str) -> str:
"""Return Tailwind classes for order status pill."""
sl = status.lower()
if sl == "paid":
return "border-emerald-300 bg-emerald-50 text-emerald-700"
if sl in ("failed", "cancelled"):
return "border-rose-300 bg-rose-50 text-rose-700"
return "border-stone-300 bg-stone-50 text-stone-700"
def _order_row_data(order: Any, detail_url: str) -> dict:
"""Extract display data from an order model object."""
status = order.status or "pending"
pill = _status_pill_cls(status)
created = order.created_at.strftime("%-d %b %Y, %H:%M") if order.created_at else "\u2014"
total = f"{order.currency or 'GBP'} {order.total_amount or 0:.2f}"
return dict(
oid=f"#{order.id}", created=created,
desc=order.description or "", total=total,
pill_desktop=f"inline-flex items-center rounded-full border px-2 py-0.5 text-[11px] sm:text-xs {pill}",
pill_mobile=f"inline-flex items-center rounded-full border px-2 py-0.5 text-[11px] {pill}",
status=status, url=detail_url,
)
def _orders_rows_sx(orders: list, page: int, total_pages: int,
url_for_fn: Any, qs_fn: Any) -> str:
"""S-expression wire format for order rows (client renders)."""
from shared.utils import route_prefix
pfx = route_prefix()
parts = []
for o in orders:
d = _order_row_data(o, pfx + url_for_fn("orders.defpage_order_detail", order_id=o.id))
parts.append(sx_call("order-row-desktop",
oid=d["oid"], created=d["created"],
desc=d["desc"], total=d["total"],
pill=d["pill_desktop"], status=d["status"],
url=d["url"]))
parts.append(sx_call("order-row-mobile",
oid=d["oid"], created=d["created"],
total=d["total"], pill=d["pill_mobile"],
status=d["status"], url=d["url"]))
if page < total_pages:
next_url = pfx + url_for_fn("orders.orders_rows") + qs_fn(page=page + 1)
parts.append(sx_call("infinite-scroll",
url=next_url, page=page,
total_pages=total_pages,
id_prefix="orders", colspan=5))
else:
parts.append(sx_call("order-end-row"))
return "(<> " + " ".join(parts) + ")"
def _orders_main_panel_sx(orders: list, rows_sx: str) -> str:
"""Main panel with table or empty state (sx)."""
if not orders:
return sx_call("order-empty-state")
return sx_call("order-table", rows=SxExpr(rows_sx))
def _orders_summary_sx(ctx: dict) -> str:
"""Filter section for orders list (sx)."""
return sx_call("order-list-header", search_mobile=SxExpr(search_mobile_sx(ctx)))
# ---------------------------------------------------------------------------
# Public API: orders list
# ---------------------------------------------------------------------------
# ---------------------------------------------------------------------------
# Single order detail
# ---------------------------------------------------------------------------
def _order_items_sx(order: Any) -> str:
"""Render order items list as sx."""
if not order or not order.items:
return ""
items = []
for item in order.items:
prod_url = market_product_url(item.product_slug)
if item.product_image:
img = sx_call(
"order-item-image",
src=item.product_image, alt=item.product_title or "Product image",
)
else:
img = sx_call("order-item-no-image")
items.append(sx_call(
"order-item-row",
href=prod_url, img=SxExpr(img),
title=item.product_title or "Unknown product",
pid=f"Product ID: {item.product_id}",
qty=f"Qty: {item.quantity}",
price=f"{item.currency or order.currency or 'GBP'} {item.unit_price or 0:.2f}",
))
items_sx = "(<> " + " ".join(items) + ")"
return sx_call("order-items-panel", items=SxExpr(items_sx))
def _calendar_items_sx(calendar_entries: list | None) -> str:
"""Render calendar bookings for an order as sx."""
if not calendar_entries:
return ""
items = []
for e in calendar_entries:
st = e.state or ""
pill = (
"bg-emerald-100 text-emerald-800" if st == "confirmed"
else "bg-amber-100 text-amber-800" if st == "provisional"
else "bg-blue-100 text-blue-800" if st == "ordered"
else "bg-stone-100 text-stone-700"
)
ds = e.start_at.strftime("%-d %b %Y, %H:%M") if e.start_at else ""
if e.end_at:
ds += f" \u2013 {e.end_at.strftime('%-d %b %Y, %H:%M')}"
items.append(sx_call(
"order-calendar-entry",
name=e.name,
pill=f"inline-flex items-center rounded-full px-2 py-0.5 text-[11px] font-medium {pill}",
status=st.capitalize(), date_str=ds,
cost=f"\u00a3{e.cost or 0:.2f}",
))
items_sx = "(<> " + " ".join(items) + ")"
return sx_call("order-calendar-section", items=SxExpr(items_sx))
def _order_main_sx(order: Any, calendar_entries: list | None) -> str:
"""Main panel for single order detail (sx)."""
summary = sx_call(
"order-summary-card",
order_id=order.id,
created_at=order.created_at.strftime("%-d %b %Y, %H:%M") if order.created_at else None,
description=order.description, status=order.status, currency=order.currency,
total_amount=f"{order.total_amount:.2f}" if order.total_amount else None,
)
items = _order_items_sx(order)
calendar = _calendar_items_sx(calendar_entries)
return sx_call(
"order-detail-panel",
summary=SxExpr(summary),
items=SxExpr(items) if items else None,
calendar=SxExpr(calendar) if calendar else None,
)
def _order_filter_sx(order: Any, list_url: str, recheck_url: str,
pay_url: str, csrf_token: str) -> str:
"""Filter section for single order detail (sx)."""
created = order.created_at.strftime("%-d %b %Y, %H:%M") if order.created_at else "\u2014"
status = order.status or "pending"
pay = ""
if status != "paid":
pay = sx_call("order-pay-btn", url=pay_url)
return sx_call(
"order-detail-filter",
info=f"Placed {created} \u00b7 Status: {status}",
list_url=list_url, recheck_url=recheck_url,
csrf=csrf_token,
pay=SxExpr(pay) if pay else None,
)
# ---------------------------------------------------------------------------
# Public API: Checkout error
# ---------------------------------------------------------------------------
def _checkout_error_filter_sx() -> str:
return sx_call("checkout-error-header")
def _checkout_error_content_sx(error: str | None, order: Any | None) -> str:
err_msg = error or "Unexpected error while creating the hosted checkout session."
order_sx = ""
if order:
order_sx = sx_call("checkout-error-order-id", oid=f"#{order.id}")
back_url = cart_url("/")
return sx_call(
"checkout-error-content",
msg=err_msg,
order=SxExpr(order_sx) if order_sx else None,
back_url=back_url,
)
async def render_checkout_error_page(ctx: dict, error: str | None = None, order: Any | None = None) -> str:
"""Full page: checkout error (sx wire format)."""
hdr = root_header_sx(ctx)
inner = _auth_header_sx(ctx)
hdr = "(<> " + hdr + " " + header_child_sx(inner) + ")"
filt = _checkout_error_filter_sx()
content = _checkout_error_content_sx(error, order)
return full_page_sx(ctx, header_rows=hdr, filter=filt, content=content)
# ---------------------------------------------------------------------------
# Public API: Checkout return
# ---------------------------------------------------------------------------
def _ticket_items_sx(order_tickets: list | None) -> str:
"""Render ticket items for an order as sx."""
if not order_tickets:
return ""
items = []
for tk in order_tickets:
st = tk.state or ""
pill = (
"bg-emerald-100 text-emerald-800" if st == "confirmed"
else "bg-amber-100 text-amber-800" if st == "reserved"
else "bg-blue-100 text-blue-800" if st == "checked_in"
else "bg-stone-100 text-stone-700"
)
pill_cls = f"inline-flex items-center rounded-full px-2 py-0.5 text-[11px] font-medium {pill}"
ds = tk.entry_start_at.strftime("%-d %b %Y, %H:%M") if tk.entry_start_at else ""
if tk.entry_end_at:
ds += f" {tk.entry_end_at.strftime('%-d %b %Y, %H:%M')}"
items.append(sx_call(
"checkout-return-ticket",
name=tk.entry_name,
pill=pill_cls,
state=st.replace("_", " ").capitalize(),
type_name=tk.ticket_type_name or None,
date_str=ds,
code=tk.code,
price=f"£{tk.price or 0:.2f}",
))
items_sx = "(<> " + " ".join(items) + ")"
return sx_call("checkout-return-tickets", items=SxExpr(items_sx))
async def render_checkout_return_page(ctx: dict, order: Any | None,
status: str,
calendar_entries: list | None = None,
order_tickets: list | None = None) -> str:
"""Full page: checkout return after SumUp payment (sx wire format)."""
filt = sx_call("checkout-return-header", status=status)
if not order:
content = sx_call("checkout-return-missing")
else:
summary = sx_call(
"order-summary-card",
order_id=order.id,
created_at=order.created_at.strftime("%-d %b %Y, %H:%M") if order.created_at else None,
description=order.description, status=order.status,
currency=order.currency,
total_amount=f"{order.total_amount:.2f}" if order.total_amount else None,
)
items = _order_items_sx(order)
calendar = _calendar_items_sx(calendar_entries)
tickets = _ticket_items_sx(order_tickets)
status_msg = ""
if order.status == "failed":
status_msg = sx_call("checkout-return-failed", order_id=order.id)
elif order.status == "paid":
status_msg = sx_call("checkout-return-paid")
content = sx_call(
"checkout-return-content",
summary=SxExpr(summary),
items=SxExpr(items) if items else None,
calendar=SxExpr(calendar) if calendar else None,
tickets=SxExpr(tickets) if tickets else None,
status_message=SxExpr(status_msg) if status_msg else None,
)
hdr = root_header_sx(ctx)
inner = _auth_header_sx(ctx)
hdr = "(<> " + hdr + " " + header_child_sx(inner) + ")"
return full_page_sx(ctx, header_rows=hdr, filter=filt, content=content)