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 settings import settings
|
||||
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
|
||||
|
||||
router = APIRouter(prefix="/auth", tags=["auth-v2"])
|
||||
|
|
@ -38,17 +46,32 @@ class TokenResponse(ApiModel):
|
|||
|
||||
|
||||
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
|
||||
|
||||
|
||||
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)
|
||||
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()}"
|
||||
|
||||
|
||||
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:
|
||||
alg, iter_s, salt_b64, hash_b64 = stored.split("$", 3)
|
||||
if alg != PBKDF2_ALG:
|
||||
|
|
@ -57,7 +80,6 @@ def _verify_pw(pw: str, stored: str) -> bool:
|
|||
salt = base64.b64decode(salt_b64)
|
||||
expected = base64.b64decode(hash_b64)
|
||||
dk = hashlib.pbkdf2_hmac("sha256", pw.encode("utf-8"), salt, iters)
|
||||
# constant-time compare
|
||||
return hmac.compare_digest(dk, expected)
|
||||
except Exception:
|
||||
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
|
||||
|
||||
import meals
|
||||
import persons
|
||||
import shopping
|
||||
import ingredients
|
||||
from common import ProblemDetails, ApiModel
|
||||
from api.deps import error_response
|
||||
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"])
|
||||
|
||||
|
||||
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):
|
||||
consumed_date: Optional[datetime.datetime] = None
|
||||
|
||||
|
|
@ -29,7 +66,7 @@ async def get_upcoming_meals_scoped(
|
|||
date_from: datetime.datetime = Query(..., alias="from"),
|
||||
to: datetime.datetime = Query(...),
|
||||
conn: aiosqlite.Connection = Depends(get_db),
|
||||
) -> List[meals.Meal]:
|
||||
) -> List[MealOut]:
|
||||
hid = household["id"]
|
||||
# Use a scoped repository function, falling back to filtering in SQL until repository is fully scoped
|
||||
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)}))
|
||||
|
||||
if not result:
|
||||
return result
|
||||
return []
|
||||
|
||||
# Load relateds similar to v1
|
||||
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_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(
|
||||
"/{meal_id}",
|
||||
operation_id="getMealV2",
|
||||
summary="Get a meal by id (scoped)",
|
||||
response_model=meals.Meal,
|
||||
response_model=MealOut,
|
||||
responses={404: {"model": ProblemDetails}},
|
||||
)
|
||||
async def get_meal_scoped(
|
||||
|
|
@ -80,19 +136,33 @@ async def get_meal_scoped(
|
|||
request: Request,
|
||||
household=Depends(get_household_from_slug),
|
||||
conn: aiosqlite.Connection = Depends(get_db),
|
||||
) -> meals.Meal | Response:
|
||||
) -> MealOut | Response:
|
||||
hid = household["id"]
|
||||
meal = await meals.find_meal_by_id_scoped(conn, meal_id, hid)
|
||||
if not meal:
|
||||
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(
|
||||
"/{meal_id}/consumed",
|
||||
operation_id="markMealConsumedV2",
|
||||
summary="Mark a meal as consumed (scoped)",
|
||||
response_model=meals.Meal,
|
||||
response_model=MealOut,
|
||||
responses={
|
||||
400: {"model": ProblemDetails},
|
||||
404: {"model": ProblemDetails},
|
||||
|
|
@ -104,7 +174,7 @@ async def mark_meal_consumed_scoped(
|
|||
body: Optional[MarkConsumedBody] = None,
|
||||
household=Depends(get_household_from_slug),
|
||||
conn: aiosqlite.Connection = Depends(get_db),
|
||||
) -> meals.Meal | Response:
|
||||
) -> MealOut | Response:
|
||||
hid = household["id"]
|
||||
consumed_date: Optional[datetime.datetime] = 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
|
||||
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(
|
||||
"",
|
||||
operation_id="createMealV2",
|
||||
summary="Create a new meal (scoped)",
|
||||
response_model=meals.Meal,
|
||||
response_model=MealOut,
|
||||
responses={400: {"model": ProblemDetails}},
|
||||
)
|
||||
async def create_meal_scoped(
|
||||
meal: meals.Meal,
|
||||
meal: MealIn,
|
||||
response: Response,
|
||||
request: Request,
|
||||
household=Depends(get_household_from_slug),
|
||||
conn: aiosqlite.Connection = Depends(get_db),
|
||||
) -> meals.Meal | Response:
|
||||
) -> MealOut | Response:
|
||||
# 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:
|
||||
return error_response(request, 400, msg)
|
||||
hid = household["id"]
|
||||
await meals.insert_meal_scoped(conn, meal, hid)
|
||||
response.headers["Location"] = f"/api/v1/households/{household['slug']}/meals/{meal.id}"
|
||||
return meal
|
||||
await meals.insert_meal_scoped(conn, domain_meal, hid)
|
||||
response.headers["Location"] = f"/api/v1/households/{household['slug']}/meals/{domain_meal.id}"
|
||||
|
||||
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(
|
||||
"/{meal_id}",
|
||||
operation_id="updateMealV2",
|
||||
summary="Update an existing meal (scoped)",
|
||||
response_model=meals.Meal,
|
||||
response_model=MealOut,
|
||||
responses={400: {"model": ProblemDetails}, 404: {"model": ProblemDetails}},
|
||||
)
|
||||
async def update_meal_scoped(
|
||||
meal_id: int,
|
||||
meal: meals.Meal,
|
||||
meal: MealIn,
|
||||
request: Request,
|
||||
household=Depends(get_household_from_slug),
|
||||
conn: aiosqlite.Connection = Depends(get_db),
|
||||
) -> meals.Meal | Response:
|
||||
) -> MealOut | Response:
|
||||
if meal.id != meal_id:
|
||||
return error_response(request, 400, "Meal ID in URL does not match meal ID in body")
|
||||
hid = household["id"]
|
||||
existing = await meals.find_meal_by_id_scoped(conn, meal_id, hid)
|
||||
if not existing:
|
||||
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:
|
||||
return error_response(request, 400, msg)
|
||||
await meals.update_meal(conn, meal)
|
||||
await meals.update_meal(conn, domain_meal)
|
||||
# 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(
|
||||
"/{meal_id}",
|
||||
operation_id="deleteMealV2",
|
||||
summary="Delete a meal (scoped)",
|
||||
response_model=meals.Meal,
|
||||
response_model=MealOut,
|
||||
responses={404: {"model": ProblemDetails}},
|
||||
)
|
||||
async def delete_meal_scoped(
|
||||
|
|
@ -188,7 +335,7 @@ async def delete_meal_scoped(
|
|||
request: Request,
|
||||
household=Depends(get_household_from_slug),
|
||||
conn: aiosqlite.Connection = Depends(get_db),
|
||||
) -> meals.Meal | Response:
|
||||
) -> MealOut | Response:
|
||||
hid = household["id"]
|
||||
meal = await meals.find_meal_by_id_scoped(conn, meal_id, hid)
|
||||
if not meal:
|
||||
|
|
@ -202,4 +349,18 @@ async def delete_meal_scoped(
|
|||
# Fallback: remove regardless of household (legacy cleanup)
|
||||
await shopping.remove_request(conn, person=None, meal=meal)
|
||||
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": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/Meal-Output"
|
||||
"$ref": "#/components/schemas/MealOut"
|
||||
},
|
||||
"title": "Response Getupcomingmealsv2"
|
||||
}
|
||||
|
|
@ -758,7 +758,7 @@
|
|||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Meal-Output"
|
||||
"$ref": "#/components/schemas/MealOut"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -818,7 +818,7 @@
|
|||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Meal-Input"
|
||||
"$ref": "#/components/schemas/MealIn"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -829,7 +829,7 @@
|
|||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Meal-Output"
|
||||
"$ref": "#/components/schemas/MealOut"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -893,7 +893,7 @@
|
|||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Meal-Output"
|
||||
"$ref": "#/components/schemas/MealOut"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -973,7 +973,7 @@
|
|||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Meal-Output"
|
||||
"$ref": "#/components/schemas/MealOut"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1029,7 +1029,7 @@
|
|||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Meal-Input"
|
||||
"$ref": "#/components/schemas/MealIn"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1040,7 +1040,7 @@
|
|||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Meal-Output"
|
||||
"$ref": "#/components/schemas/MealOut"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1444,7 +1444,7 @@
|
|||
},
|
||||
"mealsLookup": {
|
||||
"additionalProperties": {
|
||||
"$ref": "#/components/schemas/Meal-Output"
|
||||
"$ref": "#/components/schemas/Meal"
|
||||
},
|
||||
"type": "object",
|
||||
"title": "Mealslookup"
|
||||
|
|
@ -1458,7 +1458,7 @@
|
|||
},
|
||||
"recipesLookup": {
|
||||
"additionalProperties": {
|
||||
"$ref": "#/components/schemas/Recipe-Output"
|
||||
"$ref": "#/components/schemas/Recipe"
|
||||
},
|
||||
"type": "object",
|
||||
"title": "Recipeslookup"
|
||||
|
|
@ -1799,7 +1799,7 @@
|
|||
"type": "object",
|
||||
"title": "MarkConsumedBody"
|
||||
},
|
||||
"Meal-Input": {
|
||||
"Meal": {
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "integer",
|
||||
|
|
@ -1846,85 +1846,7 @@
|
|||
},
|
||||
"recipes": {
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/MealRecipe-Input"
|
||||
},
|
||||
"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"
|
||||
"$ref": "#/components/schemas/MealRecipe"
|
||||
},
|
||||
"type": "array",
|
||||
"title": "Recipes"
|
||||
|
|
@ -1968,7 +1890,161 @@
|
|||
],
|
||||
"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": {
|
||||
"mealId": {
|
||||
"type": "integer",
|
||||
|
|
@ -1985,7 +2061,7 @@
|
|||
"recipe": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/components/schemas/Recipe-Input"
|
||||
"$ref": "#/components/schemas/Recipe"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
|
|
@ -2001,7 +2077,7 @@
|
|||
],
|
||||
"title": "MealRecipe"
|
||||
},
|
||||
"MealRecipe-Output": {
|
||||
"MealRecipeIn": {
|
||||
"properties": {
|
||||
"mealId": {
|
||||
"type": "integer",
|
||||
|
|
@ -2014,16 +2090,6 @@
|
|||
"servings": {
|
||||
"type": "number",
|
||||
"title": "Servings"
|
||||
},
|
||||
"recipe": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/components/schemas/Recipe-Output"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"type": "object",
|
||||
|
|
@ -2032,7 +2098,25 @@
|
|||
"recipeId",
|
||||
"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": {
|
||||
"properties": {
|
||||
|
|
@ -2244,7 +2328,7 @@
|
|||
},
|
||||
"mealsLookup": {
|
||||
"additionalProperties": {
|
||||
"$ref": "#/components/schemas/Meal-Output"
|
||||
"$ref": "#/components/schemas/Meal"
|
||||
},
|
||||
"type": "object",
|
||||
"title": "Mealslookup"
|
||||
|
|
@ -2258,7 +2342,7 @@
|
|||
},
|
||||
"recipesLookup": {
|
||||
"additionalProperties": {
|
||||
"$ref": "#/components/schemas/Recipe-Output"
|
||||
"$ref": "#/components/schemas/Recipe"
|
||||
},
|
||||
"type": "object",
|
||||
"title": "Recipeslookup"
|
||||
|
|
@ -2273,113 +2357,7 @@
|
|||
],
|
||||
"title": "PurchasedShoppingList"
|
||||
},
|
||||
"Recipe-Input": {
|
||||
"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": {
|
||||
"Recipe": {
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "integer",
|
||||
|
|
|
|||
|
|
@ -4,3 +4,4 @@ httpx==0.27.2
|
|||
ingredient-parser-nlp==1.1.2
|
||||
beautifulsoup4==4.12.3
|
||||
aiosqlite==0.20.0
|
||||
argon2-cffi==23.1.0
|
||||
|
|
@ -57,7 +57,7 @@ class TestHealthAndLocationHeaders(unittest.IsolatedAsyncioTestCase):
|
|||
|
||||
def test_location_headers_on_create(self):
|
||||
# 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
|
||||
|
||||
|
|
|
|||
|
|
@ -42,9 +42,9 @@ class TestMealsWriteV2(unittest.IsolatedAsyncioTestCase):
|
|||
# Create a meal with suggested date and one extra ingredient
|
||||
body = {
|
||||
"suggestedDate": datetime.datetime.now().astimezone().isoformat(),
|
||||
"chefs": [{"id": 1, "name": "A"}],
|
||||
"cleanup": [{"id": 1, "name": "A"}],
|
||||
"consumers": [{"id": 1, "name": "A"}],
|
||||
"chefs": [{"id": 1, "displayName": "A"}],
|
||||
"cleanup": [{"id": 1, "displayName": "A"}],
|
||||
"consumers": [{"id": 1, "displayName": "A"}],
|
||||
"recipes": [],
|
||||
"extraIngredients": [
|
||||
{"name": "Salt", "line": "Salt", "unit": "Items", "quantity": 1, "preparation": ""}
|
||||
|
|
@ -80,9 +80,9 @@ class TestMealsWriteV2(unittest.IsolatedAsyncioTestCase):
|
|||
# Base valid body
|
||||
base = {
|
||||
"suggestedDate": datetime.datetime.now().astimezone().isoformat(),
|
||||
"chefs": [{"id": 1, "name": "A"}],
|
||||
"cleanup": [{"id": 1, "name": "A"}],
|
||||
"consumers": [{"id": 1, "name": "A"}],
|
||||
"chefs": [{"id": 1, "displayName": "A"}],
|
||||
"cleanup": [{"id": 1, "displayName": "A"}],
|
||||
"consumers": [{"id": 1, "displayName": "A"}],
|
||||
"recipes": [],
|
||||
"extraIngredients": [
|
||||
{"name": "Salt", "line": "Salt", "unit": "Items", "quantity": 1, "preparation": ""}
|
||||
|
|
@ -139,9 +139,9 @@ class TestMealsWriteV2(unittest.IsolatedAsyncioTestCase):
|
|||
# Create a valid meal first
|
||||
body = {
|
||||
"suggestedDate": datetime.datetime.now().astimezone().isoformat(),
|
||||
"chefs": [{"id": 1, "name": "A"}],
|
||||
"cleanup": [{"id": 1, "name": "A"}],
|
||||
"consumers": [{"id": 1, "name": "A"}],
|
||||
"chefs": [{"id": 1, "displayName": "A"}],
|
||||
"cleanup": [{"id": 1, "displayName": "A"}],
|
||||
"consumers": [{"id": 1, "displayName": "A"}],
|
||||
"recipes": [],
|
||||
"extraIngredients": [
|
||||
{"name": "Salt", "line": "Salt", "unit": "Items", "quantity": 1, "preparation": ""}
|
||||
|
|
|
|||
Loading…
Reference in a new issue