Allowed for null products, move ingredient spreading server-side #1

Merged
jacob merged 7 commits from nullproducts into master 2025-07-28 23:23:10 +00:00
3 changed files with 48 additions and 10 deletions
Showing only changes of commit e06ad7611b - Show all commits

11
main.py
View file

@ -234,19 +234,19 @@ async def mark_consumed(meal_id: int, consumed_date: Optional[datetime.datetime]
return JSONResponse(status_code=404, content={'message': 'Meal not found'}) return JSONResponse(status_code=404, content={'message': 'Meal not found'})
await meals.mark_consumed(conn, meal, consumed_date or datetime.datetime.now().astimezone()) await meals.mark_consumed(conn, meal, consumed_date or datetime.datetime.now().astimezone())
await shopping.unrequest_meal(conn, meal) await shopping.remove_request(conn, person, meal=meal)
await conn.commit() await conn.commit()
return meal return meal
@app.delete("/api/meals/{meal_id}") @app.delete("/api/meals/{meal_id}")
async def delete_meal(meal_id: int, conn: sqlite3.Connection = Depends(get_db)) -> meals.Meal: async def delete_meal(meal_id: int, conn: sqlite3.Connection = Depends(get_db), person: persons.Person = Depends(cookie_person)) -> meals.Meal:
meal = await meals.find_meal_by_id(conn, meal_id) meal = await meals.find_meal_by_id(conn, meal_id)
if not meal: if not meal:
return JSONResponse(status_code=404, content={'message': 'Meal not found'}) return JSONResponse(status_code=404, content={'message': 'Meal not found'})
await shopping.remove_request(conn, person, meal=meal)
await meals.delete_meal(conn, meal.id) await meals.delete_meal(conn, meal.id)
await shopping.unrequest_meal(conn, meal)
await conn.commit() await conn.commit()
return meal return meal
@ -291,9 +291,8 @@ class PurchasedShoppingList(BaseModel):
@app.get("/api/shopping/{list_id}") @app.get("/api/shopping/{list_id}")
async def get_shopping_list(list_id: int, conn: sqlite3.Connection = Depends(get_db)) -> PurchasedShoppingList: async def get_shopping_list(list_id: int, conn: sqlite3.Connection = Depends(get_db)) -> PurchasedShoppingList:
shopping_list = await shopping.load_shopping_list(conn, list_id) shopping_list = await shopping.load_shopping_list(conn, list_id)
result = PurchasedShoppingList(list=shopping_list) meals_lookup, recipes_lookup, ingredients_lookup = shopping.remove_references(shopping_list.items)
shopping.remove_references(result.list.items, result.meals_lookup, result.recipes_lookup, result.ingredients_lookup) return PurchasedShoppingList(list=shopping_list, meals_lookup=meals_lookup, recipes_lookup=recipes_lookup, ingredients_lookup=ingredients_lookup)
return result
@app.post("/api/shopping/") @app.post("/api/shopping/")
async def purchase_ingredients(shopping_list: shopping.ShoppingList, conn: sqlite3.Connection = Depends(get_db), person: persons.Person = Depends(cookie_person)) -> PurchasedShoppingList: async def purchase_ingredients(shopping_list: shopping.ShoppingList, conn: sqlite3.Connection = Depends(get_db), person: persons.Person = Depends(cookie_person)) -> PurchasedShoppingList:

View file

@ -3,7 +3,11 @@ from shopping.db import ShoppingList, ShoppingListItem, load_shopping_list, purc
from shopping.db import find_items_by_list_id as _find_items_by_list_id, get_purchased_ingredients as _get_purchased_ingredients from shopping.db import find_items_by_list_id as _find_items_by_list_id, get_purchased_ingredients as _get_purchased_ingredients
def remove_references(items: List[ShoppingListItem], meals_lookup = {}, recipes_lookup = {}, ingredients_lookup = {}) -> Tuple[Dict[int, Any], Dict[int, Any], Dict[int, Any]]: def remove_references(items: List[ShoppingListItem], meals_lookup = None, recipes_lookup = None, ingredients_lookup = None) -> Tuple[Dict[int, Any], Dict[int, Any], Dict[int, Any]]:
meals_lookup = meals_lookup or {}
recipes_lookup = recipes_lookup or {}
ingredients_lookup = ingredients_lookup or {}
for item in items: for item in items:
if item.meal and not item.meal.id in meals_lookup: if item.meal and not item.meal.id in meals_lookup:
meals_lookup[item.meal.id] = item.meal meals_lookup[item.meal.id] = item.meal

View file

@ -1,6 +1,6 @@
from common import BaseLinkedModel from common import BaseLinkedModel
from recipes import Recipe from recipes import Recipe
from meals import Meal, find_meal_by_id from meals import Meal, find_meal_by_id, mark_purchased
from ingredients import Ingredient, insert_ingredient from ingredients import Ingredient, insert_ingredient
from persons import Person from persons import Person
from products import Product from products import Product
@ -71,7 +71,6 @@ async def create(conn):
FOREIGN KEY(recipe_id) REFERENCES Recipe(id) FOREIGN KEY(recipe_id) REFERENCES Recipe(id)
);''') );''')
def validate_request(request: ShoppingListItem) -> None: def validate_request(request: ShoppingListItem) -> None:
if request.person_id < 0: if request.person_id < 0:
raise ValueError('Requests must have a person') raise ValueError('Requests must have a person')
@ -139,7 +138,40 @@ async def purchase(conn, shopping_list: ShoppingList) -> None:
''', (item.ingredient_id, shopping_list.id, item.person_id, item.meal_id, item.recipe_id, item.created_date.isoformat())) as cursor: ''', (item.ingredient_id, shopping_list.id, item.person_id, item.meal_id, item.recipe_id, item.created_date.isoformat())) as cursor:
item.id = cursor.lastrowid item.id = cursor.lastrowid
# TODO: Calculate which meals have been fulfilled and update meal status meal_ids = list({ item.meal_id for item in shopping_list.items if item.meal_id is not None and item.meal_id >= 0 })
await update_purchased_meals(conn, meal_ids)
async def update_purchased_meals(conn, meal_ids: List[int]) -> None:
if not meal_ids:
return
purchased_ingredient_ids = {item.ingredient_id async for item in get_purchased_ingredients(conn, meal_ids)}
for meal_id in meal_ids:
meal = await find_meal_by_id(conn, meal_id)
ingredients = {ingredient.id for recipe in meal.recipes for ingredient in recipe.recipe.ingredients} | \
{ingredient.id for ingredient in meal.extra_ingredients}
remaining_ingredients = ingredients - purchased_ingredient_ids
if not remaining_ingredients:
await mark_purchased(conn, meal)
# If all ingredients are purchased, update the meal status
await conn.execute('''
UPDATE Meal
SET purchase_date = ?
WHERE id = ?
''', (datetime.now().isoformat(), meal.id))
async def is_requested(conn, meal: Meal) -> bool:
if meal.id < 0:
return False
async with conn.execute('''
SELECT COUNT(*) FROM ShoppingListItem
WHERE meal_id = ? AND list_id IS NULL
''', (meal.id,)) as cursor:
row = await cursor.fetchone()
return row[0] > 0
async def request(conn, person: Person, ingredient: Optional[Ingredient] = None, meal: Optional[Meal] = None) -> ShoppingListItem: async def request(conn, person: Person, ingredient: Optional[Ingredient] = None, meal: Optional[Meal] = None) -> ShoppingListItem:
if ingredient is not None and meal is not None: if ingredient is not None and meal is not None:
@ -158,6 +190,9 @@ async def request(conn, person: Person, ingredient: Optional[Ingredient] = None,
validate_request(item) validate_request(item)
if meal is not None and await is_requested(conn, meal):
raise ValueError('Meal is already requested')
async with conn.execute(''' async with conn.execute('''
INSERT INTO ShoppingListItem (ingredient_id, person_id, meal_id, created_date) INSERT INTO ShoppingListItem (ingredient_id, person_id, meal_id, created_date)
VALUES (?, ?, ?, ?) VALUES (?, ?, ?, ?)