Continues the pattern of eliminating Python sx_call tree-building in favour of data-driven .sx defcomps. POST/PUT/DELETE routes now pass plain data (dicts, lists, scalars) and let .sx handle iteration, conditionals, and layout via map/let/when/if. Single response components wrap OOB swaps. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
174 lines
6.2 KiB
Python
174 lines
6.2 KiB
Python
"""Price helpers, product detail/meta data builders."""
|
|
from __future__ import annotations
|
|
|
|
from shared.sx.helpers import sx_call
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 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)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Product detail data extraction
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def _product_detail_data(d: dict, ctx: dict) -> dict:
|
|
"""Extract product detail page data for .sx composition."""
|
|
from shared.browser.app.csrf import generate_csrf_token
|
|
from .cards import _like_button_data
|
|
|
|
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", "")
|
|
|
|
# Like button data
|
|
like_data = None
|
|
if user:
|
|
like_data = _like_button_data(slug, liked_by_current_user, csrf, ctx)
|
|
|
|
# Label overlay URLs
|
|
label_urls = []
|
|
if callable(asset_url_fn):
|
|
label_urls = [asset_url_fn("labels/" + l + ".svg") for l in labels]
|
|
|
|
# Image data
|
|
image_data = [{"src": u, "alt": d.get("title", "")} for u in images] if images else []
|
|
|
|
# Thumbnail data
|
|
thumb_data = []
|
|
if len(images) > 1:
|
|
for i, u in enumerate(images):
|
|
thumb_data.append({"title": f"Image {i+1}", "src": u, "alt": f"thumb {i+1}"})
|
|
|
|
# Sticker items
|
|
sticker_items = []
|
|
if stickers and callable(asset_url_fn):
|
|
for s in stickers:
|
|
sticker_items.append({"src": asset_url_fn("stickers/" + s + ".svg"), "name": s})
|
|
|
|
# Extras (unit price, case size)
|
|
extras = []
|
|
ppu = d.get("price_per_unit") or d.get("price_per_unit_raw")
|
|
if ppu:
|
|
extras.append({
|
|
"type": "unit-price",
|
|
"value": _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"):
|
|
extras.append({"type": "case-size", "value": d["case_size_raw"]})
|
|
|
|
return {
|
|
"images": image_data or None,
|
|
"labels": label_urls or None,
|
|
"brand": brand,
|
|
"like-data": like_data,
|
|
"has-nav-buttons": len(images) > 1,
|
|
"thumbs": thumb_data or None,
|
|
"sticker-items": sticker_items or None,
|
|
"extras": extras or None,
|
|
"desc-short": d.get("description_short") or None,
|
|
"desc-html": d.get("description_html") or None,
|
|
"sections": d.get("sections") or None,
|
|
}
|
|
|
|
|
|
def _product_detail_sx(d: dict, ctx: dict) -> str:
|
|
"""Build product detail content — delegates to .sx defcomp."""
|
|
data = _product_detail_data(d, ctx)
|
|
return sx_call("market-product-detail-from-data", **data)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Product meta data extraction
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def _product_meta_data(d: dict, ctx: dict) -> dict:
|
|
"""Extract product meta/SEO data for .sx composition."""
|
|
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"))
|
|
|
|
# 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",
|
|
}
|
|
|
|
return {
|
|
"title": title,
|
|
"description": description,
|
|
"canonical": canonical or None,
|
|
"image-url": image_url or None,
|
|
"site-title": ctx.get("base_title", ""),
|
|
"brand": brand or None,
|
|
"price": f"{price:.2f}" if price and price_currency else None,
|
|
"price-currency": price_currency if price else None,
|
|
"jsonld-json": json.dumps(jsonld),
|
|
}
|
|
|
|
|
|
def _product_meta_sx(d: dict, ctx: dict) -> str:
|
|
"""Build product meta tags — delegates to .sx defcomp."""
|
|
data = _product_meta_data(d, ctx)
|
|
return sx_call("market-product-meta-from-data", **data)
|