Files
rose-ash/market/sxc/pages/utils.py
giles 959e63d440
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 1m44s
Remove render_to_sx from public API: enforce sx_call for all service code
Replace ~250 render_to_sx calls across all services with sync sx_call,
converting many async functions to sync where no other awaits remained.
Make render_to_sx/render_to_sx_with_env private (_render_to_sx).
Add (post-header-ctx) IO primitive and shared post/post-admin defmacros.
Convert built-in post/post-admin layouts from Python to register_sx_layout
with .sx defcomps. Remove dead post_admin_mobile_nav_sx.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 19:30:45 +00:00

288 lines
11 KiB
Python

"""Price helpers, OOB helpers, product detail/meta builders."""
from __future__ import annotations
from typing import Any
from shared.sx.parser import SxExpr
from shared.sx.helpers import sx_call
# ---------------------------------------------------------------------------
# OOB orphan cleanup
# ---------------------------------------------------------------------------
_MARKET_DEEP_IDS = [
"product-admin-row", "product-admin-header-child",
"product-row", "product-header-child",
"market-admin-row", "market-admin-header-child",
"market-row", "market-header-child",
"post-admin-row", "post-admin-header-child",
]
def _clear_deeper_oob(*keep_ids: str) -> str:
"""Clear all market header rows/children NOT in keep_ids."""
to_clear = [i for i in _MARKET_DEEP_IDS if i not in keep_ids]
return " ".join(sx_call("clear-oob-div", id=i) for i in to_clear)
# ---------------------------------------------------------------------------
# Price helpers
# ---------------------------------------------------------------------------
_SYM = {"GBP": "\u00a3", "EUR": "\u20ac", "USD": "$"}
def _price_str(val, raw, cur) -> str:
if raw:
return str(raw)
if isinstance(val, (int, float)):
return f"{_SYM.get(cur, '')}{val:.2f}"
return str(val or "")
def _set_prices(item: dict) -> dict:
"""Extract price values from product dict (mirrors prices.html set_prices macro)."""
oe = item.get("oe_list_price") or {}
sp_val = item.get("special_price") or (oe.get("special") if oe else None)
sp_raw = item.get("special_price_raw") or (oe.get("special_raw") if oe else None)
sp_cur = item.get("special_price_currency") or (oe.get("special_currency") if oe else None)
rp_val = item.get("regular_price") or item.get("rrp") or (oe.get("rrp") if oe else None)
rp_raw = item.get("regular_price_raw") or item.get("rrp_raw") or (oe.get("rrp_raw") if oe else None)
rp_cur = item.get("regular_price_currency") or item.get("rrp_currency") or (oe.get("rrp_currency") if oe else None)
return dict(sp_val=sp_val, sp_raw=sp_raw, sp_cur=sp_cur,
rp_val=rp_val, rp_raw=rp_raw, rp_cur=rp_cur)
def _card_price_sx(p: dict) -> str:
"""Build price line for product card as sx call."""
pr = _set_prices(p)
sp_str = _price_str(pr["sp_val"], pr["sp_raw"], pr["sp_cur"])
rp_str = _price_str(pr["rp_val"], pr["rp_raw"], pr["rp_cur"])
parts: list[str] = []
if pr["sp_val"]:
parts.append(sx_call("market-price-special", price=sp_str))
if pr["rp_val"]:
parts.append(sx_call("market-price-regular-strike", price=rp_str))
elif pr["rp_val"]:
parts.append(sx_call("market-price-regular", price=rp_str))
inner = "(<> " + " ".join(parts) + ")" if parts else None
return sx_call("market-price-line", inner=SxExpr(inner) if inner else None)
# ---------------------------------------------------------------------------
# Product detail page content
# ---------------------------------------------------------------------------
def _product_detail_sx(d: dict, ctx: dict) -> str:
"""Build product detail main panel content as sx."""
from quart import url_for
from shared.browser.app.csrf import generate_csrf_token
from .cards import _like_button_sx
asset_url_fn = ctx.get("asset_url")
user = ctx.get("user")
liked_by_current_user = ctx.get("liked_by_current_user", False)
csrf = generate_csrf_token()
images = d.get("images", [])
labels = d.get("labels", [])
stickers = d.get("stickers", [])
brand = d.get("brand", "")
slug = d.get("slug", "")
# Gallery
if images:
# Like button
like_sx = ""
if user:
like_sx = _like_button_sx(slug, liked_by_current_user, csrf, ctx)
# Main image + labels
label_parts: list[str] = []
if callable(asset_url_fn):
for l in labels:
label_parts.append(sx_call(
"market-label-overlay",
src=asset_url_fn("labels/" + l + ".svg"),
))
labels_sx = "(<> " + " ".join(label_parts) + ")" if label_parts else None
gallery_inner = sx_call(
"market-detail-gallery-inner",
like=SxExpr(like_sx) if like_sx else None,
image=images[0], alt=d.get("title", ""),
labels=SxExpr(labels_sx) if labels_sx else None,
brand=brand,
)
# Prev/next buttons
nav_buttons = ""
if len(images) > 1:
nav_buttons = sx_call("market-detail-nav-buttons")
gallery_sx = sx_call(
"market-detail-gallery",
inner=SxExpr(gallery_inner),
nav=SxExpr(nav_buttons) if nav_buttons else None,
)
# Thumbnails
gallery_parts = [gallery_sx]
if len(images) > 1:
thumb_parts = []
for i, u in enumerate(images):
thumb_parts.append(sx_call(
"market-detail-thumb",
title=f"Image {i+1}", src=u, alt=f"thumb {i+1}",
))
thumbs_sx = "(<> " + " ".join(thumb_parts) + ")"
gallery_parts.append(sx_call("market-detail-thumbs", thumbs=SxExpr(thumbs_sx)))
gallery_final = "(<> " + " ".join(gallery_parts) + ")"
else:
like_sx = ""
if user:
like_sx = _like_button_sx(slug, liked_by_current_user, csrf, ctx)
gallery_final = sx_call("market-detail-no-image",
like=SxExpr(like_sx) if like_sx else None)
# Stickers below gallery
stickers_sx = ""
if stickers and callable(asset_url_fn):
sticker_parts = []
for s in stickers:
sticker_parts.append(sx_call(
"market-detail-sticker",
src=asset_url_fn("stickers/" + s + ".svg"), name=s,
))
sticker_items_sx = "(<> " + " ".join(sticker_parts) + ")"
stickers_sx = sx_call("market-detail-stickers", items=SxExpr(sticker_items_sx))
# Right column: prices, description, sections
pr = _set_prices(d)
detail_parts: list[str] = []
# Unit price / case size extras
extra_parts: list[str] = []
ppu = d.get("price_per_unit") or d.get("price_per_unit_raw")
if ppu:
extra_parts.append(sx_call(
"market-detail-unit-price",
price=_price_str(d.get("price_per_unit"), d.get("price_per_unit_raw"), d.get("price_per_unit_currency")),
))
if d.get("case_size_raw"):
extra_parts.append(sx_call("market-detail-case-size", size=d["case_size_raw"]))
if extra_parts:
extras_sx = "(<> " + " ".join(extra_parts) + ")"
detail_parts.append(sx_call("market-detail-extras", inner=SxExpr(extras_sx)))
# Description
desc_short = d.get("description_short")
desc_html_val = d.get("description_html")
if desc_short or desc_html_val:
desc_parts: list[str] = []
if desc_short:
desc_parts.append(sx_call("market-detail-desc-short", text=desc_short))
if desc_html_val:
desc_parts.append(sx_call("market-detail-desc-html", html=desc_html_val))
desc_inner = "(<> " + " ".join(desc_parts) + ")"
detail_parts.append(sx_call("market-detail-desc-wrapper", inner=SxExpr(desc_inner)))
# Sections (expandable)
sections = d.get("sections", [])
if sections:
sec_parts = []
for sec in sections:
sec_parts.append(sx_call(
"market-detail-section",
title=sec.get("title", ""), html=sec.get("html", ""),
))
sec_items_sx = "(<> " + " ".join(sec_parts) + ")"
detail_parts.append(sx_call("market-detail-sections", items=SxExpr(sec_items_sx)))
details_inner_sx = "(<> " + " ".join(detail_parts) + ")" if detail_parts else "(<>)"
details_sx = sx_call("market-detail-right-col", inner=SxExpr(details_inner_sx))
return sx_call(
"market-detail-layout",
gallery=SxExpr(gallery_final),
stickers=SxExpr(stickers_sx) if stickers_sx else None,
details=SxExpr(details_sx),
)
# ---------------------------------------------------------------------------
# Product meta (OpenGraph, JSON-LD)
# ---------------------------------------------------------------------------
def _product_meta_sx(d: dict, ctx: dict) -> str:
"""Build product meta tags as sx (auto-hoisted to <head> by sx.js)."""
import json
from quart import request
title = d.get("title", "")
desc_source = d.get("description_short") or ""
if not desc_source and d.get("description_html"):
import re
desc_source = re.sub(r"<[^>]+>", "", d.get("description_html", ""))
description = desc_source.strip().replace("\n", " ")[:160]
image_url = d.get("image") or (d.get("images", [None])[0] if d.get("images") else None)
canonical = request.url if request else ""
brand = d.get("brand", "")
sku = d.get("sku", "")
price = d.get("special_price") or d.get("regular_price") or d.get("rrp")
price_currency = d.get("special_price_currency") or d.get("regular_price_currency") or d.get("rrp_currency")
parts = [sx_call("market-meta-title", title=title)]
parts.append(sx_call("market-meta-description", description=description))
if canonical:
parts.append(sx_call("market-meta-canonical", href=canonical))
# OpenGraph
site_title = ctx.get("base_title", "")
parts.append(sx_call("market-meta-og", property="og:site_name", content=site_title))
parts.append(sx_call("market-meta-og", property="og:type", content="product"))
parts.append(sx_call("market-meta-og", property="og:title", content=title))
parts.append(sx_call("market-meta-og", property="og:description", content=description))
if canonical:
parts.append(sx_call("market-meta-og", property="og:url", content=canonical))
if image_url:
parts.append(sx_call("market-meta-og", property="og:image", content=image_url))
if price and price_currency:
parts.append(sx_call("market-meta-og", property="product:price:amount", content=f"{price:.2f}"))
parts.append(sx_call("market-meta-og", property="product:price:currency", content=price_currency))
if brand:
parts.append(sx_call("market-meta-og", property="product:brand", content=brand))
# Twitter
card_type = "summary_large_image" if image_url else "summary"
parts.append(sx_call("market-meta-twitter", name="twitter:card", content=card_type))
parts.append(sx_call("market-meta-twitter", name="twitter:title", content=title))
parts.append(sx_call("market-meta-twitter", name="twitter:description", content=description))
if image_url:
parts.append(sx_call("market-meta-twitter", name="twitter:image", content=image_url))
# JSON-LD
jsonld = {
"@context": "https://schema.org",
"@type": "Product",
"name": title,
"image": image_url,
"description": description,
"sku": sku,
"url": canonical,
}
if brand:
jsonld["brand"] = {"@type": "Brand", "name": brand}
if price and price_currency:
jsonld["offers"] = {
"@type": "Offer",
"price": price,
"priceCurrency": price_currency,
"url": canonical,
"availability": "https://schema.org/InStock",
}
parts.append(sx_call("market-meta-jsonld", json=json.dumps(jsonld)))
return "(<> " + " ".join(parts) + ")"