Allowed for null products, move ingredient spreading server-side #1
6 changed files with 223 additions and 209 deletions
23
common.py
Normal file
23
common.py
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
from pydantic import BaseModel, Field, model_validator
|
||||||
|
from typing import Optional, Any
|
||||||
|
|
||||||
|
class BaseLinkedModel(BaseModel):
|
||||||
|
model_config = dict(arbitrary_types_allowed=True)
|
||||||
|
|
||||||
|
@model_validator(mode="before")
|
||||||
|
@classmethod
|
||||||
|
def auto_populate_ids(cls, data: dict[str, Any]) -> dict[str, Any]:
|
||||||
|
if isinstance(data, dict):
|
||||||
|
for key, value in data.copy().items():
|
||||||
|
if not key.endswith("_id") and hasattr(value, "id") and value is not None:
|
||||||
|
id_key = key + "_id"
|
||||||
|
|
||||||
|
if id_key in data:
|
||||||
|
# If the id_key already exists, ensure it matches the value's id
|
||||||
|
if data[id_key] != value.id:
|
||||||
|
raise ValueError(f"ID mismatch for {key}: {data[id_key]} != {value.id}")
|
||||||
|
else:
|
||||||
|
# If the id_key does not exist, set it to the value's id
|
||||||
|
data[id_key] = value.id
|
||||||
|
|
||||||
|
return data
|
||||||
2
db.py
2
db.py
|
|
@ -1,6 +1,6 @@
|
||||||
import aiosqlite
|
import aiosqlite
|
||||||
|
|
||||||
async def connect(path = './data/your_database.db') -> aiosqlite.Connection:
|
async def connect(path = './data/doof.sqlite') -> aiosqlite.Connection:
|
||||||
return await aiosqlite.connect(path)
|
return await aiosqlite.connect(path)
|
||||||
|
|
||||||
async def create(conn: aiosqlite.Connection):
|
async def create(conn: aiosqlite.Connection):
|
||||||
|
|
|
||||||
|
|
@ -55,7 +55,7 @@ def parse_ingredient_from_nlp(ingredient_string: str) -> Ingredient:
|
||||||
if unit is None:
|
if unit is None:
|
||||||
unit = units.ITEMS.name
|
unit = units.ITEMS.name
|
||||||
|
|
||||||
return Ingredient(id=0,
|
return Ingredient(id=-1,
|
||||||
line=ingredient.sentence,
|
line=ingredient.sentence,
|
||||||
name=name,
|
name=name,
|
||||||
quantity=quantity,
|
quantity=quantity,
|
||||||
|
|
|
||||||
62
main.py
62
main.py
|
|
@ -252,53 +252,65 @@ async def delete_meal(meal_id: int, conn: sqlite3.Connection = Depends(get_db))
|
||||||
return meal
|
return meal
|
||||||
|
|
||||||
class CurrentShoppingList(BaseModel):
|
class CurrentShoppingList(BaseModel):
|
||||||
requests: List[shopping.ShoppingListRequest]
|
requests: List[shopping.ShoppingListItem]
|
||||||
overlapping_previous_shops: List[shopping.ShoppingList]
|
previously_purchased: List[shopping.ShoppingListItem] = []
|
||||||
|
other_shopping_lists: List[shopping.ShoppingList] = []
|
||||||
|
|
||||||
@app.get("/api/shopping/current")
|
@app.get("/api/shopping/current")
|
||||||
async def get_current_shopping_list(conn: sqlite3.Connection = Depends(get_db)) -> CurrentShoppingList:
|
async def get_current_shopping_list(conn: sqlite3.Connection = Depends(get_db)) -> CurrentShoppingList:
|
||||||
current_requests = [r async for r in shopping.get_current_requests(conn)]
|
outstanding_requests, purchased_requests = await shopping.get_outstanding_requests(conn)
|
||||||
|
other_shopping_list_ids = {item.shopping_list for item in purchased_requests}
|
||||||
requested_meals = [r.meal_id for r in current_requests if r.meal_id]
|
other_shopping_lists = [await shopping.load_shopping_list(conn, list_id) for list_id in other_shopping_list_ids if list_id is not None]
|
||||||
upcoming_meals = [m.id async for m in meals.find_upcoming_meals_by_date_range(conn, datetime.datetime.now().astimezone(), datetime.datetime.now().astimezone() + datetime.timedelta(days=14))]
|
|
||||||
|
|
||||||
overlapping = {}
|
return CurrentShoppingList(requests=outstanding_requests, previously_purchased=purchased_requests, other_shopping_lists=other_shopping_lists)
|
||||||
for meal_id in set(requested_meals + upcoming_meals):
|
|
||||||
async for r in shopping.get_shopping_list_with_meal(conn, meal_id):
|
|
||||||
overlapping[r.id] = r
|
|
||||||
|
|
||||||
return CurrentShoppingList(requests=current_requests, overlapping_previous_shops=list(overlapping.values()))
|
|
||||||
|
|
||||||
@app.get("/api/shopping/{list_id}")
|
@app.get("/api/shopping/{list_id}")
|
||||||
async def get_shopping_list(list_id: int, conn: sqlite3.Connection = Depends(get_db)) -> shopping.ShoppingList:
|
async def get_shopping_list(list_id: int, conn: sqlite3.Connection = Depends(get_db)) -> shopping.ShoppingList:
|
||||||
return await shopping.load_shopping_list(conn, list_id)
|
return await shopping.load_shopping_list(conn, list_id)
|
||||||
|
|
||||||
@app.post("/api/shopping/")
|
@app.post("/api/shopping/")
|
||||||
async def purchase_ingredients(lst: shopping.ShoppingListPurchase, conn: sqlite3.Connection = Depends(get_db)) -> shopping.ShoppingList:
|
async def purchase_ingredients(shopping_list: shopping.ShoppingList, conn: sqlite3.Connection = Depends(get_db), person: persons.Person = Depends(cookie_person)) -> shopping.ShoppingList:
|
||||||
await shopping.purchase_ingredients(conn, lst)
|
shopping_list = shopping.ShoppingList(purchased_by=person, items=shopping_list.items, store_name=shopping_list.store_name)
|
||||||
|
|
||||||
|
await shopping.purchase(conn, shopping_list)
|
||||||
await conn.commit()
|
await conn.commit()
|
||||||
return lst
|
|
||||||
|
return shopping_list
|
||||||
|
|
||||||
@app.get("/api/shopping/current/me/ingredients")
|
@app.get("/api/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]:
|
async def get_my_shopping_list(conn: sqlite3.Connection = Depends(get_db), person: persons.Person = Depends(cookie_person)) -> List[ingredients.Ingredient]:
|
||||||
return [r async for r in shopping.get_current_requests(conn) if r.ingredient and r.person_id == person.id]
|
return [r.ingredient async for r in shopping.get_persons_requests(conn, person.id)]
|
||||||
|
|
||||||
@app.post("/api/shopping/current/me/ingredients")
|
@app.post("/api/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]:
|
async def sync_my_shopping_list(requests: List[ingredients.Ingredient], conn: sqlite3.Connection = Depends(get_db), person: persons.Person = Depends(cookie_person)) -> List[ingredients.Ingredient]:
|
||||||
result = [r async for r in shopping.sync_persons_requested_ingredients(conn, person, requests) if r.ingredient]
|
def isMatching(a: ingredients.Ingredient, b: ingredients.Ingredient) -> bool:
|
||||||
|
return a.id == b.id or a.line == b.line
|
||||||
|
|
||||||
|
my_shopping_list = [r.ingredient async for r in shopping.get_persons_requests(conn, person.id) if r.ingredient is not None]
|
||||||
|
to_remove = [r for r in my_shopping_list if not any(isMatching(r, req) for req in requests)]
|
||||||
|
to_add = [req for req in requests if not any(isMatching(req, r) for r in my_shopping_list)]
|
||||||
|
|
||||||
|
for r in to_remove:
|
||||||
|
await shopping.remove_request(conn, person, ingredient=r)
|
||||||
|
|
||||||
|
for r in to_add:
|
||||||
|
if r.id < 0:
|
||||||
|
await ingredients.insert_ingredient(conn, r)
|
||||||
|
await shopping.request(conn, person, ingredient=r)
|
||||||
|
|
||||||
await conn.commit()
|
await conn.commit()
|
||||||
return result
|
return await get_my_shopping_list(conn, person)
|
||||||
|
|
||||||
class MealIdWrapper(BaseModel):
|
class MealIdWrapper(BaseModel):
|
||||||
meal_id: int
|
meal_id: int
|
||||||
|
|
||||||
@app.post("/api/shopping/current/meals/me")
|
@app.post("/api/shopping/current/meals/me")
|
||||||
async def request_meal(r: MealIdWrapper, conn: sqlite3.Connection = Depends(get_db), person: persons.Person = Depends(cookie_person)) -> shopping.ShoppingListRequest:
|
async def request_meal(r: MealIdWrapper, conn: sqlite3.Connection = Depends(get_db), person: persons.Person = Depends(cookie_person)) -> shopping.ShoppingListItem:
|
||||||
meal = await meals.find_meal_by_id(conn, r.meal_id)
|
meal = await meals.find_meal_by_id(conn, r.meal_id)
|
||||||
if not meal:
|
if not meal:
|
||||||
return JSONResponse(status_code=404, content={'message': 'Meal not found'})
|
return JSONResponse(status_code=404, content={'message': 'Meal not found'})
|
||||||
|
|
||||||
response = await shopping.request_meal(conn, person, meal)
|
response = await shopping.request(conn, person, meal=meal)
|
||||||
await conn.commit()
|
await conn.commit()
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
|
@ -308,7 +320,7 @@ async def unrequest_meal(meal_id: int, conn: sqlite3.Connection = Depends(get_db
|
||||||
if not meal:
|
if not meal:
|
||||||
return JSONResponse(status_code=404, content={'message': 'Meal not found'})
|
return JSONResponse(status_code=404, content={'message': 'Meal not found'})
|
||||||
|
|
||||||
await shopping.unrequest_meal(conn, meal)
|
await shopping.remove_request(conn, person, meal=meal)
|
||||||
await conn.commit()
|
await conn.commit()
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,2 +1,29 @@
|
||||||
from shopping.db import ShoppingList, ShoppingListRequest, ShoppingListResult, ShoppingListPurchase, sync_persons_requested_ingredients, load_shopping_list, get_current_requests, request_meal, unrequest_meal, purchase_ingredients, get_shopping_list_with_meal, remove_request
|
from typing import AsyncIterator, Iterator, List, Tuple
|
||||||
|
from shopping.db import ShoppingList, ShoppingListItem, load_shopping_list, purchase, remove_request, request
|
||||||
|
|
||||||
|
from shopping.db import find_items_by_list_id as _find_items_by_list_id, get_purchased_ingredients as _get_purchased_ingredients
|
||||||
|
|
||||||
|
async def get_persons_requests(conn, person_id: int) -> AsyncIterator[ShoppingListItem]:
|
||||||
|
async for item in _find_items_by_list_id(conn, None):
|
||||||
|
if item.person_id == person_id and item.ingredient_id is not None:
|
||||||
|
yield item
|
||||||
|
|
||||||
|
def flatten_items(items: Iterator[ShoppingListItem]) -> Iterator[ShoppingListItem]:
|
||||||
|
for item in items:
|
||||||
|
if item.meal:
|
||||||
|
for ingredient in item.meal.ingredients + item.meal.extra_ingredients:
|
||||||
|
yield ShoppingListItem(ingredient=ingredient, meal=item.meal, person_id=item.person_id, created_date=item.created_date)
|
||||||
|
else:
|
||||||
|
yield item
|
||||||
|
|
||||||
|
async def get_outstanding_requests(conn) -> Tuple[List[ShoppingListItem], List[ShoppingListItem]]:
|
||||||
|
current_requests = [r async for r in _find_items_by_list_id(conn, None)]
|
||||||
|
|
||||||
|
meal_ids = {item.meal_id for item in current_requests if item.meal_id is not None and item.meal_id > 0}
|
||||||
|
purchased_ingredients = {r.ingredient_id async for r in _get_purchased_ingredients(conn, meal_ids)}
|
||||||
|
|
||||||
|
flattened = flatten_items(current_requests)
|
||||||
|
outstanding_items = [r for r in flattened if r.ingredient_id not in purchased_ingredients]
|
||||||
|
purchased_items = [r for r in flattened if r.ingredient_id in purchased_ingredients]
|
||||||
|
|
||||||
|
return outstanding_items, purchased_items
|
||||||
|
|
|
||||||
314
shopping/db.py
314
shopping/db.py
|
|
@ -1,38 +1,29 @@
|
||||||
|
from common import BaseLinkedModel
|
||||||
from meals import Meal, find_meal_by_id
|
from meals import Meal, 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
|
||||||
|
|
||||||
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
|
||||||
|
|
||||||
class ShoppingListRequest(BaseModel):
|
class ShoppingListItem(BaseLinkedModel):
|
||||||
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 = -1
|
id: int = -1
|
||||||
list_id: Optional[int] = None
|
list_id: Optional[int] = None
|
||||||
|
|
||||||
|
person_id: int = -1
|
||||||
|
person: Optional[Person] = None
|
||||||
|
|
||||||
ingredient_id: Optional[int] = None
|
ingredient_id: Optional[int] = None
|
||||||
ingredient: Optional[Ingredient] = None
|
ingredient: Optional[Ingredient] = None
|
||||||
|
|
||||||
person_id: Optional[int] = None
|
|
||||||
person: Optional[Person] = None
|
|
||||||
|
|
||||||
meal_id: Optional[int] = None
|
meal_id: Optional[int] = None
|
||||||
meal: Optional[Meal] = None
|
meal: Optional[Meal] = None
|
||||||
|
|
||||||
created_date: datetime = datetime.now().astimezone()
|
created_date: datetime = datetime.now().astimezone()
|
||||||
|
|
||||||
class ShoppingListResult(BaseModel):
|
|
||||||
KEYS: ClassVar[List[str]] = ['id', 'product_id', 'list_id', 'quantity', 'unit' ]
|
|
||||||
id: int = -1
|
|
||||||
list_id: int
|
|
||||||
product_id: int
|
|
||||||
product: Optional[Product] = None
|
|
||||||
|
|
||||||
quantity: float
|
|
||||||
unit: str
|
|
||||||
|
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
|
|
||||||
|
|
@ -41,17 +32,14 @@ class StoreEnum(str, Enum):
|
||||||
coles = 'coles'
|
coles = 'coles'
|
||||||
home = ''
|
home = ''
|
||||||
|
|
||||||
class ShoppingList(BaseModel):
|
class ShoppingList(BaseLinkedModel):
|
||||||
KEYS: ClassVar[List[str]] = ['id', 'created_date', 'store_name']
|
KEYS: ClassVar[List[str]] = ['id', 'created_date', 'store_name']
|
||||||
id: int = -1
|
id: int = -1
|
||||||
created_date: datetime = datetime.now().astimezone()
|
created_date: datetime = datetime.now().astimezone()
|
||||||
store_name: StoreEnum = ''
|
store_name: StoreEnum = ''
|
||||||
|
purchased_by_id: int = -1
|
||||||
requests: List[ShoppingListRequest] = []
|
purchased_by: Optional[Person] = None
|
||||||
results: List[ShoppingListResult] = []
|
items: List[ShoppingListItem] = []
|
||||||
|
|
||||||
class ShoppingListPurchase(ShoppingList):
|
|
||||||
completed_requests: List[ShoppingListRequest] = []
|
|
||||||
|
|
||||||
async def create(conn):
|
async def create(conn):
|
||||||
await conn.execute('''
|
await conn.execute('''
|
||||||
|
|
@ -59,10 +47,12 @@ async def create(conn):
|
||||||
id INTEGER PRIMARY KEY,
|
id INTEGER PRIMARY KEY,
|
||||||
created_date DATETIME NOT NULL,
|
created_date DATETIME NOT NULL,
|
||||||
store_name TEXT NOT NULL,
|
store_name TEXT NOT NULL,
|
||||||
|
purchased_by_id INTEGER,
|
||||||
|
FOREIGN KEY(purchased_by_id) REFERENCES Person(id)
|
||||||
);''')
|
);''')
|
||||||
|
|
||||||
await conn.execute('''
|
await conn.execute('''
|
||||||
CREATE TABLE IF NOT EXISTS ShoppingListRequest (
|
CREATE TABLE IF NOT EXISTS ShoppingListItem (
|
||||||
id INTEGER PRIMARY KEY,
|
id INTEGER PRIMARY KEY,
|
||||||
ingredient_id INTEGER,
|
ingredient_id INTEGER,
|
||||||
list_id INTEGER,
|
list_id INTEGER,
|
||||||
|
|
@ -70,115 +60,144 @@ async def create(conn):
|
||||||
meal_id INTEGER,
|
meal_id INTEGER,
|
||||||
created_date DATETIME NOT NULL,
|
created_date DATETIME NOT NULL,
|
||||||
FOREIGN KEY(ingredient_id) REFERENCES Ingredient(id),
|
FOREIGN KEY(ingredient_id) REFERENCES Ingredient(id),
|
||||||
|
FOREIGN KEY(list_id) REFERENCES ShoppingList(id),
|
||||||
FOREIGN KEY(person_id) REFERENCES Person(id),
|
FOREIGN KEY(person_id) REFERENCES Person(id),
|
||||||
FOREIGN KEY(meal_id) REFERENCES Meal(id),
|
FOREIGN KEY(meal_id) REFERENCES Meal(id)
|
||||||
FOREIGN KEY(list_id) REFERENCES ShoppingList(id)
|
|
||||||
);''')
|
);''')
|
||||||
|
|
||||||
await conn.execute('''
|
|
||||||
CREATE TABLE IF NOT EXISTS ShoppingListResult (
|
|
||||||
id INTEGER PRIMARY KEY,
|
|
||||||
product_id INTEGER,
|
|
||||||
list_id INTEGER,
|
|
||||||
quantity REAL,
|
|
||||||
unit TEXT,
|
|
||||||
FOREIGN KEY(product_id) REFERENCES Product(id),
|
|
||||||
FOREIGN KEY(list_id) REFERENCES ShoppingList(id)
|
|
||||||
);''')
|
|
||||||
|
|
||||||
def validate_request(request: ShoppingListRequest) -> None:
|
def validate_request(request: ShoppingListItem) -> None:
|
||||||
# A request must always have a list id
|
if request.person_id < 0:
|
||||||
if request.list_id < 0:
|
raise ValueError('Requests must have a person')
|
||||||
raise ValueError('Request must have a list id')
|
|
||||||
|
|
||||||
# A request must have either an ingredient or a meal, but not both
|
# A request must have either an ingredient or a meal, but not both
|
||||||
if not request.ingredient and not request.meal:
|
if not request.ingredient and not request.meal:
|
||||||
raise ValueError('Request must have either an ingredient or a meal')
|
raise ValueError('Request must have either an ingredient or a meal')
|
||||||
|
|
||||||
# If an ingredient is provided, it must have a person
|
|
||||||
if request.ingredient and not request.person:
|
|
||||||
raise ValueError('Ingredient requests must have a person')
|
|
||||||
|
|
||||||
async def purchase_ingredients(conn, shopping_list: ShoppingListPurchase):
|
async def purchase(conn, shopping_list: ShoppingList) -> None:
|
||||||
|
if shopping_list.purchased_by_id is None:
|
||||||
|
raise ValueError('Shopping list must have a person id')
|
||||||
|
|
||||||
|
if shopping_list.items is None or len(shopping_list.items) == 0:
|
||||||
|
raise ValueError('Shopping list must have items')
|
||||||
|
|
||||||
shopping_list.created_date = datetime.now().astimezone()
|
shopping_list.created_date = datetime.now().astimezone()
|
||||||
|
|
||||||
async with conn.execute('''
|
async with conn.execute('''
|
||||||
INSERT INTO ShoppingList (created_date, store_name)
|
INSERT INTO ShoppingList (created_date, store_name, purchased_by_id)
|
||||||
VALUES (?, ?)
|
VALUES (?, ?, ?)
|
||||||
''', (shopping_list.created_date.isoformat(), shopping_list.store_name,)) as cursor:
|
''', (shopping_list.created_date.isoformat(), shopping_list.store_name, shopping_list.purchased_by_id)) as cursor:
|
||||||
shopping_list.id = cursor.lastrowid
|
shopping_list.id = cursor.lastrowid
|
||||||
|
|
||||||
for request in shopping_list.completed_requests:
|
for item in shopping_list.items:
|
||||||
request.list_id = shopping_list.id
|
item.list_id = shopping_list.id
|
||||||
|
validate_request(item)
|
||||||
|
|
||||||
validate_request(request)
|
if item.ingredient and item.ingredient.id < 0:
|
||||||
|
await insert_ingredient(conn, item.ingredient)
|
||||||
|
|
||||||
if request.ingredient and request.ingredient.id < 0:
|
if item.ingredient_id is None or item.ingredient_id < 0:
|
||||||
await insert_ingredient(conn, request.ingredient)
|
raise ValueError('Ingredient request must have a valid ingredient id')
|
||||||
|
|
||||||
if request.ingredient:
|
isMeal = item.meal_id is None or item.meal_id < 0
|
||||||
request.ingredient_id = request.ingredient.id
|
isPersonRequest = (not isMeal) and item.person_id is not None and item.person_id >= 0
|
||||||
|
|
||||||
if request.meal:
|
if not isMeal and not isPersonRequest:
|
||||||
request.meal_id = request.meal.id
|
raise ValueError('Ingredient request must have either a meal or a person id')
|
||||||
|
|
||||||
|
if isPersonRequest:
|
||||||
|
# Update existing request from its null id, or throw
|
||||||
|
async with conn.execute('''
|
||||||
|
UPDATE ShoppingListItem
|
||||||
|
SET list_id = ?
|
||||||
|
WHERE ingredient_id = ? AND person_id = ? AND meal_id IS NULL
|
||||||
|
''', (shopping_list.id, item.ingredient_id, item.person_id)) as cursor:
|
||||||
|
if cursor.rowcount == 0:
|
||||||
|
raise ValueError('Ingredient request must have a valid person id and ingredient id')
|
||||||
|
|
||||||
|
elif isMeal:
|
||||||
|
# Insert new request for meal
|
||||||
|
if item.meal_id is None or item.meal_id < 0:
|
||||||
|
raise ValueError('Meal request must have a valid meal id')
|
||||||
|
|
||||||
|
async with conn.execute('''
|
||||||
|
INSERT INTO ShoppingListItem (ingredient_id, list_id, person_id, meal_id, created_date)
|
||||||
|
VALUES (?, ?, ?, ?, ?)
|
||||||
|
''', (item.ingredient_id, shopping_list.id, item.person_id, item.meal_id, item.created_date.isoformat())) as cursor:
|
||||||
|
item.id = cursor.lastrowid
|
||||||
|
|
||||||
|
# TODO: Calculate which meals have been fulfilled and update meal status
|
||||||
|
|
||||||
|
async def requested_meal_ids(conn) -> AsyncIterator[ShoppingListItem]:
|
||||||
|
async with conn.execute(f'''
|
||||||
|
SELECT {ShoppingListItem.KEYS}
|
||||||
|
FROM ShoppingListItem
|
||||||
|
WHERE list_id IS NULL AND meal_id IS NOT NULL AND meal_id > 0
|
||||||
|
''') as cursor:
|
||||||
|
async for row in cursor:
|
||||||
|
yield ShoppingListItem(**{k:v for k,v in zip(ShoppingListItem.KEYS, row)})
|
||||||
|
|
||||||
|
async def request(conn, person: Person, ingredient: Optional[Ingredient] = None, meal: Optional[Meal] = None) -> ShoppingListItem:
|
||||||
|
if ingredient is not None and meal is not None:
|
||||||
|
raise ValueError('Cannot request both an ingredient and a meal')
|
||||||
|
|
||||||
|
if ingredient is None and meal is None:
|
||||||
|
raise ValueError('Must specify either an ingredient or a meal to request')
|
||||||
|
|
||||||
|
if meal is not None and meal.id < 0:
|
||||||
|
raise ValueError('Meal must have a valid id')
|
||||||
|
|
||||||
|
if ingredient is not None and ingredient.id < 0:
|
||||||
|
await insert_ingredient(conn, ingredient)
|
||||||
|
|
||||||
|
item = ShoppingListItem(ingredient=ingredient, person=person, meal=meal)
|
||||||
|
|
||||||
|
validate_request(item)
|
||||||
|
|
||||||
|
async with conn.execute('''
|
||||||
|
INSERT INTO ShoppingListItem (ingredient_id, person_id, meal_id, created_date)
|
||||||
|
VALUES (?, ?, ?, ?)
|
||||||
|
''', (item.ingredient_id, item.person_id, item.meal_id, item.created_date.isoformat())) as cursor:
|
||||||
|
item.id = cursor.lastrowid
|
||||||
|
|
||||||
|
return item
|
||||||
|
|
||||||
|
async def remove_request(conn, person: Person, meal: Optional[Meal] = None, ingredient: Optional[Ingredient] = None) -> None:
|
||||||
|
failed = True
|
||||||
|
if meal is not None:
|
||||||
|
# Ensure a record is removed
|
||||||
async with conn.execute('''
|
async with conn.execute('''
|
||||||
INSERT INTO ShoppingListRequest (ingredient_id, list_id, person_id, meal_id, created_date)
|
DELETE FROM ShoppingListItem
|
||||||
VALUES (?, ?, ?, ?, ?)
|
WHERE list_id IS NULL AND meal_id = ?
|
||||||
''', (request.ingredient_id, shopping_list.id, request.person_id, request.meal_id, request.created_date.isoformat())) as cursor:
|
''', (meal.id,)) as cursor:
|
||||||
request.id = cursor.lastrowid
|
if cursor.rowcount > 0:
|
||||||
|
failed = False
|
||||||
|
|
||||||
if request.ingredient_id and request.person_id:
|
elif ingredient is not None:
|
||||||
await conn.execute('''
|
# Ensure a record is removed
|
||||||
DELETE FROM ShoppingListRequest
|
async with conn.execute('''
|
||||||
WHERE ingredient_id = ? AND person_id = ? AND list_id IS NULL
|
DELETE FROM ShoppingListItem
|
||||||
''', (request.ingredient_id, request.person_id))
|
WHERE list_id IS NULL AND ingredient_id = ? AND person_id = ?
|
||||||
|
''', (ingredient.id, person.id)) as cursor:
|
||||||
|
if cursor.rowcount > 0:
|
||||||
|
failed = False
|
||||||
|
|
||||||
from meals import find_meal_by_id
|
if failed:
|
||||||
async def find_completed_meals(conn, shopping_list_request: List[ShoppingListRequest]) -> AsyncIterator[Meal]:
|
raise ValueError('Must specify either a meal or an ingredient to remove')
|
||||||
meal_ids = {request.meal_id for request in shopping_list_request if request.meal_id is not None}
|
|
||||||
if not meal_ids:
|
|
||||||
return
|
|
||||||
|
|
||||||
meals = [await find_meal_by_id(conn, meal_id) for meal_id in meal_ids]
|
|
||||||
ingredients_as_requests = [ShoppingListRequest(meal_id=meal.id, meal=meal, list_id=shopping_list_request[0].list_id) for meal in meals]
|
|
||||||
|
|
||||||
|
async def find_items_by_list_id(conn, list_id: Optional[int]) -> AsyncIterator[ShoppingListItem]:
|
||||||
async def remove_request(conn, request: ShoppingListRequest) -> None:
|
|
||||||
if request.list_id != None:
|
|
||||||
raise ValueError('Request is already completed')
|
|
||||||
|
|
||||||
if request.meal and not request.meal_id:
|
|
||||||
raise ValueError('Meal request must have a meal id')
|
|
||||||
|
|
||||||
if request.meal_id != None:
|
|
||||||
await conn.execute('''
|
|
||||||
DELETE FROM ShoppingListRequest
|
|
||||||
WHERE meal_id = ? AND list_id IS NULL
|
|
||||||
''', (request.meal_id,))
|
|
||||||
|
|
||||||
elif request.person_id != None and request.ingredient_id != None:
|
|
||||||
await conn.execute('''
|
|
||||||
DELETE FROM ShoppingListRequest
|
|
||||||
WHERE person_id = ? AND ingredient_id = ? AND list_id IS NULL
|
|
||||||
''', (request.person_id, request.ingredient_id))
|
|
||||||
|
|
||||||
else:
|
|
||||||
raise ValueError('Request is invalid')
|
|
||||||
|
|
||||||
async def find_requests_by_list_id(conn, list_id: Optional[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]
|
||||||
product_keys = [f'product.{key}' for key in Product.KEYS]
|
product_keys = [f'product.{key}' for key in Product.KEYS]
|
||||||
request_keys = [f'shoppinglistrequest.{key}' for key in ShoppingListRequest.KEYS]
|
request_keys = [f'shoppinglistitem.{key}' for key in ShoppingListItem.KEYS]
|
||||||
person_keys = [f'person.{key}' for key in Person.KEYS]
|
person_keys = [f'person.{key}' for key in Person.KEYS]
|
||||||
|
|
||||||
select = f'''
|
select = f'''
|
||||||
SELECT {','.join(ingredient_keys + product_keys + request_keys + person_keys)}
|
SELECT {','.join(ingredient_keys + product_keys + request_keys + person_keys)}
|
||||||
FROM ShoppingListRequest
|
FROM ShoppingListItem
|
||||||
LEFT JOIN Ingredient ON ShoppingListRequest.ingredient_id = Ingredient.id
|
LEFT JOIN Ingredient ON ShoppingListItem.ingredient_id = Ingredient.id
|
||||||
LEFT JOIN Product ON Ingredient.product_id = Product.id
|
LEFT JOIN Product ON Ingredient.product_id = Product.id
|
||||||
LEFT JOIN Person ON ShoppingListRequest.person_id = Person.id
|
LEFT JOIN Person ON ShoppingListItem.person_id = Person.id
|
||||||
'''
|
'''
|
||||||
|
|
||||||
where, params = ' WHERE list_id IS NULL', ()
|
where, params = ' WHERE list_id IS NULL', ()
|
||||||
|
|
@ -197,39 +216,14 @@ async def find_requests_by_list_id(conn, list_id: Optional[int]) -> AsyncIterato
|
||||||
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(ShoppingListItem.KEYS, row[len(Ingredient.KEYS) + len(Product.KEYS):-len(Person.KEYS)])}
|
||||||
request = ShoppingListRequest(**request_keys, ingredient=ingredient, person=person)
|
request = ShoppingListItem(**request_keys, ingredient=ingredient, person=person)
|
||||||
|
|
||||||
if request.meal_id is not None:
|
if request.meal_id is not None:
|
||||||
request.meal = await find_meal_by_id(conn, request.meal_id)
|
request.meal = await find_meal_by_id(conn, request.meal_id)
|
||||||
|
|
||||||
yield request
|
yield request
|
||||||
|
|
||||||
async def find_results_by_list_id(conn, list_id: int) -> AsyncIterator[ShoppingListResult]:
|
|
||||||
product_keys = [f'product.{key}' for key in Product.KEYS]
|
|
||||||
result_keys = [f'shoppinglistresult.{key}' for key in ShoppingListResult.KEYS]
|
|
||||||
|
|
||||||
async with conn.execute(f'''
|
|
||||||
SELECT {','.join(product_keys + result_keys)}
|
|
||||||
FROM ShoppingListResult
|
|
||||||
LEFT JOIN Product ON ShoppingListResult.product_id = Product.id
|
|
||||||
WHERE list_id = ?
|
|
||||||
''', (list_id,)) as cursor:
|
|
||||||
async for row in cursor:
|
|
||||||
product_keys = {k:v for k,v in zip(Product.KEYS, row[:len(Product.KEYS)])}
|
|
||||||
product = Product(**product_keys) if product_keys['id'] else None
|
|
||||||
|
|
||||||
result_keys = {k:v for k,v in zip(ShoppingListResult.KEYS, row[len(Product.KEYS):])}
|
|
||||||
result = ShoppingListResult(**result_keys, product=product)
|
|
||||||
yield result
|
|
||||||
|
|
||||||
async def fill_related(conn, shopping_list: ShoppingList) -> ShoppingList:
|
|
||||||
async for request in find_requests_by_list_id(conn, shopping_list.id):
|
|
||||||
shopping_list.requests.append(request)
|
|
||||||
|
|
||||||
async for item in find_results_by_list_id(conn, shopping_list.id):
|
|
||||||
shopping_list.results.append(item)
|
|
||||||
|
|
||||||
async def load_shopping_list(conn, id: int) -> ShoppingList:
|
async def load_shopping_list(conn, id: int) -> ShoppingList:
|
||||||
shopping_list = None
|
shopping_list = None
|
||||||
async with conn.execute(f'''
|
async with conn.execute(f'''
|
||||||
|
|
@ -242,61 +236,19 @@ async def load_shopping_list(conn, id: int) -> ShoppingList:
|
||||||
break
|
break
|
||||||
|
|
||||||
if shopping_list:
|
if shopping_list:
|
||||||
await fill_related(conn, shopping_list)
|
async for item in find_items_by_list_id(conn, shopping_list.id):
|
||||||
|
shopping_list.items.append(item)
|
||||||
|
|
||||||
return shopping_list
|
return shopping_list
|
||||||
|
|
||||||
async def request_ingredient(conn, person: Person, ingredient: Ingredient) -> ShoppingListRequest:
|
async def get_purchased_ingredients(conn, meal_ids: List[int]) -> AsyncIterator[ShoppingListItem]:
|
||||||
if ingredient.id >= 0:
|
if not meal_ids:
|
||||||
raise ValueError('How did you get an existing ingredient?')
|
return
|
||||||
|
|
||||||
await insert_ingredient(conn, ingredient)
|
async with conn.execute(f'''
|
||||||
|
SELECT {','.join(ShoppingListItem.KEYS)}
|
||||||
request = ShoppingListRequest(ingredient_id=ingredient.id, ingredient=ingredient, person_id=person.id, created_date=datetime.now().astimezone())
|
FROM ShoppingListItem
|
||||||
async with conn.execute('''
|
WHERE meal_id IN ({','.join(['?'] * len(meal_ids))})
|
||||||
INSERT INTO ShoppingListRequest (ingredient_id, person_id, created_date)
|
''', meal_ids) as cursor:
|
||||||
VALUES (?, ?, ?)
|
|
||||||
''', (request.ingredient_id, request.person_id, request.created_date.isoformat())) as cursor:
|
|
||||||
request.id = cursor.lastrowid
|
|
||||||
|
|
||||||
return request
|
|
||||||
|
|
||||||
async def request_meal(conn, person: Person, meal: Meal) -> ShoppingListRequest:
|
|
||||||
request = ShoppingListRequest(meal_id=meal.id, meal=meal, person_id=person.id, created_date=datetime.now().astimezone())
|
|
||||||
|
|
||||||
async with conn.execute('''
|
|
||||||
INSERT INTO ShoppingListRequest (meal_id, person_id, created_date)
|
|
||||||
VALUES (?, ?, ?)
|
|
||||||
''', (request.meal_id, request.person_id, request.created_date.isoformat())) as cursor:
|
|
||||||
request.id = cursor.lastrowid
|
|
||||||
|
|
||||||
return request
|
|
||||||
|
|
||||||
async def unrequest_meal(conn, meal: Meal) -> None:
|
|
||||||
await conn.execute('''
|
|
||||||
DELETE FROM ShoppingListRequest
|
|
||||||
WHERE list_id IS NULL AND meal_id = ?
|
|
||||||
''', (meal.id,))
|
|
||||||
|
|
||||||
def get_current_requests(conn) -> AsyncIterator[ShoppingListRequest]:
|
|
||||||
return find_requests_by_list_id(conn, None)
|
|
||||||
|
|
||||||
async def sync_persons_requested_ingredients(conn, person: Person, requests: List[Ingredient]) -> AsyncIterator[ShoppingListRequest]:
|
|
||||||
# Delete existing and insert all as new
|
|
||||||
await conn.execute('''
|
|
||||||
DELETE FROM ShoppingListRequest
|
|
||||||
WHERE list_id IS NULL AND person_id = ? AND ingredient_id IS NOT NULL
|
|
||||||
''', (person.id,))
|
|
||||||
|
|
||||||
for ingredient in requests:
|
|
||||||
ingredient.id = -1
|
|
||||||
yield await request_ingredient(conn, person, ingredient)
|
|
||||||
|
|
||||||
async def get_shopping_list_with_meal(conn, meal_id: int) -> AsyncIterator[ShoppingList]:
|
|
||||||
async with conn.execute('''
|
|
||||||
SELECT list_id FROM ShoppingListRequest
|
|
||||||
WHERE meal_id = ? AND list_id IS NOT NULL
|
|
||||||
''', (meal_id,)) as cursor:
|
|
||||||
async for row in cursor:
|
async for row in cursor:
|
||||||
list_id = row[0]
|
yield ShoppingListItem(**{k:v for k,v in zip(ShoppingListItem.KEYS, row)})
|
||||||
yield await load_shopping_list(conn, list_id)
|
|
||||||
Loading…
Reference in a new issue