diff --git a/main.py b/main.py index bb801e9..25dc8fd 100644 --- a/main.py +++ b/main.py @@ -3,7 +3,7 @@ import products, recipes, db, meals, persons, ingredients, shopping import datetime 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.responses import JSONResponse 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 class CurrentShoppingList(BaseModel): - requests: List[shopping.ShoppingListItem] - previously_purchased: List[shopping.ShoppingListItem] = [] - other_shopping_lists: List[shopping.ShoppingList] = [] + outstanding_items: List[shopping.ShoppingListItem] + requested_meals: List[shopping.ShoppingListItem] + 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") async def get_current_shopping_list(conn: sqlite3.Connection = Depends(get_db)) -> CurrentShoppingList: - outstanding_requests, purchased_requests = await shopping.get_outstanding_requests(conn) - other_shopping_list_ids = {item.shopping_list 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] + outstanding_requests, purchased_requests, meal_requests = await shopping.get_outstanding_requests(conn) + other_shopping_list_ids = {item.list_id for item in purchased_requests} - 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}") async def get_shopping_list(list_id: int, conn: sqlite3.Connection = Depends(get_db)) -> shopping.ShoppingList: diff --git a/shopping/__init__.py b/shopping/__init__.py index cc69981..8e1ec32 100644 --- a/shopping/__init__.py +++ b/shopping/__init__.py @@ -11,19 +11,23 @@ async def get_persons_requests(conn, person_id: int) -> AsyncIterator[ShoppingLi def flatten_items(items: Iterator[ShoppingListItem]) -> Iterator[ShoppingListItem]: for item in items: 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) else: 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)] - - meal_ids = {item.meal_id for item in current_requests if item.meal_id is not None and item.meal_id > 0} - purchased_ingredients = {r.ingredient_id async for r in _get_purchased_ingredients(conn, meal_ids)} + 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] + meals = {r.meal_id: r.meal for r in meal_requests} + purchased_ingredients = {r.ingredient_id async for r in _get_purchased_ingredients(conn, list(meals.keys()))} flattened = flatten_items(current_requests) 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] - return outstanding_items, purchased_items + return outstanding_items, purchased_items, meal_requests diff --git a/shopping/db.py b/shopping/db.py index 1a378a5..6f2f877 100644 --- a/shopping/db.py +++ b/shopping/db.py @@ -1,4 +1,5 @@ from common import BaseLinkedModel +from recipes import Recipe from meals import Meal, find_meal_by_id from ingredients import Ingredient, insert_ingredient from persons import Person @@ -19,6 +20,9 @@ class ShoppingListItem(BaseLinkedModel): ingredient_id: Optional[int] = None ingredient: Optional[Ingredient] = None + recipe_id: Optional[int] = None + recipe: Optional[Recipe] = None + meal_id: Optional[int] = None meal: Optional[Meal] = None @@ -58,11 +62,13 @@ async def create(conn): list_id INTEGER, person_id INTEGER, meal_id INTEGER, + recipe_id INTEGER, created_date DATETIME NOT NULL, FOREIGN KEY(ingredient_id) REFERENCES Ingredient(id), FOREIGN KEY(list_id) REFERENCES ShoppingList(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(''' UPDATE ShoppingListItem 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: if cursor.rowcount == 0: 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') async with conn.execute(''' - INSERT INTO ShoppingListItem (ingredient_id, list_id, person_id, meal_id, created_date) - VALUES (?, ?, ?, ?, ?) - ''', (item.ingredient_id, shopping_list.id, item.person_id, item.meal_id, item.created_date.isoformat())) as cursor: + INSERT INTO ShoppingListItem (ingredient_id, list_id, person_id, meal_id, recipe_id, created_date) + VALUES (?, ?, ?, ?, ?, ?) + ''', (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 # 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: if ingredient is not None and meal is not None: raise ValueError('Cannot request both an ingredient and a meal')