mirror of
				https://github.com/mealie-recipes/mealie.git
				synced 2025-10-31 02:03:35 -04:00 
			
		
		
		
	fix: error when trying to change recipe image (#5771)
Co-authored-by: Michael Genson <71845777+michael-genson@users.noreply.github.com> Co-authored-by: Kuchenpirat <24235032+Kuchenpirat@users.noreply.github.com>
This commit is contained in:
		| @@ -390,8 +390,6 @@ const props = defineProps({ | ||||
|  | ||||
| const emit = defineEmits(["click-instruction-field", "update:assets"]); | ||||
|  | ||||
| const BASE_URL = useRequestURL().origin; | ||||
|  | ||||
| const { isCookMode, toggleCookMode, isEditForm } = usePageState(props.recipe.slug); | ||||
|  | ||||
| const dialog = ref(false); | ||||
| @@ -695,7 +693,7 @@ async function handleImageDrop(index: number, files: File[]) { | ||||
|   } | ||||
|  | ||||
|   emit("update:assets", [...assets.value, data]); | ||||
|   const assetUrl = BASE_URL + recipeAssetPath(props.recipe.id, data.fileName as string); | ||||
|   const assetUrl = recipeAssetPath(props.recipe.id, data.fileName as string); | ||||
|   const text = `<img src="${assetUrl}" height="100%" width="100%"/>`; | ||||
|   instructionList.value[index].text += text; | ||||
| } | ||||
|   | ||||
| @@ -4,43 +4,39 @@ function UnknownToString(ukn: string | unknown) { | ||||
|  | ||||
| export const useStaticRoutes = () => { | ||||
|   const { $config } = useNuxtApp(); | ||||
|   const serverBase = useRequestURL().origin; | ||||
|  | ||||
|   const prefix = `${$config.public.SUB_PATH}/api`.replace("//", "/"); | ||||
|  | ||||
|   const fullBase = serverBase + prefix; | ||||
|  | ||||
|   // Methods to Generate reference urls for assets/images * | ||||
|   function recipeImage(recipeId: string, version: string | unknown = "", key: string | number = 1) { | ||||
|     return `${fullBase}/media/recipes/${recipeId}/images/original.webp?rnd=${key}&version=${UnknownToString(version)}`; | ||||
|     return `${prefix}/media/recipes/${recipeId}/images/original.webp?rnd=${key}&version=${UnknownToString(version)}`; | ||||
|   } | ||||
|  | ||||
|   function recipeSmallImage(recipeId: string, version: string | unknown = "", key: string | number = 1) { | ||||
|     return `${fullBase}/media/recipes/${recipeId}/images/min-original.webp?rnd=${key}&version=${UnknownToString( | ||||
|     return `${prefix}/media/recipes/${recipeId}/images/min-original.webp?rnd=${key}&version=${UnknownToString( | ||||
|       version, | ||||
|     )}`; | ||||
|   } | ||||
|  | ||||
|   function recipeTinyImage(recipeId: string, version: string | unknown = "", key: string | number = 1) { | ||||
|     return `${fullBase}/media/recipes/${recipeId}/images/tiny-original.webp?rnd=${key}&version=${UnknownToString( | ||||
|     return `${prefix}/media/recipes/${recipeId}/images/tiny-original.webp?rnd=${key}&version=${UnknownToString( | ||||
|       version, | ||||
|     )}`; | ||||
|   } | ||||
|  | ||||
|   function recipeTimelineEventImage(recipeId: string, timelineEventId: string) { | ||||
|     return `${fullBase}/media/recipes/${recipeId}/images/timeline/${timelineEventId}/original.webp`; | ||||
|     return `${prefix}/media/recipes/${recipeId}/images/timeline/${timelineEventId}/original.webp`; | ||||
|   } | ||||
|  | ||||
|   function recipeTimelineEventSmallImage(recipeId: string, timelineEventId: string) { | ||||
|     return `${fullBase}/media/recipes/${recipeId}/images/timeline/${timelineEventId}/min-original.webp`; | ||||
|     return `${prefix}/media/recipes/${recipeId}/images/timeline/${timelineEventId}/min-original.webp`; | ||||
|   } | ||||
|  | ||||
|   function recipeTimelineEventTinyImage(recipeId: string, timelineEventId: string) { | ||||
|     return `${fullBase}/media/recipes/${recipeId}/images/timeline/${timelineEventId}/tiny-original.webp`; | ||||
|     return `${prefix}/media/recipes/${recipeId}/images/timeline/${timelineEventId}/tiny-original.webp`; | ||||
|   } | ||||
|  | ||||
|   function recipeAssetPath(recipeId: string, assetName: string) { | ||||
|     return `${fullBase}/media/recipes/${recipeId}/assets/${assetName}`; | ||||
|     return `${prefix}/media/recipes/${recipeId}/assets/${assetName}`; | ||||
|   } | ||||
|  | ||||
|   return { | ||||
|   | ||||
| @@ -85,7 +85,6 @@ class RepositoryGeneric[Schema: MealieModel, Model: SqlAlchemyBase]: | ||||
|  | ||||
|     def _filter_builder(self, **kwargs) -> dict[str, Any]: | ||||
|         dct = {} | ||||
|  | ||||
|         if self.group_id: | ||||
|             dct["group_id"] = self.group_id | ||||
|         if self.household_id: | ||||
| @@ -146,7 +145,11 @@ class RepositoryGeneric[Schema: MealieModel, Model: SqlAlchemyBase]: | ||||
|         return self.session.execute(self._query().filter_by(**fltr)).unique().scalars().one() | ||||
|  | ||||
|     def get_one( | ||||
|         self, value: str | int | UUID4, key: str | None = None, any_case=False, override_schema=None | ||||
|         self, | ||||
|         value: str | int | UUID4, | ||||
|         key: str | None = None, | ||||
|         any_case=False, | ||||
|         override_schema=None, | ||||
|     ) -> Schema | None: | ||||
|         key = key or self.primary_key | ||||
|         eff_schema = override_schema or self.schema | ||||
|   | ||||
| @@ -523,12 +523,12 @@ class RecipeController(BaseRecipeController): | ||||
|  | ||||
|     @router.put("/{slug}/image", response_model=UpdateImageResponse, tags=["Recipe: Images and Assets"]) | ||||
|     def update_recipe_image(self, slug: str, image: bytes = File(...), extension: str = Form(...)): | ||||
|         recipe = self.mixins.get_one(slug) | ||||
|         data_service = RecipeDataService(recipe.id) | ||||
|         data_service.write_image(image, extension) | ||||
|  | ||||
|         new_version = self.recipes.update_image(slug, extension) | ||||
|         return UpdateImageResponse(image=new_version) | ||||
|         try: | ||||
|             new_version = self.service.update_recipe_image(slug, image, extension) | ||||
|             return UpdateImageResponse(image=new_version) | ||||
|         except Exception as e: | ||||
|             self.handle_exceptions(e) | ||||
|             return None | ||||
|  | ||||
|     @router.post("/{slug}/assets", response_model=RecipeAsset, tags=["Recipe: Images and Assets"]) | ||||
|     def upload_recipe_asset( | ||||
| @@ -550,7 +550,7 @@ class RecipeController(BaseRecipeController): | ||||
|         file_name = f"{file_slug}.{extension}" | ||||
|         asset_in = RecipeAsset(name=name, icon=icon, file_name=file_name) | ||||
|  | ||||
|         recipe = self.mixins.get_one(slug) | ||||
|         recipe = self.service.get_one(slug) | ||||
|  | ||||
|         dest = recipe.asset_dir / file_name | ||||
|  | ||||
| @@ -567,9 +567,9 @@ class RecipeController(BaseRecipeController): | ||||
|         if not dest.is_file(): | ||||
|             raise HTTPException(status.HTTP_500_INTERNAL_SERVER_ERROR) | ||||
|  | ||||
|         recipe = self.mixins.get_one(slug) | ||||
|         recipe.assets.append(asset_in) | ||||
|         if recipe.assets is not None: | ||||
|             recipe.assets.append(asset_in) | ||||
|  | ||||
|         self.mixins.update_one(recipe, slug) | ||||
|         self.service.update_one(slug, recipe) | ||||
|  | ||||
|         return asset_in | ||||
|   | ||||
| @@ -408,6 +408,16 @@ class RecipeService(RecipeServiceBase): | ||||
|         self.check_assets(new_data, recipe.slug) | ||||
|         return new_data | ||||
|  | ||||
|     def update_recipe_image(self, slug: str, image: bytes, extension: str): | ||||
|         recipe = self.get_one(slug) | ||||
|         if not self.can_update(recipe): | ||||
|             raise exceptions.PermissionDenied("You do not have permission to edit this recipe.") | ||||
|  | ||||
|         data_service = RecipeDataService(recipe.id) | ||||
|         data_service.write_image(image, extension) | ||||
|  | ||||
|         return self.group_recipes.update_image(slug, extension) | ||||
|  | ||||
|     def patch_one(self, slug_or_id: str | UUID, patch_data: Recipe) -> Recipe: | ||||
|         recipe: Recipe = self._pre_update_check(slug_or_id, patch_data) | ||||
|  | ||||
|   | ||||
| @@ -7,6 +7,7 @@ from fastapi.testclient import TestClient | ||||
| from mealie.schema.cookbook.cookbook import SaveCookBook | ||||
| from mealie.schema.recipe.recipe import Recipe | ||||
| from mealie.schema.recipe.recipe_category import TagSave | ||||
| from tests import data | ||||
| from tests.utils import api_routes | ||||
| from tests.utils.factories import random_string | ||||
| from tests.utils.fixture_schemas import TestUser | ||||
| @@ -371,3 +372,53 @@ def test_cookbooks_from_other_households(api_client: TestClient, unique_user: Te | ||||
|  | ||||
|     response = api_client.get(api_routes.recipes, params={"cookbook": h2_cookbook.slug}, headers=unique_user.token) | ||||
|     assert response.status_code == 200 | ||||
|  | ||||
|  | ||||
| @pytest.mark.parametrize("is_private_household", [True, False]) | ||||
| @pytest.mark.parametrize("household_lock_recipe_edits", [True, False]) | ||||
| def test_update_recipe_image_from_other_households( | ||||
|     api_client: TestClient, | ||||
|     unique_user: TestUser, | ||||
|     h2_user: TestUser, | ||||
|     is_private_household: bool, | ||||
|     household_lock_recipe_edits: bool, | ||||
| ): | ||||
|     household = unique_user.repos.households.get_one(h2_user.household_id) | ||||
|     assert household and household.preferences | ||||
|     household.preferences.private_household = is_private_household | ||||
|     household.preferences.lock_recipe_edits_from_other_households = household_lock_recipe_edits | ||||
|     unique_user.repos.household_preferences.update(household.id, household.preferences) | ||||
|  | ||||
|     response = api_client.post(api_routes.recipes, json={"name": random_string()}, headers=h2_user.token) | ||||
|     assert response.status_code == 201 | ||||
|     h2_recipe = h2_user.repos.recipes.get_one(response.json()) | ||||
|     assert h2_recipe and h2_recipe.id | ||||
|     h2_recipe_id = str(h2_recipe.id) | ||||
|  | ||||
|     response = api_client.get(api_routes.recipes_slug(h2_recipe_id), headers=unique_user.token) | ||||
|     assert response.status_code == 200 | ||||
|     recipe_json = response.json() | ||||
|     assert recipe_json["id"] == h2_recipe_id | ||||
|     image_version = response.json()["image"] | ||||
|  | ||||
|     data_payload = {"extension": "jpg"} | ||||
|     file_payload = {"image": data.images_test_image_1.read_bytes()} | ||||
|  | ||||
|     response = api_client.put( | ||||
|         api_routes.recipes_slug_image(recipe_json["slug"]), | ||||
|         data=data_payload, | ||||
|         files=file_payload, | ||||
|         headers=unique_user.token, | ||||
|     ) | ||||
|  | ||||
|     if household_lock_recipe_edits: | ||||
|         assert response.status_code == 403 | ||||
|         response = api_client.get(api_routes.recipes_slug(h2_recipe_id), headers=unique_user.token) | ||||
|         recipe_respons = response.json() | ||||
|         assert recipe_respons["image"] == image_version | ||||
|  | ||||
|     else: | ||||
|         assert response.status_code == 200 | ||||
|         response = api_client.get(api_routes.recipes_slug(h2_recipe_id), headers=unique_user.token) | ||||
|         recipe_respons = response.json() | ||||
|         assert recipe_respons["image"] is not None | ||||
|   | ||||
| @@ -6,6 +6,7 @@ from fastapi.testclient import TestClient | ||||
| from mealie.repos.repository_factory import AllRepositories | ||||
| from mealie.schema.recipe.recipe import Recipe | ||||
| from mealie.schema.recipe.recipe_settings import RecipeSettings | ||||
| from tests import data | ||||
| from tests.utils import api_routes | ||||
| from tests.utils.factories import random_string | ||||
| from tests.utils.fixture_schemas import TestUser | ||||
| @@ -180,3 +181,33 @@ def test_admin_can_delete_locked_recipe_owned_by_another_user( | ||||
|  | ||||
|     response = api_client.delete(api_routes.recipes_slug(slug), headers=admin_user.token) | ||||
|     assert response.status_code == 200 | ||||
|  | ||||
|  | ||||
| def test_user_can_update_recipe_image(api_client: TestClient, unique_user: TestUser): | ||||
|     data_payload = {"extension": "jpg"} | ||||
|     file_payload = {"image": data.images_test_image_1.read_bytes()} | ||||
|  | ||||
|     household = unique_user.repos.households.get_one(unique_user.household_id) | ||||
|     assert household and household.preferences | ||||
|     household.preferences.private_household = True | ||||
|     household.preferences.lock_recipe_edits_from_other_households = True | ||||
|     unique_user.repos.household_preferences.update(household.id, household.preferences) | ||||
|  | ||||
|     response = api_client.post(api_routes.recipes, json={"name": random_string()}, headers=unique_user.token) | ||||
|     assert response.status_code == 201 | ||||
|     recipe_json = unique_user.repos.recipes.get_one(response.json()) | ||||
|     assert recipe_json and recipe_json.id | ||||
|     assert recipe_json.image is None | ||||
|     recipe_id = str(recipe_json.id) | ||||
|  | ||||
|     response = api_client.put( | ||||
|         api_routes.recipes_slug_image(recipe_json.slug), | ||||
|         data=data_payload, | ||||
|         files=file_payload, | ||||
|         headers=unique_user.token, | ||||
|     ) | ||||
|     assert response.status_code == 200 | ||||
|  | ||||
|     response = api_client.get(api_routes.recipes_slug(recipe_id), headers=unique_user.token) | ||||
|     recipe_respons = response.json() | ||||
|     assert recipe_respons["image"] is not None | ||||
|   | ||||
		Reference in New Issue
	
	Block a user