Skip to content

Commit 2436f3a

Browse files
committed
Refactor catalog browsing and product search functionality
- Moved the product serialization logic into a separate function for better code organization and reusability. - Reintroduced the catalog browsing endpoint with improved handling of subcategory lookups and product grouping. - Added a new product search endpoint that allows freeform searches across brand and product names, returning grouped results. - Enhanced product variant details in the response structure to include additional attributes.
1 parent 5d411cb commit 2436f3a

1 file changed

Lines changed: 76 additions & 30 deletions

File tree

app/api/resources.py

Lines changed: 76 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
from fastapi import APIRouter, Depends, HTTPException, Query
1010
from fastapi_sqlalchemy import db
1111
from pydantic import BaseModel
12-
from sqlalchemy import func
12+
from sqlalchemy import func, or_
1313
from sqlalchemy.exc import IntegrityError
1414
from sqlalchemy.orm import joinedload
1515

@@ -212,35 +212,9 @@ def catalog_categories():
212212
)
213213

214214

215-
@route.get("/catalog/browse/{slug}")
216-
def catalog_browse(slug: str):
217-
# Build slug -> subcategory name lookup from live data
218-
distinct = (
219-
db.session.query(CatalogProduct.subcategory)
220-
.filter(
221-
CatalogProduct.status == "approved",
222-
CatalogProduct.subcategory.isnot(None),
223-
)
224-
.distinct()
225-
.all()
226-
)
227-
slug_map = {_slugify(r[0]): r[0] for r in distinct}
228-
subcategory_name = slug_map.get(slug)
229-
if not subcategory_name:
230-
raise HTTPException(404, "Subcategory not found")
231-
232-
entries = (
233-
db.session.query(CatalogProduct)
234-
.filter(
235-
CatalogProduct.status == "approved",
236-
CatalogProduct.subcategory == subcategory_name,
237-
)
238-
.order_by(CatalogProduct.brand_name, CatalogProduct.product_name)
239-
.all()
240-
)
241-
242-
category_name = entries[0].category_suggestion if entries else None
243-
215+
def _serialize_product_groups(entries):
216+
"""Group CatalogProduct rows (sorted by brand, product) into products
217+
with nested variants. Shared by catalog browse and product search."""
244218
products = []
245219
key_fn = attrgetter("brand_name", "product_name")
246220
for (brand, product), group_iter in groupby(entries, key=key_fn):
@@ -261,24 +235,65 @@ def catalog_browse(slug: str):
261235

262236
variants.append({
263237
"id": v.id,
238+
"brand_id": v.brand_id,
239+
"product_id": v.product_id,
240+
"product_variant_id": v.product_variant_id,
264241
"variant_name": v.variant_name,
265242
"display_name": v.display_name,
266243
"weight": float(v.weight) if v.weight is not None else None,
267244
"weight_unit": v.weight_unit,
268245
"image_url": v.image_url,
269246
"description": v.description,
270247
"additional_specs": v.additional_specs,
248+
"kcal": v.kcal,
271249
})
272250

251+
first = variants_raw[0]
273252
products.append({
274253
"brand_name": brand,
275254
"product_name": product,
276255
"product_url": product_url,
277256
"catalog_url_slug": catalog_url_slug,
257+
"category": first.category_suggestion,
258+
"subcategory": first.subcategory,
259+
"subcategory_slug": _slugify(first.subcategory) if first.subcategory else None,
278260
"lightest_weight_g": round(lightest_g, 2) if lightest_g is not None else None,
279261
"variants": variants,
280262
})
281263

264+
return products
265+
266+
267+
@route.get("/catalog/browse/{slug}")
268+
def catalog_browse(slug: str):
269+
# Build slug -> subcategory name lookup from live data
270+
distinct = (
271+
db.session.query(CatalogProduct.subcategory)
272+
.filter(
273+
CatalogProduct.status == "approved",
274+
CatalogProduct.subcategory.isnot(None),
275+
)
276+
.distinct()
277+
.all()
278+
)
279+
slug_map = {_slugify(r[0]): r[0] for r in distinct}
280+
subcategory_name = slug_map.get(slug)
281+
if not subcategory_name:
282+
raise HTTPException(404, "Subcategory not found")
283+
284+
entries = (
285+
db.session.query(CatalogProduct)
286+
.filter(
287+
CatalogProduct.status == "approved",
288+
CatalogProduct.subcategory == subcategory_name,
289+
)
290+
.order_by(CatalogProduct.brand_name, CatalogProduct.product_name)
291+
.all()
292+
)
293+
294+
category_name = entries[0].category_suggestion if entries else None
295+
296+
products = _serialize_product_groups(entries)
282297
products.sort(key=lambda p: (
283298
p["lightest_weight_g"] is None,
284299
p["lightest_weight_g"] or 0,
@@ -293,6 +308,37 @@ def catalog_browse(slug: str):
293308
}
294309

295310

311+
MAX_GEAR_SEARCH_PRODUCTS = 50
312+
313+
314+
@route.get("/catalog/products/search")
315+
def catalog_product_search(q: str = ""):
316+
"""Freeform gear search across brand and product names.
317+
Returns grouped products in the same shape as catalog browse."""
318+
q = q.strip()
319+
if len(q) < 2:
320+
return []
321+
322+
search = f"%{q}%"
323+
entries = (
324+
db.session.query(CatalogProduct)
325+
.filter(
326+
CatalogProduct.status == "approved",
327+
or_(
328+
CatalogProduct.brand_name.ilike(search),
329+
CatalogProduct.product_name.ilike(search),
330+
(CatalogProduct.brand_name + " " +
331+
CatalogProduct.product_name).ilike(search),
332+
),
333+
)
334+
.order_by(CatalogProduct.brand_name, CatalogProduct.product_name)
335+
.limit(500)
336+
.all()
337+
)
338+
339+
return _serialize_product_groups(entries)[:MAX_GEAR_SEARCH_PRODUCTS]
340+
341+
296342
@route.get("/brand/search/{query}")
297343
def search_brands(query: str, user: User = Depends(authenticate)):
298344
search = "%{}%".format(query.strip())

0 commit comments

Comments
 (0)