From 410b097d5cf9b5568774a2ea3044818379955e15 Mon Sep 17 00:00:00 2001 From: jableader Date: Wed, 30 Jul 2025 18:19:20 +1000 Subject: [PATCH] Fixed purchased item matching --- ingredients/__init__.py | 2 +- ingredients/db.py | 17 ++++++++++++++++- main.py | 6 +++--- shopping/__init__.py | 36 +++++++++++++++++++++++++++++++----- shopping/db.py | 2 +- 5 files changed, 52 insertions(+), 11 deletions(-) diff --git a/ingredients/__init__.py b/ingredients/__init__.py index f19bd10..4713903 100644 --- a/ingredients/__init__.py +++ b/ingredients/__init__.py @@ -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 from products import Product, find_product_by_tag, get_or_create, add_missing_tags diff --git a/ingredients/db.py b/ingredients/db.py index b10dad1..d4d983e 100644 --- a/ingredients/db.py +++ b/ingredients/db.py @@ -37,7 +37,7 @@ async def insert_ingredient(conn, ingredient: Ingredient): if ingredient.product: 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 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.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]: ingredient_keys = [f'ingredient.{key}' for key in Ingredient.KEYS] product_keys = [f'product.{key}' for key in Product.KEYS] diff --git a/main.py b/main.py index 1345e62..a36206f 100644 --- a/main.py +++ b/main.py @@ -270,7 +270,7 @@ async def get_current_shopping_list(conn: sqlite3.Connection = Depends(get_db)) # 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] - meals_lookup, recipes_lookup, ingredients_lookup = shopping.remove_references(items) + meals_lookup, recipes_lookup, ingredients_lookup = await shopping.to_lookups(conn, items) return CurrentShoppingList( outstanding_items=outstanding_requests, @@ -291,7 +291,7 @@ class PurchasedShoppingList(BaseModel): @app.get("/api/shopping/{list_id}") 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) - 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) @app.post("/api/shopping/") @@ -302,7 +302,7 @@ async def purchase_ingredients(shopping_list: shopping.ShoppingList, conn: sqlit await conn.commit() 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 @app.get("/api/shopping/current/me/ingredients") diff --git a/shopping/__init__.py b/shopping/__init__.py index 5443f04..f86c9b5 100644 --- a/shopping/__init__.py +++ b/shopping/__init__.py @@ -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 -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 {} recipes_lookup = recipes_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: if item.meal and not item.meal.id in meals_lookup: 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 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 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)] 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()))} + 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) - 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] + for r in flattened: + # 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 diff --git a/shopping/db.py b/shopping/db.py index f4963d8..76b9015 100644 --- a/shopping/db.py +++ b/shopping/db.py @@ -10,7 +10,7 @@ from typing import AsyncIterator, List, ClassVar, Optional from datetime import datetime 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 list_id: Optional[int] = None