feat: initialize market app with browsing, product, and scraping code
Some checks failed
Build and Deploy / build-and-deploy (push) Has been cancelled
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
This commit is contained in:
3
bp/product/services/__init__.py
Normal file
3
bp/product/services/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from .product_operations import toggle_product_like, massage_full_product
|
||||
|
||||
__all__ = ["toggle_product_like", "massage_full_product"]
|
||||
95
bp/product/services/product_operations.py
Normal file
95
bp/product/services/product_operations.py
Normal file
@@ -0,0 +1,95 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Optional
|
||||
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from models.market import Product, ProductLike
|
||||
|
||||
|
||||
def massage_full_product(product: Product) -> dict:
|
||||
"""
|
||||
Convert a Product ORM model to a dictionary with all fields.
|
||||
Used for rendering product detail pages.
|
||||
"""
|
||||
from suma_browser.app.bp.browse.services import _massage_product
|
||||
|
||||
gallery = []
|
||||
if product.image:
|
||||
gallery.append(product.image)
|
||||
|
||||
d = {
|
||||
"id": product.id,
|
||||
"slug": product.slug,
|
||||
"title": product.title,
|
||||
"brand": product.brand,
|
||||
"image": product.image,
|
||||
"description_short": product.description_short,
|
||||
"description_html": product.description_html or "",
|
||||
"suma_href": product.suma_href,
|
||||
"rrp": float(product.rrp) if product.rrp else None,
|
||||
"special_price": float(product.special_price) if product.special_price else None,
|
||||
"regular_price": float(product.regular_price) if product.regular_price else None,
|
||||
"images": gallery or [img.url for img in product.images],
|
||||
"all_image_urls": gallery or [img.url for img in product.images],
|
||||
"sections": [{"title": s.title, "html": s.html} for s in product.sections],
|
||||
"stickers": [s.name.lower() for s in product.stickers],
|
||||
"labels": [l.name for l in product.labels],
|
||||
"nutrition": [{"key": n.key, "value": n.value, "unit": n.unit} for n in product.nutrition],
|
||||
"allergens": [{"name": a.name, "contains": a.contains} for a in product.allergens],
|
||||
"is_liked": False,
|
||||
}
|
||||
|
||||
return _massage_product(d)
|
||||
|
||||
|
||||
async def toggle_product_like(
|
||||
session: AsyncSession,
|
||||
user_id: int,
|
||||
product_slug: str,
|
||||
) -> tuple[bool, Optional[str]]:
|
||||
"""
|
||||
Toggle a product like for a given user using soft deletes.
|
||||
Returns (liked_state, error_message).
|
||||
- If error_message is not None, an error occurred.
|
||||
- liked_state indicates whether product is now liked (True) or unliked (False).
|
||||
"""
|
||||
from sqlalchemy import func, update
|
||||
|
||||
# Get product_id from slug
|
||||
product_id = await session.scalar(
|
||||
select(Product.id).where(Product.slug == product_slug, Product.deleted_at.is_(None))
|
||||
)
|
||||
if not product_id:
|
||||
return False, "Product not found"
|
||||
|
||||
# Check if like exists (not deleted)
|
||||
existing = await session.scalar(
|
||||
select(ProductLike).where(
|
||||
ProductLike.user_id == user_id,
|
||||
ProductLike.product_slug == product_slug,
|
||||
ProductLike.deleted_at.is_(None),
|
||||
)
|
||||
)
|
||||
|
||||
if existing:
|
||||
# Unlike: soft delete the like
|
||||
await session.execute(
|
||||
update(ProductLike)
|
||||
.where(
|
||||
ProductLike.user_id == user_id,
|
||||
ProductLike.product_slug == product_slug,
|
||||
ProductLike.deleted_at.is_(None),
|
||||
)
|
||||
.values(deleted_at=func.now())
|
||||
)
|
||||
return False, None
|
||||
else:
|
||||
# Like: add a new like
|
||||
new_like = ProductLike(
|
||||
user_id=user_id,
|
||||
product_slug=product_slug,
|
||||
)
|
||||
session.add(new_like)
|
||||
return True, None
|
||||
Reference in New Issue
Block a user