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 65 additions and 28 deletions
Showing only changes of commit ba6a859ec7 - Show all commits

48
main.py
View file

@ -3,7 +3,7 @@ import products, recipes, db, meals, persons, ingredients, shopping
import datetime import datetime
from pydantic import BaseModel from pydantic import BaseModel
from typing import List, Annotated, Optional, Union from typing import Dict, List, Annotated, Optional, Union
from fastapi import FastAPI, Depends, Query, Cookie from fastapi import FastAPI, Depends, Query, Cookie
from fastapi.responses import JSONResponse from fastapi.responses import JSONResponse
from fastapi.encoders import jsonable_encoder from fastapi.encoders import jsonable_encoder
@ -252,17 +252,49 @@ async def delete_meal(meal_id: int, conn: sqlite3.Connection = Depends(get_db))
return meal return meal
class CurrentShoppingList(BaseModel): class CurrentShoppingList(BaseModel):
requests: List[shopping.ShoppingListItem] outstanding_items: List[shopping.ShoppingListItem]
previously_purchased: List[shopping.ShoppingListItem] = [] requested_meals: List[shopping.ShoppingListItem]
other_shopping_lists: List[shopping.ShoppingList] = [] purchased_items: List[shopping.ShoppingListItem] = []
ingredients_lookup: Dict[int, ingredients.Ingredient] = {}
meals_lookup: Dict[int, meals.Meal] = {}
shopping_list_lookup: Dict[int, shopping.ShoppingList] = {}
recipes_lookup: Dict[int, recipes.Recipe] = {}
@app.get("/api/shopping/current") @app.get("/api/shopping/current")
async def get_current_shopping_list(conn: sqlite3.Connection = Depends(get_db)) -> CurrentShoppingList: async def get_current_shopping_list(conn: sqlite3.Connection = Depends(get_db)) -> CurrentShoppingList:
outstanding_requests, purchased_requests = await shopping.get_outstanding_requests(conn) outstanding_requests, purchased_requests, meal_requests = await shopping.get_outstanding_requests(conn)
other_shopping_list_ids = {item.shopping_list for item in purchased_requests} other_shopping_list_ids = {item.list_id for item in purchased_requests}
other_shopping_lists = [await shopping.load_shopping_list(conn, list_id) for list_id in other_shopping_list_ids if list_id is not None]
return CurrentShoppingList(requests=outstanding_requests, previously_purchased=purchased_requests, other_shopping_lists=other_shopping_lists) shopping_list_lookup = { list_id: await shopping.load_shopping_list(conn, list_id) for list_id in other_shopping_list_ids }
meals_lookup = {}
ingredients_lookup = {}
recipes_lookup = {}
# Reduce the data structure to items and lookups
items = meal_requests + outstanding_requests + purchased_requests + [item for sl in shopping_list_lookup.values() for item in sl.items]
for item in items:
if item.meal and not item.meal.id in meals_lookup:
meals_lookup[item.meal.id] = item.meal
item.meal = None
if item.ingredient and not item.ingredient.id in ingredients_lookup:
ingredients_lookup[item.ingredient.id] = item.ingredient
item.ingredient = None
if item.recipe and not item.recipe.id in recipes_lookup:
recipes_lookup[item.recipe.id] = item.recipe
item.recipe = None
return CurrentShoppingList(
outstanding_items=outstanding_requests,
requested_meals=meal_requests,
purchased_items=purchased_requests,
meals_lookup=meals_lookup,
shopping_list_lookup=shopping_list_lookup,
ingredients_lookup=ingredients_lookup,
recipes_lookup=recipes_lookup
)
@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)) -> shopping.ShoppingList: async def get_shopping_list(list_id: int, conn: sqlite3.Connection = Depends(get_db)) -> shopping.ShoppingList:

View file

