Allowed for null products, move ingredient spreading server-side #1
3 changed files with 65 additions and 28 deletions
48
main.py
48
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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
|
|
|
|||
Loading…
Reference in a new issue