Can edit own ingredient requests

This commit is contained in:
jableader 2025-07-27 15:24:54 +10:00
parent a9981005a4
commit efd0dabc87
6 changed files with 223 additions and 209 deletions

23
common.py Normal file
View 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
View file

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

View file

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

58
main.py
View file

@ -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}
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]
requested_meals = [r.meal_id for r in current_requests if r.meal_id] return CurrentShoppingList(requests=outstanding_requests, previously_purchased=purchased_requests, other_shopping_lists=other_shopping_lists)
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 = {}
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 {}

View file

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

View file

@ -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 async def purchase(conn, shopping_list: ShoppingList) -> None:
if request.ingredient and not request.person: if shopping_list.purchased_by_id is None:
raise ValueError('Ingredient requests must have a person') 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')
async def purchase_ingredients(conn, shopping_list: ShoppingListPurchase):
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] async def find_items_by_list_id(conn, list_id: Optional[int]) -> AsyncIterator[ShoppingListItem]:
ingredients_as_requests = [ShoppingListRequest(meal_id=meal.id, meal=meal, list_id=shopping_list_request[0].list_id) for meal in meals]
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)