Python no longer generates s-expression strings. All SX rendering now goes through render_to_sx() which builds AST from native Python values and evaluates via async_eval_to_sx() — no SX string literals in Python. - Add render_to_sx()/render_to_html() infrastructure in shared/sx/helpers.py - Add (abort status msg) IO primitive in shared/sx/primitives_io.py - Convert all 9 services: ~650 sx_call() invocations replaced - Convert shared helpers (root_header_sx, full_page_sx, etc.) to async - Fix likes service import bug (likes.models → models) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
318 lines
13 KiB
Python
318 lines
13 KiB
Python
"""Cart defpage setup — registers layouts, page helpers, and loads .sx pages."""
|
|
from __future__ import annotations
|
|
|
|
from typing import Any
|
|
|
|
|
|
def setup_cart_pages() -> None:
|
|
"""Register cart-specific layouts, page helpers, and load page definitions."""
|
|
_register_cart_layouts()
|
|
_register_cart_helpers()
|
|
_load_cart_page_files()
|
|
|
|
|
|
def _load_cart_page_files() -> None:
|
|
import os
|
|
from shared.sx.pages import load_page_dir
|
|
load_page_dir(os.path.dirname(__file__), "cart")
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Layouts
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def _register_cart_layouts() -> None:
|
|
from shared.sx.layouts import register_custom_layout
|
|
register_custom_layout("cart-page", _cart_page_full, _cart_page_oob)
|
|
register_custom_layout("cart-admin", _cart_admin_full, _cart_admin_oob)
|
|
|
|
|
|
async def _cart_page_full(ctx: dict, **kw: Any) -> str:
|
|
from shared.sx.helpers import root_header_sx, render_to_sx
|
|
from shared.sx.parser import SxExpr
|
|
from sx.sx_components import _cart_header_sx, _page_cart_header_sx
|
|
|
|
page_post = ctx.get("page_post")
|
|
root_hdr = await root_header_sx(ctx)
|
|
child = await _cart_header_sx(ctx)
|
|
page_hdr = await _page_cart_header_sx(ctx, page_post)
|
|
inner_child = await render_to_sx("header-child-sx", id="cart-header-child", inner=SxExpr(page_hdr))
|
|
nested = await render_to_sx(
|
|
"header-child-sx",
|
|
inner=SxExpr("(<> " + child + " " + inner_child + ")"),
|
|
)
|
|
return "(<> " + root_hdr + " " + nested + ")"
|
|
|
|
|
|
async def _cart_page_oob(ctx: dict, **kw: Any) -> str:
|
|
from shared.sx.helpers import root_header_sx, render_to_sx
|
|
from shared.sx.parser import SxExpr
|
|
from sx.sx_components import _cart_header_sx, _page_cart_header_sx
|
|
|
|
page_post = ctx.get("page_post")
|
|
page_hdr = await _page_cart_header_sx(ctx, page_post)
|
|
child_oob = await render_to_sx("oob-header-sx",
|
|
parent_id="cart-header-child",
|
|
row=SxExpr(page_hdr))
|
|
cart_hdr_oob = await _cart_header_sx(ctx, oob=True)
|
|
root_hdr_oob = await root_header_sx(ctx, oob=True)
|
|
return "(<> " + child_oob + " " + cart_hdr_oob + " " + root_hdr_oob + ")"
|
|
|
|
|
|
async def _cart_admin_full(ctx: dict, **kw: Any) -> str:
|
|
from shared.sx.helpers import root_header_sx
|
|
from sx.sx_components import _post_header_sx, _cart_page_admin_header_sx
|
|
|
|
page_post = ctx.get("page_post")
|
|
selected = kw.get("selected", "")
|
|
root_hdr = await root_header_sx(ctx)
|
|
post_hdr = await _post_header_sx(ctx, page_post)
|
|
admin_hdr = await _cart_page_admin_header_sx(ctx, page_post, selected=selected)
|
|
return "(<> " + root_hdr + " " + post_hdr + " " + admin_hdr + ")"
|
|
|
|
|
|
async def _cart_admin_oob(ctx: dict, **kw: Any) -> str:
|
|
from sx.sx_components import _cart_page_admin_header_sx
|
|
|
|
page_post = ctx.get("page_post")
|
|
selected = kw.get("selected", "")
|
|
return await _cart_page_admin_header_sx(ctx, page_post, oob=True, selected=selected)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Page helpers
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def _register_cart_helpers() -> None:
|
|
from shared.sx.pages import register_page_helpers
|
|
|
|
register_page_helpers("cart", {
|
|
"overview-content": _h_overview_content,
|
|
"page-cart-content": _h_page_cart_content,
|
|
"cart-admin-content": _h_cart_admin_content,
|
|
"cart-payments-content": _h_cart_payments_content,
|
|
})
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Serialization helpers
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def _serialize_cart_item(item: Any) -> dict:
|
|
"""Serialize a cart item + product for SX defcomps."""
|
|
from quart import url_for
|
|
from shared.infrastructure.urls import market_product_url
|
|
|
|
p = item.product if hasattr(item, "product") else item
|
|
slug = p.slug if hasattr(p, "slug") else ""
|
|
unit_price = getattr(p, "special_price", None) or getattr(p, "regular_price", None)
|
|
currency = getattr(p, "regular_price_currency", "GBP") or "GBP"
|
|
return {
|
|
"slug": slug,
|
|
"title": p.title if hasattr(p, "title") else "",
|
|
"image": p.image if hasattr(p, "image") else None,
|
|
"brand": getattr(p, "brand", None),
|
|
"is_deleted": getattr(item, "is_deleted", False),
|
|
"unit_price": float(unit_price) if unit_price else None,
|
|
"special_price": float(p.special_price) if getattr(p, "special_price", None) else None,
|
|
"regular_price": float(p.regular_price) if getattr(p, "regular_price", None) else None,
|
|
"currency": currency,
|
|
"quantity": item.quantity,
|
|
"product_id": p.id,
|
|
"product_url": market_product_url(slug),
|
|
"qty_url": url_for("cart_global.update_quantity", product_id=p.id),
|
|
}
|
|
|
|
|
|
def _serialize_cal_entry(e: Any) -> dict:
|
|
"""Serialize a calendar entry for SX defcomps."""
|
|
name = getattr(e, "name", None) or getattr(e, "calendar_name", "")
|
|
start = e.start_at if hasattr(e, "start_at") else ""
|
|
end = getattr(e, "end_at", None)
|
|
cost = getattr(e, "cost", 0) or 0
|
|
end_str = f" \u2013 {end}" if end else ""
|
|
return {
|
|
"name": name,
|
|
"date_str": f"{start}{end_str}",
|
|
"cost": float(cost),
|
|
}
|
|
|
|
|
|
def _serialize_ticket_group(tg: Any) -> dict:
|
|
"""Serialize a ticket group for SX defcomps."""
|
|
name = tg.entry_name if hasattr(tg, "entry_name") else tg.get("entry_name", "")
|
|
tt_name = tg.ticket_type_name if hasattr(tg, "ticket_type_name") else tg.get("ticket_type_name", "")
|
|
price = tg.price if hasattr(tg, "price") else tg.get("price", 0)
|
|
quantity = tg.quantity if hasattr(tg, "quantity") else tg.get("quantity", 0)
|
|
line_total = tg.line_total if hasattr(tg, "line_total") else tg.get("line_total", 0)
|
|
entry_id = tg.entry_id if hasattr(tg, "entry_id") else tg.get("entry_id", "")
|
|
tt_id = tg.ticket_type_id if hasattr(tg, "ticket_type_id") else tg.get("ticket_type_id", "")
|
|
start_at = tg.entry_start_at if hasattr(tg, "entry_start_at") else tg.get("entry_start_at")
|
|
end_at = tg.entry_end_at if hasattr(tg, "entry_end_at") else tg.get("entry_end_at")
|
|
|
|
date_str = start_at.strftime("%-d %b %Y, %H:%M") if start_at else ""
|
|
if end_at:
|
|
date_str += f" \u2013 {end_at.strftime('%-d %b %Y, %H:%M')}"
|
|
|
|
return {
|
|
"entry_name": name,
|
|
"ticket_type_name": tt_name or None,
|
|
"price": float(price or 0),
|
|
"quantity": quantity,
|
|
"line_total": float(line_total or 0),
|
|
"entry_id": entry_id,
|
|
"ticket_type_id": tt_id or None,
|
|
"date_str": date_str,
|
|
}
|
|
|
|
|
|
def _serialize_page_group(grp: Any) -> dict:
|
|
"""Serialize a page group for SX defcomps."""
|
|
post = grp.get("post") if isinstance(grp, dict) else getattr(grp, "post", None)
|
|
cart_items = grp.get("cart_items", []) if isinstance(grp, dict) else getattr(grp, "cart_items", [])
|
|
cal_entries = grp.get("calendar_entries", []) if isinstance(grp, dict) else getattr(grp, "calendar_entries", [])
|
|
tickets = grp.get("tickets", []) if isinstance(grp, dict) else getattr(grp, "tickets", [])
|
|
|
|
if not cart_items and not cal_entries and not tickets:
|
|
return None
|
|
|
|
post_data = None
|
|
if post:
|
|
post_data = {
|
|
"slug": post.slug if hasattr(post, "slug") else post.get("slug", ""),
|
|
"title": post.title if hasattr(post, "title") else post.get("title", ""),
|
|
"feature_image": post.feature_image if hasattr(post, "feature_image") else post.get("feature_image"),
|
|
}
|
|
market_place = grp.get("market_place") if isinstance(grp, dict) else getattr(grp, "market_place", None)
|
|
mp_data = None
|
|
if market_place:
|
|
mp_data = {"name": market_place.name if hasattr(market_place, "name") else market_place.get("name", "")}
|
|
|
|
return {
|
|
"post": post_data,
|
|
"product_count": grp.get("product_count", 0) if isinstance(grp, dict) else getattr(grp, "product_count", 0),
|
|
"calendar_count": grp.get("calendar_count", 0) if isinstance(grp, dict) else getattr(grp, "calendar_count", 0),
|
|
"ticket_count": grp.get("ticket_count", 0) if isinstance(grp, dict) else getattr(grp, "ticket_count", 0),
|
|
"total": float(grp.get("total", 0) if isinstance(grp, dict) else getattr(grp, "total", 0)),
|
|
"market_place": mp_data,
|
|
}
|
|
|
|
|
|
def _build_summary_data(ctx: dict, cart: list, cal_entries: list, tickets: list,
|
|
total_fn, cal_total_fn, ticket_total_fn) -> dict:
|
|
"""Build cart summary data dict for SX defcomps."""
|
|
from quart import g, request, url_for
|
|
from shared.infrastructure.urls import login_url
|
|
from shared.utils import route_prefix
|
|
|
|
product_qty = sum(ci.quantity for ci in cart) if cart else 0
|
|
ticket_qty = len(tickets) if tickets else 0
|
|
item_count = product_qty + ticket_qty
|
|
|
|
product_total = total_fn(cart) or 0
|
|
cal_total = cal_total_fn(cal_entries) or 0
|
|
tk_total = ticket_total_fn(tickets) or 0
|
|
grand = float(product_total) + float(cal_total) + float(tk_total)
|
|
|
|
symbol = "\u00a3"
|
|
if cart and hasattr(cart[0], "product") and getattr(cart[0].product, "regular_price_currency", None):
|
|
cur = cart[0].product.regular_price_currency
|
|
symbol = "\u00a3" if cur == "GBP" else cur
|
|
|
|
user = getattr(g, "user", None)
|
|
page_post = ctx.get("page_post")
|
|
|
|
result = {
|
|
"item_count": item_count,
|
|
"grand_total": grand,
|
|
"symbol": symbol,
|
|
"is_logged_in": bool(user),
|
|
}
|
|
|
|
if user:
|
|
if page_post:
|
|
action = url_for("page_cart.page_checkout")
|
|
else:
|
|
action = url_for("cart_global.checkout")
|
|
result["checkout_action"] = route_prefix() + action
|
|
result["user_email"] = user.email
|
|
else:
|
|
result["login_href"] = login_url(request.url)
|
|
|
|
return result
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Page helper implementations
|
|
# ---------------------------------------------------------------------------
|
|
|
|
async def _h_overview_content(**kw):
|
|
from quart import g
|
|
from shared.sx.helpers import render_to_sx
|
|
from shared.infrastructure.urls import cart_url
|
|
from bp.cart.services import get_cart_grouped_by_page
|
|
|
|
page_groups = await get_cart_grouped_by_page(g.s)
|
|
grp_dicts = [d for d in (_serialize_page_group(grp) for grp in page_groups) if d]
|
|
return await render_to_sx("cart-overview-content",
|
|
page_groups=grp_dicts,
|
|
cart_url_base=cart_url(""))
|
|
|
|
|
|
async def _h_page_cart_content(page_slug=None, **kw):
|
|
from quart import g
|
|
from shared.sx.helpers import render_to_sx
|
|
from shared.sx.parser import SxExpr
|
|
from shared.sx.page import get_template_context
|
|
from bp.cart.services import total, calendar_total, ticket_total
|
|
from bp.cart.services.page_cart import (
|
|
get_cart_for_page, get_calendar_entries_for_page, get_tickets_for_page,
|
|
)
|
|
from bp.cart.services.ticket_groups import group_tickets
|
|
|
|
post = g.page_post
|
|
cart = await get_cart_for_page(g.s, post.id)
|
|
cal_entries = await get_calendar_entries_for_page(g.s, post.id)
|
|
page_tickets = await get_tickets_for_page(g.s, post.id)
|
|
ticket_groups = group_tickets(page_tickets)
|
|
|
|
ctx = await get_template_context()
|
|
sd = _build_summary_data(ctx, cart, cal_entries, page_tickets,
|
|
total, calendar_total, ticket_total)
|
|
|
|
summary_sx = await render_to_sx("cart-summary-from-data",
|
|
item_count=sd["item_count"],
|
|
grand_total=sd["grand_total"],
|
|
symbol=sd["symbol"],
|
|
is_logged_in=sd["is_logged_in"],
|
|
checkout_action=sd.get("checkout_action"),
|
|
login_href=sd.get("login_href"),
|
|
user_email=sd.get("user_email"))
|
|
|
|
return await render_to_sx("cart-page-cart-content",
|
|
cart_items=[_serialize_cart_item(i) for i in cart],
|
|
cal_entries=[_serialize_cal_entry(e) for e in cal_entries],
|
|
ticket_groups=[_serialize_ticket_group(tg) for tg in ticket_groups],
|
|
summary=SxExpr(summary_sx))
|
|
|
|
|
|
async def _h_cart_admin_content(page_slug=None, **kw):
|
|
return '(~cart-admin-content)'
|
|
|
|
|
|
async def _h_cart_payments_content(page_slug=None, **kw):
|
|
from shared.sx.page import get_template_context
|
|
from shared.sx.helpers import render_to_sx
|
|
|
|
ctx = await get_template_context()
|
|
page_config = ctx.get("page_config")
|
|
pc_data = None
|
|
if page_config:
|
|
pc_data = {
|
|
"sumup_api_key": bool(getattr(page_config, "sumup_api_key", None)),
|
|
"sumup_merchant_code": getattr(page_config, "sumup_merchant_code", None) or "",
|
|
"sumup_checkout_prefix": getattr(page_config, "sumup_checkout_prefix", None) or "",
|
|
}
|
|
return await render_to_sx("cart-payments-content",
|
|
page_config=pc_data)
|