feat: initialize cart app with blueprints, templates, and CI
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
Extract cart, order, and orders blueprints with their service layer, templates, Dockerfile (APP_MODULE=app:app, IMAGE=cart), entrypoint, and Gitea CI workflow from the coop monolith. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
139
bp/orders/routes.py
Normal file
139
bp/orders/routes.py
Normal file
@@ -0,0 +1,139 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from quart import Blueprint, g, render_template, redirect, url_for, make_response
|
||||
from sqlalchemy import select, func, or_, cast, String, exists
|
||||
from sqlalchemy.orm import selectinload
|
||||
|
||||
|
||||
from models.market import Product
|
||||
from models.order import Order, OrderItem
|
||||
from suma_browser.app.payments.sumup import create_checkout as sumup_create_checkout
|
||||
from config import config
|
||||
|
||||
from shared.http_utils import vary as _vary, current_url_without_page as _current_url_without_page
|
||||
from suma_browser.app.bp.cart.services import check_sumup_status
|
||||
from suma_browser.app.utils.htmx import is_htmx_request
|
||||
from suma_browser.app.bp import register_order
|
||||
|
||||
from .filters.qs import makeqs_factory, decode
|
||||
|
||||
|
||||
def register(url_prefix: str) -> Blueprint:
|
||||
bp = Blueprint("orders", __name__, url_prefix=url_prefix)
|
||||
bp.register_blueprint(
|
||||
register_order(),
|
||||
)
|
||||
ORDERS_PER_PAGE = 10 # keep in sync with browse page size / your preference
|
||||
|
||||
@bp.before_request
|
||||
def route():
|
||||
# this is the crucial bit for the |qs filter
|
||||
g.makeqs_factory = makeqs_factory
|
||||
|
||||
@bp.get("/")
|
||||
async def list_orders():
|
||||
|
||||
# --- decode filters from query string (page + search) ---
|
||||
q = decode()
|
||||
page, search = q.page, q.search
|
||||
|
||||
# sanity clamp page
|
||||
if page < 1:
|
||||
page = 1
|
||||
|
||||
# --- build where clause for search ---
|
||||
where_clause = None
|
||||
if search:
|
||||
term = f"%{search.strip()}%"
|
||||
conditions = [
|
||||
Order.status.ilike(term),
|
||||
Order.currency.ilike(term),
|
||||
Order.sumup_checkout_id.ilike(term),
|
||||
Order.sumup_status.ilike(term),
|
||||
Order.description.ilike(term),
|
||||
]
|
||||
|
||||
conditions.append(
|
||||
exists(
|
||||
select(1)
|
||||
.select_from(OrderItem)
|
||||
.join(Product, Product.id == OrderItem.product_id)
|
||||
.where(
|
||||
OrderItem.order_id == Order.id,
|
||||
or_(
|
||||
OrderItem.product_title.ilike(term),
|
||||
Product.title.ilike(term),
|
||||
Product.description_short.ilike(term),
|
||||
Product.description_html.ilike(term),
|
||||
Product.slug.ilike(term),
|
||||
Product.brand.ilike(term),
|
||||
),
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
# allow exact ID match or partial (string) match
|
||||
try:
|
||||
search_id = int(search)
|
||||
except (TypeError, ValueError):
|
||||
search_id = None
|
||||
|
||||
if search_id is not None:
|
||||
conditions.append(Order.id == search_id)
|
||||
else:
|
||||
conditions.append(cast(Order.id, String).ilike(term))
|
||||
|
||||
where_clause = or_(*conditions)
|
||||
|
||||
# --- total count & total pages (respecting search) ---
|
||||
count_stmt = select(func.count()).select_from(Order)
|
||||
if where_clause is not None:
|
||||
count_stmt = count_stmt.where(where_clause)
|
||||
|
||||
total_count_result = await g.s.execute(count_stmt)
|
||||
total_count = total_count_result.scalar_one() or 0
|
||||
total_pages = max(1, (total_count + ORDERS_PER_PAGE - 1) // ORDERS_PER_PAGE)
|
||||
|
||||
# clamp page if beyond range (just in case)
|
||||
if page > total_pages:
|
||||
page = total_pages
|
||||
|
||||
# --- paginated orders (respecting search) ---
|
||||
offset = (page - 1) * ORDERS_PER_PAGE
|
||||
stmt = (
|
||||
select(Order)
|
||||
.order_by(Order.created_at.desc())
|
||||
.offset(offset)
|
||||
.limit(ORDERS_PER_PAGE)
|
||||
)
|
||||
if where_clause is not None:
|
||||
stmt = stmt.where(where_clause)
|
||||
|
||||
result = await g.s.execute(stmt)
|
||||
orders = result.scalars().all()
|
||||
|
||||
context = {
|
||||
"orders": orders,
|
||||
"page": page,
|
||||
"total_pages": total_pages,
|
||||
"search": search,
|
||||
"search_count": total_count, # For search display
|
||||
}
|
||||
|
||||
# Determine which template to use based on request type and pagination
|
||||
if not is_htmx_request():
|
||||
# Normal browser request: full page with layout
|
||||
html = await render_template("_types/orders/index.html", **context)
|
||||
elif page > 1:
|
||||
# HTMX pagination: just table rows + sentinel
|
||||
html = await render_template("_types/orders/_rows.html", **context)
|
||||
else:
|
||||
# HTMX navigation (page 1): main panel + OOB elements
|
||||
html = await render_template("_types/orders/_oob_elements.html", **context)
|
||||
|
||||
resp = await make_response(html)
|
||||
resp.headers["Hx-Push-Url"] = _current_url_without_page()
|
||||
return _vary(resp)
|
||||
|
||||
return bp
|
||||
|
||||
Reference in New Issue
Block a user