Many misc changes for shopping pages

This commit is contained in:
jableader 2024-05-18 17:05:01 +10:00
parent ac34da7824
commit be1d2b75ad
7 changed files with 169 additions and 51 deletions

14
db.py
View file

@ -21,3 +21,17 @@ async def create(conn: aiosqlite.Connection):
import shopping.db as shopping_db import shopping.db as shopping_db
await shopping_db.create(conn) 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
View file

@ -1,9 +1,9 @@
import sqlite3 import sqlite3
import products, recipes, db as db, meals, persons, ingredients import products, recipes, db, meals, persons, ingredients, shopping
import datetime import datetime
from pydantic import BaseModel from pydantic import BaseModel
from typing import List, Annotated from typing import List, Annotated, Union
from fastapi import FastAPI, Depends, Query, Cookie from fastapi import FastAPI, Depends, Query, Cookie
from fastapi.responses import JSONResponse from fastapi.responses import JSONResponse
from fastapi.encoders import jsonable_encoder 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() await conn.commit()
return meal 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/") @app.get("/persons/")
async def get_persons(q: str = None, conn: sqlite3.Connection = Depends(get_db)) -> List[meals.Person]: 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) query = persons.search_by_name(conn, q) if q else persons.get_all(conn)

View file

@ -91,7 +91,11 @@ async def find_meal_by_id(conn, meal_id: int) -> Meal:
LIMIT 1 LIMIT 1
''', (meal_id,)) as cursor: ''', (meal_id,)) as cursor:
async for row in 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 def find_meal_by_date(conn, date: datetime) -> Meal:
async with conn.execute(f''' async with conn.execute(f'''

View file

@ -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

View file

@ -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 ingredients import Ingredient, insert_ingredient
from persons import Person from persons import Person
from products import Product from products import Product
@ -6,14 +6,14 @@ from products import Product
from pydantic import BaseModel from pydantic import BaseModel
from typing import AsyncIterator, List, ClassVar, Optional from typing import AsyncIterator, List, ClassVar, Optional
from datetime import datetime from datetime import datetime, timedelta
class ShoppingListRequest(BaseModel): class ShoppingListRequest(BaseModel):
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', 'created_date']
id: int = 0 id: int = 0
list_id: int list_id: int
ingredient_id: int ingredient_id: Optional[int] = None
ingredient: Optional[Ingredient] = None ingredient: Optional[Ingredient] = None
person_id: Optional[int] = None person_id: Optional[int] = None
@ -104,6 +104,35 @@ async def insert_shopping_list(conn, shopping_list: ShoppingList):
VALUES (?, ?, ?, ?, ?, ?, ?) VALUES (?, ?, ?, ?, ?, ?, ?)
''', (item.id, item.product_id, shopping_list.id, item.quantity, item.unit, item.created_date, item.found_date)) ''', (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]: async def find_requests_by_list_id(conn, list_id: int) -> AsyncIterator[ShoppingListRequest]:
# Join Ingredient and Product to also load ingredient and product # Join Ingredient and Product to also load ingredient and product
ingredient_keys = [f'ingredient.{key}' for key in Ingredient.KEYS] 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 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_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_keys = {k:v for k,v in zip(Person.KEYS, row[-len(Person.KEYS):])}
person = Person(**person_keys) if person_keys['id'] else None 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_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) 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 yield request
async def find_items_by_list_id(conn, list_id: int) -> AsyncIterator[ShoppingListResult]: 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 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: async def current_shopping_list(conn) -> ShoppingList:
shopping_list = None shopping_list = None
async with conn.execute(f''' async with conn.execute(f'''
@ -189,6 +228,10 @@ async def current_shopping_list(conn) -> ShoppingList:
if not shopping_list: if not shopping_list:
shopping_list = ShoppingList() 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) await insert_shopping_list(conn, shopping_list)
return 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: if request.person_id == person.id:
yield request 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: if ingredient.id:
raise ValueError('How did you get an existing ingredient?') raise ValueError('How did you get an existing ingredient?')
await insert_ingredient(conn, 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(''' async with conn.execute('''
INSERT INTO ShoppingListRequest (ingredient_id, list_id, person_id, created_date) INSERT INTO ShoppingListRequest (ingredient_id, list_id, person_id, created_date)
VALUES (?, ?, ?, ?) VALUES (?, ?, ?, ?)
@ -223,18 +266,35 @@ async def request_ingredient(conn, shopping_list: ShoppingList, person: Person,
request.id = cursor.lastrowid request.id = cursor.lastrowid
shopping_list.requests.append(request) shopping_list.requests.append(request)
return request
async def sync_persons_requests(conn, shopping_list: ShoppingList, person: Person, requests: List[Ingredient]): async def request_meal(conn, shopping_list: ShoppingList, person: Person, meal: Meal) -> ShoppingListRequest:
# Delete existing and insert all as new request = ShoppingListRequest(meal_id=meal.id, meal=meal, list_id=shopping_list.id, person_id=person.id, created_date=datetime.now())
async for request in get_persons_requests(conn, shopping_list, person):
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(''' await conn.execute('''
DELETE FROM ShoppingListRequest DELETE FROM ShoppingListRequest
WHERE id = ? WHERE list_id = ? AND meal_id = ?
''', (request.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: for ingredient in requests:
ingredient.id = 0 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: async def mark_found(conn, product: Product, quantity: float, unit: str) -> ShoppingListResult:
shopping_list = await current_shopping_list(conn) shopping_list = await current_shopping_list(conn)

View file

@ -83,7 +83,7 @@ class Products:
apple = products.Product( apple = products.Product(
id=0, id=0,
name="Apple", name="Apple",
product_id="0", product_id="3542",
link="https://www.woolworths.com.au/shop/productdetails/0/apple", link="https://www.woolworths.com.au/shop/productdetails/0/apple",
img_small="https://cdn0.woolworths.media/content/wowproductimages/small/0.jpg", img_small="https://cdn0.woolworths.media/content/wowproductimages/small/0.jpg",
img_large="https://cdn0.woolworths.media/content/wowproductimages/large/0.jpg", img_large="https://cdn0.woolworths.media/content/wowproductimages/large/0.jpg",
@ -93,7 +93,7 @@ class Products:
banana = products.Product( banana = products.Product(
id=0, id=0,
name="Banana", name="Banana",
product_id="0", product_id="214",
link="https://www.woolworths.com.au/shop/productdetails/0/banana", link="https://www.woolworths.com.au/shop/productdetails/0/banana",
img_small="https://cdn0.woolworths.media/content/wowproductimages/small/0.jpg", img_small="https://cdn0.woolworths.media/content/wowproductimages/small/0.jpg",
img_large="https://cdn0.woolworths.media/content/wowproductimages/large/0.jpg", img_large="https://cdn0.woolworths.media/content/wowproductimages/large/0.jpg",
@ -101,6 +101,8 @@ class Products:
) )
_tags = { _tags = {
apple.product_id: ['apple', 'fruit', 'fresh fruit'],
banana.product_id: ['banana', 'fruit', 'fresh fruit'],
broccoli.product_id: ['broccoli', 'fresh broccoli'], broccoli.product_id: ['broccoli', 'fresh broccoli'],
garlic_bread.product_id: ['garlic bread', 'bread', 'garlic', 'frozen garlic bread'], garlic_bread.product_id: ['garlic bread', 'bread', 'garlic', 'frozen garlic bread'],
beans_round.product_id: ['beans', 'green beans', 'fresh green beans', 'fresh beans'], 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(): for person in class_fields(Persons).values():
await persons.insert_person(conn, person) await persons.insert_person(conn, person)
if __name__ == '__main__': async def create_test_data(conn):
import asyncio
from db import connect, create
async def initdb():
conn = await connect()
await create(conn)
await conn.commit()
await create_persons(conn) await create_persons(conn)
for product in class_fields(Products).values(): for product in class_fields(Products).values():
@ -251,11 +245,6 @@ if __name__ == '__main__':
for meal in class_fields(Meals).values(): for meal in class_fields(Meals).values():
await meals_db.insert_meal(conn, meal) await meals_db.insert_meal(conn, meal)
await conn.commit()
await conn.close()
asyncio.run(initdb())
""" """
import re import re
def to_name(thing): def to_name(thing):

View file

@ -40,7 +40,7 @@ class TestShopping(unittest.IsolatedAsyncioTestCase):
await products.insert_product(self.conn, ingredient.product, {}) await products.insert_product(self.conn, ingredient.product, {})
shopping_list = await shopping.current_shopping_list(self.conn) 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) shopping_list = await shopping.current_shopping_list(self.conn)
self.assertEqual(len(shopping_list.requests), 1) self.assertEqual(len(shopping_list.requests), 1)
@ -60,8 +60,8 @@ class TestShopping(unittest.IsolatedAsyncioTestCase):
await products.insert_product(self.conn, second.product, {}) await products.insert_product(self.conn, second.product, {})
shopping_list = await shopping.current_shopping_list(self.conn) shopping_list = await shopping.current_shopping_list(self.conn)
await shopping.sync_persons_requests(self.conn, shopping_list, person, [first]) await shopping.sync_persons_requested_ingredients(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, second])
shopping_list = await shopping.current_shopping_list(self.conn) shopping_list = await shopping.current_shopping_list(self.conn)
self.assertEqual(len(shopping_list.requests), 2) self.assertEqual(len(shopping_list.requests), 2)
@ -82,7 +82,7 @@ class TestShopping(unittest.IsolatedAsyncioTestCase):
await products.insert_product(self.conn, ingredient.product, {}) await products.insert_product(self.conn, ingredient.product, {})
shopping_list = await shopping.current_shopping_list(self.conn) 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') await shopping.mark_found(self.conn, ingredient.product, 2, 'items')
shopping_list = await shopping.current_shopping_list(self.conn) shopping_list = await shopping.current_shopping_list(self.conn)