feat(meals): replace Person with MemberRef (displayName) in v2 Meal DTOs; add Argon2 password hashing
This commit is contained in:
parent
87e3dba6f8
commit
8fd780ee17
6 changed files with 413 additions and 251 deletions
|
|
@ -15,6 +15,14 @@ from common import ApiModel
|
||||||
from security import JwtConfig, create_jwt
|
from security import JwtConfig, create_jwt
|
||||||
from settings import settings
|
from settings import settings
|
||||||
from users import repository as users_db
|
from users import repository as users_db
|
||||||
|
|
||||||
|
# Prefer Argon2 for new passwords; keep PBKDF2 verify for backward compatibility
|
||||||
|
try:
|
||||||
|
from argon2 import PasswordHasher
|
||||||
|
|
||||||
|
_ph: PasswordHasher | None = PasswordHasher()
|
||||||
|
except Exception: # pragma: no cover - optional dependency in some environments
|
||||||
|
_ph = None
|
||||||
from users.models import User
|
from users.models import User
|
||||||
|
|
||||||
router = APIRouter(prefix="/auth", tags=["auth-v2"])
|
router = APIRouter(prefix="/auth", tags=["auth-v2"])
|
||||||
|
|
@ -38,17 +46,32 @@ class TokenResponse(ApiModel):
|
||||||
|
|
||||||
|
|
||||||
PBKDF2_ALG = "pbkdf2_sha256"
|
PBKDF2_ALG = "pbkdf2_sha256"
|
||||||
PBKDF2_ITER = 390000 # similar to Django default; adjust in settings if needed
|
PBKDF2_ITER = 390000 # kept for verifying older hashes
|
||||||
SALT_BYTES = 16
|
SALT_BYTES = 16
|
||||||
|
|
||||||
|
|
||||||
def _hash_pw(pw: str) -> str:
|
def _hash_pw(pw: str) -> str:
|
||||||
|
"""Hash a password.
|
||||||
|
|
||||||
|
Uses Argon2 when available; falls back to PBKDF2 for environments without argon2-cffi.
|
||||||
|
"""
|
||||||
|
if _ph is not None:
|
||||||
|
return _ph.hash(pw)
|
||||||
|
# Fallback
|
||||||
salt = os.urandom(SALT_BYTES)
|
salt = os.urandom(SALT_BYTES)
|
||||||
dk = hashlib.pbkdf2_hmac("sha256", pw.encode("utf-8"), salt, PBKDF2_ITER)
|
dk = hashlib.pbkdf2_hmac("sha256", pw.encode("utf-8"), salt, PBKDF2_ITER)
|
||||||
return f"{PBKDF2_ALG}${PBKDF2_ITER}${base64.b64encode(salt).decode()}${base64.b64encode(dk).decode()}"
|
return f"{PBKDF2_ALG}${PBKDF2_ITER}${base64.b64encode(salt).decode()}${base64.b64encode(dk).decode()}"
|
||||||
|
|
||||||
|
|
||||||
def _verify_pw(pw: str, stored: str) -> bool:
|
def _verify_pw(pw: str, stored: str) -> bool:
|
||||||
|
"""Verify password against either Argon2 or PBKDF2 stored hashes."""
|
||||||
|
# Try Argon2 first
|
||||||
|
if _ph is not None and stored.startswith("$argon2"):
|
||||||
|
try:
|
||||||
|
return _ph.verify(stored, pw)
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
# PBKDF2 fallback
|
||||||
try:
|
try:
|
||||||
alg, iter_s, salt_b64, hash_b64 = stored.split("$", 3)
|
alg, iter_s, salt_b64, hash_b64 = stored.split("$", 3)
|
||||||
if alg != PBKDF2_ALG:
|
if alg != PBKDF2_ALG:
|
||||||
|
|
@ -57,7 +80,6 @@ def _verify_pw(pw: str, stored: str) -> bool:
|
||||||
salt = base64.b64decode(salt_b64)
|
salt = base64.b64decode(salt_b64)
|
||||||
expected = base64.b64decode(hash_b64)
|
expected = base64.b64decode(hash_b64)
|
||||||
dk = hashlib.pbkdf2_hmac("sha256", pw.encode("utf-8"), salt, iters)
|
dk = hashlib.pbkdf2_hmac("sha256", pw.encode("utf-8"), salt, iters)
|
||||||
# constant-time compare
|
|
||||||
return hmac.compare_digest(dk, expected)
|
return hmac.compare_digest(dk, expected)
|
||||||
except Exception:
|
except Exception:
|
||||||
return False
|
return False
|
||||||
|
|
|
||||||
211
api/meals_v2.py
211
api/meals_v2.py
|
|
@ -7,7 +7,9 @@ import aiosqlite
|
||||||
from fastapi import APIRouter, Depends, Query, Request, Response
|
from fastapi import APIRouter, Depends, Query, Request, Response
|
||||||
|
|
||||||
import meals
|
import meals
|
||||||
|
import persons
|
||||||
import shopping
|
import shopping
|
||||||
|
import ingredients
|
||||||
from common import ProblemDetails, ApiModel
|
from common import ProblemDetails, ApiModel
|
||||||
from api.deps import error_response
|
from api.deps import error_response
|
||||||
from api.deps import get_db, get_household_from_slug
|
from api.deps import get_db, get_household_from_slug
|
||||||
|
|
@ -15,6 +17,41 @@ from api.deps import get_db, get_household_from_slug
|
||||||
router = APIRouter(prefix="/households/{householdSlug}/meals", tags=["meals-v2"])
|
router = APIRouter(prefix="/households/{householdSlug}/meals", tags=["meals-v2"])
|
||||||
|
|
||||||
|
|
||||||
|
class MemberRef(ApiModel):
|
||||||
|
id: int
|
||||||
|
# Align outward schema to users/household members; use displayName
|
||||||
|
display_name: str
|
||||||
|
|
||||||
|
|
||||||
|
class MealRecipeIn(ApiModel):
|
||||||
|
meal_id: int
|
||||||
|
recipe_id: int
|
||||||
|
servings: float
|
||||||
|
|
||||||
|
|
||||||
|
class MealIn(ApiModel):
|
||||||
|
id: int = -1
|
||||||
|
suggested_date: datetime.datetime
|
||||||
|
consumed_date: Optional[datetime.datetime] = None
|
||||||
|
chefs: List[MemberRef]
|
||||||
|
cleanup: List[MemberRef]
|
||||||
|
consumers: List[MemberRef]
|
||||||
|
recipes: List[MealRecipeIn] = []
|
||||||
|
extra_ingredients: List[ingredients.Ingredient] = []
|
||||||
|
|
||||||
|
|
||||||
|
class MealOut(ApiModel):
|
||||||
|
id: int = -1
|
||||||
|
suggested_date: datetime.datetime
|
||||||
|
consumed_date: Optional[datetime.datetime] = None
|
||||||
|
chefs: List[MemberRef]
|
||||||
|
cleanup: List[MemberRef]
|
||||||
|
consumers: List[MemberRef]
|
||||||
|
recipes: List[meals.MealRecipe]
|
||||||
|
extra_ingredients: List[ingredients.Ingredient]
|
||||||
|
purchase_date: Optional[datetime.datetime] = None
|
||||||
|
|
||||||
|
|
||||||
class MarkConsumedBody(ApiModel):
|
class MarkConsumedBody(ApiModel):
|
||||||
consumed_date: Optional[datetime.datetime] = None
|
consumed_date: Optional[datetime.datetime] = None
|
||||||
|
|
||||||
|
|
@ -29,7 +66,7 @@ async def get_upcoming_meals_scoped(
|
||||||
date_from: datetime.datetime = Query(..., alias="from"),
|
date_from: datetime.datetime = Query(..., alias="from"),
|
||||||
to: datetime.datetime = Query(...),
|
to: datetime.datetime = Query(...),
|
||||||
conn: aiosqlite.Connection = Depends(get_db),
|
conn: aiosqlite.Connection = Depends(get_db),
|
||||||
) -> List[meals.Meal]:
|
) -> List[MealOut]:
|
||||||
hid = household["id"]
|
hid = household["id"]
|
||||||
# Use a scoped repository function, falling back to filtering in SQL until repository is fully scoped
|
# Use a scoped repository function, falling back to filtering in SQL until repository is fully scoped
|
||||||
try:
|
try:
|
||||||
|
|
@ -57,7 +94,7 @@ async def get_upcoming_meals_scoped(
|
||||||
result.append(meals.Meal(**{k: v for k, v in zip(meals.Meal.KEYS, row)}))
|
result.append(meals.Meal(**{k: v for k, v in zip(meals.Meal.KEYS, row)}))
|
||||||
|
|
||||||
if not result:
|
if not result:
|
||||||
return result
|
return []
|
||||||
|
|
||||||
# Load relateds similar to v1
|
# Load relateds similar to v1
|
||||||
await meals.bulk_load_participants(conn, result)
|
await meals.bulk_load_participants(conn, result)
|
||||||
|
|
@ -65,14 +102,33 @@ async def get_upcoming_meals_scoped(
|
||||||
await meals.load_recipes(conn, meal)
|
await meals.load_recipes(conn, meal)
|
||||||
await meals.load_extra_ingredients(conn, meal)
|
await meals.load_extra_ingredients(conn, meal)
|
||||||
|
|
||||||
return result
|
# Map domain Meal -> outward MealOut
|
||||||
|
def _to_member(p: persons.Person) -> MemberRef:
|
||||||
|
return MemberRef(id=p.id, display_name=p.name)
|
||||||
|
|
||||||
|
out: List[MealOut] = []
|
||||||
|
for m in result:
|
||||||
|
out.append(
|
||||||
|
MealOut(
|
||||||
|
id=m.id,
|
||||||
|
suggested_date=m.suggested_date,
|
||||||
|
consumed_date=m.consumed_date,
|
||||||
|
chefs=[_to_member(p) for p in m.chefs],
|
||||||
|
cleanup=[_to_member(p) for p in m.cleanup],
|
||||||
|
consumers=[_to_member(p) for p in m.consumers],
|
||||||
|
recipes=m.recipes,
|
||||||
|
extra_ingredients=m.extra_ingredients,
|
||||||
|
purchase_date=m.purchase_date,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
@router.get(
|
@router.get(
|
||||||
"/{meal_id}",
|
"/{meal_id}",
|
||||||
operation_id="getMealV2",
|
operation_id="getMealV2",
|
||||||
summary="Get a meal by id (scoped)",
|
summary="Get a meal by id (scoped)",
|
||||||
response_model=meals.Meal,
|
response_model=MealOut,
|
||||||
responses={404: {"model": ProblemDetails}},
|
responses={404: {"model": ProblemDetails}},
|
||||||
)
|
)
|
||||||
async def get_meal_scoped(
|
async def get_meal_scoped(
|
||||||
|
|
@ -80,19 +136,33 @@ async def get_meal_scoped(
|
||||||
request: Request,
|
request: Request,
|
||||||
household=Depends(get_household_from_slug),
|
household=Depends(get_household_from_slug),
|
||||||
conn: aiosqlite.Connection = Depends(get_db),
|
conn: aiosqlite.Connection = Depends(get_db),
|
||||||
) -> meals.Meal | Response:
|
) -> MealOut | Response:
|
||||||
hid = household["id"]
|
hid = household["id"]
|
||||||
meal = await meals.find_meal_by_id_scoped(conn, meal_id, hid)
|
meal = await meals.find_meal_by_id_scoped(conn, meal_id, hid)
|
||||||
if not meal:
|
if not meal:
|
||||||
return error_response(request, 404, "Meal not found")
|
return error_response(request, 404, "Meal not found")
|
||||||
return meal
|
|
||||||
|
def _to_member(p: persons.Person) -> MemberRef:
|
||||||
|
return MemberRef(id=p.id, display_name=p.name)
|
||||||
|
|
||||||
|
return MealOut(
|
||||||
|
id=meal.id,
|
||||||
|
suggested_date=meal.suggested_date,
|
||||||
|
consumed_date=meal.consumed_date,
|
||||||
|
chefs=[_to_member(p) for p in meal.chefs],
|
||||||
|
cleanup=[_to_member(p) for p in meal.cleanup],
|
||||||
|
consumers=[_to_member(p) for p in meal.consumers],
|
||||||
|
recipes=meal.recipes,
|
||||||
|
extra_ingredients=meal.extra_ingredients,
|
||||||
|
purchase_date=meal.purchase_date,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@router.post(
|
@router.post(
|
||||||
"/{meal_id}/consumed",
|
"/{meal_id}/consumed",
|
||||||
operation_id="markMealConsumedV2",
|
operation_id="markMealConsumedV2",
|
||||||
summary="Mark a meal as consumed (scoped)",
|
summary="Mark a meal as consumed (scoped)",
|
||||||
response_model=meals.Meal,
|
response_model=MealOut,
|
||||||
responses={
|
responses={
|
||||||
400: {"model": ProblemDetails},
|
400: {"model": ProblemDetails},
|
||||||
404: {"model": ProblemDetails},
|
404: {"model": ProblemDetails},
|
||||||
|
|
@ -104,7 +174,7 @@ async def mark_meal_consumed_scoped(
|
||||||
body: Optional[MarkConsumedBody] = None,
|
body: Optional[MarkConsumedBody] = None,
|
||||||
household=Depends(get_household_from_slug),
|
household=Depends(get_household_from_slug),
|
||||||
conn: aiosqlite.Connection = Depends(get_db),
|
conn: aiosqlite.Connection = Depends(get_db),
|
||||||
) -> meals.Meal | Response:
|
) -> MealOut | Response:
|
||||||
hid = household["id"]
|
hid = household["id"]
|
||||||
consumed_date: Optional[datetime.datetime] = None
|
consumed_date: Optional[datetime.datetime] = None
|
||||||
if body is not None:
|
if body is not None:
|
||||||
|
|
@ -121,66 +191,143 @@ async def mark_meal_consumed_scoped(
|
||||||
# Clear any outstanding meal request entries for this meal
|
# Clear any outstanding meal request entries for this meal
|
||||||
await shopping.remove_request(conn, person=None, meal=meal)
|
await shopping.remove_request(conn, person=None, meal=meal)
|
||||||
|
|
||||||
return meal
|
def _to_member(p: persons.Person) -> MemberRef:
|
||||||
|
return MemberRef(id=p.id, display_name=p.name)
|
||||||
|
|
||||||
|
return MealOut(
|
||||||
|
id=meal.id,
|
||||||
|
suggested_date=meal.suggested_date,
|
||||||
|
consumed_date=meal.consumed_date,
|
||||||
|
chefs=[_to_member(p) for p in meal.chefs],
|
||||||
|
cleanup=[_to_member(p) for p in meal.cleanup],
|
||||||
|
consumers=[_to_member(p) for p in meal.consumers],
|
||||||
|
recipes=meal.recipes,
|
||||||
|
extra_ingredients=meal.extra_ingredients,
|
||||||
|
purchase_date=meal.purchase_date,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@router.post(
|
@router.post(
|
||||||
"",
|
"",
|
||||||
operation_id="createMealV2",
|
operation_id="createMealV2",
|
||||||
summary="Create a new meal (scoped)",
|
summary="Create a new meal (scoped)",
|
||||||
response_model=meals.Meal,
|
response_model=MealOut,
|
||||||
responses={400: {"model": ProblemDetails}},
|
responses={400: {"model": ProblemDetails}},
|
||||||
)
|
)
|
||||||
async def create_meal_scoped(
|
async def create_meal_scoped(
|
||||||
meal: meals.Meal,
|
meal: MealIn,
|
||||||
response: Response,
|
response: Response,
|
||||||
request: Request,
|
request: Request,
|
||||||
household=Depends(get_household_from_slug),
|
household=Depends(get_household_from_slug),
|
||||||
conn: aiosqlite.Connection = Depends(get_db),
|
conn: aiosqlite.Connection = Depends(get_db),
|
||||||
) -> meals.Meal | Response:
|
) -> MealOut | Response:
|
||||||
# Validate using existing service logic
|
# Validate using existing service logic
|
||||||
msg = meals.validate_meal(meal)
|
# Map MealIn -> domain Meal
|
||||||
|
def _from_member(m: MemberRef) -> persons.Person:
|
||||||
|
return persons.Person(id=m.id, name=m.display_name)
|
||||||
|
|
||||||
|
domain_meal = meals.Meal(
|
||||||
|
id=meal.id,
|
||||||
|
suggested_date=meal.suggested_date,
|
||||||
|
consumed_date=meal.consumed_date,
|
||||||
|
chefs=[_from_member(p) for p in meal.chefs],
|
||||||
|
cleanup=[_from_member(p) for p in meal.cleanup],
|
||||||
|
consumers=[_from_member(p) for p in meal.consumers],
|
||||||
|
recipes=[
|
||||||
|
meals.MealRecipe(meal_id=r.meal_id, recipe_id=r.recipe_id, servings=r.servings)
|
||||||
|
for r in meal.recipes
|
||||||
|
],
|
||||||
|
extra_ingredients=list(meal.extra_ingredients),
|
||||||
|
)
|
||||||
|
msg = meals.validate_meal(domain_meal)
|
||||||
if msg:
|
if msg:
|
||||||
return error_response(request, 400, msg)
|
return error_response(request, 400, msg)
|
||||||
hid = household["id"]
|
hid = household["id"]
|
||||||
await meals.insert_meal_scoped(conn, meal, hid)
|
await meals.insert_meal_scoped(conn, domain_meal, hid)
|
||||||
response.headers["Location"] = f"/api/v1/households/{household['slug']}/meals/{meal.id}"
|
response.headers["Location"] = f"/api/v1/households/{household['slug']}/meals/{domain_meal.id}"
|
||||||
return meal
|
|
||||||
|
def _to_member(p: persons.Person) -> MemberRef:
|
||||||
|
return MemberRef(id=p.id, display_name=p.name)
|
||||||
|
|
||||||
|
return MealOut(
|
||||||
|
id=domain_meal.id,
|
||||||
|
suggested_date=domain_meal.suggested_date,
|
||||||
|
consumed_date=domain_meal.consumed_date,
|
||||||
|
chefs=[_to_member(p) for p in domain_meal.chefs],
|
||||||
|
cleanup=[_to_member(p) for p in domain_meal.cleanup],
|
||||||
|
consumers=[_to_member(p) for p in domain_meal.consumers],
|
||||||
|
recipes=domain_meal.recipes,
|
||||||
|
extra_ingredients=domain_meal.extra_ingredients,
|
||||||
|
purchase_date=domain_meal.purchase_date,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@router.put(
|
@router.put(
|
||||||
"/{meal_id}",
|
"/{meal_id}",
|
||||||
operation_id="updateMealV2",
|
operation_id="updateMealV2",
|
||||||
summary="Update an existing meal (scoped)",
|
summary="Update an existing meal (scoped)",
|
||||||
response_model=meals.Meal,
|
response_model=MealOut,
|
||||||
responses={400: {"model": ProblemDetails}, 404: {"model": ProblemDetails}},
|
responses={400: {"model": ProblemDetails}, 404: {"model": ProblemDetails}},
|
||||||
)
|
)
|
||||||
async def update_meal_scoped(
|
async def update_meal_scoped(
|
||||||
meal_id: int,
|
meal_id: int,
|
||||||
meal: meals.Meal,
|
meal: MealIn,
|
||||||
request: Request,
|
request: Request,
|
||||||
household=Depends(get_household_from_slug),
|
household=Depends(get_household_from_slug),
|
||||||
conn: aiosqlite.Connection = Depends(get_db),
|
conn: aiosqlite.Connection = Depends(get_db),
|
||||||
) -> meals.Meal | Response:
|
) -> MealOut | Response:
|
||||||
if meal.id != meal_id:
|
if meal.id != meal_id:
|
||||||
return error_response(request, 400, "Meal ID in URL does not match meal ID in body")
|
return error_response(request, 400, "Meal ID in URL does not match meal ID in body")
|
||||||
hid = household["id"]
|
hid = household["id"]
|
||||||
existing = await meals.find_meal_by_id_scoped(conn, meal_id, hid)
|
existing = await meals.find_meal_by_id_scoped(conn, meal_id, hid)
|
||||||
if not existing:
|
if not existing:
|
||||||
return error_response(request, 404, "Meal not found")
|
return error_response(request, 404, "Meal not found")
|
||||||
msg = meals.validate_meal(meal)
|
|
||||||
|
def _from_member(m: MemberRef) -> persons.Person:
|
||||||
|
return persons.Person(id=m.id, name=m.display_name)
|
||||||
|
|
||||||
|
domain_meal = meals.Meal(
|
||||||
|
id=meal.id,
|
||||||
|
suggested_date=meal.suggested_date,
|
||||||
|
consumed_date=meal.consumed_date,
|
||||||
|
chefs=[_from_member(p) for p in meal.chefs],
|
||||||
|
cleanup=[_from_member(p) for p in meal.cleanup],
|
||||||
|
consumers=[_from_member(p) for p in meal.consumers],
|
||||||
|
recipes=[
|
||||||
|
meals.MealRecipe(meal_id=r.meal_id, recipe_id=r.recipe_id, servings=r.servings)
|
||||||
|
for r in meal.recipes
|
||||||
|
],
|
||||||
|
extra_ingredients=list(meal.extra_ingredients),
|
||||||
|
)
|
||||||
|
msg = meals.validate_meal(domain_meal)
|
||||||
if msg:
|
if msg:
|
||||||
return error_response(request, 400, msg)
|
return error_response(request, 400, msg)
|
||||||
await meals.update_meal(conn, meal)
|
await meals.update_meal(conn, domain_meal)
|
||||||
# Return updated state
|
# Return updated state
|
||||||
return await get_meal_scoped(meal_id, request, household, conn)
|
updated = await meals.find_meal_by_id_scoped(conn, meal_id, hid)
|
||||||
|
assert updated is not None
|
||||||
|
|
||||||
|
def _to_member(p: persons.Person) -> MemberRef:
|
||||||
|
return MemberRef(id=p.id, display_name=p.name)
|
||||||
|
|
||||||
|
return MealOut(
|
||||||
|
id=updated.id,
|
||||||
|
suggested_date=updated.suggested_date,
|
||||||
|
consumed_date=updated.consumed_date,
|
||||||
|
chefs=[_to_member(p) for p in updated.chefs],
|
||||||
|
cleanup=[_to_member(p) for p in updated.cleanup],
|
||||||
|
consumers=[_to_member(p) for p in updated.consumers],
|
||||||
|
recipes=updated.recipes,
|
||||||
|
extra_ingredients=updated.extra_ingredients,
|
||||||
|
purchase_date=updated.purchase_date,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@router.delete(
|
@router.delete(
|
||||||
"/{meal_id}",
|
"/{meal_id}",
|
||||||
operation_id="deleteMealV2",
|
operation_id="deleteMealV2",
|
||||||
summary="Delete a meal (scoped)",
|
summary="Delete a meal (scoped)",
|
||||||
response_model=meals.Meal,
|
response_model=MealOut,
|
||||||
responses={404: {"model": ProblemDetails}},
|
responses={404: {"model": ProblemDetails}},
|
||||||
)
|
)
|
||||||
async def delete_meal_scoped(
|
async def delete_meal_scoped(
|
||||||
|
|
@ -188,7 +335,7 @@ async def delete_meal_scoped(
|
||||||
request: Request,
|
request: Request,
|
||||||
household=Depends(get_household_from_slug),
|
household=Depends(get_household_from_slug),
|
||||||
conn: aiosqlite.Connection = Depends(get_db),
|
conn: aiosqlite.Connection = Depends(get_db),
|
||||||
) -> meals.Meal | Response:
|
) -> MealOut | Response:
|
||||||
hid = household["id"]
|
hid = household["id"]
|
||||||
meal = await meals.find_meal_by_id_scoped(conn, meal_id, hid)
|
meal = await meals.find_meal_by_id_scoped(conn, meal_id, hid)
|
||||||
if not meal:
|
if not meal:
|
||||||
|
|
@ -202,4 +349,18 @@ async def delete_meal_scoped(
|
||||||
# Fallback: remove regardless of household (legacy cleanup)
|
# Fallback: remove regardless of household (legacy cleanup)
|
||||||
await shopping.remove_request(conn, person=None, meal=meal)
|
await shopping.remove_request(conn, person=None, meal=meal)
|
||||||
await meals.delete_meal(conn, meal.id)
|
await meals.delete_meal(conn, meal.id)
|
||||||
return meal
|
|
||||||
|
def _to_member(p: persons.Person) -> MemberRef:
|
||||||
|
return MemberRef(id=p.id, display_name=p.name)
|
||||||
|
|
||||||
|
return MealOut(
|
||||||
|
id=meal.id,
|
||||||
|
suggested_date=meal.suggested_date,
|
||||||
|
consumed_date=meal.consumed_date,
|
||||||
|
chefs=[_to_member(p) for p in meal.chefs],
|
||||||
|
cleanup=[_to_member(p) for p in meal.cleanup],
|
||||||
|
consumers=[_to_member(p) for p in meal.consumers],
|
||||||
|
recipes=meal.recipes,
|
||||||
|
extra_ingredients=meal.extra_ingredients,
|
||||||
|
purchase_date=meal.purchase_date,
|
||||||
|
)
|
||||||
|
|
|
||||||
404
openapi.json
404
openapi.json
|
|
@ -696,7 +696,7 @@
|
||||||
"schema": {
|
"schema": {
|
||||||
"type": "array",
|
"type": "array",
|
||||||
"items": {
|
"items": {
|
||||||
"$ref": "#/components/schemas/Meal-Output"
|
"$ref": "#/components/schemas/MealOut"
|
||||||
},
|
},
|
||||||
"title": "Response Getupcomingmealsv2"
|
"title": "Response Getupcomingmealsv2"
|
||||||
}
|
}
|
||||||
|
|
@ -758,7 +758,7 @@
|
||||||
"content": {
|
"content": {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
"schema": {
|
"schema": {
|
||||||
"$ref": "#/components/schemas/Meal-Output"
|
"$ref": "#/components/schemas/MealOut"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -818,7 +818,7 @@
|
||||||
"content": {
|
"content": {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
"schema": {
|
"schema": {
|
||||||
"$ref": "#/components/schemas/Meal-Input"
|
"$ref": "#/components/schemas/MealIn"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -829,7 +829,7 @@
|
||||||
"content": {
|
"content": {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
"schema": {
|
"schema": {
|
||||||
"$ref": "#/components/schemas/Meal-Output"
|
"$ref": "#/components/schemas/MealOut"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -893,7 +893,7 @@
|
||||||
"content": {
|
"content": {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
"schema": {
|
"schema": {
|
||||||
"$ref": "#/components/schemas/Meal-Output"
|
"$ref": "#/components/schemas/MealOut"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -973,7 +973,7 @@
|
||||||
"content": {
|
"content": {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
"schema": {
|
"schema": {
|
||||||
"$ref": "#/components/schemas/Meal-Output"
|
"$ref": "#/components/schemas/MealOut"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1029,7 +1029,7 @@
|
||||||
"content": {
|
"content": {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
"schema": {
|
"schema": {
|
||||||
"$ref": "#/components/schemas/Meal-Input"
|
"$ref": "#/components/schemas/MealIn"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1040,7 +1040,7 @@
|
||||||
"content": {
|
"content": {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
"schema": {
|
"schema": {
|
||||||
"$ref": "#/components/schemas/Meal-Output"
|
"$ref": "#/components/schemas/MealOut"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1444,7 +1444,7 @@
|
||||||
},
|
},
|
||||||
"mealsLookup": {
|
"mealsLookup": {
|
||||||
"additionalProperties": {
|
"additionalProperties": {
|
||||||
"$ref": "#/components/schemas/Meal-Output"
|
"$ref": "#/components/schemas/Meal"
|
||||||
},
|
},
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"title": "Mealslookup"
|
"title": "Mealslookup"
|
||||||
|
|
@ -1458,7 +1458,7 @@
|
||||||
},
|
},
|
||||||
"recipesLookup": {
|
"recipesLookup": {
|
||||||
"additionalProperties": {
|
"additionalProperties": {
|
||||||
"$ref": "#/components/schemas/Recipe-Output"
|
"$ref": "#/components/schemas/Recipe"
|
||||||
},
|
},
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"title": "Recipeslookup"
|
"title": "Recipeslookup"
|
||||||
|
|
@ -1799,7 +1799,7 @@
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"title": "MarkConsumedBody"
|
"title": "MarkConsumedBody"
|
||||||
},
|
},
|
||||||
"Meal-Input": {
|
"Meal": {
|
||||||
"properties": {
|
"properties": {
|
||||||
"id": {
|
"id": {
|
||||||
"type": "integer",
|
"type": "integer",
|
||||||
|
|
@ -1846,85 +1846,7 @@
|
||||||
},
|
},
|
||||||
"recipes": {
|
"recipes": {
|
||||||
"items": {
|
"items": {
|
||||||
"$ref": "#/components/schemas/MealRecipe-Input"
|
"$ref": "#/components/schemas/MealRecipe"
|
||||||
},
|
|
||||||
"type": "array",
|
|
||||||
"title": "Recipes"
|
|
||||||
},
|
|
||||||
"extraIngredients": {
|
|
||||||
"items": {
|
|
||||||
"$ref": "#/components/schemas/Ingredient"
|
|
||||||
},
|
|
||||||
"type": "array",
|
|
||||||
"title": "Extraingredients"
|
|
||||||
},
|
|
||||||
"purchaseDate": {
|
|
||||||
"anyOf": [
|
|
||||||
{
|
|
||||||
"type": "string",
|
|
||||||
"format": "date-time"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "null"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"title": "Purchasedate"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"type": "object",
|
|
||||||
"required": [
|
|
||||||
"suggestedDate"
|
|
||||||
],
|
|
||||||
"title": "Meal"
|
|
||||||
},
|
|
||||||
"Meal-Output": {
|
|
||||||
"properties": {
|
|
||||||
"id": {
|
|
||||||
"type": "integer",
|
|
||||||
"title": "Id",
|
|
||||||
"default": -1
|
|
||||||
},
|
|
||||||
"suggestedDate": {
|
|
||||||
"type": "string",
|
|
||||||
"format": "date-time",
|
|
||||||
"title": "Suggesteddate"
|
|
||||||
},
|
|
||||||
"consumedDate": {
|
|
||||||
"anyOf": [
|
|
||||||
{
|
|
||||||
"type": "string",
|
|
||||||
"format": "date-time"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "null"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"title": "Consumeddate"
|
|
||||||
},
|
|
||||||
"chefs": {
|
|
||||||
"items": {
|
|
||||||
"$ref": "#/components/schemas/Person"
|
|
||||||
},
|
|
||||||
"type": "array",
|
|
||||||
"title": "Chefs"
|
|
||||||
},
|
|
||||||
"cleanup": {
|
|
||||||
"items": {
|
|
||||||
"$ref": "#/components/schemas/Person"
|
|
||||||
},
|
|
||||||
"type": "array",
|
|
||||||
"title": "Cleanup"
|
|
||||||
},
|
|
||||||
"consumers": {
|
|
||||||
"items": {
|
|
||||||
"$ref": "#/components/schemas/Person"
|
|
||||||
},
|
|
||||||
"type": "array",
|
|
||||||
"title": "Consumers"
|
|
||||||
},
|
|
||||||
"recipes": {
|
|
||||||
"items": {
|
|
||||||
"$ref": "#/components/schemas/MealRecipe-Output"
|
|
||||||
},
|
},
|
||||||
"type": "array",
|
"type": "array",
|
||||||
"title": "Recipes"
|
"title": "Recipes"
|
||||||
|
|
@ -1968,7 +1890,161 @@
|
||||||
],
|
],
|
||||||
"title": "MealIdWrapper"
|
"title": "MealIdWrapper"
|
||||||
},
|
},
|
||||||
"MealRecipe-Input": {
|
"MealIn": {
|
||||||
|
"properties": {
|
||||||
|
"id": {
|
||||||
|
"type": "integer",
|
||||||
|
"title": "Id",
|
||||||
|
"default": -1
|
||||||
|
},
|
||||||
|
"suggestedDate": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "date-time",
|
||||||
|
"title": "Suggesteddate"
|
||||||
|
},
|
||||||
|
"consumedDate": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"format": "date-time"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"title": "Consumeddate"
|
||||||
|
},
|
||||||
|
"chefs": {
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/components/schemas/MemberRef"
|
||||||
|
},
|
||||||
|
"type": "array",
|
||||||
|
"title": "Chefs"
|
||||||
|
},
|
||||||
|
"cleanup": {
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/components/schemas/MemberRef"
|
||||||
|
},
|
||||||
|
"type": "array",
|
||||||
|
"title": "Cleanup"
|
||||||
|
},
|
||||||
|
"consumers": {
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/components/schemas/MemberRef"
|
||||||
|
},
|
||||||
|
"type": "array",
|
||||||
|
"title": "Consumers"
|
||||||
|
},
|
||||||
|
"recipes": {
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/components/schemas/MealRecipeIn"
|
||||||
|
},
|
||||||
|
"type": "array",
|
||||||
|
"title": "Recipes",
|
||||||
|
"default": []
|
||||||
|
},
|
||||||
|
"extraIngredients": {
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/components/schemas/Ingredient"
|
||||||
|
},
|
||||||
|
"type": "array",
|
||||||
|
"title": "Extraingredients",
|
||||||
|
"default": []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"suggestedDate",
|
||||||
|
"chefs",
|
||||||
|
"cleanup",
|
||||||
|
"consumers"
|
||||||
|
],
|
||||||
|
"title": "MealIn"
|
||||||
|
},
|
||||||
|
"MealOut": {
|
||||||
|
"properties": {
|
||||||
|
"id": {
|
||||||
|
"type": "integer",
|
||||||
|
"title": "Id",
|
||||||
|
"default": -1
|
||||||
|
},
|
||||||
|
"suggestedDate": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "date-time",
|
||||||
|
"title": "Suggesteddate"
|
||||||
|
},
|
||||||
|
"consumedDate": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"format": "date-time"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"title": "Consumeddate"
|
||||||
|
},
|
||||||
|
"chefs": {
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/components/schemas/MemberRef"
|
||||||
|
},
|
||||||
|
"type": "array",
|
||||||
|
"title": "Chefs"
|
||||||
|
},
|
||||||
|
"cleanup": {
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/components/schemas/MemberRef"
|
||||||
|
},
|
||||||
|
"type": "array",
|
||||||
|
"title": "Cleanup"
|
||||||
|
},
|
||||||
|
"consumers": {
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/components/schemas/MemberRef"
|
||||||
|
},
|
||||||
|
"type": "array",
|
||||||
|
"title": "Consumers"
|
||||||
|
},
|
||||||
|
"recipes": {
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/components/schemas/MealRecipe"
|
||||||
|
},
|
||||||
|
"type": "array",
|
||||||
|
"title": "Recipes"
|
||||||
|
},
|
||||||
|
"extraIngredients": {
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/components/schemas/Ingredient"
|
||||||
|
},
|
||||||
|
"type": "array",
|
||||||
|
"title": "Extraingredients"
|
||||||
|
},
|
||||||
|
"purchaseDate": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"format": "date-time"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"title": "Purchasedate"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"suggestedDate",
|
||||||
|
"chefs",
|
||||||
|
"cleanup",
|
||||||
|
"consumers",
|
||||||
|
"recipes",
|
||||||
|
"extraIngredients"
|
||||||
|
],
|
||||||
|
"title": "MealOut"
|
||||||
|
},
|
||||||
|
"MealRecipe": {
|
||||||
"properties": {
|
"properties": {
|
||||||
"mealId": {
|
"mealId": {
|
||||||
"type": "integer",
|
"type": "integer",
|
||||||
|
|
@ -1985,7 +2061,7 @@
|
||||||
"recipe": {
|
"recipe": {
|
||||||
"anyOf": [
|
"anyOf": [
|
||||||
{
|
{
|
||||||
"$ref": "#/components/schemas/Recipe-Input"
|
"$ref": "#/components/schemas/Recipe"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "null"
|
"type": "null"
|
||||||
|
|
@ -2001,7 +2077,7 @@
|
||||||
],
|
],
|
||||||
"title": "MealRecipe"
|
"title": "MealRecipe"
|
||||||
},
|
},
|
||||||
"MealRecipe-Output": {
|
"MealRecipeIn": {
|
||||||
"properties": {
|
"properties": {
|
||||||
"mealId": {
|
"mealId": {
|
||||||
"type": "integer",
|
"type": "integer",
|
||||||
|
|
@ -2014,16 +2090,6 @@
|
||||||
"servings": {
|
"servings": {
|
||||||
"type": "number",
|
"type": "number",
|
||||||
"title": "Servings"
|
"title": "Servings"
|
||||||
},
|
|
||||||
"recipe": {
|
|
||||||
"anyOf": [
|
|
||||||
{
|
|
||||||
"$ref": "#/components/schemas/Recipe-Output"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "null"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"type": "object",
|
"type": "object",
|
||||||
|
|
@ -2032,7 +2098,25 @@
|
||||||
"recipeId",
|
"recipeId",
|
||||||
"servings"
|
"servings"
|
||||||
],
|
],
|
||||||
"title": "MealRecipe"
|
"title": "MealRecipeIn"
|
||||||
|
},
|
||||||
|
"MemberRef": {
|
||||||
|
"properties": {
|
||||||
|
"id": {
|
||||||
|
"type": "integer",
|
||||||
|
"title": "Id"
|
||||||
|
},
|
||||||
|
"displayName": {
|
||||||
|
"type": "string",
|
||||||
|
"title": "Displayname"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"id",
|
||||||
|
"displayName"
|
||||||
|
],
|
||||||
|
"title": "MemberRef"
|
||||||
},
|
},
|
||||||
"Ok": {
|
"Ok": {
|
||||||
"properties": {
|
"properties": {
|
||||||
|
|
@ -2244,7 +2328,7 @@
|
||||||
},
|
},
|
||||||
"mealsLookup": {
|
"mealsLookup": {
|
||||||
"additionalProperties": {
|
"additionalProperties": {
|
||||||
"$ref": "#/components/schemas/Meal-Output"
|
"$ref": "#/components/schemas/Meal"
|
||||||
},
|
},
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"title": "Mealslookup"
|
"title": "Mealslookup"
|
||||||
|
|
@ -2258,7 +2342,7 @@
|
||||||
},
|
},
|
||||||
"recipesLookup": {
|
"recipesLookup": {
|
||||||
"additionalProperties": {
|
"additionalProperties": {
|
||||||
"$ref": "#/components/schemas/Recipe-Output"
|
"$ref": "#/components/schemas/Recipe"
|
||||||
},
|
},
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"title": "Recipeslookup"
|
"title": "Recipeslookup"
|
||||||
|
|
@ -2273,113 +2357,7 @@
|
||||||
],
|
],
|
||||||
"title": "PurchasedShoppingList"
|
"title": "PurchasedShoppingList"
|
||||||
},
|
},
|
||||||
"Recipe-Input": {
|
"Recipe": {
|
||||||
"properties": {
|
|
||||||
"id": {
|
|
||||||
"type": "integer",
|
|
||||||
"title": "Id",
|
|
||||||
"default": -1
|
|
||||||
},
|
|
||||||
"name": {
|
|
||||||
"type": "string",
|
|
||||||
"title": "Name"
|
|
||||||
},
|
|
||||||
"link": {
|
|
||||||
"type": "string",
|
|
||||||
"title": "Link"
|
|
||||||
},
|
|
||||||
"serves": {
|
|
||||||
"type": "integer",
|
|
||||||
"title": "Serves"
|
|
||||||
},
|
|
||||||
"imageUrls": {
|
|
||||||
"items": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"type": "array",
|
|
||||||
"title": "Imageurls"
|
|
||||||
},
|
|
||||||
"ingredients": {
|
|
||||||
"items": {
|
|
||||||
"$ref": "#/components/schemas/Ingredient"
|
|
||||||
},
|
|
||||||
"type": "array",
|
|
||||||
"title": "Ingredients"
|
|
||||||
},
|
|
||||||
"basedOnRecipe": {
|
|
||||||
"anyOf": [
|
|
||||||
{
|
|
||||||
"type": "integer"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "null"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"title": "Basedonrecipe"
|
|
||||||
},
|
|
||||||
"dateCreated": {
|
|
||||||
"type": "string",
|
|
||||||
"format": "date-time",
|
|
||||||
"title": "Datecreated"
|
|
||||||
},
|
|
||||||
"createdById": {
|
|
||||||
"type": "integer",
|
|
||||||
"title": "Createdbyid"
|
|
||||||
},
|
|
||||||
"createdBy": {
|
|
||||||
"anyOf": [
|
|
||||||
{
|
|
||||||
"$ref": "#/components/schemas/Person"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "null"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"dateHidden": {
|
|
||||||
"anyOf": [
|
|
||||||
{
|
|
||||||
"type": "string",
|
|
||||||
"format": "date-time"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "null"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"title": "Datehidden"
|
|
||||||
},
|
|
||||||
"hiddenById": {
|
|
||||||
"anyOf": [
|
|
||||||
{
|
|
||||||
"type": "integer"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "null"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"title": "Hiddenbyid"
|
|
||||||
},
|
|
||||||
"hiddenBy": {
|
|
||||||
"anyOf": [
|
|
||||||
{
|
|
||||||
"$ref": "#/components/schemas/Person"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "null"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"type": "object",
|
|
||||||
"required": [
|
|
||||||
"name",
|
|
||||||
"link",
|
|
||||||
"serves",
|
|
||||||
"createdById"
|
|
||||||
],
|
|
||||||
"title": "Recipe"
|
|
||||||
},
|
|
||||||
"Recipe-Output": {
|
|
||||||
"properties": {
|
"properties": {
|
||||||
"id": {
|
"id": {
|
||||||
"type": "integer",
|
"type": "integer",
|
||||||
|
|
|
||||||
|
|
@ -4,3 +4,4 @@ httpx==0.27.2
|
||||||
ingredient-parser-nlp==1.1.2
|
ingredient-parser-nlp==1.1.2
|
||||||
beautifulsoup4==4.12.3
|
beautifulsoup4==4.12.3
|
||||||
aiosqlite==0.20.0
|
aiosqlite==0.20.0
|
||||||
|
argon2-cffi==23.1.0
|
||||||
|
|
@ -57,7 +57,7 @@ class TestHealthAndLocationHeaders(unittest.IsolatedAsyncioTestCase):
|
||||||
|
|
||||||
def test_location_headers_on_create(self):
|
def test_location_headers_on_create(self):
|
||||||
# Use the registered user id placeholder for v2 meal participants
|
# Use the registered user id placeholder for v2 meal participants
|
||||||
person = {"id": 1, "name": "Loc"}
|
person = {"id": 1, "displayName": "Loc"}
|
||||||
|
|
||||||
# Skip recipe endpoint complexity here; covered by other tests
|
# Skip recipe endpoint complexity here; covered by other tests
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -42,9 +42,9 @@ class TestMealsWriteV2(unittest.IsolatedAsyncioTestCase):
|
||||||
# Create a meal with suggested date and one extra ingredient
|
# Create a meal with suggested date and one extra ingredient
|
||||||
body = {
|
body = {
|
||||||
"suggestedDate": datetime.datetime.now().astimezone().isoformat(),
|
"suggestedDate": datetime.datetime.now().astimezone().isoformat(),
|
||||||
"chefs": [{"id": 1, "name": "A"}],
|
"chefs": [{"id": 1, "displayName": "A"}],
|
||||||
"cleanup": [{"id": 1, "name": "A"}],
|
"cleanup": [{"id": 1, "displayName": "A"}],
|
||||||
"consumers": [{"id": 1, "name": "A"}],
|
"consumers": [{"id": 1, "displayName": "A"}],
|
||||||
"recipes": [],
|
"recipes": [],
|
||||||
"extraIngredients": [
|
"extraIngredients": [
|
||||||
{"name": "Salt", "line": "Salt", "unit": "Items", "quantity": 1, "preparation": ""}
|
{"name": "Salt", "line": "Salt", "unit": "Items", "quantity": 1, "preparation": ""}
|
||||||
|
|
@ -80,9 +80,9 @@ class TestMealsWriteV2(unittest.IsolatedAsyncioTestCase):
|
||||||
# Base valid body
|
# Base valid body
|
||||||
base = {
|
base = {
|
||||||
"suggestedDate": datetime.datetime.now().astimezone().isoformat(),
|
"suggestedDate": datetime.datetime.now().astimezone().isoformat(),
|
||||||
"chefs": [{"id": 1, "name": "A"}],
|
"chefs": [{"id": 1, "displayName": "A"}],
|
||||||
"cleanup": [{"id": 1, "name": "A"}],
|
"cleanup": [{"id": 1, "displayName": "A"}],
|
||||||
"consumers": [{"id": 1, "name": "A"}],
|
"consumers": [{"id": 1, "displayName": "A"}],
|
||||||
"recipes": [],
|
"recipes": [],
|
||||||
"extraIngredients": [
|
"extraIngredients": [
|
||||||
{"name": "Salt", "line": "Salt", "unit": "Items", "quantity": 1, "preparation": ""}
|
{"name": "Salt", "line": "Salt", "unit": "Items", "quantity": 1, "preparation": ""}
|
||||||
|
|
@ -139,9 +139,9 @@ class TestMealsWriteV2(unittest.IsolatedAsyncioTestCase):
|
||||||
# Create a valid meal first
|
# Create a valid meal first
|
||||||
body = {
|
body = {
|
||||||
"suggestedDate": datetime.datetime.now().astimezone().isoformat(),
|
"suggestedDate": datetime.datetime.now().astimezone().isoformat(),
|
||||||
"chefs": [{"id": 1, "name": "A"}],
|
"chefs": [{"id": 1, "displayName": "A"}],
|
||||||
"cleanup": [{"id": 1, "name": "A"}],
|
"cleanup": [{"id": 1, "displayName": "A"}],
|
||||||
"consumers": [{"id": 1, "name": "A"}],
|
"consumers": [{"id": 1, "displayName": "A"}],
|
||||||
"recipes": [],
|
"recipes": [],
|
||||||
"extraIngredients": [
|
"extraIngredients": [
|
||||||
{"name": "Salt", "line": "Salt", "unit": "Items", "quantity": 1, "preparation": ""}
|
{"name": "Salt", "line": "Salt", "unit": "Items", "quantity": 1, "preparation": ""}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue