diff --git a/main.py b/main.py index 2c12da9..1ad129d 100644 --- a/main.py +++ b/main.py @@ -191,6 +191,10 @@ def validate_meal(meal : meals.Meal) -> JSONResponse | None: duplicates = get_duplicates(meal.consumers) if duplicates: return JSONResponse(status_code=400, content={'message': f'Duplicate consumer: {", ".join(duplicates)}'}) + + zero_servings = [r for r in meal.recipes if r.servings == 0] + if zero_servings: + return JSONResponse(status_code=400, content={'message': 'Recipe servings must be greater than 0'}) return None diff --git a/meals/db.py b/meals/db.py index 7cfc6f4..d39f1c1 100644 --- a/meals/db.py +++ b/meals/db.py @@ -9,6 +9,13 @@ from persons import Person import datetime +class MealRecipe(BaseModel): + meal_id: int + recipe_id: int + servings: float + + recipe: Optional[Recipe] = None + class Meal(BaseModel): KEYS: ClassVar[List[str]] = ['id', 'suggested_date', 'consumed_date'] id: int = -1 @@ -18,7 +25,7 @@ class Meal(BaseModel): chefs: List[Person] = [] cleanup: List[Person] = [] consumers: List[Person] = [] - recipes: List[Recipe] = [] + recipes: List[MealRecipe] = [] extra_ingredients: List[Ingredient] = [] # Set from shopping list @@ -45,6 +52,7 @@ async def create(conn): CREATE TABLE IF NOT EXISTS MealRecipe ( meal_id INTEGER, recipe_id INTEGER, + servings REAL, FOREIGN KEY(meal_id) REFERENCES Meal(id), FOREIGN KEY(recipe_id) REFERENCES Recipe(id) );''') @@ -64,14 +72,20 @@ async def sync_meal_participants(conn, meal_id: int, participants: List[Person], for person in participants: await insert_meal_participant(conn, meal_id, person.id, role) -async def insert_meal_recipe(conn, meal_id: int, recipe_id: int): - if recipe_id < 0: +async def insert_meal_recipe(conn, r: MealRecipe): + if r.meal_id < 0: + raise ValueError('Meal must be inserted before meal recipe') + + if r.recipe_id < 0 and r.recipe: + r.recipe_id = r.recipe.id + + if r.recipe_id < 0: raise ValueError('Recipe must be inserted before meal') await conn.execute(''' - INSERT INTO MealRecipe (meal_id, recipe_id) - VALUES (?, ?) - ''', (meal_id, recipe_id)) + INSERT INTO MealRecipe (meal_id, recipe_id, servings) + VALUES (?, ?, ?) + ''', (r.meal_id, r.recipe_id, r.servings)) async def insert_meal(conn, meal: Meal): async with conn.execute(''' @@ -84,8 +98,10 @@ async def insert_meal(conn, meal: Meal): await sync_meal_participants(conn, meal.id, meal.cleanup, 'cleanup') await sync_meal_participants(conn, meal.id, meal.consumers, 'consumer') - for recipe in meal.recipes: - await insert_meal_recipe(conn, meal.id, recipe.id) + for meal_recipe in meal.recipes: + meal_recipe.meal_id = meal.id + + await insert_meal_recipe(conn, meal_recipe) await sync_extra_ingredients(conn, meal.id, meal.extra_ingredients) @@ -129,15 +145,16 @@ async def load_participants(conn, meal: Meal) -> None: async def load_recipes(conn, meal: Meal) -> None: async with conn.execute(f''' - SELECT {','.join(Recipe.KEYS)} FROM Recipe + SELECT {','.join(Recipe.KEYS)}, MealRecipe.servings as requested_servings + FROM Recipe JOIN MealRecipe ON MealRecipe.recipe_id = Recipe.id WHERE MealRecipe.meal_id = ? ''', (meal.id,)) as cursor: async for row in cursor: - recipe = row_to_recipe(zip(Recipe.KEYS, row)) + recipe = row_to_recipe(zip(Recipe.KEYS, row[:-1])) await load_recipe_ingredients(conn, recipe) - meal.recipes.append(recipe) + meal.recipes.append(MealRecipe(meal_id=meal.id, recipe_id=recipe.id, servings=row[-1], recipe=recipe)) async def load_extra_ingredients(conn, meal: Meal) -> None: async for ingredient in find_ingredients_by_meal_id(conn, meal.id): @@ -173,14 +190,18 @@ async def sync_extra_ingredients(conn, meal_id: int, ingredients: List[Ingredien await insert_ingredient(conn, ingredient) -async def sync_recipes(conn, meal_id: int, recipes: List[Recipe]) -> None: +async def sync_meal_recipes(conn, meal_id: int, recipes: List[Recipe]) -> None: await conn.execute(''' DELETE FROM MealRecipe WHERE meal_id = ? ''', (meal_id,)) - for recipe in recipes: - await insert_meal_recipe(conn, meal_id, recipe.id) + for meal_recipe in recipes: + if meal_recipe.meal_id >= 0 and meal_recipe.meal_id != meal_id: + raise ValueError('Already associated with another meal') + + meal_recipe.meal_id = meal_id + await insert_meal_recipe(conn, meal_recipe) async def update_meal(conn, meal: Meal) -> None: await conn.execute(''' @@ -194,7 +215,7 @@ async def update_meal(conn, meal: Meal) -> None: await sync_meal_participants(conn, meal.id, meal.consumers, 'consumer') await sync_extra_ingredients(conn, meal.id, meal.extra_ingredients) - await sync_recipes(conn, meal.id, meal.recipes) + await sync_meal_recipes(conn, meal.id, meal.recipes) async def mark_consumed(conn, meal: Meal, date: datetime.datetime = datetime.datetime.now()) -> None: meal.consumed_date = date diff --git a/tests/test_data.py b/tests/test_data.py index cba0aae..7526e1f 100644 --- a/tests/test_data.py +++ b/tests/test_data.py @@ -235,7 +235,7 @@ class Meals: chefs=[Persons.jacob], cleanup=[Persons.ryan], consumers=[Persons.ellie, Persons.chris], - recipes=[Recipes.broccoli_soup], + recipes=[meals_db.MealRecipe(meal_id = -1, recipe_id = -1, servings = 2, recipe = Recipes.broccoli_soup)], extra_ingredients=[Ingredients.garlic_bread_1_loaf], ) diff --git a/tests/test_main.py b/tests/test_main.py index d13dca0..01fff08 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -126,7 +126,7 @@ class TestMeals(unittest.IsolatedAsyncioTestCase): async def testMultiplePariticpants(self) -> None: meal = test_data.Meals.broccoli_soup_for_jacob - recipe = meal.recipes[0] + recipe = meal.recipes[0].recipe recipe_ingredient = recipe.ingredients[0] recipe_product = recipe_ingredient.product extra_ingredient = meal.extra_ingredients[0] @@ -168,7 +168,8 @@ class TestMeals(unittest.IsolatedAsyncioTestCase): async def testCreateAndFind(self) -> None: meal = test_data.Meals.broccoli_soup_for_jacob - recipe = meal.recipes[0] + meal_recipe = meal.recipes[0] + recipe = meal_recipe.recipe recipe_ingredient = recipe.ingredients[0] recipe_product = recipe_ingredient.product extra_ingredient = meal.extra_ingredients[0] @@ -204,13 +205,17 @@ class TestMeals(unittest.IsolatedAsyncioTestCase): actual = [p.id for p in meal_by_id.consumers] self.assertEqual(sorted(expected), sorted(actual)) - self.assertEqual(meal_by_id.recipes[0].id, recipe.id) - self.assertEqual(meal_by_id.recipes[0].name, recipe.name) - self.assertEqual(meal_by_id.recipes[0].link, recipe.link) - self.assertEqual(meal_by_id.recipes[0].image_urls, recipe.image_urls) - self.assertEqual(len(meal_by_id.recipes[0].ingredients), 1) - self.assertEqual(meal_by_id.recipes[0].ingredients[0].id, recipe_ingredient.id) - self.assertEqual(meal_by_id.recipes[0].ingredients[0].line, recipe_ingredient.line) + self.assertEqual(len(meal_by_id.recipes), 1) + self.assertEqual(meal_by_id.recipes[0].servings, meal_recipe.servings) + + self.assertIsNotNone(meal_by_id.recipes[0].recipe) + self.assertEqual(meal_by_id.recipes[0].recipe.id, recipe.id) + self.assertEqual(meal_by_id.recipes[0].recipe.name, recipe.name) + self.assertEqual(meal_by_id.recipes[0].recipe.link, recipe.link) + self.assertEqual(meal_by_id.recipes[0].recipe.image_urls, recipe.image_urls) + self.assertEqual(len(meal_by_id.recipes[0].recipe.ingredients), 1) + self.assertEqual(meal_by_id.recipes[0].recipe.ingredients[0].id, recipe_ingredient.id) + self.assertEqual(meal_by_id.recipes[0].recipe.ingredients[0].line, recipe_ingredient.line) self.assertEqual(meal_by_id.extra_ingredients[0].id, extra_ingredient.id) self.assertEqual(meal_by_id.extra_ingredients[0].line, extra_ingredient.line) @@ -227,12 +232,13 @@ class TestMeals(unittest.IsolatedAsyncioTestCase): self.assertEqual(meals_by_date_range[0].cleanup[0].id, meal.cleanup[0].id) self.assertEqual(meals_by_date_range[0].cleanup[0].name, meal.cleanup[0].name) self.assertEqual(len(meals_by_date_range[0].consumers), 2) - self.assertEqual(meals_by_date_range[0].recipes[0].id, recipe.id) - self.assertEqual(meals_by_date_range[0].recipes[0].name, recipe.name) + self.assertEqual(meals_by_date_range[0].recipes[0].recipe.id, recipe.id) + self.assertEqual(meals_by_date_range[0].recipes[0].recipe.name, recipe.name) + self.assertEqual(meals_by_date_range[0].recipes[0].servings, meal_recipe.servings) async def testMarkConsumed(self) -> None: meal = test_data.Meals.broccoli_soup_for_jacob - recipe = meal.recipes[0] + recipe = meal.recipes[0].recipe recipe_ingredient = recipe.ingredients[0] recipe_product = recipe_ingredient.product extra_ingredient = meal.extra_ingredients[0] @@ -276,15 +282,15 @@ class TestMeals(unittest.IsolatedAsyncioTestCase): meal = test_data.Meals.broccoli_soup_for_jacob person = test_data.Persons.jacob - recipes = meal.recipes + [test_data.Recipes.how_to_steam_green_beans] + recipes = [r.recipe for r in meal.recipes] + [test_data.Recipes.how_to_steam_green_beans] ingredients = meal.extra_ingredients + [ingredient for recipe in recipes for ingredient in recipe.ingredients] + [test_data.Ingredients.butter] products = [ingredient.product for ingredient in ingredients] for product in unique(products, lambda p: p.name): await products_db.insert_product(self.conn, product, {}) - for recipe in recipes: - created_recipe = await main.create_recipe(recipe, self.conn, person) - recipe.id = created_recipe.id + for mr in recipes: + created_recipe = await main.create_recipe(mr, self.conn, person) + mr.id = created_recipe.id create_response = await main.create_meal(meal, self.conn) self.assertIsInstance(create_response, meals.Meal, msg=create_response.body if hasattr(create_response, 'body') else create_response) @@ -294,7 +300,7 @@ class TestMeals(unittest.IsolatedAsyncioTestCase): self.assertIsInstance(saved_meal, meals.Meal, msg=saved_meal.body if hasattr(saved_meal, 'body') else saved_meal) saved_meal.suggested_date = datetime.datetime.fromisoformat('2021-01-01T12:00:00') - saved_meal.recipes = [test_data.Recipes.how_to_steam_green_beans] + saved_meal.recipes = [meals.MealRecipe(recipe_id=-1, meal_id=-1, recipe=test_data.Recipes.how_to_steam_green_beans, servings=4)] saved_meal.extra_ingredients.append(test_data.Ingredients.butter) saved_meal.chefs.append(test_data.Persons.ryan) saved_meal.cleanup = [test_data.Persons.chris] @@ -311,13 +317,14 @@ class TestMeals(unittest.IsolatedAsyncioTestCase): self.assertPersons(updated_meal.consumers, saved_meal.consumers) self.assertEqual(len(updated_meal.recipes), 1) - self.assertEqual(updated_meal.recipes[0].id, test_data.Recipes.how_to_steam_green_beans.id) - self.assertEqual(updated_meal.recipes[0].name, test_data.Recipes.how_to_steam_green_beans.name) - self.assertEqual(updated_meal.recipes[0].link, test_data.Recipes.how_to_steam_green_beans.link) - self.assertEqual(updated_meal.recipes[0].image_urls, test_data.Recipes.how_to_steam_green_beans.image_urls) + self.assertEqual(updated_meal.recipes[0].servings, 4) + self.assertEqual(updated_meal.recipes[0].recipe.id, test_data.Recipes.how_to_steam_green_beans.id) + self.assertEqual(updated_meal.recipes[0].recipe.name, test_data.Recipes.how_to_steam_green_beans.name) + self.assertEqual(updated_meal.recipes[0].recipe.link, test_data.Recipes.how_to_steam_green_beans.link) + self.assertEqual(updated_meal.recipes[0].recipe.image_urls, test_data.Recipes.how_to_steam_green_beans.image_urls) expected = [i.name for i in test_data.Recipes.how_to_steam_green_beans.ingredients] - actual = [i.name for i in updated_meal.recipes[0].ingredients] + actual = [i.name for i in updated_meal.recipes[0].recipe.ingredients] self.assertEqual(sorted(expected), sorted(actual)) expected = [i.name for i in saved_meal.extra_ingredients] diff --git a/tests/test_shopping.py b/tests/test_shopping.py index c41d209..a992758 100644 --- a/tests/test_shopping.py +++ b/tests/test_shopping.py @@ -38,12 +38,13 @@ class TestShopping(unittest.IsolatedAsyncioTestCase): import meals, recipes meal = test_data.Meals.broccoli_soup_for_jacob - for product in [i.product for i in meal.extra_ingredients] + [i.product for r in meal.recipes for i in r.ingredients]: + for product in [i.product for i in meal.extra_ingredients] + [i.product for r in meal.recipes for i in r.recipe.ingredients]: if product.id < 0: await products.insert_product(self.conn, product, {}) - for recipe in meal.recipes: - await recipes.insert_recipe(self.conn, recipe) + for mr in meal.recipes: + await recipes.insert_recipe(self.conn, mr.recipe) + mr.recipe_id = mr.recipe.id meal.suggested_date = datetime.now() + timedelta(days=1) await meals.insert_meal(self.conn, meal)