Add shopping cart and related models

This commit is contained in:
2025-11-11 12:08:06 +01:00
parent b3166811e5
commit f4618f4d05
8 changed files with 148 additions and 41 deletions

View File

@@ -10,8 +10,7 @@ templates = get_jinja_renderer()
@root_router.get("/")
async def landing_page(request: Request, user_info: UserDep, session: SessionDep):
user = ensure_user(user_info, session)
async def landing_page(request: Request, user: UserDep, session: SessionDep):
print(f"User {user.username} ({user.display_name}) accessed landing page")
transactions = []
for account in user.accounts:

View File

@@ -4,22 +4,8 @@ from typing import Annotated
from fastapi import Depends, HTTPException, Request
from sqlalchemy.orm import Session
from allmende_payment_system.database import SessionLocal
async def get_user(request: Request) -> dict:
if username := os.environ.get("APS_username", None):
return {
"username": username,
"display_name": os.environ.get("APS_display_name", "Missing Display Name"),
}
if "ynh_user" not in request.headers:
raise HTTPException(status_code=401, detail="Missing ynh_user header")
return {"username": request.headers["ynh_user"]}
UserDep = Annotated[dict, Depends(get_user)]
from allmende_payment_system.database import SessionLocal, ensure_user
from allmende_payment_system.models import User
def get_session() -> Session:
@@ -31,3 +17,27 @@ def get_session() -> Session:
SessionDep = Annotated[Session, Depends(get_session)]
async def get_user(request: Request) -> dict:
if username := os.environ.get("APS_username", None):
return {
"username": username,
"display_name": os.environ.get("APS_display_name", "Missing Display Name"),
}
if "ynh_user" not in request.headers:
raise HTTPException(status_code=401, detail="Missing ynh_user header")
return {"username": request.headers["ynh_user"]}
async def get_user_object(request: Request, session: SessionDep) -> User:
user_info = await get_user(request)
user = ensure_user(user_info, session)
request.state.user = user
return user
UserDep = Annotated[dict, Depends(get_user_object)]

View File

@@ -4,11 +4,11 @@ from fastapi import Depends, FastAPI
from fastapi.staticfiles import StaticFiles
from allmende_payment_system.api import root_router
from allmende_payment_system.api.dependencies import get_user
from allmende_payment_system.api.dependencies import get_user_object
from allmende_payment_system.api.shop import shop_router
locale.setlocale(locale.LC_ALL, "de_DE.UTF-8")
app = FastAPI(dependencies=[Depends(get_user)])
app = FastAPI(dependencies=[Depends(get_user_object)])
app.mount(

View File

@@ -2,8 +2,15 @@ import datetime
import decimal
import typing
from sqlalchemy import Column, ForeignKey, Numeric, Table
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship
from sqlalchemy import Column, ForeignKey, Numeric, Table, select
from sqlalchemy.exc import NoResultFound
from sqlalchemy.orm import (
DeclarativeBase,
Mapped,
mapped_column,
object_session,
relationship,
)
TABLE_PREFIX = "aps_"
@@ -46,6 +53,20 @@ class User(Base):
accounts: Mapped[list["Account"]] = relationship(
"Account", secondary=user_account_association, back_populates="users"
)
orders: Mapped[list["Order"]] = relationship("Order", back_populates="user")
@property
def shopping_cart(self):
for order in self.orders:
if order.account_id is None:
cart = order
break
else:
cart = Order(user=self)
session = object_session(self)
session.add(cart)
return cart
class Area(Base):
@@ -81,6 +102,40 @@ class Product(Base):
image_path: Mapped[str] = mapped_column(nullable=True)
class Order(Base):
__tablename__ = TABLE_PREFIX + "order"
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
user_id: Mapped[int] = mapped_column(ForeignKey(TABLE_PREFIX + "user.id"))
user: Mapped[User] = relationship("User", back_populates="orders")
account_id: Mapped[int] = mapped_column(
ForeignKey(TABLE_PREFIX + "account.id"), nullable=True
)
account: Mapped[Account | None] = relationship("Account")
items: Mapped[list["OrderItem"]] = relationship(
"OrderItem", cascade="all, delete-orphan", back_populates="order"
)
@property
def is_in_shopping_cart(self):
return self.account is None
class OrderItem(Base):
__tablename__ = TABLE_PREFIX + "order_item"
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
order_id: Mapped[int] = mapped_column(ForeignKey(TABLE_PREFIX + "order.id"))
order: Mapped[Order] = relationship("Order", back_populates="items")
product_id: Mapped[int] = mapped_column(ForeignKey(TABLE_PREFIX + "product.id"))
product: Mapped[Product] = relationship("Product")
quantity: Mapped[int] = mapped_column(nullable=False)
total_amount: Mapped[decimal.Decimal] = mapped_column(
Numeric(10, 2), nullable=False
)
TransactionTypes = typing.Literal[
"product",
"deposit",

View File

@@ -40,6 +40,21 @@
</a>
</li>
</ul>
<!-- Shopping Cart at Bottom -->
<div class="mt-auto pt-3 border-top">
<a href="/cart" class="btn btn-primary w-100 position-relative">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor" class="bi bi-cart3 me-2" viewBox="0 0 16 16">
<path d="M0 1.5A.5.5 0 0 1 .5 1H2a.5.5 0 0 1 .485.379L2.89 3H14.5a.5.5 0 0 1 .49.598l-1 5a.5.5 0 0 1-.465.401l-9.397.472L4.415 11H13a.5.5 0 0 1 0 1H4a.5.5 0 0 1-.491-.408L2.01 3.607 1.61 2H.5a.5.5 0 0 1-.5-.5M3.102 4l.84 4.479 9.144-.459L13.89 4zM5 12a2 2 0 1 0 0 4 2 2 0 0 0 0-4m7 0a2 2 0 1 0 0 4 2 2 0 0 0 0-4m-7 1a1 1 0 1 1 0 2 1 1 0 0 1 0-2m7 0a1 1 0 1 1 0 2 1 1 0 0 1 0-2"/>
</svg>
Warenkorb
<span class="position-absolute top-0 start-100 translate-middle badge rounded-pill bg-danger">
{{ request.state.user.shopping_cart.items|length }}
<span class="visually-hidden">Artikel im Warenkorb</span>
</span>
</a>
</div>
</div>
</div>