feat(v2): add household-scoped delete for recipes; tests and OpenAPI updated

This commit is contained in:
jableader 2025-11-01 16:01:29 +11:00
parent ce75603582
commit b75fee41c6
4 changed files with 131 additions and 1 deletions

View file

@ -86,6 +86,25 @@ async def get_recipe(
return r return r
@router.delete("/{recipe_id}", response_model=RecipeOut, responses={404: {"model": ProblemDetails}})
async def delete_recipe(
recipe_id: int,
household=Depends(get_household_from_slug),
conn: aiosqlite.Connection = Depends(get_db),
):
# Load recipe in-scope
r = await recipes.find_recipe_by_id_scoped(conn, recipe_id, household["id"])
if not r:
return error_response(None, 404, "Recipe not found")
# Hide within household (no hidden_by in v2 yet)
from recipes.repository import hide_recipe_scoped
ok = await hide_recipe_scoped(conn, recipe_id, household["id"])
if not ok:
return error_response(None, 404, "Recipe not found")
return r
@router.post("", response_model=RecipeOut, responses={400: {"model": ProblemDetails}}) @router.post("", response_model=RecipeOut, responses={400: {"model": ProblemDetails}})
async def create_recipe( async def create_recipe(
recipe: RecipeCreate, recipe: RecipeCreate,

View file

@ -1912,6 +1912,67 @@
"bearerAuth": [] "bearerAuth": []
} }
] ]
},
"delete": {
"tags": [
"v2",
"recipes-v2"
],
"summary": "Delete Recipe",
"operationId": "delete_recipe_api_v1_households__householdSlug__recipes__recipe_id__delete",
"parameters": [
{
"name": "recipe_id",
"in": "path",
"required": true,
"schema": {
"type": "integer",
"title": "Recipe Id"
}
},
{
"name": "householdSlug",
"in": "path",
"required": true,
"schema": {
"type": "string",
"title": "Householdslug"
}
}
],
"responses": {
"200": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/api__recipes_v2__RecipeOut"
}
}
}
},
"404": {
"$ref": "#/components/responses/Problem404"
},
"422": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPValidationError"
}
}
}
},
"403": {
"$ref": "#/components/responses/Problem403"
}
},
"security": [
{
"bearerAuth": []
}
]
} }
}, },
"/api/v1/households/{householdSlug}/meals/upcoming": { "/api/v1/households/{householdSlug}/meals/upcoming": {

View file

@ -85,6 +85,22 @@ async def hide_recipe(conn, recipe_id: int, person: Person):
) )
async def hide_recipe_scoped(conn, recipe_id: int, household_id: int) -> bool:
"""Soft-delete a recipe by household for v2.
Returns True if updated, False if not found or not in household.
"""
async with conn.execute(
"""
UPDATE Recipe
SET date_hidden = ?
WHERE id = ? AND household_id = ?
""",
(datetime.datetime.now().astimezone().isoformat(), recipe_id, household_id),
) as cur:
return cur.rowcount > 0
def row_to_recipe(col_tuples: Iterable[Tuple[str, object]]) -> Recipe: def row_to_recipe(col_tuples: Iterable[Tuple[str, object]]) -> Recipe:
d: dict[str, Any] = {k: v for k, v in col_tuples} d: dict[str, Any] = {k: v for k, v in col_tuples}
img_raw = ( img_raw = (
@ -112,7 +128,7 @@ async def find_recipe_by_id_scoped(conn, recipe_id: int, household_id: int) -> O
async with conn.execute( async with conn.execute(
f""" f"""
SELECT {",".join(Recipe.KEYS)} FROM Recipe SELECT {",".join(Recipe.KEYS)} FROM Recipe
WHERE id = ? AND household_id = ? WHERE id = ? AND household_id = ? AND date_hidden IS NULL
LIMIT 1 LIMIT 1
""", """,
(recipe_id, household_id), (recipe_id, household_id),

View file

@ -82,3 +82,37 @@ class TestRecipesHouseholdV2(unittest.IsolatedAsyncioTestCase):
# Get in H2 by id should 404 # Get in H2 by id should 404
r = self.client.get(f"/api/v1/households/{self.h2}/recipes/{rid}", headers=self.headers) r = self.client.get(f"/api/v1/households/{self.h2}/recipes/{rid}", headers=self.headers)
assert r.status_code == 404 assert r.status_code == 404
def test_delete_recipe_scoped(self):
# Create a recipe in H1
recipe = {
"id": -1,
"name": "ToDelete",
"link": "https://example.com/del",
"serves": 2,
"imageUrls": [],
"ingredients": [
{
"id": 0,
"line": "1 Apple",
"name": "Apple",
"unit": "Items",
"quantity": 1,
"preparation": "",
"product": None,
}
],
}
r = self.client.post(
f"/api/v1/households/{self.h1}/recipes", headers=self.headers, json=recipe
)
assert r.status_code == 200, r.text
rid = r.json()["id"]
# Delete it via v2 scoped route
r2 = self.client.delete(f"/api/v1/households/{self.h1}/recipes/{rid}", headers=self.headers)
assert r2.status_code == 200, r2.text
# Subsequent get in same household should be 404
r3 = self.client.get(f"/api/v1/households/{self.h1}/recipes/{rid}", headers=self.headers)
assert r3.status_code == 404