Phase 1-3 of decoupling: - path_setup.py adds project root to sys.path - Market-owned models in market/models/ (market, market_place) - All imports updated: shared.infrastructure, shared.db, shared.browser, etc. - MarketPlace uses container_type/container_id instead of post_id FK Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
715 lines
25 KiB
Python
715 lines
25 KiB
Python
from __future__ import annotations
|
|
from typing import Dict, List, Optional
|
|
|
|
from sqlalchemy import select, and_
|
|
from sqlalchemy.orm import selectinload
|
|
|
|
from shared.config import config # if unused elsewhere, you can remove this import
|
|
|
|
# ORM models
|
|
from models.market import (
|
|
Product, ProductImage, ProductSection,
|
|
Listing, ListingItem,
|
|
NavTop, NavSub,
|
|
ProductSticker, ProductLabel,
|
|
ProductAttribute, ProductNutrition, ProductAllergen, ProductLike
|
|
|
|
)
|
|
from sqlalchemy import func, case
|
|
|
|
|
|
# ---------- helpers ----------
|
|
def _regular_price_of(p: Product) -> Optional[float]:
|
|
try:
|
|
return (
|
|
float(p.regular_price)
|
|
if p.regular_price is not None
|
|
else (
|
|
float(p.special_price)
|
|
if p.special_price is not None
|
|
else None
|
|
)
|
|
)
|
|
except Exception:
|
|
return None
|
|
|
|
# ---------- NAV ----------
|
|
async def db_nav(session, market_id=None) -> Dict:
|
|
top_q = select(NavTop).where(NavTop.deleted_at.is_(None))
|
|
if market_id is not None:
|
|
top_q = top_q.where(NavTop.market_id == market_id)
|
|
tops = (await session.execute(top_q)).scalars().all()
|
|
|
|
top_ids = [t.id for t in tops]
|
|
if top_ids:
|
|
subs = (await session.execute(
|
|
select(NavSub).where(NavSub.top_id.in_(top_ids), NavSub.deleted_at.is_(None))
|
|
)).scalars().all()
|
|
else:
|
|
subs = []
|
|
|
|
subs_by_top: Dict[int, List[Dict]] = {}
|
|
for s in subs:
|
|
sub_name = (s.label or s.slug or "").strip()
|
|
subs_by_top.setdefault(s.top_id, []).append({
|
|
"label": s.label,
|
|
"name": sub_name, # back-compat for callers expecting "name"
|
|
"slug": s.slug,
|
|
"href": s.href,
|
|
})
|
|
|
|
cats: Dict[str, Dict] = {}
|
|
for t in tops:
|
|
top_label = (t.label or t.slug or "").strip()
|
|
cats[top_label] = {
|
|
"label": t.label,
|
|
"name": top_label, # back-compat
|
|
"slug": t.slug,
|
|
"subs": sorted(subs_by_top.get(t.id, []), key=lambda x: (x["name"] or "").lower()),
|
|
}
|
|
return {"cats": cats}
|
|
|
|
|
|
async def db_product_full(session, slug: str, user_id=0) -> Optional[dict]:
|
|
|
|
liked_product_ids_subq = (
|
|
select(ProductLike.product_slug)
|
|
.where(
|
|
and_(
|
|
ProductLike.user_id == user_id,
|
|
ProductLike.deleted_at.is_(None)
|
|
)
|
|
)
|
|
)
|
|
|
|
is_liked_case = case(
|
|
(and_(
|
|
(Product.slug.in_(liked_product_ids_subq)),
|
|
Product.deleted_at.is_(None)
|
|
), True),
|
|
else_=False
|
|
).label("is_liked")
|
|
|
|
q = (
|
|
select(Product, is_liked_case)
|
|
.where(Product.slug == slug, Product.deleted_at.is_(None))
|
|
.options(
|
|
selectinload(Product.images.and_(ProductImage.deleted_at.is_(None))),
|
|
selectinload(Product.sections.and_(ProductSection.deleted_at.is_(None))),
|
|
selectinload(Product.labels.and_(ProductLabel.deleted_at.is_(None))),
|
|
selectinload(Product.stickers.and_(ProductSticker.deleted_at.is_(None))),
|
|
selectinload(Product.attributes.and_(ProductAttribute.deleted_at.is_(None))),
|
|
selectinload(Product.nutrition.and_(ProductNutrition.deleted_at.is_(None))),
|
|
selectinload(Product.allergens.and_(ProductAllergen.deleted_at.is_(None))),
|
|
)
|
|
)
|
|
result = await session.execute(q)
|
|
|
|
row = result.first() if result is not None else None
|
|
p, is_liked = row if row else (None, None)
|
|
if not p:
|
|
return None
|
|
|
|
gallery = [
|
|
img.url
|
|
for img in sorted(p.images, key=lambda i: (i.kind or "gallery", i.position or 0))
|
|
if (img.kind or "gallery") == "gallery"
|
|
]
|
|
embedded = [
|
|
img.url
|
|
for img in sorted(p.images, key=lambda i: i.position or 0)
|
|
if (img.kind or "") == "embedded"
|
|
]
|
|
all_imgs = [
|
|
img.url
|
|
for img in sorted(p.images, key=lambda i: i.position or 0)
|
|
if (img.kind or "") == "all"
|
|
]
|
|
return {
|
|
"id": p.id,
|
|
"slug": p.slug,
|
|
"title": p.title,
|
|
"brand": p.brand,
|
|
"image": p.image,
|
|
"description_short": p.description_short,
|
|
"description_html": p.description_html,
|
|
"suma_href": p.suma_href,
|
|
"rrp": float(p.rrp) if p.rrp is not None else None,
|
|
"special_price": float(p.special_price) if p.special_price is not None else None,
|
|
"special_price_raw": p.special_price_raw,
|
|
"special_price_currency": p.special_price_currency,
|
|
"regular_price": _regular_price_of(p),
|
|
"regular_price_raw": p.regular_price_raw,
|
|
"regular_price_currency": p.regular_price_currency,
|
|
"rrp_raw": p.rrp_raw,
|
|
"rrp_currency": p.rrp_currency,
|
|
"price_per_unit_raw": p.price_per_unit_raw,
|
|
"price_per_unit": p.price_per_unit,
|
|
"price_per_unit_currency": p.price_per_unit_currency,
|
|
"oe_list_price": p.oe_list_price,
|
|
"images": gallery,
|
|
"embedded_image_urls": embedded,
|
|
"all_image_urls": all_imgs,
|
|
"sections": [{"title": s.title, "html": s.html} for s in p.sections],
|
|
"stickers": [v.name.strip().lower() for v in p.stickers if v.name],
|
|
"labels": [v.name for v in p.labels if v.name],
|
|
"ean": p.ean,
|
|
"sku": p.sku,
|
|
"unit_size": p.unit_size,
|
|
"pack_size": p.pack_size,
|
|
"case_size_raw": p.case_size_raw,
|
|
"case_size_count": p.case_size_count,
|
|
"case_size_item_qty": p.case_size_item_qty,
|
|
"case_size_item_unit": p.case_size_item_unit,
|
|
"info_table": {a.key: a.value for a in p.attributes if a.key},
|
|
"nutrition": [{"key": n.key, "value": n.value, "unit": n.unit} for n in p.nutrition if n.key],
|
|
"allergens": [{"name": a.name, "contains": a.contains} for a in p.allergens if a.name],
|
|
"is_liked": is_liked,
|
|
"deleted_at": p.deleted_at
|
|
}
|
|
|
|
|
|
async def db_product_full_id(session, id:int, user_id=0) -> Optional[dict]:
|
|
liked_product_ids_subq = (
|
|
select(ProductLike.product_slug)
|
|
.where(
|
|
and_(
|
|
ProductLike.user_id == user_id,
|
|
ProductLike.deleted_at.is_(None)
|
|
)
|
|
)
|
|
)
|
|
|
|
is_liked_case = case(
|
|
(
|
|
(Product.slug.in_(liked_product_ids_subq)),
|
|
True
|
|
),
|
|
else_=False
|
|
).label("is_liked")
|
|
|
|
q = (
|
|
select(Product, is_liked_case)
|
|
.where(Product.id == id)
|
|
.options(
|
|
selectinload(Product.images.and_(ProductImage.deleted_at.is_(None))),
|
|
selectinload(Product.sections.and_(ProductSection.deleted_at.is_(None))),
|
|
selectinload(Product.labels.and_(ProductLabel.deleted_at.is_(None))),
|
|
selectinload(Product.stickers.and_(ProductSticker.deleted_at.is_(None))),
|
|
selectinload(Product.attributes.and_(ProductAttribute.deleted_at.is_(None))),
|
|
selectinload(Product.nutrition.and_(ProductNutrition.deleted_at.is_(None))),
|
|
selectinload(Product.allergens.and_(ProductAllergen.deleted_at.is_(None))),
|
|
)
|
|
)
|
|
result = await session.execute(q)
|
|
|
|
row = result.first() if result is not None else None
|
|
p, is_liked = row if row else (None, None)
|
|
if not p:
|
|
return None
|
|
|
|
gallery = [
|
|
img.url
|
|
for img in sorted(p.images, key=lambda i: (i.kind or "gallery", i.position or 0))
|
|
if (img.kind or "gallery") == "gallery"
|
|
]
|
|
embedded = [
|
|
img.url
|
|
for img in sorted(p.images, key=lambda i: i.position or 0)
|
|
if (img.kind or "") == "embedded"
|
|
]
|
|
all_imgs = [
|
|
img.url
|
|
for img in sorted(p.images, key=lambda i: i.position or 0)
|
|
if (img.kind or "") == "all"
|
|
]
|
|
return {
|
|
"id": p.id,
|
|
"slug": p.slug,
|
|
"title": p.title,
|
|
"brand": p.brand,
|
|
"image": p.image,
|
|
"description_short": p.description_short,
|
|
"description_html": p.description_html,
|
|
"suma_href": p.suma_href,
|
|
"rrp": float(p.rrp) if p.rrp is not None else None,
|
|
"special_price": float(p.special_price) if p.special_price is not None else None,
|
|
"special_price_raw": p.special_price_raw,
|
|
"special_price_currency": p.special_price_currency,
|
|
"regular_price": _regular_price_of(p),
|
|
"regular_price_raw": p.regular_price_raw,
|
|
"regular_price_currency": p.regular_price_currency,
|
|
"rrp_raw": p.rrp_raw,
|
|
"rrp_currency": p.rrp_currency,
|
|
"price_per_unit_raw": p.price_per_unit_raw,
|
|
"price_per_unit": p.price_per_unit,
|
|
"price_per_unit_currency": p.price_per_unit_currency,
|
|
"oe_list_price": p.oe_list_price,
|
|
"images": gallery,
|
|
"embedded_image_urls": embedded,
|
|
"all_image_urls": all_imgs,
|
|
"sections": [{"title": s.title, "html": s.html} for s in p.sections],
|
|
"stickers": [v.name.strip().lower() for v in p.stickers if v.name],
|
|
"labels": [v.name for v in p.labels if v.name],
|
|
"ean": p.ean,
|
|
"sku": p.sku,
|
|
"unit_size": p.unit_size,
|
|
"pack_size": p.pack_size,
|
|
"case_size_raw": p.case_size_raw,
|
|
"case_size_count": p.case_size_count,
|
|
"case_size_item_qty": p.case_size_item_qty,
|
|
"case_size_item_unit": p.case_size_item_unit,
|
|
"info_table": {a.key: a.value for a in p.attributes if a.key},
|
|
"nutrition": [{"key": n.key, "value": n.value, "unit": n.unit} for n in p.nutrition if n.key],
|
|
"allergens": [{"name": a.name, "contains": a.contains} for a in p.allergens if a.name],
|
|
"is_liked": is_liked,
|
|
"deleted_at": p.deleted_at
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
# ---------- PRODUCTS LISTING ----------
|
|
|
|
async def db_products_nocounts(
|
|
session,
|
|
top_slug: str | None,
|
|
sub_slug: str | None,
|
|
selected_brands: Optional[List[str]] = None,
|
|
selected_stickers: Optional[List[str]] = None,
|
|
selected_labels: Optional[List[str]] = None,
|
|
page: int = 1,
|
|
search: Optional[str] = None,
|
|
sort: Optional[str] = None,
|
|
page_size: int = 20,
|
|
liked: bool = None,
|
|
user_id: int=0,
|
|
market_id: int | None = None,
|
|
) -> Dict:
|
|
BLOCKED_SLUGS = set((config().get("blacklist", {}).get("product", []) or []))
|
|
base_conditions = []
|
|
if BLOCKED_SLUGS:
|
|
base_conditions.append(
|
|
~Product.slug.in_(BLOCKED_SLUGS),
|
|
)
|
|
|
|
if top_slug:
|
|
q_list_conditions = [
|
|
Listing.deleted_at.is_(None),
|
|
NavTop.deleted_at.is_(None),
|
|
NavTop.slug == top_slug,
|
|
NavSub.deleted_at.is_(None),
|
|
NavSub.slug == sub_slug if sub_slug else Listing.sub_id.is_(None),
|
|
]
|
|
if market_id is not None:
|
|
q_list_conditions.append(NavTop.market_id == market_id)
|
|
|
|
q_list = (
|
|
select(Listing.id)
|
|
.join(NavTop, Listing.top)
|
|
.outerjoin(NavSub, Listing.sub)
|
|
.where(*q_list_conditions)
|
|
)
|
|
|
|
listing_id = (await session.execute(q_list)).scalars().first()
|
|
if not listing_id:
|
|
return {"total_pages": 1, "items": []}
|
|
|
|
base_conditions.append(Product.slug.in_(
|
|
select(ListingItem.slug).where(ListingItem.listing_id == listing_id, ListingItem.deleted_at.is_(None))
|
|
))
|
|
elif market_id is not None:
|
|
# Browse all within a specific market: filter products through market's nav hierarchy
|
|
market_product_slugs = (
|
|
select(ListingItem.slug)
|
|
.join(Listing, ListingItem.listing_id == Listing.id)
|
|
.join(NavTop, Listing.top_id == NavTop.id)
|
|
.where(
|
|
ListingItem.deleted_at.is_(None),
|
|
Listing.deleted_at.is_(None),
|
|
NavTop.deleted_at.is_(None),
|
|
NavTop.market_id == market_id,
|
|
)
|
|
)
|
|
base_conditions.append(Product.slug.in_(market_product_slugs))
|
|
|
|
base_ids_subq = select(Product.id).where(*base_conditions, Product.deleted_at.is_(None))
|
|
base_ids = (await session.execute(base_ids_subq)).scalars().all()
|
|
|
|
if not base_ids:
|
|
return {"total_pages": 1, "items": []}
|
|
sel_brands = [(b or "").strip().lower() for b in (selected_brands or []) if (b or "").strip()]
|
|
sel_stickers = [(s or "").strip().lower() for s in (selected_stickers or []) if (s or "").strip()]
|
|
sel_labels = [(l or "").strip().lower() for l in (selected_labels or []) if (l or "").strip()]
|
|
search_q = (search or "").strip().lower()
|
|
|
|
filter_conditions = []
|
|
if sel_brands:
|
|
filter_conditions.append(func.lower(Product.brand).in_(sel_brands))
|
|
for sticker_name in sel_stickers:
|
|
filter_conditions.append(
|
|
Product.stickers.any(
|
|
and_(
|
|
func.lower(ProductSticker.name) == sticker_name,
|
|
ProductSticker.deleted_at.is_(None)
|
|
)
|
|
)
|
|
)
|
|
for label_name in sel_labels:
|
|
filter_conditions.append(
|
|
Product.labels.any(
|
|
and_(
|
|
func.lower(ProductLabel.name) == label_name,
|
|
ProductLabel.deleted_at.is_(None),
|
|
)
|
|
)
|
|
)
|
|
if search_q:
|
|
filter_conditions.append(func.lower(Product.description_short).contains(search_q))
|
|
if liked:
|
|
liked_subq = liked_subq = (
|
|
select(ProductLike.product_slug)
|
|
.where(
|
|
and_(
|
|
ProductLike.user_id == user_id,
|
|
ProductLike.deleted_at.is_(None)
|
|
)
|
|
)
|
|
.subquery()
|
|
)
|
|
filter_conditions.append(Product.slug.in_(liked_subq))
|
|
|
|
filtered_count_query = select(func.count(Product.id)).where(Product.id.in_(base_ids), *filter_conditions)
|
|
total_filtered = (await session.execute(filtered_count_query)).scalars().one()
|
|
total_pages = max(1, (total_filtered + page_size - 1) // page_size)
|
|
page = max(1, page)
|
|
|
|
|
|
liked_product_slugs_subq = (
|
|
select(ProductLike.product_slug)
|
|
.where(
|
|
and_(
|
|
ProductLike.user_id == user_id,
|
|
ProductLike.deleted_at.is_(None)
|
|
)
|
|
)
|
|
)
|
|
is_liked_case = case(
|
|
(Product.slug.in_(liked_product_slugs_subq), True),
|
|
else_=False
|
|
).label("is_liked")
|
|
|
|
q_filtered = select(Product, is_liked_case).where(Product.id.in_(base_ids), *filter_conditions).options(
|
|
selectinload(Product.images),
|
|
selectinload(Product.sections),
|
|
selectinload(Product.labels),
|
|
selectinload(Product.stickers),
|
|
selectinload(Product.attributes),
|
|
selectinload(Product.nutrition),
|
|
selectinload(Product.allergens),
|
|
)
|
|
|
|
sort_mode = (sort or "az").strip().lower()
|
|
if sort_mode == "az":
|
|
q_filtered = q_filtered.order_by(func.lower(Product.title), Product.slug)
|
|
elif sort_mode == "za":
|
|
q_filtered = q_filtered.order_by(func.lower(Product.title).desc(), Product.slug.desc())
|
|
elif sort_mode in ("price-asc", "price_asc", "price-low", "price-low-high", "low-high", "lo-hi"):
|
|
q_filtered = q_filtered.order_by(
|
|
case((Product.regular_price.is_(None), 1), else_=0),
|
|
Product.regular_price.asc(),
|
|
func.lower(Product.title),
|
|
Product.slug
|
|
)
|
|
elif sort_mode in ("price-desc", "price_desc", "price-high", "price-high-low", "high-low", "hi-lo"):
|
|
q_filtered = q_filtered.order_by(
|
|
case((Product.regular_price.is_(None), 1), else_=0),
|
|
Product.regular_price.desc(),
|
|
func.lower(Product.title),
|
|
Product.slug
|
|
)
|
|
else:
|
|
q_filtered = q_filtered.order_by(func.lower(Product.title), Product.slug)
|
|
|
|
offset_val = (page - 1) * page_size
|
|
q_filtered = q_filtered.offset(offset_val).limit(page_size)
|
|
products_page = (await session.execute(q_filtered)).all()
|
|
|
|
items: List[Dict] = []
|
|
for p, is_liked in products_page:
|
|
gallery_imgs = sorted((img for img in p.images), key=lambda i: (i.kind or "gallery", i.position or 0))
|
|
gallery = [img.url for img in gallery_imgs if (img.kind or "gallery") == "gallery"]
|
|
embedded = [img.url for img in sorted(p.images, key=lambda i: i.position or 0) if (img.kind or "") == "embedded"]
|
|
all_imgs = [img.url for img in sorted(p.images, key=lambda i: i.position or 0) if (img.kind or "") == "all"]
|
|
|
|
items.append({
|
|
"slug": p.slug,
|
|
"title": p.title,
|
|
"brand": p.brand,
|
|
"description_short": p.description_short,
|
|
"description_html": p.description_html,
|
|
"image": p.image,
|
|
"rrp": float(p.rrp) if p.rrp is not None else None,
|
|
"special_price": float(p.special_price) if p.special_price is not None else None,
|
|
"special_price_raw": p.special_price_raw,
|
|
"special_price_currency": p.special_price_currency,
|
|
"regular_price": _regular_price_of(p),
|
|
"regular_price_raw": p.regular_price_raw,
|
|
"regular_price_currency": p.regular_price_currency,
|
|
"rrp_raw": p.rrp_raw,
|
|
"rrp_currency": p.rrp_currency,
|
|
"price_per_unit_raw": p.price_per_unit_raw,
|
|
"price_per_unit": p.price_per_unit,
|
|
"price_per_unit_currency": p.price_per_unit_currency,
|
|
"images": gallery,
|
|
"embedded_image_urls": embedded,
|
|
"all_image_urls": all_imgs,
|
|
"sections": [{"title": s.title, "html": s.html} for s in p.sections],
|
|
"labels": [l.name for l in p.labels if l.name],
|
|
"stickers": [s.name.strip().lower() for s in p.stickers if s.name],
|
|
"info_table": {a.key: a.value for a in p.attributes if a.key},
|
|
"nutrition": [{"key": n.key, "value": n.value, "unit": n.unit} for n in p.nutrition if n.key],
|
|
"allergens": [{"name": a.name, "contains": a.contains} for a in p.allergens if a.name],
|
|
"ean": p.ean,
|
|
"sku": p.sku,
|
|
"unit_size": p.unit_size,
|
|
"pack_size": p.pack_size,
|
|
"is_liked": is_liked,
|
|
})
|
|
|
|
return {
|
|
"total_pages": total_pages,
|
|
"items": items,
|
|
}
|
|
|
|
|
|
async def db_products_counts(
|
|
session,
|
|
top_slug: str | None,
|
|
sub_slug: str | None,
|
|
search: Optional[str] = None,
|
|
user_id: int=0,
|
|
market_id: int | None = None,
|
|
) -> Dict:
|
|
BLOCKED_SLUGS = set((config().get("blacklist", {}).get("product", []) or []))
|
|
base_conditions = []
|
|
|
|
if top_slug:
|
|
q_list_conditions = [
|
|
Listing.deleted_at.is_(None),
|
|
Listing.top.has(slug=top_slug),
|
|
Listing.sub.has(slug=sub_slug) if sub_slug else Listing.sub_id.is_(None),
|
|
]
|
|
if market_id is not None:
|
|
q_list_conditions.append(Listing.top.has(market_id=market_id))
|
|
q_list = select(Listing.id).where(*q_list_conditions)
|
|
listing_id = (await session.execute(q_list)).scalars().first()
|
|
if not listing_id:
|
|
return {
|
|
"brands": [],
|
|
"stickers": [],
|
|
"labels": [],
|
|
"liked_count": 0,
|
|
"search_count": 0,
|
|
}
|
|
|
|
listing_slug_subquery = select(ListingItem.slug).where(ListingItem.listing_id == listing_id, ListingItem.deleted_at.is_(None))
|
|
|
|
if BLOCKED_SLUGS:
|
|
base_conditions.append(
|
|
and_(
|
|
Product.slug.in_(listing_slug_subquery),
|
|
~Product.slug.in_(BLOCKED_SLUGS),
|
|
)
|
|
)
|
|
else:
|
|
base_conditions.append(Product.slug.in_(listing_slug_subquery))
|
|
else:
|
|
if market_id is not None:
|
|
# Browse all within a specific market
|
|
market_product_slugs = (
|
|
select(ListingItem.slug)
|
|
.join(Listing, ListingItem.listing_id == Listing.id)
|
|
.join(NavTop, Listing.top_id == NavTop.id)
|
|
.where(
|
|
ListingItem.deleted_at.is_(None),
|
|
Listing.deleted_at.is_(None),
|
|
NavTop.deleted_at.is_(None),
|
|
NavTop.market_id == market_id,
|
|
)
|
|
)
|
|
if BLOCKED_SLUGS:
|
|
base_conditions.append(
|
|
and_(
|
|
Product.slug.in_(market_product_slugs),
|
|
~Product.slug.in_(BLOCKED_SLUGS),
|
|
)
|
|
)
|
|
else:
|
|
base_conditions.append(Product.slug.in_(market_product_slugs))
|
|
elif BLOCKED_SLUGS:
|
|
base_conditions.append(~Product.slug.in_(BLOCKED_SLUGS))
|
|
base_ids = (await session.execute(select(Product.id).where(*base_conditions, Product.deleted_at.is_(None)))).scalars().all()
|
|
if base_ids:
|
|
base_products_slugs = (await session.execute(
|
|
select(Product.slug).where(Product.id.in_(base_ids), Product.deleted_at.is_(None))
|
|
)).scalars().all()
|
|
if not base_products_slugs:
|
|
return {
|
|
"brands": [],
|
|
"stickers": [],
|
|
"labels": [],
|
|
"liked_count": 0,
|
|
"search_count": 0,
|
|
}
|
|
base_ids = (await session.execute(
|
|
select(Product.id).where(Product.slug.in_(base_products_slugs), Product.deleted_at.is_(None))
|
|
)).scalars().all()
|
|
else:
|
|
return {
|
|
"brands": [],
|
|
"stickers": [],
|
|
"labels": [],
|
|
"liked_count": 0,
|
|
"search_count": 0,
|
|
}
|
|
|
|
brands_list: List[Dict] = []
|
|
stickers_list: List[Dict] = []
|
|
labels_list: List[Dict] = []
|
|
liked_count = 0
|
|
search_count = 0
|
|
liked_product_slugs_subq = (
|
|
select(ProductLike.product_slug)
|
|
.where(ProductLike.user_id == user_id, ProductLike.deleted_at.is_(None))
|
|
)
|
|
liked_count = await session.scalar(
|
|
select(func.count(Product.id))
|
|
.where(
|
|
Product.id.in_(base_ids),
|
|
Product.slug.in_(liked_product_slugs_subq),
|
|
Product.deleted_at.is_(None)
|
|
)
|
|
)
|
|
|
|
liked_count = (await session.execute(
|
|
select(func.count())
|
|
.select_from(ProductLike)
|
|
.where(
|
|
ProductLike.user_id == user_id,
|
|
ProductLike.product_slug.in_(
|
|
select(Product.slug).where(Product.id.in_(base_ids))
|
|
),
|
|
ProductLike.deleted_at.is_(None)
|
|
)
|
|
)).scalar_one() if user_id else 0
|
|
|
|
# Brand counts
|
|
brand_count_rows = await session.execute(
|
|
select(Product.brand, func.count(Product.id))
|
|
.where(Product.id.in_(base_ids),
|
|
Product.brand.is_not(None),
|
|
func.trim(Product.brand) != "",
|
|
Product.deleted_at.is_(None)
|
|
)
|
|
.group_by(Product.brand)
|
|
)
|
|
for brand_name, count in brand_count_rows:
|
|
brands_list.append({"name": brand_name, "count": count})
|
|
brands_list.sort(key=lambda x: (-x["count"], x["name"].lower()))
|
|
|
|
# Sticker counts
|
|
sticker_count_rows = await session.execute(
|
|
select(ProductSticker.name, func.count(ProductSticker.product_id))
|
|
.where(
|
|
ProductSticker.product_id.in_(base_ids),
|
|
ProductSticker.deleted_at.is_(None)
|
|
)
|
|
.group_by(ProductSticker.name)
|
|
)
|
|
for sticker_name, count in sticker_count_rows:
|
|
if sticker_name:
|
|
stickers_list.append({"name": sticker_name.strip().lower(), "count": count})
|
|
stickers_list.sort(key=lambda x: (-x["count"], x["name"]))
|
|
|
|
# Label counts
|
|
label_count_rows = await session.execute(
|
|
select(ProductLabel.name, func.count(ProductLabel.product_id))
|
|
.where(
|
|
ProductLabel.product_id.in_(base_ids),
|
|
ProductLabel.deleted_at.is_(None)
|
|
)
|
|
.group_by(ProductLabel.name)
|
|
)
|
|
for label_name, count in label_count_rows:
|
|
if label_name:
|
|
labels_list.append({"name": label_name, "count": count})
|
|
labels_list.sort(key=lambda x: (-x["count"], x["name"]))
|
|
|
|
|
|
# Search count
|
|
search_q = (search or "").strip().lower()
|
|
if search_q:
|
|
search_count = (await session.execute(
|
|
select(func.count(Product.id))
|
|
.where(
|
|
Product.id.in_(base_ids),
|
|
func.lower(Product.description_short).contains(search_q),
|
|
Product.deleted_at.is_(None)
|
|
)
|
|
)).scalars().one()
|
|
else:
|
|
search_count = len(base_ids)
|
|
|
|
return {
|
|
"brands": brands_list,
|
|
"stickers": stickers_list,
|
|
"labels": labels_list,
|
|
"liked_count": liked_count,
|
|
"search_count": search_count,
|
|
}
|
|
|
|
async def db_products(
|
|
session,
|
|
top_slug: str | None,
|
|
sub_slug: str | None,
|
|
selected_brands: Optional[List[str]] = None,
|
|
selected_stickers: Optional[List[str]] = None,
|
|
selected_labels: Optional[List[str]] = None,
|
|
page: int = 1,
|
|
search: Optional[str] = None,
|
|
sort: Optional[str] = None,
|
|
page_size: int = 20,
|
|
liked: bool = None,
|
|
user_id: int=0,
|
|
market_id: int | None = None,
|
|
) -> Dict:
|
|
return {
|
|
**(await db_products_nocounts(
|
|
session,
|
|
top_slug=top_slug,
|
|
sub_slug=sub_slug,
|
|
selected_brands=selected_brands,
|
|
selected_stickers=selected_stickers,
|
|
selected_labels=selected_labels,
|
|
page=page,
|
|
search=search,
|
|
sort=sort,
|
|
page_size=page_size,
|
|
liked=liked,
|
|
user_id=user_id,
|
|
market_id=market_id,
|
|
)),
|
|
**(await db_products_counts(
|
|
session,
|
|
top_slug=top_slug,
|
|
sub_slug=sub_slug,
|
|
search=search,
|
|
user_id=user_id,
|
|
market_id=market_id,
|
|
)),
|
|
}
|
|
|
|
|