@ -11,19 +11,23 @@ async def get_persons_requests(conn, person_id: int) -> AsyncIterator[ShoppingLi
def flatten_items(items: Iterator[ShoppingListItem]) -> Iterator[ShoppingListItem]: def flatten_items(items: Iterator[ShoppingListItem]) -> Iterator[ShoppingListItem]:
for item in items: for item in items:
if item.meal: if item.meal:
for ingredient in item.meal.ingredients + item.meal.extra_ingredients: for mealRecipe in item.meal.recipes:
for ingredient in mealRecipe.recipe.ingredients:
yield ShoppingListItem(ingredient=ingredient, meal=item.meal, recipe=mealRecipe.recipe, person_id=item.person_id, created_date=item.created_date)
for ingredient in item.meal.extra_ingredients:
yield ShoppingListItem(ingredient=ingredient, meal=item.meal, person_id=item.person_id, created_date=item.created_date) yield ShoppingListItem(ingredient=ingredient, meal=item.meal, person_id=item.person_id, created_date=item.created_date)
else: else:
yield item yield item
async def get_outstanding_requests(conn) -> Tuple[List[ShoppingListItem], List[ShoppingListItem]]: async def get_outstanding_requests(conn) -> Tuple[List[ShoppingListItem], List[ShoppingListItem], List[ShoppingListItem]]:
current_requests = [r async for r in _find_items_by_list_id(conn, None)] current_requests = [r async for r in _find_items_by_list_id(conn, None)]
meal_requests = [r for r in current_requests if r.meal_id is not None and r.meal_id > 0 and r.meal is not None]
meal_ids = {item.meal_id for item in current_requests if item.meal_id is not None and item.meal_id > 0} meals = {r.meal_id: r.meal for r in meal_requests}
purchased_ingredients = {r.ingredient_id async for r in _get_purchased_ingredients(conn, meal_ids)} purchased_ingredients = {r.ingredient_id async for r in _get_purchased_ingredients(conn, list(meals.keys()))}
flattened = flatten_items(current_requests) flattened = flatten_items(current_requests)
outstanding_items = [r for r in flattened if r.ingredient_id not in purchased_ingredients] outstanding_items = [r for r in flattened if r.ingredient_id not in purchased_ingredients]
purchased_items = [r for r in flattened if r.ingredient_id in purchased_ingredients] purchased_items = [r for r in flattened if r.ingredient_id in purchased_ingredients]
return outstanding_items, purchased_items return outstanding_items, purchased_items, meal_requests

View file

@ -1,4 +1,5 @@
from common import BaseLinkedModel from common import BaseLinkedModel
from recipes import Recipe
from meals import Meal, find_meal_by_id from meals import Meal, find_meal_by_id
from ingredients import Ingredient, insert_ingredient from ingredients import Ingredient, insert_ingredient
from persons import Person from persons import Person
@ -19,6 +20,9 @@ class ShoppingListItem(BaseLinkedModel):
ingredient_id: Optional[int] = None ingredient_id: Optional[int] = None
ingredient: Optional[Ingredient] = None ingredient: Optional[Ingredient] = None
recipe_id: Optional[int] = None
recipe: Optional[Recipe] = None
meal_id: Optional[int] = None meal_id: Optional[int] = None
meal: Optional[Meal] = None meal: Optional[Meal] = None
@ -58,11 +62,13 @@ async def create(conn):
list_id INTEGER, list_id INTEGER,
person_id INTEGER, person_id INTEGER,
meal_id INTEGER, meal_id INTEGER,
recipe_id INTEGER,
created_date DATETIME NOT NULL, created_date DATETIME NOT NULL,
FOREIGN KEY(ingredient_id) REFERENCES Ingredient(id), FOREIGN KEY(ingredient_id) REFERENCES Ingredient(id),
FOREIGN KEY(list_id) REFERENCES ShoppingList(id), FOREIGN KEY(list_id) REFERENCES ShoppingList(id),
FOREIGN KEY(person_id) REFERENCES Person(id), FOREIGN KEY(person_id) REFERENCES Person(id),
FOREIGN KEY(meal_id) REFERENCES Meal(id) FOREIGN KEY(meal_id) REFERENCES Meal(id),
FOREIGN KEY(recipe_id) REFERENCES Recipe(id)
);''') );''')
@ -110,7 +116,11 @@ async def purchase(conn, shopping_list: ShoppingList) -> None:
async with conn.execute(''' async with conn.execute('''
UPDATE ShoppingListItem UPDATE ShoppingListItem
SET list_id = ? SET list_id = ?
WHERE ingredient_id = ? AND person_id = ? AND meal_id IS NULL WHERE ingredient_id = ?
AND list_id IS NULL
AND person_id = ?
AND meal_id IS NULL
AND recipe_id IS NULL
''', (shopping_list.id, item.ingredient_id, item.person_id)) as cursor: ''', (shopping_list.id, item.ingredient_id, item.person_id)) as cursor:
if cursor.rowcount == 0: if cursor.rowcount == 0:
raise ValueError('Ingredient request must have a valid person id and ingredient id') raise ValueError('Ingredient request must have a valid person id and ingredient id')
@ -121,22 +131,13 @@ async def purchase(conn, shopping_list: ShoppingList) -> None:
raise ValueError('Meal request must have a valid meal id') raise ValueError('Meal request must have a valid meal id')
async with conn.execute(''' async with conn.execute('''
INSERT INTO ShoppingListItem (ingredient_id, list_id, person_id, meal_id, created_date) INSERT INTO ShoppingListItem (ingredient_id, list_id, person_id, meal_id, recipe_id, created_date)
VALUES (?, ?, ?, ?, ?) VALUES (?, ?, ?, ?, ?, ?)
''', (item.ingredient_id, shopping_list.id, item.person_id, item.meal_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 # TODO: Calculate which meals have been fulfilled and update meal status
async def requested_meal_ids(conn) -> AsyncIterator[ShoppingListItem]:
async with conn.execute(f'''
SELECT {ShoppingListItem.KEYS}
FROM ShoppingListItem
WHERE list_id IS NULL AND meal_id IS NOT NULL AND meal_id > 0
''') as cursor:
async for row in cursor:
yield ShoppingListItem(**{k:v for k,v in zip(ShoppingListItem.KEYS, row)})
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:
raise ValueError('Cannot request both an ingredient and a meal') raise ValueError('Cannot request both an ingredient and a meal')