feat(v2): add household-scoped delete for recipes; tests and OpenAPI updated
This commit is contained in:
parent
ce75603582
commit
b75fee41c6
4 changed files with 131 additions and 1 deletions
|
|
@ -86,6 +86,25 @@ async def get_recipe(
|
|||
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}})
|
||||
async def create_recipe(
|
||||
recipe: RecipeCreate,
|
||||
|
|
|
|||
61
openapi.json
61
openapi.json
|
|
@ -1912,6 +1912,67 @@
|
|||
"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": {
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
d: dict[str, Any] = {k: v for k, v in col_tuples}
|
||||
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(
|
||||
f"""
|
||||
SELECT {",".join(Recipe.KEYS)} FROM Recipe
|
||||
WHERE id = ? AND household_id = ?
|
||||
WHERE id = ? AND household_id = ? AND date_hidden IS NULL
|
||||
LIMIT 1
|
||||
""",
|
||||
(recipe_id, household_id),
|
||||
|
|
|
|||
|
|
@ -82,3 +82,37 @@ class TestRecipesHouseholdV2(unittest.IsolatedAsyncioTestCase):
|
|||
# Get in H2 by id should 404
|
||||
r = self.client.get(f"/api/v1/households/{self.h2}/recipes/{rid}", headers=self.headers)
|
||||
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
|
||||
|
|
|
|||
Loading…
Reference in a new issue