From a451d2e532a1034b3deb1fce43884906d2b5d27f Mon Sep 17 00:00:00 2001 From: Niklas Meinzer Date: Sat, 17 Jan 2026 12:29:47 +0100 Subject: [PATCH] Add product admin --- src/allmende_payment_system/api/admin.py | 115 +++++++++++++++++- src/allmende_payment_system/api/types.py | 20 +++ src/allmende_payment_system/models.py | 10 +- .../templates/base.html.jinja | 7 ++ .../templates/product_edit.html.jinja | 57 +++++++++ .../templates/products.html.jinja | 43 +++++++ 6 files changed, 242 insertions(+), 10 deletions(-) create mode 100644 src/allmende_payment_system/api/types.py create mode 100644 src/allmende_payment_system/templates/product_edit.html.jinja create mode 100644 src/allmende_payment_system/templates/products.html.jinja diff --git a/src/allmende_payment_system/api/admin.py b/src/allmende_payment_system/api/admin.py index eefb380..b25244f 100644 --- a/src/allmende_payment_system/api/admin.py +++ b/src/allmende_payment_system/api/admin.py @@ -1,12 +1,14 @@ from decimal import Decimal +from typing import Annotated -from fastapi import APIRouter, HTTPException, Request +from fastapi import APIRouter, File, Form, HTTPException, Request from sqlalchemy import select from starlette import status from starlette.responses import RedirectResponse +from allmende_payment_system.api import types from allmende_payment_system.api.dependencies import SessionDep, UserDep -from allmende_payment_system.models import Permission, User, UserGroup +from allmende_payment_system.models import Area, Permission, Product, User, UserGroup from allmende_payment_system.tools import get_jinja_renderer admin_router = APIRouter(prefix="/admin") @@ -156,3 +158,112 @@ async def delete_group( ).scalar_one() session.delete(group) return RedirectResponse(url="/admin/groups", status_code=status.HTTP_303_SEE_OTHER) + + +# PRODUCTS + + +@admin_router.get("/products") +async def get_products( + request: Request, + session: SessionDep, + user: UserDep, +): + products = session.scalars( + select(Product).order_by(Product.area_id, Product.name) + ).all() + + templates = get_jinja_renderer() + + return templates.TemplateResponse( + "products.html.jinja", + context={"request": request, "products": products}, + ) + + +@admin_router.get("/products/edit/{product_id}") +async def edit_product_get( + request: Request, session: SessionDep, user: UserDep, product_id: int +): + product = session.execute(select(Product).where(Product.id == product_id)).scalar() + + areas = session.scalars(select(Area)).all() + + templates = get_jinja_renderer() + + return templates.TemplateResponse( + "product_edit.html.jinja", + context={ + "request": request, + "product": product, + "edit_mode": True, + "areas": areas, + }, + ) + + +@admin_router.post("/products/edit/{product_id}") +async def edit_product_post( + request: Request, + session: SessionDep, + user: UserDep, + product_id: int, + product_data: Annotated[types.Product, Form()], +): + if not user.has_permission("product", "edit"): + raise HTTPException(status_code=403, detail="Insufficient permissions") + + product = session.execute( + select(Product).where(Product.id == product_id) + ).scalar_one() + + for field_name, data in product_data.model_dump().items(): + setattr(product, field_name, data) + + session.flush() + + return RedirectResponse( + url="/admin/products", status_code=status.HTTP_303_SEE_OTHER + ) + + +@admin_router.get("/products/new") +async def new_product_get( + request: Request, + session: SessionDep, + user: UserDep, +): + if not user.has_permission("product", "edit"): + raise HTTPException(status_code=403, detail="Insufficient permissions") + + areas = session.scalars(select(Area)).all() + + templates = get_jinja_renderer() + + return templates.TemplateResponse( + "product_edit.html.jinja", + context={"request": request, "edit_mode": False, "areas": areas}, + ) + + +@admin_router.post("/products/new") +async def new_product_post( + request: Request, + session: SessionDep, + user: UserDep, + product_data: Annotated[types.Product, Form()], + # product_image: Annotated[bytes, File()] +): + if not user.has_permission("product", "edit"): + raise HTTPException(status_code=403, detail="Insufficient permissions") + # print(len(product_image)) + product = Product() + + for field_name, data in product_data.model_dump().items(): + setattr(product, field_name, data) + + session.add(product) + + return RedirectResponse( + url="/admin/products", status_code=status.HTTP_303_SEE_OTHER + ) diff --git a/src/allmende_payment_system/api/types.py b/src/allmende_payment_system/api/types.py new file mode 100644 index 0000000..17070ed --- /dev/null +++ b/src/allmende_payment_system/api/types.py @@ -0,0 +1,20 @@ +import typing +from decimal import Decimal + +from pydantic import BaseModel + +UnitsOfMeasure = typing.Literal[ + "g", + "kg", + "l", + "piece", +] + + +class Product(BaseModel): + name: str + price: Decimal + area_id: int + vat_rate: Decimal + allow_fractional: bool = False + unit_of_measure: UnitsOfMeasure diff --git a/src/allmende_payment_system/models.py b/src/allmende_payment_system/models.py index cf56d9c..6760d59 100644 --- a/src/allmende_payment_system/models.py +++ b/src/allmende_payment_system/models.py @@ -12,6 +12,8 @@ from sqlalchemy.orm import ( relationship, ) +from allmende_payment_system.api.types import UnitsOfMeasure + TABLE_PREFIX = "aps_" @@ -131,14 +133,6 @@ class Area(Base): products: Mapped[list["Product"]] = relationship("Product") -UnitsOfMeasure = typing.Literal[ - "g", - "kg", - "l", - "piece", -] - - class Product(Base): __tablename__ = TABLE_PREFIX + "product" id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True) diff --git a/src/allmende_payment_system/templates/base.html.jinja b/src/allmende_payment_system/templates/base.html.jinja index f7a6039..f529aa6 100644 --- a/src/allmende_payment_system/templates/base.html.jinja +++ b/src/allmende_payment_system/templates/base.html.jinja @@ -29,6 +29,13 @@ Einkaufen + {% if request.state.user.has_permission("product", "edit") %} + + {% endif %} {% if request.state.user.has_permission("user", "edit") %}