Many misc changes for shopping pages
This commit is contained in:
parent
ac34da7824
commit
be1d2b75ad
7 changed files with 169 additions and 51 deletions
14
db.py
14
db.py
|
|
@ -21,3 +21,17 @@ async def create(conn: aiosqlite.Connection):
|
|||
|
||||
import shopping.db as shopping_db
|
||||
await shopping_db.create(conn)
|
||||
|
||||
if __name__ == '__main__':
|
||||
import asyncio
|
||||
from tests.test_data import create_test_data
|
||||
|
||||
async def main():
|
||||
conn = await connect()
|
||||
await create(conn)
|
||||
await conn.commit()
|
||||
await create_test_data(conn)
|
||||
await conn.commit()
|
||||
await conn.close()
|
||||
|
||||
asyncio.run(main())
|
||||
55
main.py
55
main.py
|
|
@ -1,9 +1,9 @@
|
|||
import sqlite3
|
||||
import products, recipes, db as db, meals, persons, ingredients
|
||||
import products, recipes, db, meals, persons, ingredients, shopping
|
||||
import datetime
|
||||
|
||||
from pydantic import BaseModel
|
||||
from typing import List, Annotated
|
||||
from typing import List, Annotated, Union
|
||||
from fastapi import FastAPI, Depends, Query, Cookie
|
||||
from fastapi.responses import JSONResponse
|
||||
from fastapi.encoders import jsonable_encoder
|
||||
|
|
@ -226,6 +226,57 @@ async def delete_meal(meal_id: int, conn: sqlite3.Connection = Depends(get_db))
|
|||
await conn.commit()
|
||||
return meal
|
||||
|
||||
@app.get("/shopping/{list_id}")
|
||||
async def get_shopping_list(list_id: Union[int, str], conn: sqlite3.Connection = Depends(get_db)) -> shopping.ShoppingList:
|
||||
if isinstance(list_id, str):
|
||||
if list_id.lower() != 'current':
|
||||
return JSONResponse(status_code=400, content={'message': 'Invalid shopping list ID'})
|
||||
|
||||
return await shopping.current_shopping_list(conn)
|
||||
|
||||
return await shopping.load_shopping_list(conn, list_id)
|
||||
|
||||
@app.post("/shopping/current/found")
|
||||
async def mark_shopping_list(ingredient: ingredients.Ingredient, conn: sqlite3.Connection = Depends(get_db)) -> shopping.ShoppingListResult:
|
||||
return await shopping.mark_found(conn, ingredient.product, ingredient.quantity, ingredient.unit)
|
||||
|
||||
@app.get("/shopping/current/me/ingredients")
|
||||
async def get_my_shopping_list(conn: sqlite3.Connection = Depends(get_db), person: persons.Person = Depends(cookie_person)) -> List[shopping.ShoppingListRequest]:
|
||||
current = await shopping.current_shopping_list(conn)
|
||||
return [r async for r in shopping.get_persons_requests(conn, current, person) if r.ingredient]
|
||||
|
||||
@app.post("/shopping/current/me/ingredients")
|
||||
async def sync_my_shopping_list(requests: List[ingredients.Ingredient], conn: sqlite3.Connection = Depends(get_db), person: persons.Person = Depends(cookie_person)) -> List[shopping.ShoppingListRequest]:
|
||||
current = await shopping.current_shopping_list(conn)
|
||||
result = [r async for r in shopping.sync_persons_requested_ingredients(conn, current, person, requests) if r.ingredient]
|
||||
await conn.commit()
|
||||
return result
|
||||
|
||||
class MealIdWrapper(BaseModel):
|
||||
meal_id: int
|
||||
|
||||
@app.post("/shopping/current/meals/me")
|
||||
async def request_meal(r: MealIdWrapper, conn: sqlite3.Connection = Depends(get_db), person: persons.Person = Depends(cookie_person)) -> shopping.ShoppingListRequest:
|
||||
current = await shopping.current_shopping_list(conn)
|
||||
meal = await meals.find_meal_by_id(conn, r.meal_id)
|
||||
if not meal:
|
||||
return JSONResponse(status_code=404, content={'message': 'Meal not found'})
|
||||
|
||||
response = await shopping.request_meal(conn, current, person, meal)
|
||||
await conn.commit()
|
||||
return response
|
||||
|
||||
@app.delete("/shopping/current/meals/{meal_id}")
|
||||
async def unrequest_meal(meal_id: int, conn: sqlite3.Connection = Depends(get_db), person: persons.Person = Depends(cookie_person)) -> dict:
|
||||
current = await shopping.current_shopping_list(conn)
|
||||
meal = await meals.find_meal_by_id(conn, meal_id)
|
||||
if not meal:
|
||||
return JSONResponse(status_code=404, content={'message': 'Meal not found'})
|
||||
|
||||
await shopping.delete_requests(conn, current, meal)
|
||||
await conn.commit()
|
||||
return {}
|
||||
|
||||
@app.get("/persons/")
|
||||
async def get_persons(q: str = None, conn: sqlite3.Connection = Depends(get_db)) -> List[meals.Person]:
|
||||
query = persons.search_by_name(conn, q) if q else persons.get_all(conn)
|
||||
|
|
|
|||
|
|
@ -91,7 +91,11 @@ async def find_meal_by_id(conn, meal_id: int) -> Meal:
|
|||
LIMIT 1
|
||||
''', (meal_id,)) as cursor:
|
||||
async for row in cursor:
|
||||
return Meal(**{k:v for k,v in zip(Meal.KEYS, row)})
|
||||
meal = Meal(**{k:v for k,v in zip(Meal.KEYS, row)})
|
||||
await load_participants(conn, meal)
|
||||
await load_recipes(conn, meal)
|
||||
await load_extra_ingredients(conn, meal)
|
||||
return meal
|
||||
|
||||
async def find_meal_by_date(conn, date: datetime) -> Meal:
|
||||
async with conn.execute(f'''
|
||||
|
|
|
|||
|
|
@ -1,2 +1,2 @@
|
|||
from shopping.db import ShoppingList, ShoppingListRequest, ShoppingListResult, current_shopping_list, mark_found, sync_persons_requests
|
||||
from shopping.db import ShoppingList, ShoppingListRequest, ShoppingListResult, current_shopping_list, mark_found, sync_persons_requested_ingredients, load_shopping_list, get_persons_requests, request_meal, delete_requests
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
from meals import Meal
|
||||
from meals import Meal, find_meals_by_date_range, find_meal_by_id
|
||||
from ingredients import Ingredient, insert_ingredient
|
||||
from persons import Person
|
||||
from products import Product
|
||||
|
|
@ -6,14 +6,14 @@ from products import Product
|
|||
from pydantic import BaseModel
|
||||
from typing import AsyncIterator, List, ClassVar, Optional
|
||||
|
||||
from datetime import datetime
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
class ShoppingListRequest(BaseModel):
|
||||
KEYS: ClassVar[List[str]] = ['id', 'ingredient_id', 'list_id', 'person_id', 'meal_id', 'created_date']
|
||||
id: int = 0
|
||||
list_id: int
|
||||
|
||||
ingredient_id: int
|
||||
ingredient_id: Optional[int] = None
|
||||
ingredient: Optional[Ingredient] = None
|
||||
|
||||
person_id: Optional[int] = None
|
||||
|
|
@ -104,6 +104,35 @@ async def insert_shopping_list(conn, shopping_list: ShoppingList):
|
|||
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||
''', (item.id, item.product_id, shopping_list.id, item.quantity, item.unit, item.created_date, item.found_date))
|
||||
|
||||
async def find_request(conn, id: int) -> Optional[ShoppingListRequest]:
|
||||
# Join Ingredient and Product to also load ingredient and product
|
||||
ingredient_keys = [f'ingredient.{key}' for key in Ingredient.KEYS]
|
||||
product_keys = [f'product.{key}' for key in Product.KEYS]
|
||||
request_keys = [f'shoppinglistrequest.{key}' for key in ShoppingListRequest.KEYS]
|
||||
person_keys = [f'person.{key}' for key in Person.KEYS]
|
||||
|
||||
async with conn.execute(f'''
|
||||
SELECT {','.join(ingredient_keys + product_keys + request_keys + person_keys)}
|
||||
FROM ShoppingListRequest
|
||||
LEFT JOIN Ingredient ON ShoppingListRequest.ingredient_id = Ingredient.id
|
||||
LEFT JOIN Product ON Ingredient.product_id = Product.id
|
||||
LEFT JOIN Person ON ShoppingListRequest.person_id = Person.id
|
||||
WHERE ShoppingListRequest.id = ?
|
||||
''', (id,)) as cursor:
|
||||
async for row in cursor:
|
||||
product_keys = {k:v for k,v in zip(Product.KEYS, row[len(Ingredient.KEYS):len(Ingredient.KEYS) + len(Product.KEYS)])}
|
||||
product = Product(**product_keys) if product_keys['id'] else None
|
||||
|
||||
ingredient_keys = {k:v for k,v in zip(Ingredient.KEYS, row[:len(Ingredient.KEYS)])}
|
||||
ingredient = Ingredient(**ingredient_keys, product=product)
|
||||
|
||||
person_keys = {k:v for k,v in zip(Person.KEYS, row[-len(Person.KEYS):])}
|
||||
person = Person(**person_keys) if person_keys['id'] else None
|
||||
|
||||
request_keys = {k:v for k,v in zip(ShoppingListRequest.KEYS, row[len(Ingredient.KEYS) + len(Product.KEYS):-len(Person.KEYS)])}
|
||||
request = ShoppingListRequest(**request_keys, ingredient=ingredient, person=person)
|
||||
return request
|
||||
|
||||
async def find_requests_by_list_id(conn, list_id: int) -> AsyncIterator[ShoppingListRequest]:
|
||||
# Join Ingredient and Product to also load ingredient and product
|
||||
ingredient_keys = [f'ingredient.{key}' for key in Ingredient.KEYS]
|
||||
|
|
@ -125,13 +154,17 @@ async def find_requests_by_list_id(conn, list_id: int) -> AsyncIterator[Shopping
|
|||
product = Product(**product_keys) if product_keys['id'] else None
|
||||
|
||||
ingredient_keys = {k:v for k,v in zip(Ingredient.KEYS, row[:len(Ingredient.KEYS)])}
|
||||
ingredient = Ingredient(**ingredient_keys, product=product)
|
||||
ingredient = Ingredient(**ingredient_keys, product=product) if ingredient_keys['id'] else None
|
||||
|
||||
person_keys = {k:v for k,v in zip(Person.KEYS, row[-len(Person.KEYS):])}
|
||||
person = Person(**person_keys) if person_keys['id'] else None
|
||||
|
||||
request_keys = {k:v for k,v in zip(ShoppingListRequest.KEYS, row[len(Ingredient.KEYS) + len(Product.KEYS):-len(Person.KEYS)])}
|
||||
request = ShoppingListRequest(**request_keys, ingredient=ingredient, person=person)
|
||||
|
||||
if request.meal_id:
|
||||
request.meal = await find_meal_by_id(conn, request.meal_id)
|
||||
|
||||
yield request
|
||||
|
||||
async def find_items_by_list_id(conn, list_id: int) -> AsyncIterator[ShoppingListResult]:
|
||||
|
|
@ -175,6 +208,12 @@ async def load_shopping_list(conn, id: int) -> ShoppingList:
|
|||
|
||||
return shopping_list
|
||||
|
||||
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):
|
||||
yield meal
|
||||
|
||||
async def current_shopping_list(conn) -> ShoppingList:
|
||||
shopping_list = None
|
||||
async with conn.execute(f'''
|
||||
|
|
@ -189,6 +228,10 @@ async def current_shopping_list(conn) -> ShoppingList:
|
|||
|
||||
if not shopping_list:
|
||||
shopping_list = ShoppingList()
|
||||
|
||||
async for meal in _upcoming_meals(conn):
|
||||
shopping_list.requests.append(ShoppingListRequest(list_id=shopping_list.id, meal_id=meal.id, created_date=datetime.now(),))
|
||||
|
||||
await insert_shopping_list(conn, shopping_list)
|
||||
|
||||
return shopping_list
|
||||
|
|
@ -209,13 +252,13 @@ async def get_persons_requests(conn, shopping_list: ShoppingList, person: Person
|
|||
if request.person_id == person.id:
|
||||
yield request
|
||||
|
||||
async def request_ingredient(conn, shopping_list: ShoppingList, person: Person, ingredient: Ingredient):
|
||||
async def request_ingredient(conn, shopping_list: ShoppingList, person: Person, ingredient: Ingredient) -> ShoppingListRequest:
|
||||
if ingredient.id:
|
||||
raise ValueError('How did you get an existing ingredient?')
|
||||
|
||||
await insert_ingredient(conn, ingredient)
|
||||
|
||||
request = ShoppingListRequest(ingredient_id=ingredient.id, list_id=shopping_list.id, person_id=person.id, created_date=datetime.now())
|
||||
request = ShoppingListRequest(ingredient_id=ingredient.id, ingredient=ingredient, list_id=shopping_list.id, person_id=person.id, created_date=datetime.now())
|
||||
async with conn.execute('''
|
||||
INSERT INTO ShoppingListRequest (ingredient_id, list_id, person_id, created_date)
|
||||
VALUES (?, ?, ?, ?)
|
||||
|
|
@ -223,18 +266,35 @@ async def request_ingredient(conn, shopping_list: ShoppingList, person: Person,
|
|||
request.id = cursor.lastrowid
|
||||
|
||||
shopping_list.requests.append(request)
|
||||
return request
|
||||
|
||||
async def sync_persons_requests(conn, shopping_list: ShoppingList, person: Person, requests: List[Ingredient]):
|
||||
# Delete existing and insert all as new
|
||||
async for request in get_persons_requests(conn, shopping_list, person):
|
||||
async def request_meal(conn, shopping_list: ShoppingList, person: Person, meal: Meal) -> ShoppingListRequest:
|
||||
request = ShoppingListRequest(meal_id=meal.id, meal=meal, list_id=shopping_list.id, person_id=person.id, created_date=datetime.now())
|
||||
|
||||
async with conn.execute('''
|
||||
INSERT INTO ShoppingListRequest (meal_id, list_id, person_id, created_date)
|
||||
VALUES (?, ?, ?, ?)
|
||||
''', (request.meal_id, request.list_id, request.person_id, request.created_date)) as cursor:
|
||||
request.id = cursor.lastrowid
|
||||
|
||||
return request
|
||||
|
||||
async def delete_requests(conn, shopping_list: ShoppingList, meal: Meal) -> None:
|
||||
await conn.execute('''
|
||||
DELETE FROM ShoppingListRequest
|
||||
WHERE id = ?
|
||||
''', (request.id,))
|
||||
WHERE list_id = ? AND meal_id = ?
|
||||
''', (shopping_list.id, meal.id))
|
||||
|
||||
async def sync_persons_requested_ingredients(conn, shopping_list: ShoppingList, person: Person, requests: List[Ingredient]) -> AsyncIterator[ShoppingListRequest]:
|
||||
# Delete existing and insert all as new
|
||||
await conn.execute('''
|
||||
DELETE FROM ShoppingListRequest
|
||||
WHERE list_id = ? AND person_id = ? AND ingredient_id IS NOT NULL
|
||||
''', (shopping_list.id, person.id))
|
||||
|
||||
for ingredient in requests:
|
||||
ingredient.id = 0
|
||||
await request_ingredient(conn, shopping_list, person, ingredient)
|
||||
yield await request_ingredient(conn, shopping_list, person, ingredient)
|
||||
|
||||
async def mark_found(conn, product: Product, quantity: float, unit: str) -> ShoppingListResult:
|
||||
shopping_list = await current_shopping_list(conn)
|
||||
|
|
|
|||
|
|
@ -83,7 +83,7 @@ class Products:
|
|||
apple = products.Product(
|
||||
id=0,
|
||||
name="Apple",
|
||||
product_id="0",
|
||||
product_id="3542",
|
||||
link="https://www.woolworths.com.au/shop/productdetails/0/apple",
|
||||
img_small="https://cdn0.woolworths.media/content/wowproductimages/small/0.jpg",
|
||||
img_large="https://cdn0.woolworths.media/content/wowproductimages/large/0.jpg",
|
||||
|
|
@ -93,7 +93,7 @@ class Products:
|
|||
banana = products.Product(
|
||||
id=0,
|
||||
name="Banana",
|
||||
product_id="0",
|
||||
product_id="214",
|
||||
link="https://www.woolworths.com.au/shop/productdetails/0/banana",
|
||||
img_small="https://cdn0.woolworths.media/content/wowproductimages/small/0.jpg",
|
||||
img_large="https://cdn0.woolworths.media/content/wowproductimages/large/0.jpg",
|
||||
|
|
@ -101,6 +101,8 @@ class Products:
|
|||
)
|
||||
|
||||
_tags = {
|
||||
apple.product_id: ['apple', 'fruit', 'fresh fruit'],
|
||||
banana.product_id: ['banana', 'fruit', 'fresh fruit'],
|
||||
broccoli.product_id: ['broccoli', 'fresh broccoli'],
|
||||
garlic_bread.product_id: ['garlic bread', 'bread', 'garlic', 'frozen garlic bread'],
|
||||
beans_round.product_id: ['beans', 'green beans', 'fresh green beans', 'fresh beans'],
|
||||
|
|
@ -226,15 +228,7 @@ async def create_persons(conn):
|
|||
for person in class_fields(Persons).values():
|
||||
await persons.insert_person(conn, person)
|
||||
|
||||
if __name__ == '__main__':
|
||||
import asyncio
|
||||
from db import connect, create
|
||||
|
||||
async def initdb():
|
||||
conn = await connect()
|
||||
await create(conn)
|
||||
await conn.commit()
|
||||
|
||||
async def create_test_data(conn):
|
||||
await create_persons(conn)
|
||||
|
||||
for product in class_fields(Products).values():
|
||||
|
|
@ -251,11 +245,6 @@ if __name__ == '__main__':
|
|||
for meal in class_fields(Meals).values():
|
||||
await meals_db.insert_meal(conn, meal)
|
||||
|
||||
await conn.commit()
|
||||
await conn.close()
|
||||
|
||||
asyncio.run(initdb())
|
||||
|
||||
"""
|
||||
import re
|
||||
def to_name(thing):
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@ class TestShopping(unittest.IsolatedAsyncioTestCase):
|
|||
await products.insert_product(self.conn, ingredient.product, {})
|
||||
|
||||
shopping_list = await shopping.current_shopping_list(self.conn)
|
||||
await shopping.sync_persons_requests(self.conn, shopping_list, person, [ingredient])
|
||||
await shopping.sync_persons_requested_ingredients(self.conn, shopping_list, person, [ingredient])
|
||||
|
||||
shopping_list = await shopping.current_shopping_list(self.conn)
|
||||
self.assertEqual(len(shopping_list.requests), 1)
|
||||
|
|
@ -60,8 +60,8 @@ class TestShopping(unittest.IsolatedAsyncioTestCase):
|
|||
await products.insert_product(self.conn, second.product, {})
|
||||
|
||||
shopping_list = await shopping.current_shopping_list(self.conn)
|
||||
await shopping.sync_persons_requests(self.conn, shopping_list, person, [first])
|
||||
await shopping.sync_persons_requests(self.conn, shopping_list, person, [first, second])
|
||||
await shopping.sync_persons_requested_ingredients(self.conn, shopping_list, person, [first])
|
||||
await shopping.sync_persons_requested_ingredients(self.conn, shopping_list, person, [first, second])
|
||||
|
||||
shopping_list = await shopping.current_shopping_list(self.conn)
|
||||
self.assertEqual(len(shopping_list.requests), 2)
|
||||
|
|
@ -82,7 +82,7 @@ class TestShopping(unittest.IsolatedAsyncioTestCase):
|
|||
await products.insert_product(self.conn, ingredient.product, {})
|
||||
|
||||
shopping_list = await shopping.current_shopping_list(self.conn)
|
||||
await shopping.sync_persons_requests(self.conn, shopping_list, person, [ingredient])
|
||||
await shopping.sync_persons_requested_ingredients(self.conn, shopping_list, person, [ingredient])
|
||||
await shopping.mark_found(self.conn, ingredient.product, 2, 'items')
|
||||
|
||||
shopping_list = await shopping.current_shopping_list(self.conn)
|
||||
|
|
|
|||
Loading…
Reference in a new issue