diff --git a/main.py b/main.py index f40dda3..f85ad15 100644 --- a/main.py +++ b/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, Union +from typing import List, Annotated, Optional, Union from fastapi import FastAPI, Depends, Query, Cookie from fastapi.responses import JSONResponse from fastapi.encoders import jsonable_encoder @@ -146,10 +146,10 @@ async def delete_recipe(recipe_id: int, conn: sqlite3.Connection = Depends(get_d await conn.commit() return recipe -@app.get("/meals/") -async def get_meals(date_from: Annotated[datetime.datetime, Query(alias='from')], to: datetime.datetime, conn: sqlite3.Connection = Depends(get_db)) -> List[meals.Meal]: +@app.get("/meals/upcoming") +async def get_upcoming_meals(date_from: Annotated[datetime.datetime, Query(alias='from')], to: datetime.datetime, conn: sqlite3.Connection = Depends(get_db)) -> List[meals.Meal]: result = [] - async for meal in meals.find_meals_by_date_range(conn, date_from, to): + async for meal in meals.find_upcoming_meals_by_date_range(conn, date_from, to): await meals.load_recipes(conn, meal) await meals.load_extra_ingredients(conn, meal) await meals.load_participants(conn, meal) @@ -229,6 +229,20 @@ async def update_meal(meal_id: int, meal: meals.Meal, conn: sqlite3.Connection = return await get_meal(meal_id, conn) +class ConsumedWrapper(BaseModel): + meal_id: int + consumed_date: Optional[datetime.datetime] = None + +@app.post("/meals/{meal_id}/consumed") +async def mark_consumed(body: ConsumedWrapper, conn: sqlite3.Connection = Depends(get_db), person: persons.Person = Depends(cookie_person)) -> meals.Meal: + meal = await meals.find_meal_by_id(conn, body.meal_id) + if not meal: + return JSONResponse(status_code=404, content={'message': 'Meal not found'}) + + await meals.mark_consumed(conn, meal, body.consumed_date or datetime.datetime.now()) + await conn.commit() + return meal + @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 35a47c7..7cfc6f4 100644 --- a/meals/db.py +++ b/meals/db.py @@ -10,10 +10,10 @@ from persons import Person import datetime class Meal(BaseModel): - KEYS: ClassVar[List[str]] = ['id', 'suggested_date'] + KEYS: ClassVar[List[str]] = ['id', 'suggested_date', 'consumed_date'] id: int = -1 suggested_date: datetime.datetime - purchase_date: Optional[datetime.datetime] = None + consumed_date: Optional[datetime.datetime] = None chefs: List[Person] = [] cleanup: List[Person] = [] @@ -21,11 +21,15 @@ class Meal(BaseModel): recipes: List[Recipe] = [] extra_ingredients: List[Ingredient] = [] + # Set from shopping list + purchase_date: Optional[datetime.datetime] = None + async def create(conn): await conn.execute(''' CREATE TABLE IF NOT EXISTS Meal ( id INTEGER PRIMARY KEY, - suggested_date TEXT + suggested_date TEXT, + consumed_date TEXT );''') await conn.execute(''' @@ -99,10 +103,10 @@ async def find_meal_by_id(conn, meal_id: int) -> Meal: await load_extra_ingredients(conn, meal) return meal -async def find_meals_by_date_range(conn, start: datetime, end: datetime) -> AsyncIterator[Meal]: +async def find_upcoming_meals_by_date_range(conn, start: datetime, end: datetime) -> AsyncIterator[Meal]: async with conn.execute(f''' SELECT {','.join(Meal.KEYS)} FROM Meal - WHERE suggested_date >= ? AND suggested_date <= ? + WHERE suggested_date >= ? AND suggested_date <= ? AND consumed_date IS NULL ''', (start, end)) as cursor: async for row in cursor: yield await with_purchase_date(conn, Meal(**{k:v for k,v in zip(Meal.KEYS, row)})) @@ -192,6 +196,15 @@ async def update_meal(conn, meal: Meal) -> None: await sync_extra_ingredients(conn, meal.id, meal.extra_ingredients) await sync_recipes(conn, meal.id, meal.recipes) +async def mark_consumed(conn, meal: Meal, date: datetime.datetime = datetime.datetime.now()) -> None: + meal.consumed_date = date + + await conn.execute(''' + UPDATE Meal + SET consumed_date = ? + WHERE id = ? + ''', (date, meal.id)) + async def with_purchase_date(conn, meal: Meal) -> Meal: async with conn.execute(''' SELECT purchased_date FROM ShoppingList diff --git a/shopping/db.py b/shopping/db.py index 149e9d6..4fa62ca 100644 --- a/shopping/db.py +++ b/shopping/db.py @@ -1,4 +1,4 @@ -from meals import Meal, find_meals_by_date_range, find_meal_by_id +from meals import Meal, find_upcoming_meals_by_date_range, find_meal_by_id from ingredients import Ingredient, insert_ingredient from persons import Person from products import Product @@ -245,7 +245,7 @@ async def load_shopping_list(conn, id: int) -> ShoppingList: async def _upcoming_meals(conn) -> AsyncIterator[Meal]: start = datetime.now() end = start + timedelta(days=7) - async for meal in find_meals_by_date_range(conn, start, end): + async for meal in find_upcoming_meals_by_date_range(conn, start, end): if meal.purchase_date is None: yield meal diff --git a/tests/test_main.py b/tests/test_main.py index 05ef622..0290ded 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -214,7 +214,7 @@ class TestMeals(unittest.IsolatedAsyncioTestCase): self.assertEqual(meal_by_id.extra_ingredients[0].id, extra_ingredient.id) self.assertEqual(meal_by_id.extra_ingredients[0].line, extra_ingredient.line) - meals_by_date_range = await main.get_meals(meal.suggested_date, meal.suggested_date, self.conn) + meals_by_date_range = await main.get_upcoming_meals(meal.suggested_date, meal.suggested_date, self.conn) self.assertIsNotNone(meals_by_date_range) self.assertIsInstance(meals_by_date_range, list, msg=meals_by_date_range.body if hasattr(meals_by_date_range, 'body') else meals_by_date_range) self.assertEqual(len(meals_by_date_range), 1) @@ -230,6 +230,48 @@ 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 testMarkConsumed(self) -> None: + meal = test_data.Meals.broccoli_soup_for_jacob + recipe = meal.recipes[0] + recipe_ingredient = recipe.ingredients[0] + recipe_product = recipe_ingredient.product + extra_ingredient = meal.extra_ingredients[0] + extra_product = extra_ingredient.product + + person = test_data.Persons.jacob + + await products_db.insert_product(self.conn, recipe_product, {}) + await products_db.insert_product(self.conn, extra_product, {}) + 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.assertIsNotNone(create_response) + self.assertIsInstance(create_response, meals.Meal, msg=create_response.body if hasattr(create_response, 'body') else create_response) + + meal_by_id = await main.get_meal(meal.id, self.conn) + self.assertIsNotNone(meal_by_id) + self.assertIsInstance(meal_by_id, meals.Meal, msg=meal_by_id.body if hasattr(meal_by_id, 'body') else meal_by_id) + self.assertIsNone(meal_by_id.consumed_date) + + updated_meal = await main.mark_consumed(main.ConsumedWrapper(meal_id=meal.id), 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.assertIsNotNone(updated_meal.consumed_date) + self.assertLessEqual(datetime.datetime.now() - updated_meal.consumed_date, datetime.timedelta(seconds=1)) + + upcoming_meals = await main.get_upcoming_meals(meal.suggested_date, meal.suggested_date, self.conn) + self.assertIsNotNone(upcoming_meals) + self.assertIsInstance(upcoming_meals, list, msg=upcoming_meals.body if hasattr(upcoming_meals, 'body') else upcoming_meals) + self.assertEqual(len(upcoming_meals), 0) + + meal_by_id = await main.get_meal(meal.id, self.conn) + self.assertIsNotNone(meal_by_id) + self.assertIsInstance(meal_by_id, meals.Meal, msg=meal_by_id.body if hasattr(meal_by_id, 'body') else meal_by_id) + self.assertIsNotNone(meal_by_id.consumed_date) + self.assertLessEqual(datetime.datetime.now() - meal_by_id.consumed_date, datetime.timedelta(seconds=1)) + + async def testUpdate(self) -> None: meal = test_data.Meals.broccoli_soup_for_jacob