Compare commits

..

2 commits

Author SHA1 Message Date
54f0dcbbb9 Remove request when meal purchased 2025-07-30 18:25:07 +10:00
410b097d5c Fixed purchased item matching 2025-07-30 18:19:20 +10:00
5 changed files with 54 additions and 19 deletions

View file

@ -1,4 +1,4 @@
from ingredients.db import Ingredient, find_ingredients_by_meal_id, find_ingredients_by_recipe_id, insert_ingredient, delete_ingredients_by_meal_id from ingredients.db import Ingredient, find_ingredient_by_id, find_ingredients_by_meal_id, find_ingredients_by_recipe_id, insert_ingredient, delete_ingredients_by_meal_id
import units import units
from products import Product, find_product_by_tag, get_or_create, add_missing_tags from products import Product, find_product_by_tag, get_or_create, add_missing_tags

View file

@ -37,7 +37,7 @@ async def insert_ingredient(conn, ingredient: Ingredient):
if ingredient.product: if ingredient.product:
ingredient.product_id = ingredient.product.id ingredient.product_id = ingredient.product.id
if ingredient.product_id < 0: if ingredient.product_id is None or ingredient.product_id < 0:
ingredient.product_id = None ingredient.product_id = None
async with conn.execute(''' async with conn.execute('''
@ -46,6 +46,21 @@ async def insert_ingredient(conn, ingredient: Ingredient):
''', (ingredient.name, ingredient.line, ingredient.preparation, ingredient.unit, ingredient.quantity, ingredient.product_id, ingredient.recipe_id, ingredient.meal_id)) as cursor: ''', (ingredient.name, ingredient.line, ingredient.preparation, ingredient.unit, ingredient.quantity, ingredient.product_id, ingredient.recipe_id, ingredient.meal_id)) as cursor:
ingredient.id = cursor.lastrowid ingredient.id = cursor.lastrowid
async def find_ingredient_by_id(conn, ingredient_id: int) -> Optional[Ingredient]:
ingredient_keys = [f'ingredient.{key}' for key in Ingredient.KEYS]
product_keys = [f'product.{key}' for key in Product.KEYS]
async with conn.execute(f'''
SELECT {','.join(ingredient_keys + product_keys)} FROM Ingredient
LEFT JOIN Product ON Ingredient.product_id = Product.id
WHERE id = ?
''', (ingredient_id,)) as cursor:
async for row in cursor:
product_keys = {k:v for k,v in zip(Product.KEYS, row[len(Ingredient.KEYS):])}
product = Product(**product_keys) if product_keys['id'] else None
return Ingredient(**{k:v for k,v in zip(Ingredient.KEYS, row[:len(Ingredient.KEYS)])}, product=product)
return None
async def find_ingredients_by_recipe_id(conn, recipe_id: int) -> AsyncIterator[Ingredient]: async def find_ingredients_by_recipe_id(conn, recipe_id: int) -> AsyncIterator[Ingredient]:
ingredient_keys = [f'ingredient.{key}' for key in Ingredient.KEYS] ingredient_keys = [f'ingredient.{key}' for key in Ingredient.KEYS]
product_keys = [f'product.{key}' for key in Product.KEYS] product_keys = [f'product.{key}' for key in Product.KEYS]

View file

