diff --git a/ingredients/__init__.py b/ingredients/__init__.py index a88d3df..4d28b9c 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 +from ingredients.db import Ingredient, 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 diff --git a/ingredients/db.py b/ingredients/db.py index 08f781a..6e8b14e 100644 --- a/ingredients/db.py +++ b/ingredients/db.py @@ -73,4 +73,10 @@ async def find_ingredients_by_meal_id(conn, meal_id: int) -> List[Ingredient]: 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 - yield Ingredient(**{k:v for k,v in zip(Ingredient.KEYS, row[:len(Ingredient.KEYS)])}, product=product) \ No newline at end of file + yield Ingredient(**{k:v for k,v in zip(Ingredient.KEYS, row[:len(Ingredient.KEYS)])}, product=product) + +async def delete_ingredients_by_meal_id(conn, meal_id: int): + await conn.execute(''' + DELETE FROM Ingredient + WHERE meal_id = ? + ''', (meal_id,)) \ No newline at end of file diff --git a/main.py b/main.py index cf104f4..fc5edd0 100644 --- a/main.py +++ b/main.py @@ -160,6 +160,20 @@ async def create_meal(meal: meals.Meal, conn: sqlite3.Connection = Depends(get_d await conn.commit() return meal +@app.put("/meals/{meal_id}") +async def update_meal(meal_id: int, meal: meals.Meal, conn: sqlite3.Connection = Depends(get_db)) -> meals.Meal: + if meal.id != meal_id: + return JSONResponse(status_code=400, content={'message': 'Meal ID in URL does not match meal ID in body'}) + + existing = await meals.find_meal_by_id(conn, meal_id) + if not existing: + return JSONResponse(status_code=404, content={'message': 'Meal not found'}) + + await meals.update_meal(conn, meal) + await conn.commit() + + return await get_meal(meal_id, conn) + @app.delete("/meals/{meal_id}") async def delete_meal(meal_id: int, conn: sqlite3.Connection = Depends(get_db)) -> meals.Meal: meal = await meals.find_meal_by_id(conn, meal_id) diff --git a/meals/db.py b/meals/db.py index aaaefd3..3fe039d 100644 --- a/meals/db.py +++ b/meals/db.py @@ -1,6 +1,6 @@ from typing import List, ClassVar, Optional from pydantic import BaseModel -from ingredients import Ingredient, insert_ingredient, find_ingredients_by_meal_id +from ingredients import Ingredient, insert_ingredient, find_ingredients_by_meal_id, delete_ingredients_by_meal_id from recipes import Recipe, row_to_recipe, load_recipe_ingredients @@ -50,6 +50,15 @@ async def insert_meal_participant(conn, meal_id: int, person_id: int, role: str) VALUES (?, ?, ?) ''', (meal_id, person_id, role)) +async def sync_meal_participants(conn, meal_id: int, participants: List[Person], role: str): + await conn.execute(''' + DELETE FROM MealParticipant + WHERE meal_id = ? AND role = ? + ''', (meal_id, role)) + + 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 not recipe_id: raise ValueError('Recipe must be inserted before meal') @@ -66,21 +75,14 @@ async def insert_meal(conn, meal: Meal): ''', (meal.meal_date,)) as cursor: meal.id = cursor.lastrowid - for person in meal.chefs: - await insert_meal_participant(conn, meal.id, person.id, 'chef') - - for person in meal.cleanup: - await insert_meal_participant(conn, meal.id, person.id, 'cleanup') - - for person in meal.consumers: - await insert_meal_participant(conn, meal.id, person.id, 'consumer') + await sync_meal_participants(conn, meal.id, meal.chefs, 'chef') + 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 ingredient in meal.extra_ingredients: - ingredient.meal_id = meal.id - await insert_ingredient(conn, ingredient) + await sync_extra_ingredients(conn, meal.id, meal.extra_ingredients) async def find_meal_by_id(conn, meal_id: int) -> Meal: async with conn.execute(f''' @@ -161,4 +163,36 @@ async def delete_meal(conn, meal_id: int) -> None: await conn.execute(''' DELETE FROM Meal WHERE id = ? - ''', (meal_id,)) \ No newline at end of file + ''', (meal_id,)) + +async def sync_extra_ingredients(conn, meal_id: int, ingredients: List[Ingredient]) -> None: + await delete_ingredients_by_meal_id(conn, meal_id) + + for ingredient in ingredients: + ingredient.meal_id = meal_id + ingredient.recipe_id = None + + await insert_ingredient(conn, ingredient) + +async def sync_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) + +async def update_meal(conn, meal: Meal) -> None: + await conn.execute(''' + UPDATE Meal + SET meal_date = ? + WHERE id = ? + ''', (meal.meal_date, meal.id)) + + await sync_meal_participants(conn, meal.id, meal.chefs, 'chef') + await sync_meal_participants(conn, meal.id, meal.cleanup, 'cleanup') + 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) diff --git a/tests.py b/tests.py index 8e501e1..3689d73 100644 --- a/tests.py +++ b/tests.py @@ -1,5 +1,6 @@ import unittest import test_data +import datetime import pathlib from db import connect, create @@ -7,6 +8,14 @@ from db import connect, create import products.db as products_db import recipes.db as recipes_db +def unique(lst: list, key: callable): + seen = set() + for item in lst: + k = key(item) + if k not in seen: + seen.add(k) + yield item + class TestProducts(unittest.IsolatedAsyncioTestCase): async def asyncSetUp(self): self.conn = await connect('./testdb.db') @@ -162,5 +171,62 @@ class TestMeals(unittest.IsolatedAsyncioTestCase): self.assertEqual(meals_by_date_range[0].recipes[0].id, recipe.id) self.assertEqual(meals_by_date_range[0].recipes[0].name, recipe.name) + async def testUpdate(self) -> None: + meal = test_data.Meals.broccoli_soup_for_jacob + + person = test_data.Persons.jacob + recipes = 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 + + 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) + + saved_meal = await main.get_meal(meal.id, self.conn) + self.assertIsNotNone(saved_meal) + self.assertIsInstance(saved_meal, meals.Meal, msg=saved_meal.body if hasattr(saved_meal, 'body') else saved_meal) + + saved_meal.meal_date = datetime.datetime.fromisoformat('2021-01-01T12:00:00') + saved_meal.recipes = [test_data.Recipes.how_to_steam_green_beans] + saved_meal.extra_ingredients.append(test_data.Ingredients.butter) + saved_meal.chefs.append(test_data.Persons.ryan) + saved_meal.cleanup = [test_data.Persons.chris] + saved_meal.consumers = [test_data.Persons.ellie, test_data.Persons.jacob, test_data.Persons.ryan] + + updated_meal = await main.update_meal(saved_meal.id, saved_meal, self.conn) + self.assertIsNotNone(updated_meal) + self.assertIsInstance(updated_meal, meals.Meal, msg=updated_meal.body if hasattr(updated_meal, 'body') else updated_meal) + + self.assertEqual(updated_meal.id, saved_meal.id) + self.assertEqual(updated_meal.meal_date, saved_meal.meal_date) + self.assertPersons(updated_meal.chefs, saved_meal.chefs) + self.assertPersons(updated_meal.cleanup, saved_meal.cleanup) + 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) + + 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] + self.assertEqual(sorted(expected), sorted(actual)) + + expected = [i.name for i in saved_meal.extra_ingredients] + actual = [i.name for i in updated_meal.extra_ingredients] + self.assertEqual(sorted(expected), sorted(actual)) + + def assertPersons(self, expected, actual): + expected_names = [p.name for p in expected] + actual_names = [p.name for p in actual] + self.assertEqual(sorted(expected_names), sorted(actual_names)) + if __name__ == '__main__': unittest.main() \ No newline at end of file