diff --git a/mealie/core/exceptions.py b/mealie/core/exceptions.py index 89ed9538e..de8cf7d39 100644 --- a/mealie/core/exceptions.py +++ b/mealie/core/exceptions.py @@ -46,3 +46,9 @@ class UserLockedOut(Exception): ... class MissingClaimException(Exception): ... + +class IncompleteData(Exception): + """ + This exception is raised when a user sends incomplete data to the API + """ + pass diff --git a/mealie/routes/recipe/recipe_crud_routes.py b/mealie/routes/recipe/recipe_crud_routes.py index 0504966c8..dd4a9fa25 100644 --- a/mealie/routes/recipe/recipe_crud_routes.py +++ b/mealie/routes/recipe/recipe_crud_routes.py @@ -84,6 +84,9 @@ class RecipeController(BaseRecipeController): elif thrownType == exceptions.NoEntryFound: self.logger.error("No Entry Found on recipe controller action") raise HTTPException(status_code=404, detail=ErrorResponse.respond(message="No Entry Found")) + elif thrownType == exceptions.IncompleteData: + self.logger.error("Incomplete data provided to API route") + raise HTTPException(status_code=400, detail=ErrorResponse.respond(message="Some data were missing on the body of this API request")) elif thrownType == sqlalchemy.exc.IntegrityError: self.logger.error("SQL Integrity Error on recipe controller action") raise HTTPException(status_code=400, detail=ErrorResponse.respond(message="Recipe already exists")) @@ -360,9 +363,15 @@ class RecipeController(BaseRecipeController): @router.put("/{slug}") 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: - recipe = self.service.update_one(slug, data) + recipe = self.service.update_one(slug, data, strict=True) except Exception as e: self.handle_exceptions(e) @@ -387,7 +396,7 @@ class RecipeController(BaseRecipeController): lambda: defaultdict(list) ) for recipe in data: - r = self.service.update_one(recipe.id, recipe) # type: ignore + r = self.service.update_one(recipe.id, recipe, strict=True) # type: ignore updated_by_group_and_household[r.group_id][r.household_id].append(r) all_updated: list[Recipe] = [] diff --git a/mealie/services/recipe/recipe_service.py b/mealie/services/recipe/recipe_service.py index 0040ff88a..bbef50244 100644 --- a/mealie/services/recipe/recipe_service.py +++ b/mealie/services/recipe/recipe_service.py @@ -364,7 +364,7 @@ class RecipeService(RecipeServiceBase): 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. If the user can't update the recipe, an exception is raised. @@ -377,6 +377,7 @@ class RecipeService(RecipeServiceBase): Args: slug_or_id (str | UUID): recipe slug or id new_data (Recipe): the new recipe data + strict: if the data provided must contain ALL fields, or can be incomplete Raises: exceptions.PermissionDenied (403) @@ -384,6 +385,11 @@ class RecipeService(RecipeServiceBase): recipe = self.get_one(slug_or_id) + if strict: + for field in new_data.__class__.model_fields: + if getattr(new_data, field) is None: + raise exceptions.IncompleteData("Incomplete recipe") + if recipe is None or recipe.settings is None: raise exceptions.NoEntryFound("Recipe not found.") @@ -396,8 +402,8 @@ class RecipeService(RecipeServiceBase): return recipe - def update_one(self, slug_or_id: str | UUID, update_data: Recipe) -> Recipe: - recipe = self._pre_update_check(slug_or_id, update_data) + def update_one(self, slug_or_id: str | UUID, update_data: Recipe, strict: bool) -> Recipe: + recipe = self._pre_update_check(slug_or_id, update_data, strict) new_data = self.group_recipes.update(recipe.slug, update_data) self.check_assets(new_data, recipe.slug)