Some checks failed
Build and Deploy / build-and-deploy (push) Has been cancelled
Split from coop monolith. Includes: - Market/browse/product blueprints - Product sync API - Suma scraping pipeline - Templates for market, browse, and product views - Dockerfile and CI workflow for independent deployment
164 lines
5.1 KiB
Python
164 lines
5.1 KiB
Python
from __future__ import annotations
|
|
|
|
import time
|
|
import re
|
|
from typing import Dict, List, Tuple, Optional
|
|
from urllib.parse import urlparse, urljoin
|
|
|
|
from config import config
|
|
from . import db_backend as cb
|
|
from .blacklist.category import is_category_blocked # Reverse map: slug -> label
|
|
|
|
# ------------------ Caches ------------------
|
|
_nav_cache: Dict = {}
|
|
_nav_cache_ts: float = 0.0
|
|
_nav_ttl_seconds = 60 * 60 * 6 # 6 hours
|
|
|
|
|
|
def _now() -> float:
|
|
try:
|
|
return now() # type: ignore[name-defined]
|
|
except Exception:
|
|
return time.time()
|
|
|
|
|
|
def extract_sub_slug(href: str, top_slug: str) -> Optional[str]:
|
|
p = urlparse(href)
|
|
parts = [x for x in (p.path or "").split("/") if x]
|
|
if len(parts) >= 2 and parts[0].lower() == top_slug.lower():
|
|
sub = parts[1]
|
|
if sub.lower().endswith((".html", ".htm")):
|
|
sub = re.sub(r"\.(html?|HTML?)$", "", sub)
|
|
return sub
|
|
return None
|
|
|
|
|
|
def group_by_category(slug_to_links: Dict[str, List[Tuple[str, str]]]) -> Dict[str, Dict]:
|
|
nav = {"cats": {}}
|
|
for label, slug in config()["categories"]["allow"].items():
|
|
top_href = urljoin(config()["base_url"], f"/{slug}")
|
|
subs = []
|
|
for text, href in slug_to_links.get(slug, []):
|
|
sub_slug = extract_sub_slug(href, slug)
|
|
if sub_slug:
|
|
subs.append({
|
|
"name": text,
|
|
"href": href,
|
|
"slug": sub_slug,
|
|
# no count here yet in this path
|
|
})
|
|
subs.sort(key=lambda x: x["name"].lower())
|
|
nav["cats"][label] = {"href": top_href, "slug": slug, "subs": subs}
|
|
nav = _apply_category_blacklist(nav)
|
|
return nav
|
|
|
|
|
|
async def get_nav(session) -> Dict[str, Dict]:
|
|
"""
|
|
Return navigation structure; annotate each sub with product counts.
|
|
Uses snapshot for offline behaviour.
|
|
"""
|
|
global _nav_cache, _nav_cache_ts
|
|
now_ts = _now()
|
|
|
|
# load from snapshot
|
|
nav = await cb.db_nav(session)
|
|
|
|
# inject counts for each subcategory (and for top-level too if you like)
|
|
for label, cat in (nav.get("cats") or {}).items():
|
|
top_slug = cat.get("slug")
|
|
if not top_slug:
|
|
continue
|
|
|
|
|
|
# Counts for subs
|
|
new_subs = []
|
|
for s in cat.get("subs", []):
|
|
s.get("slug")
|
|
#if not sub_slug:
|
|
# s_count = 0
|
|
#else:
|
|
# s_count = await cb.db_count_products_in_sub(session,top_slug, sub_slug)
|
|
#print('sub', s_count)
|
|
new_subs.append({
|
|
**s,
|
|
#"count": s_count,
|
|
})
|
|
cat["subs"] = new_subs
|
|
|
|
_nav_cache = nav
|
|
_nav_cache_ts = now_ts
|
|
|
|
nav = _apply_category_blacklist(nav)
|
|
return nav
|
|
|
|
|
|
def category_context(top_slug: Optional[str], sub_slug: Optional[str], nav: Dict[str, Dict]):
|
|
"""Build template context for a category/subcategory page."""
|
|
def _order_subs_selected_first(subs, sub_slug: str | None):
|
|
"""Return subs with the selected subcategory (by slug) first."""
|
|
if not subs or not sub_slug:
|
|
return subs
|
|
head = [s for s in subs if sub_slug and sub_slug.lower() == s['slug']]
|
|
tail = [s for s in subs if not (sub_slug and sub_slug.lower() == s['slug'])]
|
|
return head + tail
|
|
|
|
REVERSE_CATEGORY = {v: k for k, v in config()["categories"]["allow"].items()}
|
|
label = REVERSE_CATEGORY.get(top_slug)
|
|
cat = nav["cats"].get(label) or {}
|
|
|
|
top_suma_href = cat.get("href") or urljoin(config()["base_url"], f"/{top_slug}")
|
|
top_local_href = f"{top_slug}"
|
|
|
|
# total products in this top-level category (all subs combined / top-level listing)
|
|
top_count = cat.get("count", 0)
|
|
|
|
subs = []
|
|
for s in cat.get("subs", []):
|
|
subs.append({
|
|
"name": s["name"],
|
|
"slug": s.get("slug"),
|
|
"local_href": f"{top_slug}/{s.get('slug')}",
|
|
"suma_href": s["href"],
|
|
"count": s.get("count", 0), # per-subcategory product count
|
|
})
|
|
|
|
current_local_href = (
|
|
f"{top_slug}/{sub_slug}" if sub_slug
|
|
else f"{top_slug}" if top_slug
|
|
else ""
|
|
)
|
|
|
|
return {
|
|
"category_label": label,
|
|
"top_slug": top_slug,
|
|
"sub_slug": sub_slug,
|
|
"top_suma_href": top_suma_href,
|
|
"top_local_href": top_local_href,
|
|
|
|
# 👇 expose total count for the parent category
|
|
"top_count": top_count,
|
|
|
|
# list of subcategories, each with its own count
|
|
"subs_local": _order_subs_selected_first(subs, sub_slug),
|
|
|
|
#"current_local_href": current_local_href,
|
|
}
|
|
|
|
def _apply_category_blacklist(nav: Dict[str, Dict]) -> Dict[str, Dict]:
|
|
cats = nav.get("cats", {})
|
|
out = {"cats": {}}
|
|
for label, data in cats.items():
|
|
top = (data or {}).get("slug")
|
|
if not top or is_category_blocked(top):
|
|
continue
|
|
# filter subs
|
|
subs = []
|
|
for s in (data.get("subs") or []):
|
|
sub_slug = s.get("slug")
|
|
if sub_slug and not is_category_blocked(top, sub_slug):
|
|
subs.append(s)
|
|
# keep everything else (including counts)
|
|
out["cats"][label] = {**data, "subs": subs}
|
|
return out
|