@ -270,7 +270,7 @@ async def get_current_shopping_list(conn: sqlite3.Connection = Depends(get_db))
# Reduce the data structure to items and lookups # 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] items = meal_requests + outstanding_requests + purchased_requests + [item for sl in shopping_list_lookup.values() for item in sl.items]
meals_lookup, recipes_lookup, ingredients_lookup = shopping.remove_references(items) meals_lookup, recipes_lookup, ingredients_lookup = await shopping.to_lookups(conn, items)
return CurrentShoppingList( return CurrentShoppingList(
outstanding_items=outstanding_requests, outstanding_items=outstanding_requests,
@ -291,7 +291,7 @@ 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)
meals_lookup, recipes_lookup, ingredients_lookup = shopping.remove_references(shopping_list.items) meals_lookup, recipes_lookup, ingredients_lookup = await shopping.to_lookups(conn, shopping_list.items)
return PurchasedShoppingList(list=shopping_list, meals_lookup=meals_lookup, recipes_lookup=recipes_lookup, ingredients_lookup=ingredients_lookup) return PurchasedShoppingList(list=shopping_list, meals_lookup=meals_lookup, recipes_lookup=recipes_lookup, ingredients_lookup=ingredients_lookup)
@app.post("/api/shopping/") @app.post("/api/shopping/")
@ -302,7 +302,7 @@ async def purchase_ingredients(shopping_list: shopping.ShoppingList, conn: sqlit
await conn.commit() await conn.commit()
result = PurchasedShoppingList(list=shopping_list) result = PurchasedShoppingList(list=shopping_list)
shopping.remove_references(shopping_list.items, result.meals_lookup, result.recipes_lookup, result.ingredients_lookup) await shopping.to_lookups(conn, shopping_list.items, result.meals_lookup, result.recipes_lookup, result.ingredients_lookup)
return result return result
@app.get("/api/shopping/current/me/ingredients") @app.get("/api/shopping/current/me/ingredients")

View file

@ -3,11 +3,20 @@ 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 = None, recipes_lookup = None, ingredients_lookup = None) -> Tuple[Dict[int, Any], Dict[int, Any], Dict[int, Any]]: import meals, recipes, ingredients
async def to_lookups(conn, items: List[ShoppingListItem], meals_lookup: Dict[int, Any] = None, recipes_lookup: Dict[int, Any] = None, ingredients_lookup: Dict[int, Any] = None) -> Tuple[Dict[int, Any], Dict[int, Any], Dict[int, Any]]:
meals_lookup = meals_lookup or {} meals_lookup = meals_lookup or {}
recipes_lookup = recipes_lookup or {} recipes_lookup = recipes_lookup or {}
ingredients_lookup = ingredients_lookup or {} ingredients_lookup = ingredients_lookup or {}
lookups = (meals_lookup, recipes_lookup, ingredients_lookup)
_move_refs_to_lookups(items, *lookups)
await _ensure_lookups_populated(conn, items, *lookups)
return lookups
def _move_refs_to_lookups(items: List[ShoppingListItem], meals_lookup: Dict[int, Any], recipes_lookup: Dict[int, Any], ingredients_lookup: Dict[int, Any]):
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
@ -21,7 +30,15 @@ def remove_references(items: List[ShoppingListItem], meals_lookup = None, recipe
recipes_lookup[item.recipe.id] = item.recipe recipes_lookup[item.recipe.id] = item.recipe
item.recipe = None item.recipe = None
return meals_lookup, recipes_lookup, ingredients_lookup async def _ensure_lookups_populated(conn, items: List[ShoppingListItem], meals_lookup, recipes_lookup, ingredients_lookup):
for item in items:
# If the any item is not in the lookup, we need to add it
if item.meal_id and item.meal_id not in meals_lookup:
meals_lookup[item.meal_id] = await meals.find_meal_by_id(conn, item.meal_id)
if item.recipe_id and item.recipe_id not in recipes_lookup:
recipes_lookup[item.recipe_id] = await recipes.find_recipe_by_id(conn, item.recipe_id)
if item.ingredient_id and item.ingredient_id not in ingredients_lookup:
ingredients_lookup[item.ingredient_id] = await ingredients.find_ingredient_by_id(conn, item.ingredient_id)
async def get_persons_requests(conn, person_id: int) -> AsyncIterator[ShoppingListItem]: async def get_persons_requests(conn, person_id: int) -> AsyncIterator[ShoppingListItem]:
async for item in _find_items_by_list_id(conn, None): async for item in _find_items_by_list_id(conn, None):
@ -44,10 +61,19 @@ async def get_outstanding_requests(conn) -> Tuple[List[ShoppingListItem], List[S
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_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} 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()))} purchased_ingredients = {(r.ingredient_id, r.meal_id, r.recipe_id): r async for r in _get_purchased_ingredients(conn, list(meals.keys()))}
outstanding_items = []
purchased_items = []
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] for r in flattened:
purchased_items = [r for r in flattened if r.ingredient_id in purchased_ingredients] # Meal ingredients may have already been purchased
if r.meal_id is not None and r.meal_id > 0:
purchased_item = purchased_ingredients.get((r.ingredient_id, r.meal_id, r.recipe_id))
if purchased_item:
purchased_items.append(purchased_item)
continue
outstanding_items.append(r)
return outstanding_items, purchased_items, meal_requests return outstanding_items, purchased_items, meal_requests

View file

@ -10,7 +10,7 @@ from typing import AsyncIterator, List, ClassVar, Optional
from datetime import datetime from datetime import datetime
class ShoppingListItem(BaseLinkedModel): class ShoppingListItem(BaseLinkedModel):
KEYS: ClassVar[List[str]] = ['id', 'ingredient_id', 'list_id', 'person_id', 'meal_id', 'created_date'] KEYS: ClassVar[List[str]] = ['id', 'ingredient_id', 'list_id', 'person_id', 'meal_id', 'recipe_id', 'created_date']
id: int = -1 id: int = -1
list_id: Optional[int] = None list_id: Optional[int] = None
@ -151,13 +151,7 @@ async def update_purchased_meals(conn, meal_ids: List[int]) -> None:
remaining_ingredients = ingredients - purchased_ingredient_ids remaining_ingredients = ingredients - purchased_ingredient_ids
if not remaining_ingredients: if not remaining_ingredients:
await mark_purchased(conn, meal) await mark_purchased(conn, meal)
await remove_request(conn, None, meal=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: async def is_requested(conn, meal: Meal) -> bool:
if meal.id < 0: if meal.id < 0:
@ -198,7 +192,7 @@ async def request(conn, person: Person, ingredient: Optional[Ingredient] = None,
return item return item
async def remove_request(conn, person: Person, meal: Optional[Meal] = None, ingredient: Optional[Ingredient] = None) -> bool: async def remove_request(conn, person: Person = None, meal: Optional[Meal] = None, ingredient: Optional[Ingredient] = None) -> bool:
if meal is not None: if meal is not None:
async with conn.execute(''' async with conn.execute('''
DELETE FROM ShoppingListItem DELETE FROM ShoppingListItem