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])
|
@router.get("", response_model=Page[RecipeOut])
|
||||||
async def list_recipes(
|
async def list_recipes(
|
||||||
household=Depends(get_household_from_slug),
|
household=Depends(get_household_from_slug),
|
||||||
|
|
@ -212,3 +216,17 @@ async def create_recipe(
|
||||||
created_by_id=r.created_by_id,
|
created_by_id=r.created_by_id,
|
||||||
created_by=created,
|
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)
|
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
|
# Re-export shared DTOs for importers
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"router",
|
"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": {
|
"/api/v1/households/{householdSlug}/meals/upcoming": {
|
||||||
"get": {
|
"get": {
|
||||||
"tags": [
|
"tags": [
|
||||||
|
|
@ -1476,6 +1535,66 @@
|
||||||
"bearerAuth": []
|
"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": {
|
"/healthz": {
|
||||||
|
|
@ -2356,6 +2475,19 @@
|
||||||
],
|
],
|
||||||
"title": "Page[RecipeOut]"
|
"title": "Page[RecipeOut]"
|
||||||
},
|
},
|
||||||
|
"ParseUrlIn": {
|
||||||
|
"properties": {
|
||||||
|
"url": {
|
||||||
|
"type": "string",
|
||||||
|
"title": "Url"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"url"
|
||||||
|
],
|
||||||
|
"title": "ParseUrlIn"
|
||||||
|
},
|
||||||
"ProblemDetails": {
|
"ProblemDetails": {
|
||||||
"properties": {
|
"properties": {
|
||||||
"type": {
|
"type": {
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ from shopping.repository import (
|
||||||
request_meal_scoped as request_meal_scoped,
|
request_meal_scoped as request_meal_scoped,
|
||||||
request_ingredient_scoped as request_ingredient_scoped,
|
request_ingredient_scoped as request_ingredient_scoped,
|
||||||
remove_meal_request_scoped as remove_meal_request_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 as load_shopping_list,
|
||||||
load_shopping_list_scoped as load_shopping_list_scoped,
|
load_shopping_list_scoped as load_shopping_list_scoped,
|
||||||
purchase as purchase,
|
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
|
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]:
|
async def find_items_by_list_id(conn, list_id: Optional[int]) -> AsyncIterator[ShoppingListItem]:
|
||||||
request_cols = [f"shoppinglistitem.{key}" for key in ShoppingListItem.KEYS]
|
request_cols = [f"shoppinglistitem.{key}" for key in ShoppingListItem.KEYS]
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue