This commit is contained in:
Litchi Pi 2025-08-19 23:21:16 +02:00 committed by GitHub
commit 7783efe244
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 49 additions and 6 deletions

View file

@ -46,3 +46,11 @@ class UserLockedOut(Exception): ...
class MissingClaimException(Exception): ... class MissingClaimException(Exception): ...
class IncompleteData(Exception):
"""
This exception is raised when a user sends incomplete data to the API
"""
pass

View file

@ -84,6 +84,13 @@ class RecipeController(BaseRecipeController):
elif thrownType == exceptions.NoEntryFound: elif thrownType == exceptions.NoEntryFound:
self.logger.error("No Entry Found on recipe controller action") self.logger.error("No Entry Found on recipe controller action")
raise HTTPException(status_code=404, detail=ErrorResponse.respond(message="No Entry Found")) raise HTTPException(status_code=404, detail=ErrorResponse.respond(message="No Entry Found"))
elif thrownType == exceptions.IncompleteData:
missing = ex.args[-1]
self.logger.error("Incomplete data provided to API route:", missing)
raise HTTPException(
status_code=400,
detail=ErrorResponse.respond(message=f"Field '{missing}' missing on the body of this API request"),
)
elif thrownType == sqlalchemy.exc.IntegrityError: elif thrownType == sqlalchemy.exc.IntegrityError:
self.logger.error("SQL Integrity Error on recipe controller action") self.logger.error("SQL Integrity Error on recipe controller action")
raise HTTPException(status_code=400, detail=ErrorResponse.respond(message="Recipe already exists")) raise HTTPException(status_code=400, detail=ErrorResponse.respond(message="Recipe already exists"))
@ -360,7 +367,13 @@ class RecipeController(BaseRecipeController):
@router.put("/{slug}") @router.put("/{slug}")
def update_one(self, slug: str, data: Recipe): def update_one(self, slug: str, data: Recipe):
"""Updates a recipe by existing slug and data.""" """
Updates a recipe by existing slug and data.
Requires all the fields of the recipe to be passed in the body
If you wish to update parts of the recipe only, use a PATCH request to this route
"""
try: try:
recipe = self.service.update_one(slug, data) recipe = self.service.update_one(slug, data)
except Exception as e: except Exception as e:

View file

