Compare commits
10 commits
be1d2b75ad
...
c098cd8fd0
| Author | SHA1 | Date | |
|---|---|---|---|
| c098cd8fd0 | |||
| 72a705140b | |||
| ba55f8a00a | |||
| d277c95895 | |||
| e0b643bfec | |||
| 6d7a8fb7b8 | |||
| a4639a79fd | |||
| a5ad6b7af3 | |||
| c1375d8d52 | |||
| 6b684fadd8 |
13 changed files with 477 additions and 168 deletions
|
|
@ -1,48 +1,68 @@
|
||||||
from ingredients.db import Ingredient, find_ingredients_by_meal_id, find_ingredients_by_recipe_id, insert_ingredient, delete_ingredients_by_meal_id
|
from ingredients.db import Ingredient, find_ingredients_by_meal_id, find_ingredients_by_recipe_id, insert_ingredient, delete_ingredients_by_meal_id
|
||||||
|
|
||||||
import units
|
import units
|
||||||
from products import Product, find_product_by_tag
|
from products import Product, find_product_by_tag, get_or_create, add_missing_tags
|
||||||
|
|
||||||
from ingredient_parser import parse_multiple_ingredients
|
from ingredient_parser import parse_ingredient
|
||||||
|
|
||||||
|
import re
|
||||||
from typing import List
|
from typing import List
|
||||||
|
|
||||||
def parse_ingredient_from_nlp(ingredients: List[str]) -> Ingredient:
|
async def parse_ingredient_from_link(conn, link: str) -> Ingredient:
|
||||||
results = []
|
match = re.match(r'^(\d+)?\s*(http.*)$', link)
|
||||||
for ingredient in parse_multiple_ingredients(ingredients):
|
if not match:
|
||||||
name = ingredient.name.text if ingredient.name else ''
|
return None
|
||||||
|
|
||||||
quantity, unit = None, None
|
quantity = int(match.group(1)) if match.group(1) else 1
|
||||||
for amount in ingredient.amount:
|
url = match.group(2)
|
||||||
if quantity is None and amount.quantity:
|
product = await get_or_create(conn, url, [])
|
||||||
quantity = amount.quantity
|
if product:
|
||||||
|
await add_missing_tags(conn, product, [product.name])
|
||||||
|
|
||||||
if unit is None and amount.unit:
|
return Ingredient(id=-1,
|
||||||
real_unit = units.get_unit(amount.unit)
|
name=product.name,
|
||||||
if real_unit:
|
line=f"{quantity}x {product.name}",
|
||||||
unit = real_unit.name
|
unit=units.ITEMS.name,
|
||||||
|
|
||||||
if isinstance(quantity, str):
|
|
||||||
try:
|
|
||||||
quantity = float(quantity)
|
|
||||||
except ValueError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
if quantity is None or not isinstance(quantity, (int, float)):
|
|
||||||
quantity = 1
|
|
||||||
|
|
||||||
if unit is None:
|
|
||||||
unit = units.ITEMS.name
|
|
||||||
|
|
||||||
results.append(Ingredient(id=0,
|
|
||||||
line=ingredient.sentence,
|
|
||||||
name=name,
|
|
||||||
quantity=quantity,
|
quantity=quantity,
|
||||||
unit=unit,
|
preparation='',
|
||||||
preparation=ingredient.preparation.text if ingredient.preparation else '',
|
product_id=product.id,
|
||||||
product_id=0
|
product=product
|
||||||
))
|
)
|
||||||
|
|
||||||
return results
|
def parse_ingredient_from_nlp(ingredient_string: str) -> Ingredient:
|
||||||
|
ingredient = parse_ingredient(ingredient_string)
|
||||||
|
name = ingredient.name.text if ingredient.name else ''
|
||||||
|
|
||||||
|
quantity, unit = None, None
|
||||||
|
for amount in ingredient.amount:
|
||||||
|
if quantity is None and amount.quantity:
|
||||||
|
quantity = amount.quantity
|
||||||
|
|
||||||
|
if unit is None and amount.unit:
|
||||||
|
real_unit = units.get_unit(amount.unit)
|
||||||
|
if real_unit:
|
||||||
|
unit = real_unit.name
|
||||||
|
|
||||||
|
if isinstance(quantity, str):
|
||||||
|
try:
|
||||||
|
quantity = float(quantity)
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if quantity is None or not isinstance(quantity, (int, float)):
|
||||||
|
quantity = 1
|
||||||
|
|
||||||
|
if unit is None:
|
||||||
|
unit = units.ITEMS.name
|
||||||
|
|
||||||
|
return Ingredient(id=0,
|
||||||
|
line=ingredient.sentence,
|
||||||
|
name=name,
|
||||||
|
quantity=quantity,
|
||||||
|
unit=unit,
|
||||||
|
preparation=ingredient.preparation.text if ingredient.preparation else '',
|
||||||
|
product_id=-1
|
||||||
|
)
|
||||||
|
|
||||||
async def _find_existing_product(conn, ingredient: str) -> Product:
|
async def _find_existing_product(conn, ingredient: str) -> Product:
|
||||||
async for item in find_product_by_tag(conn, ingredient):
|
async for item in find_product_by_tag(conn, ingredient):
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ from typing import AsyncIterator, List, ClassVar, Optional
|
||||||
|
|
||||||
class Ingredient(BaseModel):
|
class Ingredient(BaseModel):
|
||||||
KEYS: ClassVar[List[str]] = ['id', 'name', 'line', 'preparation', 'unit', 'quantity', 'product_id', 'recipe_id', 'meal_id']
|
KEYS: ClassVar[List[str]] = ['id', 'name', 'line', 'preparation', 'unit', 'quantity', 'product_id', 'recipe_id', 'meal_id']
|
||||||
id: int
|
id: int = -1
|
||||||
name: str
|
name: str
|
||||||
line: str
|
line: str
|
||||||
unit: str
|
unit: str
|
||||||
|
|
@ -34,10 +34,10 @@ async def create(conn):
|
||||||
);''')
|
);''')
|
||||||
|
|
||||||
async def insert_ingredient(conn, ingredient: Ingredient):
|
async def insert_ingredient(conn, ingredient: Ingredient):
|
||||||
if not ingredient.product_id and ingredient.product:
|
if ingredient.product:
|
||||||
ingredient.product_id = ingredient.product.id
|
ingredient.product_id = ingredient.product.id
|
||||||
|
|
||||||
if not ingredient.product_id:
|
if ingredient.product_id < 0:
|
||||||
raise ValueError('Product must be inserted before ingredient')
|
raise ValueError('Product must be inserted before ingredient')
|
||||||
|
|
||||||
async with conn.execute('''
|
async with conn.execute('''
|
||||||
|
|
|
||||||
94
main.py
94
main.py
|
|
@ -32,8 +32,8 @@ async def cookie_person(user_id: Annotated[int, Cookie(alias='user_id')], conn:
|
||||||
return await persons.get_by_id(conn, user_id)
|
return await persons.get_by_id(conn, user_id)
|
||||||
|
|
||||||
@app.get("/recipes/parse")
|
@app.get("/recipes/parse")
|
||||||
async def parse_recipe_handler(url: str, conn: sqlite3.Connection = Depends(get_db)) -> recipes.Recipe:
|
async def parse_recipe_handler(url: str, conn: sqlite3.Connection = Depends(get_db), person = Depends(cookie_person)) -> recipes.Recipe:
|
||||||
parsed = await recipes.parse_recipe(conn, url)
|
parsed = await recipes.parse_recipe(conn, person, url)
|
||||||
if not parsed:
|
if not parsed:
|
||||||
return JSONResponse(status_code=400, content={'message': 'Recipe not found'})
|
return JSONResponse(status_code=400, content={'message': 'Recipe not found'})
|
||||||
return parsed
|
return parsed
|
||||||
|
|
@ -44,7 +44,24 @@ async def parse_ingredients(lines: Annotated[
|
||||||
Query(alias="ingredients",
|
Query(alias="ingredients",
|
||||||
title="Array of ingredients to parse")],
|
title="Array of ingredients to parse")],
|
||||||
conn: sqlite3.Connection = Depends(get_db)) -> List[ingredients.Ingredient]:
|
conn: sqlite3.Connection = Depends(get_db)) -> List[ingredients.Ingredient]:
|
||||||
result = ingredients.parse_ingredient_from_nlp(lines)
|
|
||||||
|
had_links = False
|
||||||
|
result = []
|
||||||
|
for line in lines:
|
||||||
|
ingredient = await ingredients.parse_ingredient_from_link(conn, line)
|
||||||
|
if ingredient:
|
||||||
|
result.append(ingredient)
|
||||||
|
had_links = True
|
||||||
|
continue
|
||||||
|
|
||||||
|
ingredient = ingredients.parse_ingredient_from_nlp(line)
|
||||||
|
if ingredient:
|
||||||
|
result.append(ingredient)
|
||||||
|
continue
|
||||||
|
|
||||||
|
if had_links:
|
||||||
|
await conn.commit()
|
||||||
|
|
||||||
await ingredients.match_existing_products(conn, result)
|
await ingredients.match_existing_products(conn, result)
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
@ -95,29 +112,29 @@ async def get_recipe(recipe_id: int, conn: sqlite3.Connection = Depends(get_db))
|
||||||
return r
|
return r
|
||||||
|
|
||||||
@app.post('/recipes/')
|
@app.post('/recipes/')
|
||||||
async def create_recipe(item: recipes.Recipe, conn: sqlite3.Connection = Depends(get_db), user: persons.Person = Depends(cookie_person)) -> recipes.Recipe:
|
async def create_recipe(recipe: recipes.Recipe, conn: sqlite3.Connection = Depends(get_db), user: persons.Person = Depends(cookie_person)) -> recipes.Recipe:
|
||||||
if not item.ingredients:
|
if not recipe.ingredients:
|
||||||
return JSONResponse(status_code=400, content={'message': 'Recipe must have at least one ingredient'})
|
return JSONResponse(status_code=400, content={'message': 'Recipe must have at least one ingredient'})
|
||||||
|
|
||||||
for ingredient in item.ingredients:
|
for ingredient in recipe.ingredients:
|
||||||
if not ingredient.product:
|
if not ingredient.product:
|
||||||
return JSONResponse(status_code=400, content={'message': 'Ingredient must have a product'})
|
return JSONResponse(status_code=400, content={'message': 'Ingredient must have a product'})
|
||||||
|
|
||||||
if item.id:
|
if recipe.id >= 0:
|
||||||
await recipes.hide_recipe(conn, item.id, user)
|
await recipes.hide_recipe(conn, recipe.id, user)
|
||||||
item.based_on_recipe = item.id
|
recipe.based_on_recipe = recipe.id
|
||||||
item.id = 0
|
recipe.id = 0
|
||||||
|
|
||||||
item.created_by_id = user.id
|
recipe.created_by_id = user.id
|
||||||
await recipes.insert_recipe(conn, item)
|
await recipes.insert_recipe(conn, recipe)
|
||||||
for ingredient in item.ingredients:
|
for ingredient in recipe.ingredients:
|
||||||
ingredient.recipe_id = item.id
|
ingredient.recipe_id = recipe.id
|
||||||
ingredient.product_id = ingredient.product.id
|
ingredient.product_id = ingredient.product.id
|
||||||
await ingredients.insert_ingredient(conn, ingredient)
|
await ingredients.insert_ingredient(conn, ingredient)
|
||||||
|
|
||||||
await conn.commit()
|
await conn.commit()
|
||||||
|
|
||||||
return item
|
return recipe
|
||||||
|
|
||||||
@app.delete('/recipes/{recipe_id}')
|
@app.delete('/recipes/{recipe_id}')
|
||||||
async def delete_recipe(recipe_id: int, conn: sqlite3.Connection = Depends(get_db), user: persons.Person = Depends(cookie_person)) -> recipes.Recipe:
|
async def delete_recipe(recipe_id: int, conn: sqlite3.Connection = Depends(get_db), user: persons.Person = Depends(cookie_person)) -> recipes.Recipe:
|
||||||
|
|
@ -146,10 +163,6 @@ async def get_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 meals.load_participants(conn, meal)
|
|
||||||
await meals.load_recipes(conn, meal)
|
|
||||||
await meals.load_extra_ingredients(conn, meal)
|
|
||||||
|
|
||||||
return meal
|
return meal
|
||||||
|
|
||||||
def get_duplicates(items: List[meals.Person]) -> set[str]:
|
def get_duplicates(items: List[meals.Person]) -> set[str]:
|
||||||
|
|
@ -228,17 +241,48 @@ async def delete_meal(meal_id: int, conn: sqlite3.Connection = Depends(get_db))
|
||||||
|
|
||||||
@app.get("/shopping/{list_id}")
|
@app.get("/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: Union[int, str], conn: sqlite3.Connection = Depends(get_db)) -> shopping.ShoppingList:
|
||||||
if isinstance(list_id, str):
|
if list_id.lower() == 'current':
|
||||||
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.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("/shopping/current/purchased")
|
||||||
|
async def mark_purchased(conn: sqlite3.Connection = Depends(get_db), person: persons.Person = Depends(cookie_person)) -> shopping.ShoppingList:
|
||||||
|
response = await shopping.mark_purchased(conn)
|
||||||
|
|
||||||
|
# Its easier to make the next shopping list now, while we know calling it requires commit()
|
||||||
|
await shopping.current_shopping_list(conn)
|
||||||
|
await conn.commit()
|
||||||
|
return response
|
||||||
|
|
||||||
|
class FoundResult(BaseModel):
|
||||||
|
created: List[shopping.ShoppingListResult] = []
|
||||||
|
removed: List[shopping.ShoppingListResult] = []
|
||||||
|
|
||||||
@app.post("/shopping/current/found")
|
@app.post("/shopping/current/found")
|
||||||
async def mark_shopping_list(ingredient: ingredients.Ingredient, conn: sqlite3.Connection = Depends(get_db)) -> shopping.ShoppingListResult:
|
async def mark_shopping_list(ingredients: List[ingredients.Ingredient], conn: sqlite3.Connection = Depends(get_db)) -> FoundResult:
|
||||||
return await shopping.mark_found(conn, ingredient.product, ingredient.quantity, ingredient.unit)
|
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()
|
||||||
|
return result
|
||||||
|
|
||||||
|
@app.delete("/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("/shopping/current/me/ingredients")
|
@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]:
|
async def get_my_shopping_list(conn: sqlite3.Connection = Depends(get_db), person: persons.Person = Depends(cookie_person)) -> List[shopping.ShoppingListRequest]:
|
||||||
|
|
|
||||||
33
meals/db.py
33
meals/db.py
|
|
@ -11,8 +11,9 @@ import datetime
|
||||||
|
|
||||||
class Meal(BaseModel):
|
class Meal(BaseModel):
|
||||||
KEYS: ClassVar[List[str]] = ['id', 'meal_date']
|
KEYS: ClassVar[List[str]] = ['id', 'meal_date']
|
||||||
id: int
|
id: int = -1
|
||||||
meal_date: datetime.datetime
|
meal_date: datetime.datetime
|
||||||
|
purchase_date: Optional[datetime.datetime] = None
|
||||||
|
|
||||||
chefs: List[Person] = []
|
chefs: List[Person] = []
|
||||||
cleanup: List[Person] = []
|
cleanup: List[Person] = []
|
||||||
|
|
@ -60,7 +61,7 @@ async def sync_meal_participants(conn, meal_id: int, participants: List[Person],
|
||||||
await insert_meal_participant(conn, meal_id, person.id, role)
|
await insert_meal_participant(conn, meal_id, person.id, role)
|
||||||
|
|
||||||
async def insert_meal_recipe(conn, meal_id: int, recipe_id: int):
|
async def insert_meal_recipe(conn, meal_id: int, recipe_id: int):
|
||||||
if not recipe_id:
|
if recipe_id < 0:
|
||||||
raise ValueError('Recipe must be inserted before meal')
|
raise ValueError('Recipe must be inserted before meal')
|
||||||
|
|
||||||
await conn.execute('''
|
await conn.execute('''
|
||||||
|
|
@ -91,28 +92,20 @@ 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 = Meal(**{k:v for k,v in zip(Meal.KEYS, row)})
|
meal = await with_purchase_date(conn, 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)
|
||||||
await load_extra_ingredients(conn, meal)
|
await load_extra_ingredients(conn, meal)
|
||||||
return meal
|
return meal
|
||||||
|
|
||||||
async def find_meal_by_date(conn, date: datetime) -> Meal:
|
|
||||||
async with conn.execute(f'''
|
|
||||||
SELECT {','.join(Meal.KEYS)} FROM Meal
|
|
||||||
WHERE meal_date = ?
|
|
||||||
LIMIT 1
|
|
||||||
''', (date,)) as cursor:
|
|
||||||
async for row in cursor:
|
|
||||||
return Meal(**{k:v for k,v in zip(Meal.KEYS, row)})
|
|
||||||
|
|
||||||
async def find_meals_by_date_range(conn, start: datetime, end: datetime) -> AsyncIterator[Meal]:
|
async def find_meals_by_date_range(conn, start: datetime, end: datetime) -> AsyncIterator[Meal]:
|
||||||
async with conn.execute(f'''
|
async with conn.execute(f'''
|
||||||
SELECT {','.join(Meal.KEYS)} FROM Meal
|
SELECT {','.join(Meal.KEYS)} FROM Meal
|
||||||
WHERE meal_date >= ? AND meal_date <= ?
|
WHERE meal_date >= ? AND meal_date <= ?
|
||||||
''', (start, end)) as cursor:
|
''', (start, end)) as cursor:
|
||||||
async for row in cursor:
|
async for row in cursor:
|
||||||
yield Meal(**{k:v for k,v in zip(Meal.KEYS, row)})
|
yield await with_purchase_date(conn, 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'''
|
||||||
|
|
@ -198,3 +191,17 @@ async def update_meal(conn, meal: Meal) -> None:
|
||||||
|
|
||||||
await sync_extra_ingredients(conn, meal.id, meal.extra_ingredients)
|
await sync_extra_ingredients(conn, meal.id, meal.extra_ingredients)
|
||||||
await sync_recipes(conn, meal.id, meal.recipes)
|
await sync_recipes(conn, meal.id, meal.recipes)
|
||||||
|
|
||||||
|
async def with_purchase_date(conn, meal: Meal) -> Meal:
|
||||||
|
async with conn.execute('''
|
||||||
|
SELECT purchased_date FROM ShoppingList
|
||||||
|
WHERE id = (
|
||||||
|
SELECT list_id FROM ShoppingListRequest
|
||||||
|
WHERE meal_id = ?
|
||||||
|
LIMIT 1
|
||||||
|
)
|
||||||
|
''', (meal.id,)) as cursor:
|
||||||
|
async for row in cursor:
|
||||||
|
meal.purchase_date = row[0]
|
||||||
|
|
||||||
|
return meal
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ from typing import AsyncIterator, ClassVar, List
|
||||||
class Person(BaseModel):
|
class Person(BaseModel):
|
||||||
KEYS: ClassVar[List[str]] = ['id', 'name']
|
KEYS: ClassVar[List[str]] = ['id', 'name']
|
||||||
|
|
||||||
id: int
|
id: int = -1
|
||||||
name: str
|
name: str
|
||||||
|
|
||||||
async def create(conn):
|
async def create(conn):
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,16 @@ from products.db import Product, find_product_by_tag, find_product_by_product_id
|
||||||
from products.scraping import scrape_woolies_data, get_product_id, get_product_details_url
|
from products.scraping import scrape_woolies_data, get_product_id, get_product_details_url
|
||||||
|
|
||||||
from typing import List
|
from typing import List
|
||||||
|
import re
|
||||||
|
|
||||||
|
def get_package_size(data: dict) -> str:
|
||||||
|
size = data['Product']['PackageSize']
|
||||||
|
if size:
|
||||||
|
match = re.match(r'(\d+)(.*)', size)
|
||||||
|
if match:
|
||||||
|
return int(match.group(1)), match.group(2)
|
||||||
|
|
||||||
|
return 1, 'items'
|
||||||
|
|
||||||
async def create_product(link: str) -> Product:
|
async def create_product(link: str) -> Product:
|
||||||
product_id = get_product_id(link)
|
product_id = get_product_id(link)
|
||||||
|
|
@ -13,11 +23,16 @@ async def create_product(link: str) -> Product:
|
||||||
product_url = get_product_details_url(product_id)
|
product_url = get_product_details_url(product_id)
|
||||||
product, data = None, await scrape_woolies_data(product_url)
|
product, data = None, await scrape_woolies_data(product_url)
|
||||||
if data:
|
if data:
|
||||||
product = Product(
|
_dump_json_data_to_log(data, product_id)
|
||||||
|
|
||||||
|
quantity, unit = get_package_size(data)
|
||||||
|
product = Product(
|
||||||
id=0,
|
id=0,
|
||||||
product_id=product_id,
|
product_id=product_id,
|
||||||
name=data['Product']['Name'],
|
name=data['Product']['Name'],
|
||||||
link=link,
|
link=link,
|
||||||
|
quantity=quantity,
|
||||||
|
unit=unit,
|
||||||
img_small=data['Product']['SmallImageFile'],
|
img_small=data['Product']['SmallImageFile'],
|
||||||
img_large=data['Product']['LargeImageFile'],
|
img_large=data['Product']['LargeImageFile'],
|
||||||
)
|
)
|
||||||
|
|
@ -54,3 +69,17 @@ async def get_or_create(conn, url: str, tags: List[str]) -> Product:
|
||||||
await add_missing_tags(conn, product, tags)
|
await add_missing_tags(conn, product, tags)
|
||||||
|
|
||||||
return product
|
return product
|
||||||
|
|
||||||
|
def _dump_json_data_to_log(data: dict, product_id: str) -> str:
|
||||||
|
import os, re
|
||||||
|
dir = './data/dump'
|
||||||
|
if not os.path.exists(dir):
|
||||||
|
os.makedirs(dir)
|
||||||
|
|
||||||
|
prefix = f'product_{product_id}'
|
||||||
|
suffix = '.json'
|
||||||
|
file_ids = [int(re.findall(r'\d+', f)[0]) for f in os.listdir(dir) if re.match(prefix + r'\d+' + suffix, f)]
|
||||||
|
id = max(file_ids) + 1 if file_ids else 0
|
||||||
|
filename = f'{prefix}{id}{suffix}'
|
||||||
|
with open(os.path.join(dir, filename), 'w') as f:
|
||||||
|
json.dump(data, f, indent=4)
|
||||||
|
|
@ -4,11 +4,15 @@ from pydantic import BaseModel
|
||||||
import json
|
import json
|
||||||
|
|
||||||
class Product(BaseModel):
|
class Product(BaseModel):
|
||||||
KEYS: ClassVar[List[str]] = ['id', 'product_id', 'link', 'name', 'img_small', 'img_large']
|
KEYS: ClassVar[List[str]] = ['id', 'product_id', 'link', 'name', 'quantity', 'unit', 'img_small', 'img_large']
|
||||||
id: int
|
NON_INSERT_KEYS: ClassVar[List[str]] = ['id']
|
||||||
|
|
||||||
|
id: int = -1
|
||||||
product_id: str
|
product_id: str
|
||||||
link: str
|
link: str
|
||||||
name: str
|
name: str
|
||||||
|
quantity: int
|
||||||
|
unit: str
|
||||||
img_small: str
|
img_small: str
|
||||||
img_large: str
|
img_large: str
|
||||||
|
|
||||||
|
|
@ -19,6 +23,8 @@ async def create(conn):
|
||||||
product_id TEXT UNIQUE,
|
product_id TEXT UNIQUE,
|
||||||
link TEXT,
|
link TEXT,
|
||||||
name TEXT,
|
name TEXT,
|
||||||
|
quantity INTEGER,
|
||||||
|
unit TEXT,
|
||||||
img_small TEXT,
|
img_small TEXT,
|
||||||
img_large TEXT,
|
img_large TEXT,
|
||||||
raw_data TEXT
|
raw_data TEXT
|
||||||
|
|
@ -62,10 +68,13 @@ async def find_product_by_product_id(conn, product_id: str) -> Product:
|
||||||
return Product(**{k:v for k,v in zip(Product.KEYS, row)})
|
return Product(**{k:v for k,v in zip(Product.KEYS, row)})
|
||||||
|
|
||||||
async def insert_product(conn, product: Product, data: dict):
|
async def insert_product(conn, product: Product, data: dict):
|
||||||
async with conn.execute('''
|
insert_keys = [k for k in Product.KEYS if k not in Product.NON_INSERT_KEYS]
|
||||||
INSERT INTO Product (name, product_id, link, img_small, img_large, raw_data)
|
insert_values = [getattr(product, k) for k in insert_keys]
|
||||||
VALUES (?, ?, ?, ?, ?, ?)
|
|
||||||
''', (product.name, product.product_id, product.link, product.img_small, product.img_large, json.dumps(data))) as cursor:
|
async with conn.execute(f'''
|
||||||
|
INSERT INTO Product ({','.join(insert_keys)}, raw_data)
|
||||||
|
VALUES ({','.join(['?'] * len(insert_keys))}, ?)
|
||||||
|
''', (*insert_values, json.dumps(data))) as cursor:
|
||||||
product.id = cursor.lastrowid
|
product.id = cursor.lastrowid
|
||||||
|
|
||||||
await conn.commit()
|
await conn.commit()
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,42 @@
|
||||||
|
from persons import Person
|
||||||
|
|
||||||
from recipes.db import Recipe, insert_recipe, find_recipe_by_id, get_all, find_recipes_by_name, row_to_recipe, load_recipe_ingredients, hide_recipe
|
from recipes.db import Recipe, insert_recipe, find_recipe_by_id, get_all, find_recipes_by_name, row_to_recipe, load_recipe_ingredients, hide_recipe
|
||||||
from recipes.scraping import scrape_recipe_ldata as _scrape_recipe_ldata
|
from recipes.scraping import scrape_recipe_ldata as _scrape_recipe_ldata
|
||||||
from ingredients import parse_ingredient_from_nlp as _parse_ingredient_from_nlp, match_existing_products as _match_existing_products
|
from ingredients import parse_ingredient_from_nlp, match_existing_products
|
||||||
|
|
||||||
async def parse_recipe(conn, url: str) -> Recipe:
|
import re
|
||||||
|
|
||||||
|
async def parse_recipe(conn, created_by: Person, url: str) -> Recipe:
|
||||||
ldata = await _scrape_recipe_ldata(url)
|
ldata = await _scrape_recipe_ldata(url)
|
||||||
if ldata:
|
if ldata:
|
||||||
return await _get_recipe_from_ldata(conn, url, ldata)
|
return await _get_recipe_from_ldata(conn, url, ldata, created_by)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
async def _get_recipe_from_ldata(conn, url: str, ldata: dict) -> dict:
|
def find_yield(recipe_ldata: dict) -> int:
|
||||||
ingredients = _parse_ingredient_from_nlp(ldata['recipeIngredient'])
|
if 'recipeYield' in recipe_ldata:
|
||||||
ingredients = await _match_existing_products(conn, ingredients)
|
yield_vals = recipe_ldata['recipeYield']
|
||||||
|
if not isinstance(yield_vals, list):
|
||||||
|
yield_vals = [yield_vals]
|
||||||
|
|
||||||
|
for val in yield_vals:
|
||||||
|
try:
|
||||||
|
return int(val)
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
for val in yield_vals:
|
||||||
|
match = re.match(r'(\d+)', val)
|
||||||
|
if match:
|
||||||
|
return int(match.group(1))
|
||||||
|
|
||||||
|
return 4
|
||||||
|
|
||||||
|
async def _get_recipe_from_ldata(conn, url: str, ldata: dict, created_by: Person) -> dict:
|
||||||
|
ingredients = parse_ingredient_from_nlp(ldata['recipeIngredient'])
|
||||||
|
ingredients = await match_existing_products(conn, ingredients)
|
||||||
name = ldata['name'] if 'name' in ldata else url
|
name = ldata['name'] if 'name' in ldata else url
|
||||||
images = ldata['image'] if 'image' in ldata else []
|
images = ldata['image'] if 'image' in ldata else []
|
||||||
|
serves = find_yield(ldata)
|
||||||
|
|
||||||
if isinstance(images, list) and len(images) > 0 and isinstance(images[0], dict):
|
if isinstance(images, list) and len(images) > 0 and isinstance(images[0], dict):
|
||||||
images = [image['url'] for image in images]
|
images = [image['url'] for image in images]
|
||||||
|
|
@ -27,6 +51,9 @@ async def _get_recipe_from_ldata(conn, url: str, ldata: dict) -> dict:
|
||||||
id=0,
|
id=0,
|
||||||
name=name,
|
name=name,
|
||||||
link=url,
|
link=url,
|
||||||
|
serves=serves,
|
||||||
image_urls=images,
|
image_urls=images,
|
||||||
ingredients=ingredients
|
ingredients=ingredients,
|
||||||
|
created_by=created_by,
|
||||||
|
created_by_id=created_by.id,
|
||||||
)
|
)
|
||||||
|
|
@ -7,16 +7,19 @@ from pydantic import BaseModel
|
||||||
from typing import AsyncIterator, List, ClassVar, Tuple, Optional
|
from typing import AsyncIterator, List, ClassVar, Tuple, Optional
|
||||||
|
|
||||||
class Recipe(BaseModel):
|
class Recipe(BaseModel):
|
||||||
KEYS: ClassVar[List[str]] = ['id', 'name', 'link', 'image_urls', 'based_on_recipe', 'created_by_id', 'date_created', 'hidden_by_id', 'date_hidden']
|
KEYS: ClassVar[List[str]] = ['id', 'name', 'link', 'serves', 'image_urls', 'based_on_recipe', 'created_by_id', 'date_created', 'hidden_by_id', 'date_hidden']
|
||||||
id: int
|
NON_INSERT_KEYS: ClassVar[List[str]] = ['id', 'created_date', 'hidden_by_id', 'date_hidden']
|
||||||
|
|
||||||
|
id: int = -1
|
||||||
name: str
|
name: str
|
||||||
link: str
|
link: str
|
||||||
|
serves: int
|
||||||
image_urls: List[str] = []
|
image_urls: List[str] = []
|
||||||
ingredients: List[Ingredient] = []
|
ingredients: List[Ingredient] = []
|
||||||
based_on_recipe: Optional[int] = None
|
based_on_recipe: Optional[int] = None
|
||||||
|
|
||||||
date_created: Optional[datetime.datetime] = None
|
date_created: datetime.datetime = datetime.datetime.now()
|
||||||
created_by_id: Optional[int] = None
|
created_by_id: Optional[int]
|
||||||
created_by: Optional[Person] = None
|
created_by: Optional[Person] = None
|
||||||
|
|
||||||
date_hidden: Optional[datetime.datetime] = None
|
date_hidden: Optional[datetime.datetime] = None
|
||||||
|
|
@ -29,6 +32,7 @@ async def create(conn):
|
||||||
id INTEGER PRIMARY KEY,
|
id INTEGER PRIMARY KEY,
|
||||||
name TEXT NOT NULL,
|
name TEXT NOT NULL,
|
||||||
link TEXT NOT NULL,
|
link TEXT NOT NULL,
|
||||||
|
serves INTEGER NOT NULL,
|
||||||
image_urls TEXT NOT NULL,
|
image_urls TEXT NOT NULL,
|
||||||
based_on_recipe INTEGER NULL,
|
based_on_recipe INTEGER NULL,
|
||||||
|
|
||||||
|
|
@ -43,11 +47,22 @@ async def create(conn):
|
||||||
FOREIGN KEY (hidden_by_id) REFERENCES Person(id)
|
FOREIGN KEY (hidden_by_id) REFERENCES Person(id)
|
||||||
);''')
|
);''')
|
||||||
|
|
||||||
|
def _as_insert_field(recipe: Recipe, name: str):
|
||||||
|
value = getattr(recipe, name)
|
||||||
|
if name == 'image_urls':
|
||||||
|
return json.dumps(value)
|
||||||
|
return value
|
||||||
|
|
||||||
async def insert_recipe(conn, recipe: Recipe):
|
async def insert_recipe(conn, recipe: Recipe):
|
||||||
async with conn.execute('''
|
fields_to_insert = [k for k in Recipe.KEYS if k not in Recipe.NON_INSERT_KEYS]
|
||||||
INSERT INTO Recipe (name, link, image_urls, based_on_recipe, created_by_id)
|
actual_values = [_as_insert_field(recipe, k) for k in fields_to_insert]
|
||||||
VALUES (?, ?, ?, ?, ?)
|
|
||||||
''', (recipe.name, recipe.link, json.dumps(recipe.image_urls), recipe.based_on_recipe, recipe.created_by_id)) as cursor:
|
insert_stmt = f'''
|
||||||
|
INSERT INTO Recipe ({','.join(fields_to_insert)})
|
||||||
|
VALUES ({','.join(['?'] * len(fields_to_insert))})
|
||||||
|
'''
|
||||||
|
|
||||||
|
async with conn.execute(insert_stmt, actual_values) as cursor:
|
||||||
recipe.id = cursor.lastrowid
|
recipe.id = cursor.lastrowid
|
||||||
|
|
||||||
async def hide_recipe(conn, recipe_id: int, person: Person):
|
async def hide_recipe(conn, recipe_id: int, person: Person):
|
||||||
|
|
|
||||||
|
|
@ -1,2 +1,2 @@
|
||||||
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
|
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
|
||||||
|
|
||||||
|
|
|
||||||
178
shopping/db.py
178
shopping/db.py
|
|
@ -4,13 +4,13 @@ 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
|
from typing import AsyncIterator, List, ClassVar, Optional, Tuple
|
||||||
|
|
||||||
from datetime import datetime, timedelta
|
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 = -1
|
||||||
list_id: int
|
list_id: int
|
||||||
|
|
||||||
ingredient_id: Optional[int] = None
|
ingredient_id: Optional[int] = None
|
||||||
|
|
@ -26,9 +26,10 @@ class ShoppingListRequest(BaseModel):
|
||||||
|
|
||||||
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', 'created_date', 'found_date']
|
||||||
id: int = 0
|
id: int = -1
|
||||||
product_id: int
|
|
||||||
list_id: int
|
list_id: int
|
||||||
|
product_id: int
|
||||||
|
product: Optional[Product] = None
|
||||||
|
|
||||||
quantity: float
|
quantity: float
|
||||||
unit: str
|
unit: str
|
||||||
|
|
@ -38,12 +39,12 @@ class ShoppingListResult(BaseModel):
|
||||||
|
|
||||||
class ShoppingList(BaseModel):
|
class ShoppingList(BaseModel):
|
||||||
KEYS: ClassVar[List[str]] = ['id', 'created_date', 'purchased_date']
|
KEYS: ClassVar[List[str]] = ['id', 'created_date', 'purchased_date']
|
||||||
id: int = 0
|
id: int = -1
|
||||||
created_date: datetime = datetime.now()
|
created_date: datetime = datetime.now()
|
||||||
purchased_date: Optional[datetime] = None
|
purchased_date: Optional[datetime] = None
|
||||||
|
|
||||||
requests: List[ShoppingListRequest] = []
|
requests: List[ShoppingListRequest] = []
|
||||||
items: List[ShoppingListResult] = []
|
results: List[ShoppingListResult] = []
|
||||||
|
|
||||||
async def create(conn):
|
async def create(conn):
|
||||||
await conn.execute('''
|
await conn.execute('''
|
||||||
|
|
@ -74,45 +75,74 @@ async def create(conn):
|
||||||
list_id INTEGER,
|
list_id INTEGER,
|
||||||
quantity REAL,
|
quantity REAL,
|
||||||
unit TEXT,
|
unit TEXT,
|
||||||
created_date TEXT,
|
created_date TEXT DEFAULT CURRENT_TIMESTAMP,
|
||||||
found_date TEXT,
|
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)
|
||||||
);''')
|
);''')
|
||||||
|
|
||||||
|
def validate_request(request: ShoppingListRequest) -> None:
|
||||||
|
# A request must always have a list id
|
||||||
|
if request.list_id < 0:
|
||||||
|
raise ValueError('Request must have a list id')
|
||||||
|
|
||||||
|
# A request must have either an ingredient or a meal, but not both
|
||||||
|
if not request.ingredient and not request.meal:
|
||||||
|
raise ValueError('Request must have either an ingredient or a meal')
|
||||||
|
|
||||||
|
if request.ingredient and request.meal:
|
||||||
|
raise ValueError('Request cannot have both an ingredient and 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 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 (id, created_date, purchased_date)
|
INSERT INTO ShoppingList (created_date, purchased_date)
|
||||||
VALUES (?, ?, ?)
|
VALUES (CURRENT_TIMESTAMP, NULL)
|
||||||
''', (shopping_list.id, shopping_list.created_date, shopping_list.purchased_date)) as cursor:
|
''') as cursor:
|
||||||
shopping_list.id = cursor.lastrowid
|
shopping_list.id = cursor.lastrowid
|
||||||
|
|
||||||
for request in shopping_list.requests:
|
for request in shopping_list.requests:
|
||||||
if not request.ingredient_id:
|
request.list_id = shopping_list.id
|
||||||
|
|
||||||
|
validate_request(request)
|
||||||
|
|
||||||
|
if request.ingredient and request.ingredient.id < 0:
|
||||||
await insert_ingredient(conn, request.ingredient)
|
await insert_ingredient(conn, request.ingredient)
|
||||||
|
|
||||||
request.ingredient_id = request.ingredient.id
|
if request.ingredient:
|
||||||
await conn.execute('''
|
request.ingredient_id = request.ingredient.id
|
||||||
INSERT INTO ShoppingListRequest (id, ingredient_id, list_id, person_id, meal_id, created_date)
|
|
||||||
VALUES (?, ?, ?, ?, ?, ?)
|
|
||||||
''', (request.id, request.ingredient_id, shopping_list.id, request.person_id, request.meal_id, request.created_date))
|
|
||||||
|
|
||||||
for item in shopping_list.items:
|
if request.meal:
|
||||||
|
request.meal_id = request.meal.id
|
||||||
|
|
||||||
|
async with conn.execute('''
|
||||||
|
INSERT INTO ShoppingListRequest (ingredient_id, list_id, person_id, meal_id, created_date)
|
||||||
|
VALUES (?, ?, ?, ?, ?)
|
||||||
|
''', (request.ingredient_id, shopping_list.id, request.person_id, request.meal_id, request.created_date)) as cursor:
|
||||||
|
request.id = cursor.lastrowid
|
||||||
|
|
||||||
|
for item in shopping_list.results:
|
||||||
item.product_id = item.product.id
|
item.product_id = item.product.id
|
||||||
await conn.execute('''
|
item.list_id = shopping_list.id
|
||||||
INSERT INTO ShoppingListResult (id, product_id, list_id, quantity, unit, created_date, found_date)
|
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?)
|
async with conn.execute('''
|
||||||
''', (item.id, item.product_id, shopping_list.id, item.quantity, item.unit, item.created_date, item.found_date))
|
INSERT INTO ShoppingListResult (product_id, list_id, quantity, unit, created_date, found_date)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?)
|
||||||
|
''', (item.product_id, item.list_id, item.quantity, item.unit, item.created_date, item.found_date)) as cursor:
|
||||||
|
item.id = cursor.lastrowid
|
||||||
|
|
||||||
async def find_request(conn, id: int) -> Optional[ShoppingListRequest]:
|
async def find_request(conn, id: int) -> Optional[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]
|
|
||||||
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]
|
ingredient_keys = [f'ingredient.{key}' for key in Ingredient.KEYS]
|
||||||
person_keys = [f'person.{key}' for key in Person.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'''
|
async with conn.execute(f'''
|
||||||
SELECT {','.join(ingredient_keys + product_keys + request_keys + person_keys)}
|
SELECT {','.join(product_keys + ingredient_keys + person_keys + request_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
|
||||||
|
|
@ -120,17 +150,21 @@ async def find_request(conn, id: int) -> Optional[ShoppingListRequest]:
|
||||||
WHERE ShoppingListRequest.id = ?
|
WHERE ShoppingListRequest.id = ?
|
||||||
''', (id,)) as cursor:
|
''', (id,)) as cursor:
|
||||||
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(Product.KEYS)])}
|
||||||
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(Product.KEYS):len(Product.KEYS) + 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(Product.KEYS) + len(Ingredient.KEYS):-len(Person.KEYS)])}
|
||||||
request = ShoppingListRequest(**request_keys, ingredient=ingredient, person=person)
|
request = ShoppingListRequest(**request_keys, ingredient=ingredient, person=person)
|
||||||
|
|
||||||
|
if request.meal_id is not None:
|
||||||
|
request.meal = await find_meal_by_id(conn, request.meal_id)
|
||||||
|
|
||||||
return request
|
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]:
|
||||||
|
|
@ -162,7 +196,7 @@ async def find_requests_by_list_id(conn, list_id: int) -> AsyncIterator[Shopping
|
||||||
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:
|
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
|
||||||
|
|
@ -190,7 +224,7 @@ async def fill_related(conn, shopping_list: ShoppingList) -> ShoppingList:
|
||||||
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_items_by_list_id(conn, shopping_list.id):
|
||||||
shopping_list.items.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:
|
||||||
shopping_list = None
|
shopping_list = None
|
||||||
|
|
@ -212,7 +246,8 @@ async def _upcoming_meals(conn) -> AsyncIterator[Meal]:
|
||||||
start = datetime.now()
|
start = datetime.now()
|
||||||
end = start + timedelta(days=7)
|
end = start + timedelta(days=7)
|
||||||
async for meal in find_meals_by_date_range(conn, start, end):
|
async for meal in find_meals_by_date_range(conn, start, end):
|
||||||
yield meal
|
if meal.purchase_date is None:
|
||||||
|
yield meal
|
||||||
|
|
||||||
async def current_shopping_list(conn) -> ShoppingList:
|
async def current_shopping_list(conn) -> ShoppingList:
|
||||||
shopping_list = None
|
shopping_list = None
|
||||||
|
|
@ -230,7 +265,7 @@ async def current_shopping_list(conn) -> ShoppingList:
|
||||||
shopping_list = ShoppingList()
|
shopping_list = ShoppingList()
|
||||||
|
|
||||||
async for meal in _upcoming_meals(conn):
|
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(),))
|
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)
|
await insert_shopping_list(conn, shopping_list)
|
||||||
|
|
||||||
|
|
@ -296,26 +331,71 @@ async def sync_persons_requested_ingredients(conn, shopping_list: ShoppingList,
|
||||||
ingredient.id = 0
|
ingredient.id = 0
|
||||||
yield 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 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)
|
shopping_list = await current_shopping_list(conn)
|
||||||
existing = await find_existing_result(conn, product, shopping_list)
|
|
||||||
|
|
||||||
if existing:
|
created = ShoppingListResult(product=product, product_id=product.id, list_id=shopping_list.id, quantity=quantity, unit=unit, found_date=date_found)
|
||||||
existing.quantity, existing.unit, existing.found_date = quantity, unit, datetime.now()
|
removed = await find_existing_result(conn, shopping_list, product, unit)
|
||||||
|
|
||||||
|
async with conn.execute('''
|
||||||
|
INSERT INTO ShoppingListResult (product_id, list_id, quantity, unit, created_date, found_date)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?)
|
||||||
|
''', (created.product_id, created.list_id, quantity, unit, created.created_date, created.found_date)) 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('''
|
await conn.execute('''
|
||||||
UPDATE ShoppingListResult
|
DELETE FROM ShoppingListResult
|
||||||
SET quantity = ?, unit = ?, found_date = ?
|
|
||||||
WHERE id = ?
|
WHERE id = ?
|
||||||
''', (existing.quantity, existing.unit, existing.found_date, existing.id))
|
''', (removed.id,))
|
||||||
|
|
||||||
return existing
|
return removed, created
|
||||||
else:
|
|
||||||
result = ShoppingListResult(product=product, product_id=product.id, list_id=shopping_list.id, quantity=quantity, unit=unit, found_date=datetime.now())
|
async def unmark_found(conn, product_id: int) -> List[ShoppingListRequest]:
|
||||||
async with conn.execute('''
|
shopping_list = await current_shopping_list(conn)
|
||||||
INSERT INTO ShoppingListResult (product_id, list_id, quantity, unit, created_date, found_date)
|
|
||||||
VALUES (?, ?, ?, ?, ?, ?)
|
requests = []
|
||||||
''', (result.product_id, result.list_id, quantity, unit, result.created_date, result.found_date)) as cursor:
|
async with conn.execute('''
|
||||||
result.id = cursor.lastrowid
|
DELETE FROM ShoppingListResult
|
||||||
shopping_list.items.append(result)
|
WHERE list_id = ? AND product_id = ?
|
||||||
return result
|
''', (shopping_list.id, product_id)) as cursor:
|
||||||
|
async for row in cursor:
|
||||||
|
requests.append(row)
|
||||||
|
|
||||||
|
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
|
||||||
|
|
@ -24,6 +24,8 @@ class Products:
|
||||||
id=0,
|
id=0,
|
||||||
name="Fresh Broccoli",
|
name="Fresh Broccoli",
|
||||||
product_id="134681",
|
product_id="134681",
|
||||||
|
quantity=1,
|
||||||
|
unit="Items",
|
||||||
link="https://www.woolworths.com.au/shop/productdetails/134681/fresh-broccoli",
|
link="https://www.woolworths.com.au/shop/productdetails/134681/fresh-broccoli",
|
||||||
img_small="https://cdn0.woolworths.media/content/wowproductimages/small/134681.jpg",
|
img_small="https://cdn0.woolworths.media/content/wowproductimages/small/134681.jpg",
|
||||||
img_large="https://cdn0.woolworths.media/content/wowproductimages/large/134681.jpg",
|
img_large="https://cdn0.woolworths.media/content/wowproductimages/large/134681.jpg",
|
||||||
|
|
@ -34,6 +36,8 @@ class Products:
|
||||||
id=0,
|
id=0,
|
||||||
name="La Famiglia Garlic Bread",
|
name="La Famiglia Garlic Bread",
|
||||||
product_id="294517",
|
product_id="294517",
|
||||||
|
quantity=1,
|
||||||
|
unit="Loaf",
|
||||||
link="https://www.woolworths.com.au/shop/productdetails/294517/la-famiglia-garlic-bread",
|
link="https://www.woolworths.com.au/shop/productdetails/294517/la-famiglia-garlic-bread",
|
||||||
img_small="https://cdn0.woolworths.media/content/wowproductimages/small/294517.jpg",
|
img_small="https://cdn0.woolworths.media/content/wowproductimages/small/294517.jpg",
|
||||||
img_large="https://cdn0.woolworths.media/content/wowproductimages/large/294517.jpg",
|
img_large="https://cdn0.woolworths.media/content/wowproductimages/large/294517.jpg",
|
||||||
|
|
@ -44,6 +48,8 @@ class Products:
|
||||||
id=0,
|
id=0,
|
||||||
name="Beans Round",
|
name="Beans Round",
|
||||||
product_id="134072",
|
product_id="134072",
|
||||||
|
quantity=1,
|
||||||
|
unit="kg",
|
||||||
link="https://www.woolworths.com.au/shop/productdetails/134072/beans-round",
|
link="https://www.woolworths.com.au/shop/productdetails/134072/beans-round",
|
||||||
img_small="https://cdn0.woolworths.media/content/wowproductimages/small/134072.jpg",
|
img_small="https://cdn0.woolworths.media/content/wowproductimages/small/134072.jpg",
|
||||||
img_large="https://cdn0.woolworths.media/content/wowproductimages/large/134072.jpg",
|
img_large="https://cdn0.woolworths.media/content/wowproductimages/large/134072.jpg",
|
||||||
|
|
@ -54,6 +60,8 @@ class Products:
|
||||||
id=0,
|
id=0,
|
||||||
name="Western Star Unsalted Butter Chef's Choice",
|
name="Western Star Unsalted Butter Chef's Choice",
|
||||||
product_id="712251",
|
product_id="712251",
|
||||||
|
quantity=500,
|
||||||
|
unit="g",
|
||||||
link="https://www.woolworths.com.au/shop/productdetails/712251/western-star-unsalted-butter-chef-s-choice",
|
link="https://www.woolworths.com.au/shop/productdetails/712251/western-star-unsalted-butter-chef-s-choice",
|
||||||
img_small="https://cdn0.woolworths.media/content/wowproductimages/small/712251.jpg",
|
img_small="https://cdn0.woolworths.media/content/wowproductimages/small/712251.jpg",
|
||||||
img_large="https://cdn0.woolworths.media/content/wowproductimages/large/712251.jpg",
|
img_large="https://cdn0.woolworths.media/content/wowproductimages/large/712251.jpg",
|
||||||
|
|
@ -63,6 +71,8 @@ class Products:
|
||||||
saxa_iodised_table_salt_shaker = products.Product(
|
saxa_iodised_table_salt_shaker = products.Product(
|
||||||
id=0,
|
id=0,
|
||||||
name="Saxa Iodised Table Salt Shaker",
|
name="Saxa Iodised Table Salt Shaker",
|
||||||
|
quantity=750,
|
||||||
|
unit="g",
|
||||||
product_id="33245",
|
product_id="33245",
|
||||||
link="https://www.woolworths.com.au/shop/productdetails/33245/saxa-iodised-table-salt-shaker",
|
link="https://www.woolworths.com.au/shop/productdetails/33245/saxa-iodised-table-salt-shaker",
|
||||||
img_small="https://cdn0.woolworths.media/content/wowproductimages/small/033245.jpg",
|
img_small="https://cdn0.woolworths.media/content/wowproductimages/small/033245.jpg",
|
||||||
|
|
@ -73,6 +83,8 @@ class Products:
|
||||||
mckenzies_pepper_black_ground = products.Product(
|
mckenzies_pepper_black_ground = products.Product(
|
||||||
id=0,
|
id=0,
|
||||||
name="Mckenzie's Pepper Black Ground",
|
name="Mckenzie's Pepper Black Ground",
|
||||||
|
quantity=100,
|
||||||
|
unit="g",
|
||||||
product_id="75194",
|
product_id="75194",
|
||||||
link="https://www.woolworths.com.au/shop/productdetails/75194/mckenzie-s-pepper-black-ground",
|
link="https://www.woolworths.com.au/shop/productdetails/75194/mckenzie-s-pepper-black-ground",
|
||||||
img_small="https://cdn0.woolworths.media/content/wowproductimages/small/075194.jpg",
|
img_small="https://cdn0.woolworths.media/content/wowproductimages/small/075194.jpg",
|
||||||
|
|
@ -84,6 +96,8 @@ class Products:
|
||||||
id=0,
|
id=0,
|
||||||
name="Apple",
|
name="Apple",
|
||||||
product_id="3542",
|
product_id="3542",
|
||||||
|
quantity=1,
|
||||||
|
unit="Items",
|
||||||
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",
|
||||||
|
|
@ -94,6 +108,8 @@ class Products:
|
||||||
id=0,
|
id=0,
|
||||||
name="Banana",
|
name="Banana",
|
||||||
product_id="214",
|
product_id="214",
|
||||||
|
quantity=1,
|
||||||
|
unit="Items",
|
||||||
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",
|
||||||
|
|
@ -128,7 +144,7 @@ class Ingredients:
|
||||||
id=0,
|
id=0,
|
||||||
line='1kg Broccoli, Chopped',
|
line='1kg Broccoli, Chopped',
|
||||||
name='Broccoli',
|
name='Broccoli',
|
||||||
unit='1kg',
|
unit='kg',
|
||||||
quantity='1',
|
quantity='1',
|
||||||
preparation='Chopped',
|
preparation='Chopped',
|
||||||
product=Products.broccoli,
|
product=Products.broccoli,
|
||||||
|
|
@ -191,6 +207,7 @@ class Recipes:
|
||||||
id=0,
|
id=0,
|
||||||
name='Broccoli Soup',
|
name='Broccoli Soup',
|
||||||
link='https://www.bbcgoodfood.com/recipes/broccoli-soup',
|
link='https://www.bbcgoodfood.com/recipes/broccoli-soup',
|
||||||
|
serves=4,
|
||||||
image_urls=['https://www.bbcgoodfood.com/sites/default/files/styles/recipe/public/recipe/recipe-image/2018/10/broccoli-soup.jpg'],
|
image_urls=['https://www.bbcgoodfood.com/sites/default/files/styles/recipe/public/recipe/recipe-image/2018/10/broccoli-soup.jpg'],
|
||||||
ingredients=[Ingredients.broccoli_chopped_1kg],
|
ingredients=[Ingredients.broccoli_chopped_1kg],
|
||||||
created_by_id=Persons.jacob.id,
|
created_by_id=Persons.jacob.id,
|
||||||
|
|
@ -200,6 +217,7 @@ class Recipes:
|
||||||
id=0,
|
id=0,
|
||||||
name="How to Steam Green Beans",
|
name="How to Steam Green Beans",
|
||||||
link="https://www.thespruceeats.com/steamed-green-beans-3057051",
|
link="https://www.thespruceeats.com/steamed-green-beans-3057051",
|
||||||
|
serves=4,
|
||||||
image_urls=["https://www.thespruceeats.com/thmb/CLROdq9dlYbjKjlOlA_kmFdunTY=/1500x0/filters:no_upscale():max_bytes(150000):strip_icc()/steamed-green-beans-3057051-hero-01-b1c4f894da5b4bc0a01cd43886df0100.jpg"],
|
image_urls=["https://www.thespruceeats.com/thmb/CLROdq9dlYbjKjlOlA_kmFdunTY=/1500x0/filters:no_upscale():max_bytes(150000):strip_icc()/steamed-green-beans-3057051-hero-01-b1c4f894da5b4bc0a01cd43886df0100.jpg"],
|
||||||
ingredients=[Ingredients.green_beans,Ingredients.butter,Ingredients.salt,Ingredients.freshly_ground_black_pepper],
|
ingredients=[Ingredients.green_beans,Ingredients.butter,Ingredients.salt,Ingredients.freshly_ground_black_pepper],
|
||||||
created_by_id=Persons.jacob.id,
|
created_by_id=Persons.jacob.id,
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
from datetime import datetime, timedelta
|
||||||
import importlib
|
import importlib
|
||||||
import unittest
|
import unittest
|
||||||
import tests.test_data as test_data
|
import tests.test_data as test_data
|
||||||
|
|
@ -31,7 +32,29 @@ class TestShopping(unittest.IsolatedAsyncioTestCase):
|
||||||
async def test_current_shopping_list(self):
|
async def test_current_shopping_list(self):
|
||||||
shopping_list = await shopping.current_shopping_list(self.conn)
|
shopping_list = await shopping.current_shopping_list(self.conn)
|
||||||
self.assertEqual(len(shopping_list.requests), 0)
|
self.assertEqual(len(shopping_list.requests), 0)
|
||||||
self.assertEqual(len(shopping_list.items), 0)
|
self.assertEqual(len(shopping_list.results), 0)
|
||||||
|
|
||||||
|
async def test_get_current_adds_upcoming_meals(self):
|
||||||
|
import meals, recipes
|
||||||
|
|
||||||
|
meal = test_data.Meals.broccoli_soup_for_jacob
|
||||||
|
for product in [i.product for i in meal.extra_ingredients] + [i.product for r in meal.recipes for i in r.ingredients]:
|
||||||
|
if product.id < 0:
|
||||||
|
await products.insert_product(self.conn, product, {})
|
||||||
|
|
||||||
|
for recipe in meal.recipes:
|
||||||
|
await recipes.insert_recipe(self.conn, recipe)
|
||||||
|
|
||||||
|
meal.meal_date = datetime.now() + timedelta(days=1)
|
||||||
|
await meals.insert_meal(self.conn, meal)
|
||||||
|
|
||||||
|
shopping_list = await shopping.current_shopping_list(self.conn)
|
||||||
|
self.assertEqual(len(shopping_list.requests), 1)
|
||||||
|
self.assertEqual(len(shopping_list.results), 0)
|
||||||
|
|
||||||
|
request = shopping_list.requests[0]
|
||||||
|
self.assertEqual(request.meal_id, meal.id)
|
||||||
|
|
||||||
|
|
||||||
async def test_sync_persons_requests(self):
|
async def test_sync_persons_requests(self):
|
||||||
ingredient = test_data.Ingredients.one_apple
|
ingredient = test_data.Ingredients.one_apple
|
||||||
|
|
@ -40,11 +63,12 @@ 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_requested_ingredients(self.conn, shopping_list, person, [ingredient])
|
async for _ in shopping.sync_persons_requested_ingredients(self.conn, shopping_list, person, [ingredient]):
|
||||||
|
pass
|
||||||
|
|
||||||
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)
|
||||||
self.assertEqual(len(shopping_list.items), 0)
|
self.assertEqual(len(shopping_list.results), 0)
|
||||||
|
|
||||||
request = shopping_list.requests[0]
|
request = shopping_list.requests[0]
|
||||||
self.assertEqual(request.person_id, person.id)
|
self.assertEqual(request.person_id, person.id)
|
||||||
|
|
@ -60,12 +84,15 @@ 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_requested_ingredients(self.conn, shopping_list, person, [first])
|
async for _ in shopping.sync_persons_requested_ingredients(self.conn, shopping_list, person, [first]):
|
||||||
await shopping.sync_persons_requested_ingredients(self.conn, shopping_list, person, [first, second])
|
pass
|
||||||
|
|
||||||
|
async for _ in shopping.sync_persons_requested_ingredients(self.conn, shopping_list, person, [first, second]):
|
||||||
|
pass
|
||||||
|
|
||||||
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)
|
||||||
self.assertEqual(len(shopping_list.items), 0)
|
self.assertEqual(len(shopping_list.results), 0)
|
||||||
|
|
||||||
request_by_line = {r.ingredient.line: r for r in shopping_list.requests}
|
request_by_line = {r.ingredient.line: r for r in shopping_list.requests}
|
||||||
self.assertEqual(len(request_by_line), 2)
|
self.assertEqual(len(request_by_line), 2)
|
||||||
|
|
@ -77,19 +104,52 @@ class TestShopping(unittest.IsolatedAsyncioTestCase):
|
||||||
|
|
||||||
async def test_mark_found(self):
|
async def test_mark_found(self):
|
||||||
ingredient = test_data.Ingredients.one_apple
|
ingredient = test_data.Ingredients.one_apple
|
||||||
|
|
||||||
|
await products.insert_product(self.conn, ingredient.product, {})
|
||||||
|
|
||||||
|
shopping_list = await shopping.current_shopping_list(self.conn)
|
||||||
|
await shopping.mark_found(self.conn, ingredient, date_found=datetime.now())
|
||||||
|
|
||||||
|
shopping_list = await shopping.current_shopping_list(self.conn)
|
||||||
|
self.assertEqual(len(shopping_list.results), 1)
|
||||||
|
self.assertEqual(shopping_list.results[0].product_id, ingredient.product.id)
|
||||||
|
self.assertEqual(shopping_list.results[0].quantity, 1)
|
||||||
|
self.assertEqual(shopping_list.results[0].unit, ingredient.unit)
|
||||||
|
|
||||||
|
ingredient.quantity = 2
|
||||||
|
await shopping.mark_found(self.conn, ingredient, date_found=datetime.now())
|
||||||
|
|
||||||
|
shopping_list = await shopping.current_shopping_list(self.conn)
|
||||||
|
self.assertEqual(len(shopping_list.results), 1)
|
||||||
|
self.assertEqual(shopping_list.results[0].product_id, ingredient.product.id)
|
||||||
|
self.assertEqual(shopping_list.results[0].quantity, 2)
|
||||||
|
self.assertEqual(shopping_list.results[0].unit, ingredient.unit)
|
||||||
|
|
||||||
|
ingredient.unit = 'kg'
|
||||||
|
await shopping.mark_found(self.conn, ingredient, date_found=datetime.now())
|
||||||
|
|
||||||
|
shopping_list = await shopping.current_shopping_list(self.conn)
|
||||||
|
self.assertEqual(len(shopping_list.results), 2)
|
||||||
|
|
||||||
|
results_by_unit = {r.unit: r for r in shopping_list.results}
|
||||||
|
self.assertEqual(len(results_by_unit), 2)
|
||||||
|
self.assertIn('kg', results_by_unit)
|
||||||
|
self.assertIn('Items', results_by_unit)
|
||||||
|
self.assertEqual(results_by_unit['kg'].product_id, results_by_unit['Items'].product_id)
|
||||||
|
|
||||||
|
async def test_purchase(self):
|
||||||
|
ingredient = test_data.Ingredients.one_apple
|
||||||
person = test_data.Persons.jacob
|
person = test_data.Persons.jacob
|
||||||
|
|
||||||
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_requested_ingredients(self.conn, shopping_list, person, [ingredient])
|
self.assertIsNone(shopping_list.purchased_date)
|
||||||
await shopping.mark_found(self.conn, ingredient.product, 2, 'items')
|
|
||||||
|
|
||||||
shopping_list = await shopping.current_shopping_list(self.conn)
|
shopping_list = await shopping.mark_purchased(self.conn)
|
||||||
self.assertEqual(len(shopping_list.requests), 1)
|
self.assertIsNotNone(shopping_list.purchased_date)
|
||||||
|
self.assertLessEqual(shopping_list.purchased_date - datetime.now(), timedelta(seconds=1))
|
||||||
self.assertEqual(len(shopping_list.items), 1)
|
|
||||||
self.assertEqual(shopping_list.items[0].product_id, ingredient.product.id)
|
|
||||||
self.assertEqual(shopping_list.items[0].quantity, 2)
|
|
||||||
self.assertEqual(shopping_list.items[0].unit, 'items')
|
|
||||||
|
|
||||||
|
new_shopping_list = await shopping.current_shopping_list(self.conn)
|
||||||
|
self.assertNotEqual(shopping_list.id, new_shopping_list.id)
|
||||||
|
self.assertIsNone(new_shopping_list.purchased_date)
|
||||||
Loading…
Reference in a new issue