Shopping/Recipes: add scoped DELETE for ad-hoc ingredient requests and “parse-from-url”
This commit is contained in:
parent
964072391e
commit
5618b6800e
5 changed files with 195 additions and 0 deletions
|
|
@ -39,6 +39,10 @@ class RecipeCreate(ApiModel):
|
|||
)
|
||||
|
||||
|
||||
class ParseUrlIn(ApiModel):
|
||||
url: str
|
||||
|
||||
|
||||
@router.get("", response_model=Page[RecipeOut])
|
||||
async def list_recipes(
|
||||
household=Depends(get_household_from_slug),
|
||||
|
|
@ -212,3 +216,17 @@ async def create_recipe(
|
|||
created_by_id=r.created_by_id,
|
||||
created_by=created,
|
||||
)
|
||||
|
||||
|
||||
@router.post("/parse-from-url")
|
||||
async def parse_from_url(
|
||||
body: ParseUrlIn,
|
||||
household=Depends(get_household_from_slug),
|
||||
):
|
||||
# Household dependency enforces access; parsing is stateless
|
||||
from recipes.scraping import scrape_recipe_ldata
|
||||
|
||||
data = await scrape_recipe_ldata(body.url)
|
||||
if not data:
|
||||
return error_response(None, 404, "Recipe data not found at URL")
|
||||
return data
|
||||
|
|
|
|||
|
|
@ -230,6 +230,27 @@ async def request_ingredient_scoped(
|
|||
return _to_ingredient_item(item)
|
||||
|
||||
|
||||
@router.delete(
|
||||
"/current/ingredients",
|
||||
response_model=Ok,
|
||||
operation_id="unrequestIngredientV2",
|
||||
summary="Remove an ingredient request (scoped)",
|
||||
)
|
||||
async def unrequest_ingredient_scoped(
|
||||
r: IngredientIdWrapper,
|
||||
household=Depends(get_household_from_slug),
|
||||
user=Depends(get_current_user),
|
||||
conn: aiosqlite.Connection = Depends(get_db),
|
||||
):
|
||||
"""Remove a personal ad-hoc ingredient request for the current user in this household.
|
||||
|
||||
Idempotent: returns ok=true whether or not a row was actually deleted.
|
||||
"""
|
||||
hid = household["id"]
|
||||
await shopping.remove_ingredient_request_scoped(conn, r.ingredient_id, user.id, hid)
|
||||
return Ok()
|
||||
|
||||
|
||||
# Re-export shared DTOs for importers
|
||||
__all__ = [
|
||||
"router",
|
||||
|
|
|
|||
132
openapi.json
132
openapi.json
|
|
@ -704,6 +704,65 @@
|
|||
]
|
||||
}
|
||||
},
|
||||
"/api/v1/households/{householdSlug}/recipes/parse-from-url": {
|
||||
"post": {
|
||||
"tags": [
|
||||
"recipes",
|
||||
"recipes"
|
||||
],
|
||||
"summary": "Parse From Url",
|
||||
"operationId": "parse_from_url_api_v1_households__householdSlug__recipes_parse_from_url_post",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "householdSlug",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"title": "Householdslug"
|
||||
}
|
||||
}
|
||||
],
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ParseUrlIn"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful Response",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {}
|
||||
}
|
||||
}
|
||||
},
|
||||
"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": {
|
||||
"get": {
|
||||
"tags": [
|
||||
|
|
@ -1476,6 +1535,66 @@
|
|||
"bearerAuth": []
|
||||
}
|
||||
]
|
||||
},
|
||||
"delete": {
|
||||
"tags": [
|
||||
"shopping",
|
||||
"shopping"
|
||||
],
|
||||
"summary": "Remove an ingredient request (scoped)",
|
||||
"description": "Remove a personal ad-hoc ingredient request for the current user in this household.\n\nIdempotent: returns ok=true whether or not a row was actually deleted.",
|
||||
"operationId": "unrequestIngredientV2",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "householdSlug",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"title": "Householdslug"
|
||||
}
|
||||
}
|
||||
],
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/IngredientIdWrapper"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful Response",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Ok"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"422": {
|
||||
"description": "Validation Error",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/HTTPValidationError"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"403": {
|
||||
"$ref": "#/components/responses/Problem403"
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"bearerAuth": []
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"/healthz": {
|
||||
|
|
@ -2356,6 +2475,19 @@
|
|||
],
|
||||
"title": "Page[RecipeOut]"
|
||||
},
|
||||
"ParseUrlIn": {
|
||||
"properties": {
|
||||
"url": {
|
||||
"type": "string",
|
||||
"title": "Url"
|
||||
}
|
||||
},
|
||||
"type": "object",
|
||||
"required": [
|
||||
"url"
|
||||
],
|
||||
"title": "ParseUrlIn"
|
||||
},
|
||||
"ProblemDetails": {
|
||||
"properties": {
|
||||
"type": {
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ from shopping.repository import (
|
|||
request_meal_scoped as request_meal_scoped,
|
||||
request_ingredient_scoped as request_ingredient_scoped,
|
||||
remove_meal_request_scoped as remove_meal_request_scoped,
|
||||
remove_ingredient_request_scoped as remove_ingredient_request_scoped,
|
||||
load_shopping_list as load_shopping_list,
|
||||
load_shopping_list_scoped as load_shopping_list_scoped,
|
||||
purchase as purchase,
|
||||
|
|
|
|||
|
|
@ -380,6 +380,29 @@ async def remove_meal_request_scoped(conn, meal_id: int, household_id: int) -> b
|
|||
return cursor.rowcount > 0
|
||||
|
||||
|
||||
async def remove_ingredient_request_scoped(
|
||||
conn, ingredient_id: int, person_id: int, household_id: int
|
||||
) -> bool:
|
||||
"""Remove an ad-hoc ingredient request for a specific user within a household.
|
||||
|
||||
This targets items that are not yet purchased (list_id IS NULL), have no meal/recipe linkage
|
||||
(pure personal request), and match the provided ingredient/person/household ids.
|
||||
"""
|
||||
async with conn.execute(
|
||||
"""
|
||||
DELETE FROM ShoppingListItem
|
||||
WHERE list_id IS NULL
|
||||
AND meal_id IS NULL
|
||||
AND recipe_id IS NULL
|
||||
AND ingredient_id = ?
|
||||
AND person_id = ?
|
||||
AND household_id = ?
|
||||
""",
|
||||
(ingredient_id, person_id, household_id),
|
||||
) as cursor:
|
||||
return cursor.rowcount > 0
|
||||
|
||||
|
||||
async def find_items_by_list_id(conn, list_id: Optional[int]) -> AsyncIterator[ShoppingListItem]:
|
||||
request_cols = [f"shoppinglistitem.{key}" for key in ShoppingListItem.KEYS]
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue