172 lines
6.5 KiB
Python
172 lines
6.5 KiB
Python
from __future__ import annotations
|
|
|
|
from typing import List, Optional
|
|
import datetime
|
|
|
|
import aiosqlite
|
|
from fastapi import APIRouter, Depends, Query, Request
|
|
|
|
import meals
|
|
import persons
|
|
import shopping
|
|
from common import ProblemDetails
|
|
from main import get_db, cookie_person, error_response # temporary imports during extraction
|
|
from common import ApiModel, Field
|
|
import datetime
|
|
from typing import Dict
|
|
|
|
router = APIRouter(prefix="/meals", tags=["meals"])
|
|
|
|
|
|
@router.get("/upcoming", operation_id="getUpcomingMeals", summary="List upcoming meals in a date range")
|
|
async def get_upcoming_meals(
|
|
date_from: datetime.datetime = Query(..., alias="from"),
|
|
to: datetime.datetime = Query(...),
|
|
conn: aiosqlite.Connection = Depends(get_db),
|
|
) -> List[meals.Meal]:
|
|
result = []
|
|
async for meal in meals.find_upcoming_meals_by_date_range(conn, date_from, to):
|
|
await meals.load_recipes(conn, meal)
|
|
await meals.load_extra_ingredients(conn, meal)
|
|
await meals.load_participants(conn, meal)
|
|
result.append(meal)
|
|
|
|
return result
|
|
|
|
|
|
@router.get("/{meal_id}", response_model=meals.Meal, operation_id="getMeal", summary="Get a meal by id",
|
|
responses={404: {"model": ProblemDetails, "description": "Meal not found", "content": {"application/problem+json": {}}}})
|
|
async def get_meal(
|
|
meal_id: int, conn: aiosqlite.Connection = Depends(get_db), request: Request | None = None
|
|
) -> meals.Meal:
|
|
meal = await meals.find_meal_by_id(conn, meal_id)
|
|
if not meal:
|
|
return error_response(request, 404, "Meal not found")
|
|
|
|
return meal
|
|
|
|
|
|
@router.post("", response_model=meals.Meal, operation_id="createMeal", summary="Create a new meal",
|
|
responses={400: {"model": ProblemDetails, "description": "Validation error", "content": {"application/problem+json": {}}}})
|
|
async def create_meal(
|
|
meal: meals.Meal, conn: aiosqlite.Connection = Depends(get_db), request: Request | None = None
|
|
) -> meals.Meal:
|
|
validation_response = validate_meal(meal, request)
|
|
if validation_response:
|
|
return validation_response
|
|
|
|
await meals.insert_meal(conn, meal)
|
|
await conn.commit()
|
|
return meal
|
|
|
|
|
|
@router.put("/{meal_id}", response_model=meals.Meal, operation_id="updateMeal", summary="Update an existing meal",
|
|
responses={
|
|
400: {"model": ProblemDetails, "description": "Validation error", "content": {"application/problem+json": {}}},
|
|
404: {"model": ProblemDetails, "description": "Meal not found", "content": {"application/problem+json": {}}},
|
|
})
|
|
async def update_meal(
|
|
meal_id: int, meal: meals.Meal, conn: aiosqlite.Connection = Depends(get_db), request: Request | None = None
|
|
) -> meals.Meal:
|
|
if meal.id != meal_id:
|
|
return error_response(request, 400, "Meal ID in URL does not match meal ID in body")
|
|
|
|
existing = await meals.find_meal_by_id(conn, meal_id)
|
|
if not existing:
|
|
return error_response(request, 404, "Meal not found")
|
|
|
|
validation_response = validate_meal(meal, request)
|
|
if validation_response:
|
|
return validation_response
|
|
|
|
await meals.update_meal(conn, meal)
|
|
await conn.commit()
|
|
|
|
return await get_meal(meal_id, conn)
|
|
|
|
|
|
@router.post("/{meal_id}/consumed", response_model=meals.Meal, operation_id="markMealConsumed", summary="Mark a meal as consumed",
|
|
responses={
|
|
400: {"model": ProblemDetails, "description": "Validation error", "content": {"application/problem+json": {}}},
|
|
404: {"model": ProblemDetails, "description": "Meal not found", "content": {"application/problem+json": {}}},
|
|
})
|
|
async def mark_consumed(
|
|
meal_id: int,
|
|
consumed_date: Optional[datetime.datetime] = None,
|
|
conn: aiosqlite.Connection = Depends(get_db),
|
|
person: persons.Person = Depends(cookie_person),
|
|
request: Request | None = None,
|
|
) -> meals.Meal:
|
|
if consumed_date and not consumed_date.tzinfo:
|
|
return error_response(request, 400, "Consumed date must include timezone")
|
|
|
|
meal = await meals.find_meal_by_id(conn, meal_id)
|
|
if not meal:
|
|
return error_response(request, 404, "Meal not found")
|
|
|
|
await meals.mark_consumed(conn, meal, consumed_date or datetime.datetime.now().astimezone())
|
|
await shopping.remove_request(conn, person, meal=meal)
|
|
|
|
await conn.commit()
|
|
return meal
|
|
|
|
|
|
@router.delete("/{meal_id}", response_model=meals.Meal, operation_id="deleteMeal", summary="Delete a meal",
|
|
responses={404: {"model": ProblemDetails, "description": "Meal not found", "content": {"application/problem+json": {}}}})
|
|
async def delete_meal(
|
|
meal_id: int,
|
|
conn: aiosqlite.Connection = Depends(get_db),
|
|
person: persons.Person = Depends(cookie_person),
|
|
request: Request | None = None,
|
|
) -> meals.Meal:
|
|
meal = await meals.find_meal_by_id(conn, meal_id)
|
|
if not meal:
|
|
return error_response(request, 404, "Meal not found")
|
|
|
|
await shopping.remove_request(conn, person, meal=meal)
|
|
await meals.delete_meal(conn, meal.id)
|
|
|
|
await conn.commit()
|
|
return meal
|
|
|
|
|
|
def get_duplicates(items: List[meals.Person]) -> set[str]:
|
|
seen: set[int] = set()
|
|
duplicates: set[str] = set()
|
|
for item in items:
|
|
if item.id in seen:
|
|
duplicates.add(item.name)
|
|
seen.add(item.id)
|
|
return duplicates
|
|
|
|
|
|
def validate_meal(meal: meals.Meal, request: Optional[Request] = None) -> Optional[Request]:
|
|
if not meal.chefs:
|
|
return error_response(request, 400, "Meal must have at least one chef")
|
|
|
|
if not meal.cleanup:
|
|
return error_response(request, 400, "Meal must have at least one cleanup person")
|
|
|
|
if not meal.consumers:
|
|
return error_response(request, 400, "Meal must have at least one consumer")
|
|
|
|
if len(meal.recipes) == 0 and len(meal.extra_ingredients) == 0:
|
|
return error_response(request, 400, "Meal must have at least one recipe or ingredient")
|
|
|
|
duplicates = get_duplicates(meal.chefs)
|
|
if duplicates:
|
|
return error_response(request, 400, f'Duplicate chef: {", ".join(duplicates)}')
|
|
|
|
duplicates = get_duplicates(meal.cleanup)
|
|
if duplicates:
|
|
return error_response(request, 400, f'Duplicate cleanup person: {", ".join(duplicates)}')
|
|
|
|
duplicates = get_duplicates(meal.consumers)
|
|
if duplicates:
|
|
return error_response(request, 400, f'Duplicate consumer: {", ".join(duplicates)}')
|
|
|
|
zero_servings = [r for r in meal.recipes if r.servings == 0]
|
|
if zero_servings:
|
|
return error_response(request, 400, "Recipe servings must be greater than 0")
|
|
|
|
return None
|