Squashed commit of the following:

commit 2cf565c8c42387cdc57fd7ec7922e6d9e173322c
Author: jableader <jacobdunk@gmail.com>
Date:   Sun Oct 13 19:15:03 2024 +1100

    Fixed bug when saving own ingredients

commit 3ff1204a2fe88da65864109218c0a6de2130f7cc
Author: jableader <jacobdunk@gmail.com>
Date:   Sun Oct 13 18:02:04 2024 +1100

    First successful save

commit 2620e2c899c145df1c4a941d1eaaef7b01087791
Author: jableader <jacobdunk@gmail.com>
Date:   Fri Oct 4 17:53:21 2024 +1000

    Change shopping list to be post-able
This commit is contained in:
jableader 2024-10-13 19:19:57 +11:00
parent 0fdd0d10c2
commit 78e94a3503
5 changed files with 141 additions and 252 deletions

77
main.py
View file

@ -246,60 +246,49 @@ async def delete_meal(meal_id: int, conn: sqlite3.Connection = Depends(get_db))
await conn.commit() await conn.commit()
return meal return meal
class CurrentShoppingList(BaseModel):
requests: List[shopping.ShoppingListRequest]
overlapping_previous_shops: List[shopping.ShoppingList]
@app.get("/api/shopping/current")
async def get_current_shopping_list(conn: sqlite3.Connection = Depends(get_db)) -> CurrentShoppingList:
overlapping = []
current_requests = [r async for r in shopping.get_current_requests(conn)]
for request in current_requests:
if request.meal_id:
overlapping.extend([r async for r in shopping.get_shopping_list_with_meal(conn, request.meal_id)])
return CurrentShoppingList(requests=current_requests, overlapping_previous_shops=overlapping)
@app.get("/api/shopping/{list_id}") @app.get("/api/shopping/{list_id}")
async def get_shopping_list(list_id: Union[int, str], conn: sqlite3.Connection = Depends(get_db)) -> shopping.ShoppingList: async def get_shopping_list(list_id: int, conn: sqlite3.Connection = Depends(get_db)) -> shopping.ShoppingList:
if list_id.lower() == 'current':
return await shopping.current_shopping_list(conn)
try:
list_id = int(list_id)
except ValueError:
return JSONResponse(status_code=400, content={'message': 'Invalid shopping list ID'})
return await shopping.load_shopping_list(conn, list_id) return await shopping.load_shopping_list(conn, list_id)
@app.post("/api/shopping/current/purchased") class ShoppingListPurchase(shopping.ShoppingList):
async def mark_purchased(conn: sqlite3.Connection = Depends(get_db), person: persons.Person = Depends(cookie_person)) -> shopping.ShoppingList: completed_requests: List[shopping.ShoppingListRequest] = []
response = await shopping.mark_purchased(conn)
# Its easier to make the next shopping list now, while we know calling it requires commit() @app.post("/api/shopping/")
await shopping.current_shopping_list(conn) async def purchase_ingredients(lst: ShoppingListPurchase, conn: sqlite3.Connection = Depends(get_db)) -> shopping.ShoppingList:
await conn.commit() await shopping.insert_shopping_list(conn, lst)
return response for request in lst.completed_requests:
if request.meal_id:
meal = await meals.find_meal_by_id(conn, request.meal_id)
if meal:
await meals.mark_purchased(conn, meal)
class FoundResult(BaseModel): await shopping.remove_request(conn, request)
created: List[shopping.ShoppingListResult] = []
removed: List[shopping.ShoppingListResult] = []
@app.post("/api/shopping/current/found")
async def mark_shopping_list(ingredients: List[ingredients.Ingredient], conn: sqlite3.Connection = Depends(get_db)) -> FoundResult:
now = datetime.datetime.now()
result = FoundResult()
for ingredient in ingredients:
existing, created = await shopping.mark_found(conn, ingredient, now)
result.created.append(created)
if existing:
result.removed.append(existing)
await conn.commit() await conn.commit()
return result return lst
@app.delete("/api/shopping/current/found/{product_id}")
async def unmark_shopping_list(product_id: int, conn: sqlite3.Connection = Depends(get_db)) -> List[shopping.ShoppingListResult]:
response = await shopping.unmark_found(conn, product_id)
await conn.commit()
return response
@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[shopping.ShoppingListRequest]:
current = await shopping.current_shopping_list(conn) return [r async for r in shopping.get_current_requests(conn) if r.ingredient and r.person_id == person.id]
return [r async for r in shopping.get_persons_requests(conn, current, person) if r.ingredient]
@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[shopping.ShoppingListRequest]:
current = await shopping.current_shopping_list(conn) result = [r async for r in shopping.sync_persons_requested_ingredients(conn, person, requests) if r.ingredient]
result = [r async for r in shopping.sync_persons_requested_ingredients(conn, current, person, requests) if r.ingredient]
await conn.commit() await conn.commit()
return result return result
@ -308,23 +297,21 @@ class MealIdWrapper(BaseModel):
@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.ShoppingListRequest:
current = await shopping.current_shopping_list(conn)
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, current, person, meal) response = await shopping.request_meal(conn, person, meal)
await conn.commit() await conn.commit()
return response return response
@app.delete("/api/shopping/current/meals/{meal_id}") @app.delete("/api/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: 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) meal = await meals.find_meal_by_id(conn, 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'})
await shopping.delete_requests(conn, current, meal) await shopping.unrequest_meal(conn, meal)
await conn.commit() await conn.commit()
return {} return {}

View file

@ -17,7 +17,7 @@ class MealRecipe(BaseModel):
recipe: Optional[Recipe] = None recipe: Optional[Recipe] = None
class Meal(BaseModel): class Meal(BaseModel):
KEYS: ClassVar[List[str]] = ['id', 'suggested_date', 'consumed_date'] KEYS: ClassVar[List[str]] = ['id', 'suggested_date', 'consumed_date', 'purchase_date']
id: int = -1 id: int = -1
suggested_date: datetime.datetime suggested_date: datetime.datetime
consumed_date: Optional[datetime.datetime] = None consumed_date: Optional[datetime.datetime] = None
@ -37,7 +37,8 @@ async def create(conn):
id INTEGER PRIMARY KEY, id INTEGER PRIMARY KEY,
suggested_date TEXT, suggested_date TEXT,
consumed_date TEXT, consumed_date TEXT,
deleted_date TEXT DEFAULT NULL deleted_date TEXT DEFAULT NULL,
purchase_date TEXT DEFAULT NULL
);''') );''')
await conn.execute(''' await conn.execute('''
@ -113,7 +114,7 @@ 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:
meal = await with_purchase_date(conn, 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_participants(conn, meal)
await load_recipes(conn, meal) await load_recipes(conn, meal)
@ -126,7 +127,7 @@ async def find_upcoming_meals_by_date_range(conn, start: datetime, end: datetime
WHERE suggested_date >= ? AND suggested_date <= ? AND consumed_date IS NULL AND deleted_date IS NULL WHERE suggested_date >= ? AND suggested_date <= ? AND consumed_date IS NULL AND deleted_date IS NULL
''', (start, end)) as cursor: ''', (start, end)) as cursor:
async for row in cursor: async for row in cursor:
yield await with_purchase_date(conn, Meal(**{k:v for k,v in zip(Meal.KEYS, row)})) yield Meal(**{k:v for k,v in zip(Meal.KEYS, row)})
async def load_participants(conn, meal: Meal) -> None: async def load_participants(conn, meal: Meal) -> None:
async with conn.execute(f''' async with conn.execute(f'''
@ -213,16 +214,13 @@ async def mark_consumed(conn, meal: Meal, date: datetime.datetime = datetime.dat
WHERE id = ? WHERE id = ?
''', (date, meal.id)) ''', (date, meal.id))
async def with_purchase_date(conn, meal: Meal) -> Meal: async def mark_purchased(conn, meal: Meal) -> Meal:
async with conn.execute(''' meal.purchase_date = datetime.datetime.now()
SELECT purchased_date FROM ShoppingList
WHERE id = ( await conn.execute('''
SELECT list_id FROM ShoppingListRequest UPDATE Meal
WHERE meal_id = ? SET purchase_date = ?
LIMIT 1 WHERE id = ?
) ''', (meal.purchase_date, meal.id))
''', (meal.id,)) as cursor:
async for row in cursor:
meal.purchase_date = row[0]
return meal return meal

View file

@ -1,2 +1,2 @@
from shopping.db import ShoppingList, ShoppingListRequest, ShoppingListResult, current_shopping_list, mark_found, unmark_found, sync_persons_requested_ingredients, load_shopping_list, get_persons_requests, request_meal, delete_requests, mark_purchased from shopping.db import ShoppingList, ShoppingListRequest, ShoppingListResult, sync_persons_requested_ingredients, load_shopping_list, get_current_requests, request_meal, unrequest_meal, insert_shopping_list, get_shopping_list_with_meal, remove_request

View file

@ -1,17 +1,17 @@
from meals import Meal, find_upcoming_meals_by_date_range, 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 pydantic import BaseModel
from typing import AsyncIterator, List, ClassVar, Optional, Tuple from typing import AsyncIterator, List, ClassVar, Optional
from datetime import datetime, timedelta from datetime import datetime
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 = -1 id: int = -1
list_id: int list_id: Optional[int] = None
ingredient_id: Optional[int] = None ingredient_id: Optional[int] = None
ingredient: Optional[Ingredient] = None ingredient: Optional[Ingredient] = None
@ -25,7 +25,7 @@ class ShoppingListRequest(BaseModel):
created_date: datetime = datetime.now() created_date: datetime = datetime.now()
class ShoppingListResult(BaseModel): class ShoppingListResult(BaseModel):
KEYS: ClassVar[List[str]] = ['id', 'product_id', 'list_id', 'quantity', 'unit', 'created_date', 'found_date'] KEYS: ClassVar[List[str]] = ['id', 'product_id', 'list_id', 'quantity', 'unit' ]
id: int = -1 id: int = -1
list_id: int list_id: int
product_id: int product_id: int
@ -34,14 +34,18 @@ class ShoppingListResult(BaseModel):
quantity: float quantity: float
unit: str unit: str
created_date: datetime = datetime.now() from enum import Enum
found_date: Optional[datetime] = None
class StoreEnum(str, Enum):
woolworths = 'woolworths'
coles = 'coles'
home = ''
class ShoppingList(BaseModel): class ShoppingList(BaseModel):
KEYS: ClassVar[List[str]] = ['id', 'created_date', 'purchased_date'] KEYS: ClassVar[List[str]] = ['id', 'created_date', 'store_name']
id: int = -1 id: int = -1
created_date: datetime = datetime.now() created_date: datetime = datetime.now()
purchased_date: Optional[datetime] = None store_name: StoreEnum = ''
requests: List[ShoppingListRequest] = [] requests: List[ShoppingListRequest] = []
results: List[ShoppingListResult] = [] results: List[ShoppingListResult] = []
@ -51,7 +55,7 @@ async def create(conn):
CREATE TABLE IF NOT EXISTS ShoppingList ( CREATE TABLE IF NOT EXISTS ShoppingList (
id INTEGER PRIMARY KEY, id INTEGER PRIMARY KEY,
created_date TEXT, created_date TEXT,
purchased_date TEXT store_name TEXT,
);''') );''')
await conn.execute(''' await conn.execute('''
@ -75,8 +79,6 @@ async def create(conn):
list_id INTEGER, list_id INTEGER,
quantity REAL, quantity REAL,
unit TEXT, unit TEXT,
created_date TEXT DEFAULT CURRENT_TIMESTAMP,
found_date TEXT,
FOREIGN KEY(product_id) REFERENCES Product(id), FOREIGN KEY(product_id) REFERENCES Product(id),
FOREIGN KEY(list_id) REFERENCES ShoppingList(id) FOREIGN KEY(list_id) REFERENCES ShoppingList(id)
);''') );''')
@ -99,9 +101,9 @@ def validate_request(request: ShoppingListRequest) -> None:
async def insert_shopping_list(conn, shopping_list: ShoppingList): async def insert_shopping_list(conn, shopping_list: ShoppingList):
async with conn.execute(''' async with conn.execute('''
INSERT INTO ShoppingList (created_date, purchased_date) INSERT INTO ShoppingList (created_date, store_name)
VALUES (CURRENT_TIMESTAMP, NULL) VALUES (CURRENT_TIMESTAMP, ?)
''') as cursor: ''', (shopping_list.store_name,)) as cursor:
shopping_list.id = cursor.lastrowid shopping_list.id = cursor.lastrowid
for request in shopping_list.requests: for request in shopping_list.requests:
@ -129,59 +131,53 @@ async def insert_shopping_list(conn, shopping_list: ShoppingList):
item.list_id = shopping_list.id item.list_id = shopping_list.id
async with conn.execute(''' async with conn.execute('''
INSERT INTO ShoppingListResult (product_id, list_id, quantity, unit, created_date, found_date) INSERT INTO ShoppingListResult (product_id, list_id, quantity, unit)
VALUES (?, ?, ?, ?, ?, ?) VALUES (?, ?, ?, ?)
''', (item.product_id, item.list_id, item.quantity, item.unit, item.created_date, item.found_date)) as cursor: ''', (item.product_id, item.list_id, item.quantity, item.unit)) as cursor:
item.id = cursor.lastrowid item.id = cursor.lastrowid
async def find_request(conn, id: int) -> Optional[ShoppingListRequest]: async def remove_request(conn, request: ShoppingListRequest) -> None:
# Join Ingredient and Product to also load ingredient and product if request.list_id != None:
product_keys = [f'product.{key}' for key in Product.KEYS] raise ValueError('Request is already completed')
ingredient_keys = [f'ingredient.{key}' for key in Ingredient.KEYS]
person_keys = [f'person.{key}' for key in Person.KEYS]
request_keys = [f'shoppinglistrequest.{key}' for key in ShoppingListRequest.KEYS]
async with conn.execute(f''' if request.meal and not request.meal_id:
SELECT {','.join(product_keys + ingredient_keys + person_keys + request_keys)} raise ValueError('Meal request must have a meal id')
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(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(Product.KEYS):len(Product.KEYS) + len(Ingredient.KEYS)])} if request.meal_id != None:
ingredient = Ingredient(**ingredient_keys, product=product) if ingredient_keys['id'] else None await conn.execute('''
DELETE FROM ShoppingListRequest
WHERE meal_id = ? AND list_id IS NULL
''', (request.meal_id,))
person_keys = {k:v for k,v in zip(Person.KEYS, row[-len(Person.KEYS):])} elif request.person_id != None and request.ingredient_id != None:
person = Person(**person_keys) if person_keys['id'] else None await conn.execute('''
DELETE FROM ShoppingListRequest
WHERE person_id = ? AND ingredient_id = ? AND list_id IS NULL
''', (request.person_id, request.ingredient_id))
request_keys = {k:v for k,v in zip(ShoppingListRequest.KEYS, row[len(Product.KEYS) + len(Ingredient.KEYS):-len(Person.KEYS)])} else:
request = ShoppingListRequest(**request_keys, ingredient=ingredient, person=person) raise ValueError('Request is invalid')
if request.meal_id is not None: async def find_requests_by_list_id(conn, list_id: Optional[int]) -> AsyncIterator[ShoppingListRequest]:
request.meal = await find_meal_by_id(conn, request.meal_id)
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 # 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'shoppinglistrequest.{key}' for key in ShoppingListRequest.KEYS]
person_keys = [f'person.{key}' for key in Person.KEYS] person_keys = [f'person.{key}' for key in Person.KEYS]
cursor = await conn.execute(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 ShoppingListRequest
LEFT JOIN Ingredient ON ShoppingListRequest.ingredient_id = Ingredient.id LEFT JOIN Ingredient ON ShoppingListRequest.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 ShoppingListRequest.person_id = Person.id
WHERE list_id = ? '''
''', (list_id,))
where, params = ' WHERE list_id IS NULL', ()
if list_id is not None:
where, params = ' WHERE list_id = ?', (list_id,)
cursor = await conn.execute(select + where, params)
async for row in 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_keys = {k:v for k,v in zip(Product.KEYS, row[len(Ingredient.KEYS):len(Ingredient.KEYS) + len(Product.KEYS)])}
@ -201,7 +197,7 @@ async def find_requests_by_list_id(conn, list_id: int) -> AsyncIterator[Shopping
yield request yield request
async def find_items_by_list_id(conn, list_id: int) -> AsyncIterator[ShoppingListResult]: async def find_results_by_list_id(conn, list_id: int) -> AsyncIterator[ShoppingListResult]:
product_keys = [f'product.{key}' for key in Product.KEYS] product_keys = [f'product.{key}' for key in Product.KEYS]
result_keys = [f'shoppinglistresult.{key}' for key in ShoppingListResult.KEYS] result_keys = [f'shoppinglistresult.{key}' for key in ShoppingListResult.KEYS]
@ -223,7 +219,7 @@ async def fill_related(conn, shopping_list: ShoppingList) -> ShoppingList:
async for request in find_requests_by_list_id(conn, shopping_list.id): async for request in find_requests_by_list_id(conn, shopping_list.id):
shopping_list.requests.append(request) shopping_list.requests.append(request)
async for item in find_items_by_list_id(conn, shopping_list.id): async for item in find_results_by_list_id(conn, shopping_list.id):
shopping_list.results.append(item) shopping_list.results.append(item)
async def load_shopping_list(conn, id: int) -> ShoppingList: async def load_shopping_list(conn, id: int) -> ShoppingList:
@ -242,160 +238,57 @@ async def load_shopping_list(conn, id: int) -> ShoppingList:
return shopping_list return shopping_list
async def _upcoming_meals(conn) -> AsyncIterator[Meal]: async def request_ingredient(conn, person: Person, ingredient: Ingredient) -> ShoppingListRequest:
start = datetime.now() if ingredient.id >= 0:
end = start + timedelta(days=7)
async for meal in find_upcoming_meals_by_date_range(conn, start, end):
if meal.purchase_date is None:
yield meal
async def current_shopping_list(conn) -> ShoppingList:
shopping_list = None
async with conn.execute(f'''
SELECT {','.join(ShoppingList.KEYS)} FROM ShoppingList
WHERE purchased_date IS NULL
LIMIT 1
''') as cursor:
async for row in cursor:
shopping_list = ShoppingList(**{k:v for k,v in zip(ShoppingList.KEYS, row)})
await fill_related(conn, shopping_list)
break
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, meal=meal, created_date=datetime.now(),))
await insert_shopping_list(conn, shopping_list)
return shopping_list
async def find_existing_result(conn, product: Product, shopping_list: ShoppingList) -> ShoppingListResult:
async with conn.execute(f'''
SELECT {','.join(ShoppingListResult.KEYS)} FROM ShoppingListResult
WHERE product_id = ? AND list_id = ?
LIMIT 1
''', (product.id, shopping_list.id)) as cursor:
async for row in cursor:
return ShoppingListResult(**{k:v for k,v in zip(ShoppingListResult.KEYS, row)}, product=product)
return None
async def get_persons_requests(conn, shopping_list: ShoppingList, person: Person) -> AsyncIterator[ShoppingListRequest]:
async for request in find_requests_by_list_id(conn, shopping_list.id):
if request.person_id == person.id:
yield request
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?') 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, ingredient=ingredient, list_id=shopping_list.id, person_id=person.id, created_date=datetime.now()) request = ShoppingListRequest(ingredient_id=ingredient.id, ingredient=ingredient, 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, person_id, created_date)
VALUES (?, ?, ?, ?) VALUES (?, ?, ?)
''', (request.ingredient_id, request.list_id, request.person_id, request.created_date)) as cursor: ''', (request.ingredient_id, request.person_id, request.created_date)) as cursor:
request.id = cursor.lastrowid
shopping_list.requests.append(request)
return request
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 request.id = cursor.lastrowid
return request return request
async def delete_requests(conn, shopping_list: ShoppingList, meal: Meal) -> None: 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())
async with conn.execute('''
INSERT INTO ShoppingListRequest (meal_id, person_id, created_date)
VALUES (?, ?, ?)
''', (request.meal_id, request.person_id, request.created_date)) as cursor:
request.id = cursor.lastrowid
return request
async def unrequest_meal(conn, meal: Meal) -> None:
await conn.execute(''' await conn.execute('''
DELETE FROM ShoppingListRequest DELETE FROM ShoppingListRequest
WHERE list_id = ? AND meal_id = ? WHERE list_id IS NULL AND meal_id = ?
''', (shopping_list.id, meal.id)) ''', (meal.id,))
async def sync_persons_requested_ingredients(conn, shopping_list: ShoppingList, person: Person, requests: List[Ingredient]) -> AsyncIterator[ShoppingListRequest]: 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 # Delete existing and insert all as new
await conn.execute(''' await conn.execute('''
DELETE FROM ShoppingListRequest DELETE FROM ShoppingListRequest
WHERE list_id = ? AND person_id = ? AND ingredient_id IS NOT NULL WHERE list_id IS NULL AND person_id = ? AND ingredient_id IS NOT NULL
''', (shopping_list.id, person.id)) ''', (person.id,))
for ingredient in requests: for ingredient in requests:
ingredient.id = 0 ingredient.id = -1
yield await request_ingredient(conn, shopping_list, person, ingredient) yield await request_ingredient(conn, person, ingredient)
async def find_existing_result(conn, shopping_list: ShoppingList, product: Product, unit: str) -> Optional[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 ShoppingListResult.list_id = ? AND ShoppingListResult.product_id = ? AND ShoppingListResult.unit = ?
LIMIT 1
''', (shopping_list.id, product.id, unit)) 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):])}
return ShoppingListResult(**result_keys, product=product)
return None
async def mark_found(conn, ingredient: Ingredient, date_found: datetime) -> Tuple[ShoppingListResult, ShoppingListResult]:
product, quantity, unit = ingredient.product, ingredient.quantity, ingredient.unit
shopping_list = await current_shopping_list(conn)
created = ShoppingListResult(product=product, product_id=product.id, list_id=shopping_list.id, quantity=quantity, unit=unit, found_date=date_found)
removed = await find_existing_result(conn, shopping_list, product, unit)
async def get_shopping_list_with_meal(conn, meal_id: int) -> AsyncIterator[ShoppingList]:
async with conn.execute(''' async with conn.execute('''
INSERT INTO ShoppingListResult (product_id, list_id, quantity, unit, created_date, found_date) SELECT list_id FROM ShoppingListRequest
VALUES (?, ?, ?, ?, ?, ?) WHERE meal_id = ? AND list_id IS NOT NULL
''', (created.product_id, created.list_id, quantity, unit, created.created_date, created.found_date)) as cursor: ''', (meal_id,)) as cursor:
created.id = cursor.lastrowid
if removed:
shopping_list.results = [r for r in shopping_list.results if r.id != removed.id]
await conn.execute('''
DELETE FROM ShoppingListResult
WHERE id = ?
''', (removed.id,))
return removed, created
async def unmark_found(conn, product_id: int) -> List[ShoppingListRequest]:
shopping_list = await current_shopping_list(conn)
requests = []
async with conn.execute('''
DELETE FROM ShoppingListResult
WHERE list_id = ? AND product_id = ?
''', (shopping_list.id, product_id)) as cursor:
async for row in cursor: async for row in cursor:
requests.append(row) list_id = row[0]
yield await load_shopping_list(conn, list_id)
deleted = [r for r in shopping_list.results if r.product_id == product_id]
shopping_list.results = [r for r in shopping_list.results if r.product_id != product_id]
return deleted
async def mark_purchased(conn) -> ShoppingList:
shopping_list = await current_shopping_list(conn)
shopping_list.purchased_date = datetime.now()
await conn.execute('''
UPDATE ShoppingList
SET purchased_date = ?
WHERE id = ?
''', (shopping_list.purchased_date, shopping_list.id))
return shopping_list

11
transform.sql Normal file
View file

@ -0,0 +1,11 @@
alter table ShoppingList
add store_name text not null default '';
alter table ShoppingListResult
drop created_date;
alter table ShoppingListResult
drop found_date;
alter table Meal
add purchase_date TEXT DEFAULT NULL;