Eliminate Python page helpers from orders — pure .sx defpages with IO primitives
Orders defpages now fetch data via (service ...) and generate URLs via (url-for ...) and (route-prefix) directly in .sx. No Python middleman. - Add url-for, route-prefix IO primitives to shared/sx/primitives_io.py - Add generic register()/\_\_getattr\_\_ to ServiceRegistry for dynamic services - Create OrdersPageService with list_page_data/detail_page_data methods - Rewrite orders.sx defpages to use IO primitives + defcomp calls - Remove ~320 lines of Python page helpers from orders/sxc/pages/__init__.py - Convert :data env merge to use kebab-case keys for SX symbol access Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -4,3 +4,6 @@ from __future__ import annotations
|
|||||||
|
|
||||||
def register_domain_services() -> None:
|
def register_domain_services() -> None:
|
||||||
"""Register services for the orders app."""
|
"""Register services for the orders app."""
|
||||||
|
from shared.services.registry import services
|
||||||
|
from .orders_page import OrdersPageService
|
||||||
|
services.register("orders_page", OrdersPageService())
|
||||||
|
|||||||
138
orders/services/orders_page.py
Normal file
138
orders/services/orders_page.py
Normal file
@@ -0,0 +1,138 @@
|
|||||||
|
"""Orders page data service — provides serialized dicts for .sx defpages."""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from sqlalchemy import select, func, or_, cast, String, exists
|
||||||
|
from sqlalchemy.orm import selectinload
|
||||||
|
|
||||||
|
from shared.models.order import Order, OrderItem
|
||||||
|
from shared.infrastructure.cart_identity import current_cart_identity
|
||||||
|
from shared.infrastructure.urls import market_product_url
|
||||||
|
|
||||||
|
|
||||||
|
class OrdersPageService:
|
||||||
|
"""Service for orders page data, callable via (service "orders-page" ...)."""
|
||||||
|
|
||||||
|
async def list_page_data(self, session, *, search="", page=1):
|
||||||
|
"""Return orders list + pagination metadata as a dict."""
|
||||||
|
PER_PAGE = 10
|
||||||
|
ident = current_cart_identity()
|
||||||
|
if ident["user_id"]:
|
||||||
|
owner = Order.user_id == ident["user_id"]
|
||||||
|
elif ident["session_id"]:
|
||||||
|
owner = Order.session_id == ident["session_id"]
|
||||||
|
else:
|
||||||
|
return {"orders": [], "page": 1, "total_pages": 1,
|
||||||
|
"search": "", "search_count": 0}
|
||||||
|
|
||||||
|
page = max(1, int(page))
|
||||||
|
|
||||||
|
where = None
|
||||||
|
if search:
|
||||||
|
term = f"%{search.strip()}%"
|
||||||
|
conds = [
|
||||||
|
Order.status.ilike(term),
|
||||||
|
Order.currency.ilike(term),
|
||||||
|
Order.sumup_checkout_id.ilike(term),
|
||||||
|
Order.sumup_status.ilike(term),
|
||||||
|
Order.description.ilike(term),
|
||||||
|
exists(
|
||||||
|
select(1).select_from(OrderItem)
|
||||||
|
.where(OrderItem.order_id == Order.id,
|
||||||
|
or_(OrderItem.product_title.ilike(term),
|
||||||
|
OrderItem.product_slug.ilike(term)))
|
||||||
|
),
|
||||||
|
]
|
||||||
|
try:
|
||||||
|
conds.append(Order.id == int(search))
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
conds.append(cast(Order.id, String).ilike(term))
|
||||||
|
where = or_(*conds)
|
||||||
|
|
||||||
|
count_q = select(func.count()).select_from(Order).where(owner)
|
||||||
|
if where is not None:
|
||||||
|
count_q = count_q.where(where)
|
||||||
|
total_count = (await session.execute(count_q)).scalar_one() or 0
|
||||||
|
total_pages = max(1, (total_count + PER_PAGE - 1) // PER_PAGE)
|
||||||
|
if page > total_pages:
|
||||||
|
page = total_pages
|
||||||
|
|
||||||
|
stmt = (select(Order).where(owner)
|
||||||
|
.order_by(Order.created_at.desc())
|
||||||
|
.offset((page - 1) * PER_PAGE).limit(PER_PAGE))
|
||||||
|
if where is not None:
|
||||||
|
stmt = stmt.where(where)
|
||||||
|
rows = (await session.execute(stmt)).scalars().all()
|
||||||
|
|
||||||
|
orders = []
|
||||||
|
for o in rows:
|
||||||
|
orders.append({
|
||||||
|
"id": o.id,
|
||||||
|
"status": o.status or "pending",
|
||||||
|
"created_at_formatted": (
|
||||||
|
o.created_at.strftime("%-d %b %Y, %H:%M")
|
||||||
|
if o.created_at else "\u2014"),
|
||||||
|
"description": o.description or "",
|
||||||
|
"currency": o.currency or "GBP",
|
||||||
|
"total_formatted": f"{o.total_amount or 0:.2f}",
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
"orders": orders,
|
||||||
|
"page": page,
|
||||||
|
"total_pages": total_pages,
|
||||||
|
"search": search or "",
|
||||||
|
"search_count": total_count,
|
||||||
|
}
|
||||||
|
|
||||||
|
async def detail_page_data(self, session, *, order_id=None):
|
||||||
|
"""Return order detail data as a dict."""
|
||||||
|
from quart import abort
|
||||||
|
|
||||||
|
if order_id is None:
|
||||||
|
abort(404)
|
||||||
|
|
||||||
|
ident = current_cart_identity()
|
||||||
|
if ident["user_id"]:
|
||||||
|
owner = Order.user_id == ident["user_id"]
|
||||||
|
elif ident["session_id"]:
|
||||||
|
owner = Order.session_id == ident["session_id"]
|
||||||
|
else:
|
||||||
|
abort(404)
|
||||||
|
return {}
|
||||||
|
|
||||||
|
result = await session.execute(
|
||||||
|
select(Order).options(selectinload(Order.items))
|
||||||
|
.where(Order.id == int(order_id), owner)
|
||||||
|
)
|
||||||
|
order = result.scalar_one_or_none()
|
||||||
|
if not order:
|
||||||
|
abort(404)
|
||||||
|
return {}
|
||||||
|
|
||||||
|
items = []
|
||||||
|
for item in (order.items or []):
|
||||||
|
items.append({
|
||||||
|
"product_url": market_product_url(item.product_slug),
|
||||||
|
"product_image": item.product_image,
|
||||||
|
"product_title": item.product_title,
|
||||||
|
"product_id": item.product_id,
|
||||||
|
"quantity": item.quantity,
|
||||||
|
"currency": item.currency,
|
||||||
|
"unit_price_formatted": f"{item.unit_price or 0:.2f}",
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
"order": {
|
||||||
|
"id": order.id,
|
||||||
|
"status": order.status or "pending",
|
||||||
|
"created_at_formatted": (
|
||||||
|
order.created_at.strftime("%-d %b %Y, %H:%M")
|
||||||
|
if order.created_at else "\u2014"),
|
||||||
|
"description": order.description or "",
|
||||||
|
"currency": order.currency or "GBP",
|
||||||
|
"total_formatted": (
|
||||||
|
f"{order.total_amount:.2f}"
|
||||||
|
if order.total_amount else "0.00"),
|
||||||
|
"items": items,
|
||||||
|
},
|
||||||
|
}
|
||||||
@@ -1,13 +1,12 @@
|
|||||||
"""Orders defpage setup — registers layouts, page helpers, and loads .sx pages."""
|
"""Orders defpage setup — registers layouts and loads .sx pages."""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
|
|
||||||
def setup_orders_pages() -> None:
|
def setup_orders_pages() -> None:
|
||||||
"""Register orders-specific layouts, page helpers, and load page definitions."""
|
"""Register orders-specific layouts and load page definitions."""
|
||||||
_register_orders_layouts()
|
_register_orders_layouts()
|
||||||
_register_orders_helpers()
|
|
||||||
_load_orders_page_files()
|
_load_orders_page_files()
|
||||||
|
|
||||||
|
|
||||||
@@ -125,325 +124,3 @@ def _as_sx_nav(ctx: dict) -> Any:
|
|||||||
"""Convert account_nav fragment to SxExpr for use in component calls."""
|
"""Convert account_nav fragment to SxExpr for use in component calls."""
|
||||||
from shared.sx.helpers import _as_sx
|
from shared.sx.helpers import _as_sx
|
||||||
return _as_sx(ctx.get("account_nav"))
|
return _as_sx(ctx.get("account_nav"))
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
# Page helpers — Python functions callable from defpage expressions
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
def _register_orders_helpers() -> None:
|
|
||||||
from shared.sx.pages import register_page_helpers
|
|
||||||
|
|
||||||
register_page_helpers("orders", {
|
|
||||||
# Orders list
|
|
||||||
"orders-list-content": _h_orders_list_content,
|
|
||||||
"orders-list-filter": _h_orders_list_filter,
|
|
||||||
"orders-list-aside": _h_orders_list_aside,
|
|
||||||
"orders-list-url": _h_orders_list_url,
|
|
||||||
# Order detail
|
|
||||||
"order-detail-content": _h_order_detail_content,
|
|
||||||
"order-detail-filter": _h_order_detail_filter,
|
|
||||||
"order-detail-url": _h_order_detail_url,
|
|
||||||
"order-list-url-from-detail": _h_order_list_url_from_detail,
|
|
||||||
})
|
|
||||||
|
|
||||||
|
|
||||||
async def _ensure_orders_list():
|
|
||||||
"""Fetch orders list data and store in g.orders_page_data."""
|
|
||||||
from quart import g, url_for
|
|
||||||
if hasattr(g, "orders_page_data"):
|
|
||||||
return
|
|
||||||
from sqlalchemy import select, func, or_, cast, String, exists
|
|
||||||
from shared.models.order import Order, OrderItem
|
|
||||||
from shared.infrastructure.cart_identity import current_cart_identity
|
|
||||||
from shared.utils import route_prefix
|
|
||||||
|
|
||||||
ORDERS_PER_PAGE = 10
|
|
||||||
ident = current_cart_identity()
|
|
||||||
if ident["user_id"]:
|
|
||||||
owner_clause = Order.user_id == ident["user_id"]
|
|
||||||
elif ident["session_id"]:
|
|
||||||
owner_clause = Order.session_id == ident["session_id"]
|
|
||||||
else:
|
|
||||||
g.orders_page_data = None
|
|
||||||
return
|
|
||||||
|
|
||||||
from bp.orders.filters.qs import makeqs_factory, decode
|
|
||||||
q = decode()
|
|
||||||
page, search = q.page, q.search
|
|
||||||
if page < 1:
|
|
||||||
page = 1
|
|
||||||
|
|
||||||
where_clause = None
|
|
||||||
if search:
|
|
||||||
term = f"%{search.strip()}%"
|
|
||||||
conditions = [
|
|
||||||
Order.status.ilike(term),
|
|
||||||
Order.currency.ilike(term),
|
|
||||||
Order.sumup_checkout_id.ilike(term),
|
|
||||||
Order.sumup_status.ilike(term),
|
|
||||||
Order.description.ilike(term),
|
|
||||||
]
|
|
||||||
conditions.append(
|
|
||||||
exists(
|
|
||||||
select(1).select_from(OrderItem)
|
|
||||||
.where(OrderItem.order_id == Order.id,
|
|
||||||
or_(OrderItem.product_title.ilike(term),
|
|
||||||
OrderItem.product_slug.ilike(term)))
|
|
||||||
)
|
|
||||||
)
|
|
||||||
try:
|
|
||||||
search_id = int(search)
|
|
||||||
except (TypeError, ValueError):
|
|
||||||
search_id = None
|
|
||||||
if search_id is not None:
|
|
||||||
conditions.append(Order.id == search_id)
|
|
||||||
else:
|
|
||||||
conditions.append(cast(Order.id, String).ilike(term))
|
|
||||||
where_clause = or_(*conditions)
|
|
||||||
|
|
||||||
count_stmt = select(func.count()).select_from(Order).where(owner_clause)
|
|
||||||
if where_clause is not None:
|
|
||||||
count_stmt = count_stmt.where(where_clause)
|
|
||||||
|
|
||||||
total_count_result = await g.s.execute(count_stmt)
|
|
||||||
total_count = total_count_result.scalar_one() or 0
|
|
||||||
total_pages = max(1, (total_count + ORDERS_PER_PAGE - 1) // ORDERS_PER_PAGE)
|
|
||||||
if page > total_pages:
|
|
||||||
page = total_pages
|
|
||||||
|
|
||||||
offset = (page - 1) * ORDERS_PER_PAGE
|
|
||||||
stmt = (
|
|
||||||
select(Order).where(owner_clause)
|
|
||||||
.order_by(Order.created_at.desc())
|
|
||||||
.offset(offset).limit(ORDERS_PER_PAGE)
|
|
||||||
)
|
|
||||||
if where_clause is not None:
|
|
||||||
stmt = stmt.where(where_clause)
|
|
||||||
|
|
||||||
result = await g.s.execute(stmt)
|
|
||||||
orders = result.scalars().all()
|
|
||||||
pfx = route_prefix()
|
|
||||||
qs_fn = makeqs_factory()
|
|
||||||
|
|
||||||
g.orders_page_data = {
|
|
||||||
"orders": orders,
|
|
||||||
"page": page,
|
|
||||||
"total_pages": total_pages,
|
|
||||||
"search": search,
|
|
||||||
"search_count": total_count,
|
|
||||||
"url_for_fn": url_for,
|
|
||||||
"qs_fn": qs_fn,
|
|
||||||
"list_url": pfx + url_for("defpage_orders_list"),
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
async def _ensure_order_detail(order_id):
|
|
||||||
"""Fetch order detail data and store in g.order_detail_data."""
|
|
||||||
from quart import g, url_for, abort
|
|
||||||
if hasattr(g, "order_detail_data"):
|
|
||||||
return
|
|
||||||
from sqlalchemy import select
|
|
||||||
from sqlalchemy.orm import selectinload
|
|
||||||
from shared.models.order import Order
|
|
||||||
from shared.infrastructure.cart_identity import current_cart_identity
|
|
||||||
from shared.utils import route_prefix
|
|
||||||
from shared.browser.app.csrf import generate_csrf_token
|
|
||||||
|
|
||||||
if order_id is None:
|
|
||||||
abort(404)
|
|
||||||
|
|
||||||
ident = current_cart_identity()
|
|
||||||
if ident["user_id"]:
|
|
||||||
owner = Order.user_id == ident["user_id"]
|
|
||||||
elif ident["session_id"]:
|
|
||||||
owner = Order.session_id == ident["session_id"]
|
|
||||||
else:
|
|
||||||
abort(404)
|
|
||||||
return
|
|
||||||
|
|
||||||
result = await g.s.execute(
|
|
||||||
select(Order).options(selectinload(Order.items))
|
|
||||||
.where(Order.id == order_id, owner)
|
|
||||||
)
|
|
||||||
order = result.scalar_one_or_none()
|
|
||||||
if not order:
|
|
||||||
abort(404)
|
|
||||||
return
|
|
||||||
|
|
||||||
pfx = route_prefix()
|
|
||||||
g.order_detail_data = {
|
|
||||||
"order": order,
|
|
||||||
"calendar_entries": None,
|
|
||||||
"detail_url": pfx + url_for("defpage_order_detail", order_id=order.id),
|
|
||||||
"list_url": pfx + url_for("defpage_orders_list"),
|
|
||||||
"recheck_url": pfx + url_for("orders.order.order_recheck", order_id=order.id),
|
|
||||||
"pay_url": pfx + url_for("orders.order.order_pay", order_id=order.id),
|
|
||||||
"csrf_token": generate_csrf_token(),
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
async def _h_orders_list_content(**kw):
|
|
||||||
await _ensure_orders_list()
|
|
||||||
from quart import g
|
|
||||||
from shared.sx.helpers import render_to_sx
|
|
||||||
d = getattr(g, "orders_page_data", None)
|
|
||||||
if not d:
|
|
||||||
return await render_to_sx("order-empty-state")
|
|
||||||
|
|
||||||
orders = d["orders"]
|
|
||||||
url_for_fn = d["url_for_fn"]
|
|
||||||
pfx = d.get("list_url", "/").rsplit("/", 1)[0] if d.get("list_url") else ""
|
|
||||||
|
|
||||||
order_dicts = []
|
|
||||||
for o in orders:
|
|
||||||
order_dicts.append({
|
|
||||||
"id": o.id,
|
|
||||||
"status": o.status or "pending",
|
|
||||||
"created_at_formatted": o.created_at.strftime("%-d %b %Y, %H:%M") if o.created_at else "\u2014",
|
|
||||||
"description": o.description or "",
|
|
||||||
"currency": o.currency or "GBP",
|
|
||||||
"total_formatted": f"{o.total_amount or 0:.2f}",
|
|
||||||
})
|
|
||||||
|
|
||||||
from shared.utils import route_prefix
|
|
||||||
rpfx = route_prefix()
|
|
||||||
detail_prefix = rpfx + url_for_fn("defpage_order_detail", order_id=0).rsplit("0/", 1)[0]
|
|
||||||
rows_url = rpfx + url_for_fn("orders.orders_rows")
|
|
||||||
|
|
||||||
return await render_to_sx("orders-list-content",
|
|
||||||
orders=order_dicts,
|
|
||||||
page=d["page"],
|
|
||||||
total_pages=d["total_pages"],
|
|
||||||
rows_url=rows_url,
|
|
||||||
detail_url_prefix=detail_prefix)
|
|
||||||
|
|
||||||
|
|
||||||
async def _h_orders_list_filter(**kw):
|
|
||||||
await _ensure_orders_list()
|
|
||||||
from quart import g
|
|
||||||
from shared.sx.helpers import render_to_sx
|
|
||||||
from shared.sx.page import SEARCH_HEADERS_MOBILE
|
|
||||||
from shared.sx.parser import SxExpr
|
|
||||||
d = getattr(g, "orders_page_data", None)
|
|
||||||
search = d.get("search", "") if d else ""
|
|
||||||
search_count = d.get("search_count", "") if d else ""
|
|
||||||
search_mobile = await render_to_sx("search-mobile",
|
|
||||||
current_local_href="/",
|
|
||||||
search=search or "",
|
|
||||||
search_count=search_count or "",
|
|
||||||
hx_select="#main-panel",
|
|
||||||
search_headers_mobile=SEARCH_HEADERS_MOBILE,
|
|
||||||
)
|
|
||||||
return await render_to_sx("order-list-header", search_mobile=SxExpr(search_mobile))
|
|
||||||
|
|
||||||
|
|
||||||
async def _h_orders_list_aside(**kw):
|
|
||||||
await _ensure_orders_list()
|
|
||||||
from quart import g
|
|
||||||
from shared.sx.helpers import render_to_sx
|
|
||||||
from shared.sx.page import SEARCH_HEADERS_DESKTOP
|
|
||||||
d = getattr(g, "orders_page_data", None)
|
|
||||||
search = d.get("search", "") if d else ""
|
|
||||||
search_count = d.get("search_count", "") if d else ""
|
|
||||||
return await render_to_sx("search-desktop",
|
|
||||||
current_local_href="/",
|
|
||||||
search=search or "",
|
|
||||||
search_count=search_count or "",
|
|
||||||
hx_select="#main-panel",
|
|
||||||
search_headers_desktop=SEARCH_HEADERS_DESKTOP,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
async def _h_orders_list_url(**kw):
|
|
||||||
await _ensure_orders_list()
|
|
||||||
from quart import g
|
|
||||||
d = getattr(g, "orders_page_data", None)
|
|
||||||
return d["list_url"] if d else "/"
|
|
||||||
|
|
||||||
|
|
||||||
async def _h_order_detail_content(order_id=None, **kw):
|
|
||||||
await _ensure_order_detail(order_id)
|
|
||||||
from quart import g
|
|
||||||
from shared.sx.helpers import render_to_sx
|
|
||||||
from shared.infrastructure.urls import market_product_url
|
|
||||||
d = getattr(g, "order_detail_data", None)
|
|
||||||
if not d:
|
|
||||||
return ""
|
|
||||||
|
|
||||||
order = d["order"]
|
|
||||||
order_dict = {
|
|
||||||
"id": order.id,
|
|
||||||
"status": order.status or "pending",
|
|
||||||
"created_at_formatted": order.created_at.strftime("%-d %b %Y, %H:%M") if order.created_at else None,
|
|
||||||
"description": order.description,
|
|
||||||
"currency": order.currency,
|
|
||||||
"total_formatted": f"{order.total_amount:.2f}" if order.total_amount else None,
|
|
||||||
"items": [
|
|
||||||
{
|
|
||||||
"product_url": market_product_url(item.product_slug),
|
|
||||||
"product_image": item.product_image,
|
|
||||||
"product_title": item.product_title,
|
|
||||||
"product_id": item.product_id,
|
|
||||||
"quantity": item.quantity,
|
|
||||||
"currency": item.currency,
|
|
||||||
"unit_price_formatted": f"{item.unit_price or 0:.2f}",
|
|
||||||
}
|
|
||||||
for item in (order.items or [])
|
|
||||||
],
|
|
||||||
}
|
|
||||||
|
|
||||||
cal_entries = d["calendar_entries"]
|
|
||||||
cal_dicts = None
|
|
||||||
if cal_entries:
|
|
||||||
cal_dicts = []
|
|
||||||
for e in cal_entries:
|
|
||||||
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')}"
|
|
||||||
cal_dicts.append({
|
|
||||||
"name": e.name,
|
|
||||||
"state": e.state or "",
|
|
||||||
"date_str": ds,
|
|
||||||
"cost_formatted": f"{e.cost or 0:.2f}",
|
|
||||||
})
|
|
||||||
|
|
||||||
return await render_to_sx("order-detail-content",
|
|
||||||
order=order_dict,
|
|
||||||
calendar_entries=cal_dicts)
|
|
||||||
|
|
||||||
|
|
||||||
async def _h_order_detail_filter(order_id=None, **kw):
|
|
||||||
await _ensure_order_detail(order_id)
|
|
||||||
from quart import g
|
|
||||||
from shared.sx.helpers import render_to_sx
|
|
||||||
d = getattr(g, "order_detail_data", None)
|
|
||||||
if not d:
|
|
||||||
return ""
|
|
||||||
|
|
||||||
order = d["order"]
|
|
||||||
order_dict = {
|
|
||||||
"status": order.status or "pending",
|
|
||||||
"created_at_formatted": order.created_at.strftime("%-d %b %Y, %H:%M") if order.created_at else "\u2014",
|
|
||||||
}
|
|
||||||
|
|
||||||
return await render_to_sx("order-detail-filter-content",
|
|
||||||
order=order_dict,
|
|
||||||
list_url=d["list_url"],
|
|
||||||
recheck_url=d["recheck_url"],
|
|
||||||
pay_url=d["pay_url"],
|
|
||||||
csrf=d["csrf_token"])
|
|
||||||
|
|
||||||
|
|
||||||
async def _h_order_detail_url(order_id=None, **kw):
|
|
||||||
await _ensure_order_detail(order_id)
|
|
||||||
from quart import g
|
|
||||||
d = getattr(g, "order_detail_data", None)
|
|
||||||
return d["detail_url"] if d else "/"
|
|
||||||
|
|
||||||
|
|
||||||
async def _h_order_list_url_from_detail(order_id=None, **kw):
|
|
||||||
await _ensure_order_detail(order_id)
|
|
||||||
from quart import g
|
|
||||||
d = getattr(g, "order_detail_data", None)
|
|
||||||
return d["list_url"] if d else "/"
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
;; Orders app — declarative page definitions
|
;; Orders app — declarative page definitions
|
||||||
|
;; All data fetching via (service ...) IO primitives, no Python helpers.
|
||||||
|
|
||||||
;; ---------------------------------------------------------------------------
|
;; ---------------------------------------------------------------------------
|
||||||
;; Orders list
|
;; Orders list
|
||||||
@@ -7,11 +8,34 @@
|
|||||||
(defpage orders-list
|
(defpage orders-list
|
||||||
:path "/"
|
:path "/"
|
||||||
:auth :public
|
:auth :public
|
||||||
|
:data (service "orders-page" "list-page-data"
|
||||||
|
:search (or (request-arg "search") "")
|
||||||
|
:page (or (request-arg "page" "1") "1"))
|
||||||
:layout (:orders
|
:layout (:orders
|
||||||
:list-url (orders-list-url))
|
:list-url (str (route-prefix) (url-for "defpage_orders_list")))
|
||||||
:filter (orders-list-filter)
|
:filter (~order-list-header
|
||||||
:aside (orders-list-aside)
|
:search-mobile (~search-mobile
|
||||||
:content (orders-list-content))
|
:current-local-href "/"
|
||||||
|
:search (or search "")
|
||||||
|
:search-count (or search-count "")
|
||||||
|
:hx-select "#main-panel"
|
||||||
|
:search-headers-mobile "{\"X-Origin\":\"search-mobile\",\"X-Search\":\"true\"}"))
|
||||||
|
:aside (~search-desktop
|
||||||
|
:current-local-href "/"
|
||||||
|
:search (or search "")
|
||||||
|
:search-count (or search-count "")
|
||||||
|
:hx-select "#main-panel"
|
||||||
|
:search-headers-desktop "{\"X-Origin\":\"search-desktop\",\"X-Search\":\"true\"}")
|
||||||
|
:content (let* ((pfx (route-prefix))
|
||||||
|
(detail-url-raw (str pfx (url-for "defpage_order_detail" :order-id 0)))
|
||||||
|
(detail-prefix (slice detail-url-raw 0 (- (length detail-url-raw) 2)))
|
||||||
|
(rows-url (str pfx (url-for "orders.orders_rows"))))
|
||||||
|
(~orders-list-content
|
||||||
|
:orders orders
|
||||||
|
:page page
|
||||||
|
:total-pages total-pages
|
||||||
|
:rows-url rows-url
|
||||||
|
:detail-url-prefix detail-prefix)))
|
||||||
|
|
||||||
;; ---------------------------------------------------------------------------
|
;; ---------------------------------------------------------------------------
|
||||||
;; Order detail
|
;; Order detail
|
||||||
@@ -20,8 +44,17 @@
|
|||||||
(defpage order-detail
|
(defpage order-detail
|
||||||
:path "/<int:order_id>/"
|
:path "/<int:order_id>/"
|
||||||
:auth :public
|
:auth :public
|
||||||
|
:data (service "orders-page" "detail-page-data" :order-id order-id)
|
||||||
:layout (:order-detail
|
:layout (:order-detail
|
||||||
:list-url (order-list-url-from-detail order-id)
|
:list-url (str (route-prefix) (url-for "defpage_orders_list"))
|
||||||
:detail-url (order-detail-url order-id))
|
:detail-url (str (route-prefix) (url-for "defpage_order_detail" :order-id order-id)))
|
||||||
:filter (order-detail-filter order-id)
|
:filter (let* ((pfx (route-prefix)))
|
||||||
:content (order-detail-content order-id))
|
(~order-detail-filter-content
|
||||||
|
:order order
|
||||||
|
:list-url (str pfx (url-for "defpage_orders_list"))
|
||||||
|
:recheck-url (str pfx (url-for "orders.order.order_recheck" :order-id order-id))
|
||||||
|
:pay-url (str pfx (url-for "orders.order.order_pay" :order-id order-id))
|
||||||
|
:csrf (csrf-token)))
|
||||||
|
:content (~order-detail-content
|
||||||
|
:order order
|
||||||
|
:calendar-entries calendar-entries))
|
||||||
|
|||||||
@@ -15,6 +15,8 @@ Usage::
|
|||||||
"""
|
"""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
from shared.contracts.protocols import (
|
from shared.contracts.protocols import (
|
||||||
CalendarService,
|
CalendarService,
|
||||||
MarketService,
|
MarketService,
|
||||||
@@ -36,6 +38,7 @@ class _ServiceRegistry:
|
|||||||
self._market: MarketService | None = None
|
self._market: MarketService | None = None
|
||||||
self._cart: CartService | None = None
|
self._cart: CartService | None = None
|
||||||
self._federation: FederationService | None = None
|
self._federation: FederationService | None = None
|
||||||
|
self._extra: dict[str, Any] = {}
|
||||||
|
|
||||||
# -- calendar -------------------------------------------------------------
|
# -- calendar -------------------------------------------------------------
|
||||||
@property
|
@property
|
||||||
@@ -81,10 +84,27 @@ class _ServiceRegistry:
|
|||||||
def federation(self, impl: FederationService) -> None:
|
def federation(self, impl: FederationService) -> None:
|
||||||
self._federation = impl
|
self._federation = impl
|
||||||
|
|
||||||
|
# -- generic registration --------------------------------------------------
|
||||||
|
def register(self, name: str, impl: Any) -> None:
|
||||||
|
"""Register a service by name (for services without typed properties)."""
|
||||||
|
self._extra[name] = impl
|
||||||
|
|
||||||
|
def __getattr__(self, name: str) -> Any:
|
||||||
|
# Fallback to _extra dict for dynamically registered services
|
||||||
|
try:
|
||||||
|
extra = object.__getattribute__(self, "_extra")
|
||||||
|
if name in extra:
|
||||||
|
return extra[name]
|
||||||
|
except AttributeError:
|
||||||
|
pass
|
||||||
|
raise AttributeError(f"No service registered as: {name}")
|
||||||
|
|
||||||
# -- introspection --------------------------------------------------------
|
# -- introspection --------------------------------------------------------
|
||||||
def has(self, name: str) -> bool:
|
def has(self, name: str) -> bool:
|
||||||
"""Check whether a domain service is registered."""
|
"""Check whether a domain service is registered."""
|
||||||
return getattr(self, f"_{name}", None) is not None
|
if getattr(self, f"_{name}", None) is not None:
|
||||||
|
return True
|
||||||
|
return name in self._extra
|
||||||
|
|
||||||
|
|
||||||
# Module-level singleton — import this everywhere.
|
# Module-level singleton — import this everywhere.
|
||||||
|
|||||||
@@ -184,7 +184,9 @@ async def execute_page(
|
|||||||
if page_def.data_expr is not None:
|
if page_def.data_expr is not None:
|
||||||
data_result = await async_eval(page_def.data_expr, env, ctx)
|
data_result = await async_eval(page_def.data_expr, env, ctx)
|
||||||
if isinstance(data_result, dict):
|
if isinstance(data_result, dict):
|
||||||
env.update(data_result)
|
# Merge with kebab-case keys so SX symbols can reference them
|
||||||
|
for k, v in data_result.items():
|
||||||
|
env[k.replace("_", "-")] = v
|
||||||
|
|
||||||
# Render content slot (required)
|
# Render content slot (required)
|
||||||
content_sx = await _eval_slot(page_def.content_expr, env, ctx)
|
content_sx = await _eval_slot(page_def.content_expr, env, ctx)
|
||||||
|
|||||||
@@ -43,6 +43,8 @@ IO_PRIMITIVES: frozenset[str] = frozenset({
|
|||||||
"g",
|
"g",
|
||||||
"csrf-token",
|
"csrf-token",
|
||||||
"abort",
|
"abort",
|
||||||
|
"url-for",
|
||||||
|
"route-prefix",
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
@@ -345,6 +347,34 @@ async def _io_abort(
|
|||||||
abort(status, message)
|
abort(status, message)
|
||||||
|
|
||||||
|
|
||||||
|
async def _io_url_for(
|
||||||
|
args: list[Any], kwargs: dict[str, Any], ctx: RequestContext
|
||||||
|
) -> str:
|
||||||
|
"""``(url-for "endpoint" :key val ...)`` → url_for(endpoint, **kwargs).
|
||||||
|
|
||||||
|
Generates a URL for the given endpoint. Keyword args become URL
|
||||||
|
parameters (kebab-case converted to snake_case).
|
||||||
|
"""
|
||||||
|
if not args:
|
||||||
|
raise ValueError("url-for requires an endpoint name")
|
||||||
|
from quart import url_for
|
||||||
|
endpoint = str(args[0])
|
||||||
|
clean = {k.replace("-", "_"): v for k, v in _clean_kwargs(kwargs).items()}
|
||||||
|
# Convert numeric values for int URL params
|
||||||
|
for k, v in clean.items():
|
||||||
|
if isinstance(v, str) and v.isdigit():
|
||||||
|
clean[k] = int(v)
|
||||||
|
return url_for(endpoint, **clean)
|
||||||
|
|
||||||
|
|
||||||
|
async def _io_route_prefix(
|
||||||
|
args: list[Any], kwargs: dict[str, Any], ctx: RequestContext
|
||||||
|
) -> str:
|
||||||
|
"""``(route-prefix)`` → current route prefix string."""
|
||||||
|
from shared.utils import route_prefix
|
||||||
|
return route_prefix()
|
||||||
|
|
||||||
|
|
||||||
_IO_HANDLERS: dict[str, Any] = {
|
_IO_HANDLERS: dict[str, Any] = {
|
||||||
"frag": _io_frag,
|
"frag": _io_frag,
|
||||||
"query": _io_query,
|
"query": _io_query,
|
||||||
@@ -359,4 +389,6 @@ _IO_HANDLERS: dict[str, Any] = {
|
|||||||
"g": _io_g,
|
"g": _io_g,
|
||||||
"csrf-token": _io_csrf_token,
|
"csrf-token": _io_csrf_token,
|
||||||
"abort": _io_abort,
|
"abort": _io_abort,
|
||||||
|
"url-for": _io_url_for,
|
||||||
|
"route-prefix": _io_route_prefix,
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user