feat: restructure market app with per-market URL scoping
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 41s
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 41s
- URL structure changes from /<route> to /<market_slug>/<route> - Root / shows markets listing page - app.py: url_value_preprocessor, url_defaults, hydrate_market (events app pattern) - Browse queries (db_nav, db_products_nocounts, db_products_counts) accept market_id - _productInfo reads g.market.id to scope all queries - save_nav accepts market_id, sets on new NavTop rows - API save_nav passes g.market.id - Scraper default URLs point to /suma-market/ on port 8001 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
90
app.py
90
app.py
@@ -1,7 +1,11 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
import path_setup # noqa: F401 # adds shared_lib to sys.path
|
import path_setup # noqa: F401 # adds shared_lib to sys.path
|
||||||
|
|
||||||
from quart import g
|
from pathlib import Path
|
||||||
|
|
||||||
|
from quart import g, abort, render_template, make_response
|
||||||
|
from jinja2 import FileSystemLoader, ChoiceLoader
|
||||||
|
from sqlalchemy import select
|
||||||
|
|
||||||
from shared.factory import create_base_app
|
from shared.factory import create_base_app
|
||||||
from config import config
|
from config import config
|
||||||
@@ -38,17 +42,95 @@ async def market_context() -> dict:
|
|||||||
|
|
||||||
|
|
||||||
def create_app() -> "Quart":
|
def create_app() -> "Quart":
|
||||||
|
from models.market_place import MarketPlace
|
||||||
|
from models.ghost_content import Post
|
||||||
|
|
||||||
app = create_base_app("market", context_fn=market_context)
|
app = create_base_app("market", context_fn=market_context)
|
||||||
|
|
||||||
# Market blueprint at root (was /market in monolith)
|
# App-specific templates override shared templates
|
||||||
|
app_templates = str(Path(__file__).resolve().parent / "templates")
|
||||||
|
app.jinja_loader = ChoiceLoader([
|
||||||
|
FileSystemLoader(app_templates),
|
||||||
|
app.jinja_loader,
|
||||||
|
])
|
||||||
|
|
||||||
|
# Market blueprint scoped under /<market_slug>/
|
||||||
app.register_blueprint(
|
app.register_blueprint(
|
||||||
register_market_bp(
|
register_market_bp(
|
||||||
url_prefix="/",
|
url_prefix="/<market_slug>",
|
||||||
title=config()["coop_title"],
|
title=config()["coop_title"],
|
||||||
),
|
),
|
||||||
url_prefix="/",
|
url_prefix="/<market_slug>",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# --- Auto-inject market_slug into url_for() calls ---
|
||||||
|
@app.url_value_preprocessor
|
||||||
|
def pull_market_slug(endpoint, values):
|
||||||
|
if values and "market_slug" in values:
|
||||||
|
g.market_slug = values.pop("market_slug")
|
||||||
|
|
||||||
|
@app.url_defaults
|
||||||
|
def inject_market_slug(endpoint, values):
|
||||||
|
slug = g.get("market_slug")
|
||||||
|
if slug and "market_slug" not in values:
|
||||||
|
if app.url_map.is_endpoint_expecting(endpoint, "market_slug"):
|
||||||
|
values["market_slug"] = slug
|
||||||
|
|
||||||
|
# --- Load market data for market_slug ---
|
||||||
|
@app.before_request
|
||||||
|
async def hydrate_market():
|
||||||
|
slug = getattr(g, "market_slug", None)
|
||||||
|
if not slug:
|
||||||
|
return
|
||||||
|
market = (
|
||||||
|
await g.s.execute(
|
||||||
|
select(MarketPlace).where(
|
||||||
|
MarketPlace.slug == slug,
|
||||||
|
MarketPlace.deleted_at.is_(None),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
).scalar_one_or_none()
|
||||||
|
if not market:
|
||||||
|
abort(404)
|
||||||
|
g.market = market
|
||||||
|
|
||||||
|
# Load associated Post for context
|
||||||
|
post = (
|
||||||
|
await g.s.execute(
|
||||||
|
select(Post).where(Post.id == market.post_id)
|
||||||
|
)
|
||||||
|
).scalar_one_or_none()
|
||||||
|
if post:
|
||||||
|
g.post_data = {
|
||||||
|
"post": {
|
||||||
|
"id": post.id,
|
||||||
|
"title": post.title,
|
||||||
|
"slug": post.slug,
|
||||||
|
"feature_image": post.feature_image,
|
||||||
|
"html": post.html,
|
||||||
|
"status": post.status,
|
||||||
|
"visibility": post.visibility,
|
||||||
|
"is_page": post.is_page,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- Root route: market listing ---
|
||||||
|
@app.get("/")
|
||||||
|
async def markets_listing():
|
||||||
|
markets = (
|
||||||
|
await g.s.execute(
|
||||||
|
select(MarketPlace)
|
||||||
|
.where(MarketPlace.deleted_at.is_(None))
|
||||||
|
.order_by(MarketPlace.name)
|
||||||
|
)
|
||||||
|
).scalars().all()
|
||||||
|
|
||||||
|
html = await render_template(
|
||||||
|
"_types/market/markets_listing.html",
|
||||||
|
markets=markets,
|
||||||
|
)
|
||||||
|
return await make_response(html)
|
||||||
|
|
||||||
return app
|
return app
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -338,7 +338,9 @@ async def rediects():
|
|||||||
@clear_cache(tag='browse')
|
@clear_cache(tag='browse')
|
||||||
async def save_nav():
|
async def save_nav():
|
||||||
data: Dict[str, Any] = await request.get_json(force=True, silent=False)
|
data: Dict[str, Any] = await request.get_json(force=True, silent=False)
|
||||||
await _save_nav(g.s, data)
|
market = getattr(g, "market", None)
|
||||||
|
market_id = market.id if market else None
|
||||||
|
await _save_nav(g.s, data, market_id=market_id)
|
||||||
return {"ok": True}
|
return {"ok": True}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -37,14 +37,9 @@ def register():
|
|||||||
async def home():
|
async def home():
|
||||||
"""
|
"""
|
||||||
Market landing page.
|
Market landing page.
|
||||||
Shows the Ghost CMS post with slug='market'.
|
Uses the post data hydrated by the app-level before_request (g.post_data).
|
||||||
"""
|
"""
|
||||||
from shared.internal_api import get as api_get
|
p_data = getattr(g, "post_data", None) or {}
|
||||||
|
|
||||||
# Fetch the market post from coop internal API
|
|
||||||
p_data = await api_get("coop", "/internal/post/market")
|
|
||||||
if not p_data:
|
|
||||||
abort(404)
|
|
||||||
|
|
||||||
# Determine which template to use based on request type
|
# Determine which template to use based on request type
|
||||||
if not is_htmx_request():
|
if not is_htmx_request():
|
||||||
@@ -63,7 +58,9 @@ def register():
|
|||||||
Browse all products across all categories.
|
Browse all products across all categories.
|
||||||
Renders full page or just product cards (HTMX pagination fragment).
|
Renders full page or just product cards (HTMX pagination fragment).
|
||||||
"""
|
"""
|
||||||
nav = await get_nav(g.s)
|
market = getattr(g, "market", None)
|
||||||
|
market_id = market.id if market else None
|
||||||
|
nav = await get_nav(g.s, market_id=market_id)
|
||||||
ctx = {
|
ctx = {
|
||||||
"category_label": "All Products",
|
"category_label": "All Products",
|
||||||
"top_slug": "all",
|
"top_slug": "all",
|
||||||
@@ -102,7 +99,9 @@ def register():
|
|||||||
if is_category_blocked(top_slug):
|
if is_category_blocked(top_slug):
|
||||||
abort(404)
|
abort(404)
|
||||||
|
|
||||||
nav = await get_nav(g.s)
|
market = getattr(g, "market", None)
|
||||||
|
market_id = market.id if market else None
|
||||||
|
nav = await get_nav(g.s, market_id=market_id)
|
||||||
ctx = category_context(top_slug, None, nav)
|
ctx = category_context(top_slug, None, nav)
|
||||||
|
|
||||||
product_info = await _productInfo(top_slug)
|
product_info = await _productInfo(top_slug)
|
||||||
@@ -136,7 +135,9 @@ def register():
|
|||||||
if is_category_blocked(top_slug, sub_slug):
|
if is_category_blocked(top_slug, sub_slug):
|
||||||
abort(404)
|
abort(404)
|
||||||
|
|
||||||
nav = await get_nav(g.s)
|
market = getattr(g, "market", None)
|
||||||
|
market_id = market.id if market else None
|
||||||
|
nav = await get_nav(g.s, market_id=market_id)
|
||||||
ctx = category_context(top_slug, sub_slug, nav)
|
ctx = category_context(top_slug, sub_slug, nav)
|
||||||
|
|
||||||
product_info = await _productInfo(top_slug, sub_slug)
|
product_info = await _productInfo(top_slug, sub_slug)
|
||||||
|
|||||||
@@ -34,9 +34,19 @@ def _regular_price_of(p: Product) -> Optional[float]:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
# ---------- NAV ----------
|
# ---------- NAV ----------
|
||||||
async def db_nav(session) -> Dict:
|
async def db_nav(session, market_id=None) -> Dict:
|
||||||
tops = (await session.execute(select(NavTop))).scalars().all()
|
top_q = select(NavTop).where(NavTop.deleted_at.is_(None))
|
||||||
subs = (await session.execute(select(NavSub))).scalars().all()
|
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]] = {}
|
subs_by_top: Dict[int, List[Dict]] = {}
|
||||||
for s in subs:
|
for s in subs:
|
||||||
@@ -274,7 +284,8 @@ async def db_products_nocounts(
|
|||||||
sort: Optional[str] = None,
|
sort: Optional[str] = None,
|
||||||
page_size: int = 20,
|
page_size: int = 20,
|
||||||
liked: bool = None,
|
liked: bool = None,
|
||||||
user_id: int=0
|
user_id: int=0,
|
||||||
|
market_id: int | None = None,
|
||||||
) -> Dict:
|
) -> Dict:
|
||||||
BLOCKED_SLUGS = set((config().get("blacklist", {}).get("product", []) or []))
|
BLOCKED_SLUGS = set((config().get("blacklist", {}).get("product", []) or []))
|
||||||
base_conditions = []
|
base_conditions = []
|
||||||
@@ -284,18 +295,21 @@ async def db_products_nocounts(
|
|||||||
)
|
)
|
||||||
|
|
||||||
if top_slug:
|
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 = (
|
q_list = (
|
||||||
select(Listing.id)
|
select(Listing.id)
|
||||||
.join(NavTop, Listing.top)
|
.join(NavTop, Listing.top)
|
||||||
.outerjoin(NavSub, Listing.sub)
|
.outerjoin(NavSub, Listing.sub)
|
||||||
.where(
|
.where(*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),
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
listing_id = (await session.execute(q_list)).scalars().first()
|
listing_id = (await session.execute(q_list)).scalars().first()
|
||||||
@@ -305,6 +319,20 @@ async def db_products_nocounts(
|
|||||||
base_conditions.append(Product.slug.in_(
|
base_conditions.append(Product.slug.in_(
|
||||||
select(ListingItem.slug).where(ListingItem.listing_id == listing_id, ListingItem.deleted_at.is_(None))
|
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_subq = select(Product.id).where(*base_conditions, Product.deleted_at.is_(None))
|
||||||
base_ids = (await session.execute(base_ids_subq)).scalars().all()
|
base_ids = (await session.execute(base_ids_subq)).scalars().all()
|
||||||
@@ -461,17 +489,21 @@ async def db_products_counts(
|
|||||||
top_slug: str | None,
|
top_slug: str | None,
|
||||||
sub_slug: str | None,
|
sub_slug: str | None,
|
||||||
search: Optional[str] = None,
|
search: Optional[str] = None,
|
||||||
user_id: int=0
|
user_id: int=0,
|
||||||
|
market_id: int | None = None,
|
||||||
) -> Dict:
|
) -> Dict:
|
||||||
BLOCKED_SLUGS = set((config().get("blacklist", {}).get("product", []) or []))
|
BLOCKED_SLUGS = set((config().get("blacklist", {}).get("product", []) or []))
|
||||||
base_conditions = []
|
base_conditions = []
|
||||||
|
|
||||||
if top_slug:
|
if top_slug:
|
||||||
q_list = select(Listing.id).where(
|
q_list_conditions = [
|
||||||
Listing.deleted_at.is_(None),
|
Listing.deleted_at.is_(None),
|
||||||
Listing.top.has(slug=top_slug),
|
Listing.top.has(slug=top_slug),
|
||||||
Listing.sub.has(slug=sub_slug) if sub_slug else Listing.sub_id.is_(None),
|
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()
|
listing_id = (await session.execute(q_list)).scalars().first()
|
||||||
if not listing_id:
|
if not listing_id:
|
||||||
return {
|
return {
|
||||||
@@ -494,7 +526,29 @@ async def db_products_counts(
|
|||||||
else:
|
else:
|
||||||
base_conditions.append(Product.slug.in_(listing_slug_subquery))
|
base_conditions.append(Product.slug.in_(listing_slug_subquery))
|
||||||
else:
|
else:
|
||||||
if BLOCKED_SLUGS:
|
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_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()
|
base_ids = (await session.execute(select(Product.id).where(*base_conditions, Product.deleted_at.is_(None)))).scalars().all()
|
||||||
if base_ids:
|
if base_ids:
|
||||||
@@ -628,7 +682,8 @@ async def db_products(
|
|||||||
sort: Optional[str] = None,
|
sort: Optional[str] = None,
|
||||||
page_size: int = 20,
|
page_size: int = 20,
|
||||||
liked: bool = None,
|
liked: bool = None,
|
||||||
user_id: int=0
|
user_id: int=0,
|
||||||
|
market_id: int | None = None,
|
||||||
) -> Dict:
|
) -> Dict:
|
||||||
return {
|
return {
|
||||||
**(await db_products_nocounts(
|
**(await db_products_nocounts(
|
||||||
@@ -643,14 +698,16 @@ async def db_products(
|
|||||||
sort=sort,
|
sort=sort,
|
||||||
page_size=page_size,
|
page_size=page_size,
|
||||||
liked=liked,
|
liked=liked,
|
||||||
user_id=user_id
|
user_id=user_id,
|
||||||
|
market_id=market_id,
|
||||||
)),
|
)),
|
||||||
**(await db_products_counts(
|
**(await db_products_counts(
|
||||||
session,
|
session,
|
||||||
top_slug=top_slug,
|
top_slug=top_slug,
|
||||||
sub_slug=sub_slug,
|
sub_slug=sub_slug,
|
||||||
search=search,
|
search=search,
|
||||||
user_id=user_id
|
user_id=user_id,
|
||||||
|
market_id=market_id,
|
||||||
)),
|
)),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ def group_by_category(slug_to_links: Dict[str, List[Tuple[str, str]]]) -> Dict[s
|
|||||||
return nav
|
return nav
|
||||||
|
|
||||||
|
|
||||||
async def get_nav(session) -> Dict[str, Dict]:
|
async def get_nav(session, market_id=None) -> Dict[str, Dict]:
|
||||||
"""
|
"""
|
||||||
Return navigation structure; annotate each sub with product counts.
|
Return navigation structure; annotate each sub with product counts.
|
||||||
Uses snapshot for offline behaviour.
|
Uses snapshot for offline behaviour.
|
||||||
@@ -62,7 +62,7 @@ async def get_nav(session) -> Dict[str, Dict]:
|
|||||||
now_ts = _now()
|
now_ts = _now()
|
||||||
|
|
||||||
# load from snapshot
|
# load from snapshot
|
||||||
nav = await cb.db_nav(session)
|
nav = await cb.db_nav(session, market_id=market_id)
|
||||||
|
|
||||||
# inject counts for each subcategory (and for top-level too if you like)
|
# inject counts for each subcategory (and for top-level too if you like)
|
||||||
for label, cat in (nav.get("cats") or {}).items():
|
for label, cat in (nav.get("cats") or {}).items():
|
||||||
|
|||||||
@@ -19,9 +19,8 @@ async def products(
|
|||||||
search: Optional[str] = None,
|
search: Optional[str] = None,
|
||||||
sort: Optional[str] = None,
|
sort: Optional[str] = None,
|
||||||
liked: Optional[bool] = None,
|
liked: Optional[bool] = None,
|
||||||
|
|
||||||
# NEW:
|
|
||||||
user_id: Optional[int] = None,
|
user_id: Optional[int] = None,
|
||||||
|
market_id: int | None = None,
|
||||||
):
|
):
|
||||||
p = urlparse(list_url)
|
p = urlparse(list_url)
|
||||||
parts = [x for x in (p.path or "").split("/") if x]
|
parts = [x for x in (p.path or "").split("/") if x]
|
||||||
@@ -41,7 +40,8 @@ async def products(
|
|||||||
search,
|
search,
|
||||||
sort,
|
sort,
|
||||||
liked=liked,
|
liked=liked,
|
||||||
user_id = g.user.id if g.user else 0
|
user_id = g.user.id if g.user else 0,
|
||||||
|
market_id=market_id,
|
||||||
)
|
)
|
||||||
items = data.get("items", []) or []
|
items = data.get("items", []) or []
|
||||||
brands = data.get("brands", []) or []
|
brands = data.get("brands", []) or []
|
||||||
@@ -76,9 +76,8 @@ async def products_nocounts(
|
|||||||
search: Optional[str] = None,
|
search: Optional[str] = None,
|
||||||
sort: Optional[str] = None,
|
sort: Optional[str] = None,
|
||||||
liked: Optional[bool] = None,
|
liked: Optional[bool] = None,
|
||||||
|
|
||||||
# NEW:
|
|
||||||
user_id: Optional[int] = None,
|
user_id: Optional[int] = None,
|
||||||
|
market_id: int | None = None,
|
||||||
):
|
):
|
||||||
p = urlparse(list_url)
|
p = urlparse(list_url)
|
||||||
parts = [x for x in (p.path or "").split("/") if x]
|
parts = [x for x in (p.path or "").split("/") if x]
|
||||||
@@ -99,6 +98,7 @@ async def products_nocounts(
|
|||||||
sort,
|
sort,
|
||||||
liked=liked,
|
liked=liked,
|
||||||
user_id = g.user.id if g.user else 0,
|
user_id = g.user.id if g.user else 0,
|
||||||
|
market_id=market_id,
|
||||||
)
|
)
|
||||||
items = data.get("items", []) or []
|
items = data.get("items", []) or []
|
||||||
total_pages = int(data.get("total_pages", 1) or 1)
|
total_pages = int(data.get("total_pages", 1) or 1)
|
||||||
|
|||||||
@@ -26,12 +26,16 @@ async def _productInfo(top_slug=None, sub_slug=None):
|
|||||||
Shared query logic for home / category / subcategory pages.
|
Shared query logic for home / category / subcategory pages.
|
||||||
Pulls filters from qs.decode(), queries products(), and orders brands/stickers/etc.
|
Pulls filters from qs.decode(), queries products(), and orders brands/stickers/etc.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
q = decode()
|
q = decode()
|
||||||
page, search, sort = q.page, q.search, q.sort
|
page, search, sort = q.page, q.search, q.sort
|
||||||
selected_brands, selected_stickers, selected_labels = q.selected_brands, q.selected_stickers, q.selected_labels
|
selected_brands, selected_stickers, selected_labels = q.selected_brands, q.selected_stickers, q.selected_labels
|
||||||
liked = q.liked
|
liked = q.liked
|
||||||
|
|
||||||
|
# Get market_id from hydrated market context
|
||||||
|
market = getattr(g, "market", None)
|
||||||
|
market_id = market.id if market else None
|
||||||
|
|
||||||
if top_slug is not None and sub_slug is not None:
|
if top_slug is not None and sub_slug is not None:
|
||||||
list_url = urljoin(config()["base_url"], f"/{top_slug}/{sub_slug}")
|
list_url = urljoin(config()["base_url"], f"/{top_slug}/{sub_slug}")
|
||||||
else:
|
else:
|
||||||
@@ -50,6 +54,7 @@ async def _productInfo(top_slug=None, sub_slug=None):
|
|||||||
sort=sort,
|
sort=sort,
|
||||||
user_id=g.user.id if g.user else None,
|
user_id=g.user.id if g.user else None,
|
||||||
liked = liked,
|
liked = liked,
|
||||||
|
market_id=market_id,
|
||||||
)
|
)
|
||||||
|
|
||||||
brands_ordered = _order_brands_selected_first(brands, selected_brands)
|
brands_ordered = _order_brands_selected_first(brands, selected_brands)
|
||||||
@@ -82,6 +87,7 @@ async def _productInfo(top_slug=None, sub_slug=None):
|
|||||||
sort=sort,
|
sort=sort,
|
||||||
user_id=g.user.id if g.user else None,
|
user_id=g.user.id if g.user else None,
|
||||||
liked = liked,
|
liked = liked,
|
||||||
|
market_id=market_id,
|
||||||
)
|
)
|
||||||
return {
|
return {
|
||||||
"products": items,
|
"products": items,
|
||||||
|
|||||||
@@ -22,10 +22,13 @@ def register(url_prefix, title):
|
|||||||
|
|
||||||
@bp.context_processor
|
@bp.context_processor
|
||||||
async def inject_root():
|
async def inject_root():
|
||||||
|
market = getattr(g, "market", None)
|
||||||
|
market_id = market.id if market else None
|
||||||
return {
|
return {
|
||||||
"coop_title": title,
|
"coop_title": title,
|
||||||
"categories": (await get_nav(g.s))["cats"],
|
"categories": (await get_nav(g.s, market_id=market_id))["cats"],
|
||||||
"qs": makeqs_factory()(),
|
"qs": makeqs_factory()(),
|
||||||
|
"market": market,
|
||||||
}
|
}
|
||||||
|
|
||||||
bp.register_blueprint(
|
bp.register_blueprint(
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ async def capture_listing(
|
|||||||
total_pages: int
|
total_pages: int
|
||||||
):
|
):
|
||||||
|
|
||||||
sync_url = os.getenv("CAPTURE_LISTING_URL", "http://localhost:8000/market/api/products/listing/")
|
sync_url = os.getenv("CAPTURE_LISTING_URL", "http://localhost:8001/suma-market/api/products/listing/")
|
||||||
|
|
||||||
async with httpx.AsyncClient(timeout=httpx.Timeout(20.0, connect=10.0)) as client:
|
async with httpx.AsyncClient(timeout=httpx.Timeout(20.0, connect=10.0)) as client:
|
||||||
_d = {
|
_d = {
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ from typing import Dict
|
|||||||
async def save_nav(
|
async def save_nav(
|
||||||
nav: Dict,
|
nav: Dict,
|
||||||
):
|
):
|
||||||
sync_url = os.getenv("SAVE_NAV_URL", "http://localhost:8000/market/api/products/nav/")
|
sync_url = os.getenv("SAVE_NAV_URL", "http://localhost:8001/suma-market/api/products/nav/")
|
||||||
|
|
||||||
async with httpx.AsyncClient(timeout=httpx.Timeout(20.0, connect=10.0)) as client:
|
async with httpx.AsyncClient(timeout=httpx.Timeout(20.0, connect=10.0)) as client:
|
||||||
resp = await client.post(sync_url, json=nav)
|
resp = await client.post(sync_url, json=nav)
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ async def upsert_product(
|
|||||||
d["slug"] = slug
|
d["slug"] = slug
|
||||||
|
|
||||||
# Where to post; override via env if needed
|
# Where to post; override via env if needed
|
||||||
sync_url = os.getenv("PRODUCT_SYNC_URL", "http://localhost:8000/market/api/products/sync/")
|
sync_url = os.getenv("PRODUCT_SYNC_URL", "http://localhost:8001/suma-market/api/products/sync/")
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ async def save_nav(nav: Dict) -> None:
|
|||||||
await _save_nav(session, nav)
|
await _save_nav(session, nav)
|
||||||
await session.commit()
|
await session.commit()
|
||||||
|
|
||||||
async def _save_nav(session, nav: Dict) -> None:
|
async def _save_nav(session, nav: Dict, market_id=None) -> None:
|
||||||
print('===================SAVE NAV========================')
|
print('===================SAVE NAV========================')
|
||||||
print(nav)
|
print(nav)
|
||||||
now = datetime.utcnow()
|
now = datetime.utcnow()
|
||||||
@@ -79,8 +79,10 @@ async def _save_nav(session, nav: Dict) -> None:
|
|||||||
if top:
|
if top:
|
||||||
top.label = label
|
top.label = label
|
||||||
top.deleted_at = None
|
top.deleted_at = None
|
||||||
|
if market_id is not None and top.market_id is None:
|
||||||
|
top.market_id = market_id
|
||||||
else:
|
else:
|
||||||
top = NavTop(label=label, slug=top_slug)
|
top = NavTop(label=label, slug=top_slug, market_id=market_id)
|
||||||
session.add(top)
|
session.add(top)
|
||||||
|
|
||||||
await session.flush()
|
await session.flush()
|
||||||
|
|||||||
23
templates/_types/market/markets_listing.html
Normal file
23
templates/_types/market/markets_listing.html
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
{% extends '_types/root/_index.html' %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="max-w-3xl mx-auto py-8 px-4">
|
||||||
|
<h1 class="text-2xl font-bold mb-6">Markets</h1>
|
||||||
|
|
||||||
|
{% if markets %}
|
||||||
|
<div class="grid gap-4">
|
||||||
|
{% for m in markets %}
|
||||||
|
<a href="/{{ m.slug }}/"
|
||||||
|
class="block p-6 bg-white border border-stone-200 rounded-lg hover:border-stone-400 transition-colors">
|
||||||
|
<h2 class="text-lg font-semibold">{{ m.name }}</h2>
|
||||||
|
{% if m.description %}
|
||||||
|
<p class="text-stone-600 mt-1">{{ m.description }}</p>
|
||||||
|
{% endif %}
|
||||||
|
</a>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<p class="text-stone-500">No markets available.</p>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
Reference in New Issue
Block a user