Layout defcomps are now fully self-contained via IO-primitive auto-fetch macros, eliminating Python layout functions that manually threaded context values through SxExpr wrappers. Services converted: - Federation (1 layout): social - Blog (7 layouts): blog, blog-settings, blog-cache, blog-snippets, blog-menu-items, blog-tag-groups, blog-tag-group-edit - SX docs (2 layouts): sx, sx-section - Cart (2 layouts): cart-page, cart-admin + orders/order-detail - Events (9 layouts): calendar-admin, slots, slot, day-admin, entry, entry-admin, ticket-types, ticket-type, markets - Market (2 layouts): market, market-admin New IO primitives added to shared/sx/primitives_io.py: - federation-actor-ctx, cart-page-ctx, request-view-args - events-calendar-ctx, events-day-ctx, events-entry-ctx, events-slot-ctx, events-ticket-type-ctx - market-header-ctx (pre-builds desktop/mobile nav as SxExpr) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
227 lines
9.3 KiB
Python
227 lines
9.3 KiB
Python
"""Cart page data service — provides serialized dicts for .sx defpages."""
|
|
from __future__ import annotations
|
|
|
|
from typing import Any
|
|
|
|
|
|
def _serialize_cart_item(item: Any) -> dict:
|
|
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:
|
|
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:
|
|
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 | None:
|
|
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,
|
|
}
|
|
|
|
|
|
class CartPageService:
|
|
"""Service for cart page data, callable via (service "cart-page" ...)."""
|
|
|
|
async def overview_data(self, session, **kw):
|
|
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(session)
|
|
grp_dicts = [d for d in (_serialize_page_group(grp) for grp in page_groups) if d]
|
|
return {
|
|
"page_groups": grp_dicts,
|
|
"cart_url_base": cart_url(""),
|
|
}
|
|
|
|
async def page_cart_data(self, session, **kw):
|
|
from quart import g, request, url_for
|
|
from shared.infrastructure.urls import login_url
|
|
from shared.utils import route_prefix
|
|
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(session, post.id)
|
|
cal_entries = await get_calendar_entries_for_page(session, post.id)
|
|
page_tickets = await get_tickets_for_page(session, post.id)
|
|
ticket_groups = group_tickets(page_tickets)
|
|
|
|
# Build summary data
|
|
product_qty = sum(ci.quantity for ci in cart) if cart else 0
|
|
ticket_qty = len(page_tickets) if page_tickets else 0
|
|
item_count = product_qty + ticket_qty
|
|
|
|
product_total = total(cart) or 0
|
|
cal_total = calendar_total(cal_entries) or 0
|
|
tk_total = ticket_total(page_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 = getattr(g, "page_post", None)
|
|
|
|
summary = {
|
|
"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")
|
|
summary["checkout_action"] = route_prefix() + action
|
|
summary["user_email"] = user.email
|
|
else:
|
|
summary["login_href"] = login_url(request.url)
|
|
|
|
return {
|
|
"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": summary,
|
|
}
|
|
|
|
async def admin_data(self, session, **kw):
|
|
"""Populate post context for cart-admin layout headers."""
|
|
from quart import g
|
|
from shared.infrastructure.fragments import fetch_fragments
|
|
|
|
post = g.page_post
|
|
slug = post.slug if post else ""
|
|
post_id = post.id if post else None
|
|
|
|
# Fetch container_nav for post header
|
|
container_nav = ""
|
|
if post_id:
|
|
nav_params = {
|
|
"container_type": "page",
|
|
"container_id": str(post_id),
|
|
"post_slug": slug,
|
|
}
|
|
events_nav, market_nav = await fetch_fragments([
|
|
("events", "container-nav", nav_params),
|
|
("market", "container-nav", nav_params),
|
|
], required=False)
|
|
container_nav = events_nav + market_nav
|
|
|
|
return {
|
|
"post": {
|
|
"id": post_id,
|
|
"slug": slug,
|
|
"title": (post.title if post else "")[:160],
|
|
"feature_image": getattr(post, "feature_image", None),
|
|
},
|
|
"container_nav": container_nav,
|
|
}
|
|
|
|
async def payments_admin_data(self, session, **kw):
|
|
"""Admin data + payments data combined for cart-payments page."""
|
|
admin = await self.admin_data(session)
|
|
payments = await self.payments_data(session)
|
|
return {**admin, **payments}
|
|
|
|
async def payments_data(self, session, **kw):
|
|
from shared.sx.page import get_template_context
|
|
|
|
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 {"page_config": pc_data}
|