@ -36,6 +36,15 @@ from mealie.services.scraper import cleaner
from .template_service import TemplateService from .template_service import TemplateService
MANDATORY_FIELDS = [
"name",
"user_id",
"household_id",
"group_id",
"recipe_ingredient",
"recipe_instructions",
]
class RecipeServiceBase(BaseService): class RecipeServiceBase(BaseService):
def __init__(self, repos: AllRepositories, user: PrivateUser, household: HouseholdInDB, translator: Translator): def __init__(self, repos: AllRepositories, user: PrivateUser, household: HouseholdInDB, translator: Translator):
@ -144,6 +153,7 @@ class RecipeService(RecipeServiceBase):
if not additional_attrs.get("recipe_instructions"): if not additional_attrs.get("recipe_instructions"):
additional_attrs["recipe_instructions"] = [RecipeStep(text=self.t("recipe.recipe-defaults.step-text"))] additional_attrs["recipe_instructions"] = [RecipeStep(text=self.t("recipe.recipe-defaults.step-text"))]
assert all(f in additional_attrs for f in MANDATORY_FIELDS)
return Recipe(**additional_attrs) return Recipe(**additional_attrs)
def get_one(self, slug_or_id: str | UUID) -> Recipe: def get_one(self, slug_or_id: str | UUID) -> Recipe:
@ -369,7 +379,7 @@ class RecipeService(RecipeServiceBase):
return new_recipe return new_recipe
def _pre_update_check(self, slug_or_id: str | UUID, new_data: Recipe) -> Recipe: def _pre_update_check(self, slug_or_id: str | UUID, new_data: Recipe, strict: bool) -> Recipe:
""" """
gets the recipe from the database and performs a check to see if the user can update the recipe. gets the recipe from the database and performs a check to see if the user can update the recipe.
If the user can't update the recipe, an exception is raised. If the user can't update the recipe, an exception is raised.
@ -382,6 +392,7 @@ class RecipeService(RecipeServiceBase):
Args: Args:
slug_or_id (str | UUID): recipe slug or id slug_or_id (str | UUID): recipe slug or id
new_data (Recipe): the new recipe data new_data (Recipe): the new recipe data
strict: if the data provided must contain ALL fields, or can be incomplete
Raises: Raises:
exceptions.PermissionDenied (403) exceptions.PermissionDenied (403)
@ -389,6 +400,11 @@ class RecipeService(RecipeServiceBase):
recipe = self.get_one(slug_or_id) recipe = self.get_one(slug_or_id)
if strict:
for field in new_data.__class__.model_fields:
if field in MANDATORY_FIELDS and getattr(new_data, field) is None:
raise exceptions.IncompleteData("Incomplete recipe", field)
if recipe is None or recipe.settings is None: if recipe is None or recipe.settings is None:
raise exceptions.NoEntryFound("Recipe not found.") raise exceptions.NoEntryFound("Recipe not found.")
@ -402,7 +418,7 @@ class RecipeService(RecipeServiceBase):
return recipe return recipe
def update_one(self, slug_or_id: str | UUID, update_data: Recipe) -> Recipe: def update_one(self, slug_or_id: str | UUID, update_data: Recipe) -> Recipe:
recipe = self._pre_update_check(slug_or_id, update_data) recipe = self._pre_update_check(slug_or_id, update_data, True)
new_data = self.group_recipes.update(recipe.slug, update_data) new_data = self.group_recipes.update(recipe.slug, update_data)
self.check_assets(new_data, recipe.slug) self.check_assets(new_data, recipe.slug)
@ -419,7 +435,7 @@ class RecipeService(RecipeServiceBase):
return self.group_recipes.update_image(slug, extension) return self.group_recipes.update_image(slug, extension)
def patch_one(self, slug_or_id: str | UUID, patch_data: Recipe) -> Recipe: def patch_one(self, slug_or_id: str | UUID, patch_data: Recipe) -> Recipe:
recipe: Recipe = self._pre_update_check(slug_or_id, patch_data) recipe: Recipe = self._pre_update_check(slug_or_id, patch_data, False)
new_data = self.group_recipes.patch(recipe.slug, patch_data.model_dump(exclude_unset=True)) new_data = self.group_recipes.patch(recipe.slug, patch_data.model_dump(exclude_unset=True))

View file

@ -109,6 +109,7 @@ def test_organizer_update(
item[key] = update_data[key] item[key] = update_data[key]
response = api_client.put(route.item(item_id), json=item, headers=unique_user.token) response = api_client.put(route.item(item_id), json=item, headers=unique_user.token)
print(response)
assert response.status_code == 200 assert response.status_code == 200
response = api_client.get(route.item(item_id), headers=unique_user.token) response = api_client.get(route.item(item_id), headers=unique_user.token)
@ -175,7 +176,7 @@ def test_organizer_association(
] ]
# Update Recipe # Update Recipe
response = api_client.put(api_routes.recipes_slug(slug), json=as_json, headers=unique_user.token) response = api_client.patch(api_routes.recipes_slug(slug), json=as_json, headers=unique_user.token)
assert response.status_code == 200 assert response.status_code == 200
# Get Recipe Data # Get Recipe Data
@ -223,7 +224,7 @@ def test_organizer_get_by_slug(
{"id": item["id"], "group_id": unique_user.group_id, "name": item["name"], "slug": item["slug"]} {"id": item["id"], "group_id": unique_user.group_id, "name": item["name"], "slug": item["slug"]}
] ]
response = api_client.put(api_routes.recipes_slug(slug), json=as_json, headers=unique_user.token) response = api_client.patch(api_routes.recipes_slug(slug), json=as_json, headers=unique_user.token)
assert response.status_code == 200 assert response.status_code == 200
# Get Organizer by Slug # Get Organizer by Slug

View file

@ -470,6 +470,11 @@ def test_read_update(
assert response.status_code == 200 assert response.status_code == 200
assert json.loads(response.text).get("slug") == recipe_data.expected_slug assert json.loads(response.text).get("slug") == recipe_data.expected_slug
recipe_incomplete = recipe.copy()
del recipe_incomplete["group_id"]
response = api_client.put(recipe_url, json=utils.jsonify(recipe_incomplete), headers=unique_user.token)
assert response.status_code == 400
response = api_client.get(recipe_url, headers=unique_user.token) response = api_client.get(recipe_url, headers=unique_user.token)
assert response.status_code == 200 assert response.status_code == 200
recipe = json.loads(response.text) recipe = json.loads(response.text)