Initial commit
This commit is contained in:
@@ -0,0 +1,218 @@
|
||||
from uuid import uuid4
|
||||
|
||||
from fastapi import APIRouter, HTTPException, status
|
||||
from sqlmodel import Session, select
|
||||
|
||||
from app.access import product_barcode_accessible, user_can_access_dish
|
||||
from app.deps import CurrentUserDep, SessionDep
|
||||
from app.models import Dish, DishIngredient, DishShare, Product, User
|
||||
from app.schemas import DishCreate, DishIngredientRead, DishRead, DishShareCreate, DishShareRead
|
||||
|
||||
router = APIRouter(prefix="/api/dishes", tags=["dishes"])
|
||||
|
||||
NUTRIENT_FIELDS = ["calories", "carbs", "protein", "fat", "sugar", "fiber", "saturated_fat", "salt"]
|
||||
|
||||
|
||||
def _compute_nutrition_per_100g(session: Session, ingredients: list, user_id: int) -> tuple[dict, float]:
|
||||
total_weight = sum(ingredient.amount_g for ingredient in ingredients)
|
||||
if total_weight <= 0:
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Zutaten ergeben insgesamt 0g")
|
||||
|
||||
totals = dict.fromkeys(NUTRIENT_FIELDS, 0.0)
|
||||
for ingredient in ingredients:
|
||||
product = session.exec(select(Product).where(Product.barcode == ingredient.barcode)).first()
|
||||
if product is None or not product_barcode_accessible(session, ingredient.barcode, user_id):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=f"Zutat mit Barcode {ingredient.barcode} nicht gefunden",
|
||||
)
|
||||
factor = ingredient.amount_g / 100.0
|
||||
for field in NUTRIENT_FIELDS:
|
||||
totals[field] += getattr(product, field) * factor
|
||||
|
||||
per_100g = {field: totals[field] / total_weight * 100 for field in NUTRIENT_FIELDS}
|
||||
return per_100g, total_weight
|
||||
|
||||
|
||||
def _get_owned_dish(session: Session, dish_id: int, user_id: int) -> Dish:
|
||||
dish = session.get(Dish, dish_id)
|
||||
if dish is None or dish.user_id != user_id:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Gericht nicht gefunden")
|
||||
return dish
|
||||
|
||||
|
||||
def _get_accessible_dish(session: Session, dish_id: int, user_id: int) -> Dish:
|
||||
dish = session.get(Dish, dish_id)
|
||||
if dish is None or not user_can_access_dish(session, dish, user_id):
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Gericht nicht gefunden")
|
||||
return dish
|
||||
|
||||
|
||||
def _to_dish_read(session: Session, dish: Dish, current_user_id: int) -> DishRead:
|
||||
product = session.exec(select(Product).where(Product.barcode == dish.product_barcode)).first()
|
||||
ingredient_rows = session.exec(select(DishIngredient).where(DishIngredient.dish_id == dish.id)).all()
|
||||
owner = session.get(User, dish.user_id)
|
||||
|
||||
ingredients = []
|
||||
for row in ingredient_rows:
|
||||
ingredient_product = session.exec(select(Product).where(Product.barcode == row.product_barcode)).first()
|
||||
ingredients.append(
|
||||
DishIngredientRead(
|
||||
barcode=row.product_barcode,
|
||||
name=ingredient_product.name if ingredient_product else "Unbekannt",
|
||||
amount_g=row.amount_g,
|
||||
**{
|
||||
field: (getattr(ingredient_product, field) if ingredient_product else 0.0)
|
||||
for field in NUTRIENT_FIELDS
|
||||
},
|
||||
)
|
||||
)
|
||||
|
||||
return DishRead(
|
||||
id=dish.id,
|
||||
name=dish.name,
|
||||
instructions=dish.instructions,
|
||||
barcode=dish.product_barcode,
|
||||
total_weight_g=dish.total_weight_g,
|
||||
ingredients=ingredients,
|
||||
is_owner=dish.user_id == current_user_id,
|
||||
owner_username=owner.username if owner else "?",
|
||||
**{field: getattr(product, field) for field in NUTRIENT_FIELDS},
|
||||
)
|
||||
|
||||
|
||||
def _replace_ingredients(session: Session, dish: Dish, ingredients_in: list) -> None:
|
||||
existing = session.exec(select(DishIngredient).where(DishIngredient.dish_id == dish.id)).all()
|
||||
for row in existing:
|
||||
session.delete(row)
|
||||
session.flush()
|
||||
for ingredient in ingredients_in:
|
||||
session.add(DishIngredient(dish_id=dish.id, product_barcode=ingredient.barcode, amount_g=ingredient.amount_g))
|
||||
|
||||
|
||||
@router.post("", response_model=DishRead, status_code=status.HTTP_201_CREATED)
|
||||
def create_dish(dish_in: DishCreate, current_user: CurrentUserDep, session: SessionDep):
|
||||
per_100g, total_weight = _compute_nutrition_per_100g(session, dish_in.ingredients, current_user.id)
|
||||
|
||||
barcode = f"dish-{uuid4().hex[:12]}"
|
||||
product = Product(barcode=barcode, name=dish_in.name, **per_100g)
|
||||
session.add(product)
|
||||
|
||||
dish = Dish(
|
||||
user_id=current_user.id,
|
||||
product_barcode=barcode,
|
||||
name=dish_in.name,
|
||||
instructions=dish_in.instructions,
|
||||
total_weight_g=total_weight,
|
||||
)
|
||||
session.add(dish)
|
||||
session.flush()
|
||||
|
||||
_replace_ingredients(session, dish, dish_in.ingredients)
|
||||
|
||||
session.commit()
|
||||
session.refresh(dish)
|
||||
return _to_dish_read(session, dish, current_user.id)
|
||||
|
||||
|
||||
@router.get("", response_model=list[DishRead])
|
||||
def list_dishes(current_user: CurrentUserDep, session: SessionDep):
|
||||
owned = session.exec(select(Dish).where(Dish.user_id == current_user.id)).all()
|
||||
shared_dish_ids = session.exec(
|
||||
select(DishShare.dish_id).where(DishShare.shared_with_user_id == current_user.id)
|
||||
).all()
|
||||
shared = session.exec(select(Dish).where(Dish.id.in_(shared_dish_ids))).all() if shared_dish_ids else []
|
||||
dishes = sorted(owned + shared, key=lambda dish: dish.name.lower())
|
||||
return [_to_dish_read(session, dish, current_user.id) for dish in dishes]
|
||||
|
||||
|
||||
@router.get("/{dish_id}", response_model=DishRead)
|
||||
def get_dish(dish_id: int, current_user: CurrentUserDep, session: SessionDep):
|
||||
dish = _get_accessible_dish(session, dish_id, current_user.id)
|
||||
return _to_dish_read(session, dish, current_user.id)
|
||||
|
||||
|
||||
@router.put("/{dish_id}", response_model=DishRead)
|
||||
def update_dish(dish_id: int, dish_in: DishCreate, current_user: CurrentUserDep, session: SessionDep):
|
||||
dish = _get_owned_dish(session, dish_id, current_user.id)
|
||||
per_100g, total_weight = _compute_nutrition_per_100g(session, dish_in.ingredients, current_user.id)
|
||||
|
||||
product = session.exec(select(Product).where(Product.barcode == dish.product_barcode)).first()
|
||||
product.name = dish_in.name
|
||||
for field in NUTRIENT_FIELDS:
|
||||
setattr(product, field, per_100g[field])
|
||||
session.add(product)
|
||||
|
||||
dish.name = dish_in.name
|
||||
dish.instructions = dish_in.instructions
|
||||
dish.total_weight_g = total_weight
|
||||
session.add(dish)
|
||||
|
||||
_replace_ingredients(session, dish, dish_in.ingredients)
|
||||
|
||||
session.commit()
|
||||
session.refresh(dish)
|
||||
return _to_dish_read(session, dish, current_user.id)
|
||||
|
||||
|
||||
@router.delete("/{dish_id}", status_code=status.HTTP_204_NO_CONTENT)
|
||||
def delete_dish(dish_id: int, current_user: CurrentUserDep, session: SessionDep):
|
||||
dish = _get_owned_dish(session, dish_id, current_user.id)
|
||||
|
||||
ingredient_rows = session.exec(select(DishIngredient).where(DishIngredient.dish_id == dish.id)).all()
|
||||
for row in ingredient_rows:
|
||||
session.delete(row)
|
||||
|
||||
share_rows = session.exec(select(DishShare).where(DishShare.dish_id == dish.id)).all()
|
||||
for row in share_rows:
|
||||
session.delete(row)
|
||||
|
||||
product = session.exec(select(Product).where(Product.barcode == dish.product_barcode)).first()
|
||||
session.delete(dish)
|
||||
if product:
|
||||
session.delete(product)
|
||||
session.commit()
|
||||
|
||||
|
||||
@router.get("/{dish_id}/shares", response_model=list[DishShareRead])
|
||||
def list_shares(dish_id: int, current_user: CurrentUserDep, session: SessionDep):
|
||||
dish = _get_owned_dish(session, dish_id, current_user.id)
|
||||
shares = session.exec(select(DishShare).where(DishShare.dish_id == dish.id)).all()
|
||||
users = {user.id: user for user in session.exec(select(User)).all()}
|
||||
return [DishShareRead(username=users[s.shared_with_user_id].username) for s in shares if s.shared_with_user_id in users]
|
||||
|
||||
|
||||
@router.post("/{dish_id}/shares", response_model=list[DishShareRead], status_code=status.HTTP_201_CREATED)
|
||||
def add_share(dish_id: int, share_in: DishShareCreate, current_user: CurrentUserDep, session: SessionDep):
|
||||
dish = _get_owned_dish(session, dish_id, current_user.id)
|
||||
|
||||
target = session.exec(select(User).where(User.username == share_in.username)).first()
|
||||
if target is None:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Nutzer nicht gefunden")
|
||||
if target.id == current_user.id:
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Kann nicht mit sich selbst geteilt werden")
|
||||
|
||||
existing = session.exec(
|
||||
select(DishShare).where(DishShare.dish_id == dish.id, DishShare.shared_with_user_id == target.id)
|
||||
).first()
|
||||
if existing is None:
|
||||
session.add(DishShare(dish_id=dish.id, shared_with_user_id=target.id))
|
||||
session.commit()
|
||||
|
||||
return list_shares(dish_id, current_user, session)
|
||||
|
||||
|
||||
@router.delete("/{dish_id}/shares/{username}", status_code=status.HTTP_204_NO_CONTENT)
|
||||
def remove_share(dish_id: int, username: str, current_user: CurrentUserDep, session: SessionDep):
|
||||
dish = _get_owned_dish(session, dish_id, current_user.id)
|
||||
|
||||
target = session.exec(select(User).where(User.username == username)).first()
|
||||
if target is None:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Nutzer nicht gefunden")
|
||||
|
||||
row = session.exec(
|
||||
select(DishShare).where(DishShare.dish_id == dish.id, DishShare.shared_with_user_id == target.id)
|
||||
).first()
|
||||
if row:
|
||||
session.delete(row)
|
||||
session.commit()
|
||||
Reference in New Issue
Block a user