mirror of
https://github.com/hay-kot/mealie.git
synced 2025-08-21 22:13:31 -07:00
fix: error when trying to change recipe image (#5771)
Some checks are pending
CodeQL / Analyze (push) Waiting to run
Docker Nightly Production / Backend Server Tests (push) Waiting to run
Docker Nightly Production / Frontend Tests (push) Waiting to run
Docker Nightly Production / Build Package (push) Waiting to run
Docker Nightly Production / Build Tagged Release (push) Blocked by required conditions
Docker Nightly Production / Notify Discord (push) Blocked by required conditions
Build Containers / publish (push) Waiting to run
Release Drafter / ✏️ Draft release (push) Waiting to run
Some checks are pending
CodeQL / Analyze (push) Waiting to run
Docker Nightly Production / Backend Server Tests (push) Waiting to run
Docker Nightly Production / Frontend Tests (push) Waiting to run
Docker Nightly Production / Build Package (push) Waiting to run
Docker Nightly Production / Build Tagged Release (push) Blocked by required conditions
Docker Nightly Production / Notify Discord (push) Blocked by required conditions
Build Containers / publish (push) Waiting to run
Release Drafter / ✏️ Draft release (push) Waiting to run
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:
parent
6cbc308d83
commit
c41a4a52ed
7 changed files with 115 additions and 26 deletions
|
@ -390,8 +390,6 @@ const props = defineProps({
|
||||||
|
|
||||||
const emit = defineEmits(["click-instruction-field", "update:assets"]);
|
const emit = defineEmits(["click-instruction-field", "update:assets"]);
|
||||||
|
|
||||||
const BASE_URL = useRequestURL().origin;
|
|
||||||
|
|
||||||
const { isCookMode, toggleCookMode, isEditForm } = usePageState(props.recipe.slug);
|
const { isCookMode, toggleCookMode, isEditForm } = usePageState(props.recipe.slug);
|
||||||
|
|
||||||
const dialog = ref(false);
|
const dialog = ref(false);
|
||||||
|
@ -695,7 +693,7 @@ async function handleImageDrop(index: number, files: File[]) {
|
||||||
}
|
}
|
||||||
|
|
||||||
emit("update:assets", [...assets.value, data]);
|
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%"/>`;
|
const text = `<img src="${assetUrl}" height="100%" width="100%"/>`;
|
||||||
instructionList.value[index].text += text;
|
instructionList.value[index].text += text;
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,43 +4,39 @@ function UnknownToString(ukn: string | unknown) {
|
||||||
|
|
||||||
export const useStaticRoutes = () => {
|
export const useStaticRoutes = () => {
|
||||||
const { $config } = useNuxtApp();
|
const { $config } = useNuxtApp();
|
||||||
const serverBase = useRequestURL().origin;
|
|
||||||
|
|
||||||
const prefix = `${$config.public.SUB_PATH}/api`.replace("//", "/");
|
const prefix = `${$config.public.SUB_PATH}/api`.replace("//", "/");
|
||||||
|
|
||||||
const fullBase = serverBase + prefix;
|
|
||||||
|
|
||||||
// Methods to Generate reference urls for assets/images *
|
// Methods to Generate reference urls for assets/images *
|
||||||
function recipeImage(recipeId: string, version: string | unknown = "", key: string | number = 1) {
|
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) {
|
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,
|
version,
|
||||||
)}`;
|
)}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function recipeTinyImage(recipeId: string, version: string | unknown = "", key: string | number = 1) {
|
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,
|
version,
|
||||||
)}`;
|
)}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function recipeTimelineEventImage(recipeId: string, timelineEventId: string) {
|
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) {
|
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) {
|
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) {
|
function recipeAssetPath(recipeId: string, assetName: string) {
|
||||||
return `${fullBase}/media/recipes/${recipeId}/assets/${assetName}`;
|
return `${prefix}/media/recipes/${recipeId}/assets/${assetName}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
|
@ -85,7 +85,6 @@ class RepositoryGeneric[Schema: MealieModel, Model: SqlAlchemyBase]:
|
||||||
|
|
||||||
def _filter_builder(self, **kwargs) -> dict[str, Any]:
|
def _filter_builder(self, **kwargs) -> dict[str, Any]:
|
||||||
dct = {}
|
dct = {}
|
||||||
|
|
||||||
if self.group_id:
|
if self.group_id:
|
||||||
dct["group_id"] = self.group_id
|
dct["group_id"] = self.group_id
|
||||||
if self.household_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()
|
return self.session.execute(self._query().filter_by(**fltr)).unique().scalars().one()
|
||||||
|
|
||||||
def get_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:
|
) -> Schema | None:
|
||||||
key = key or self.primary_key
|
key = key or self.primary_key
|
||||||
eff_schema = override_schema or self.schema
|
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"])
|
@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(...)):
|
def update_recipe_image(self, slug: str, image: bytes = File(...), extension: str = Form(...)):
|
||||||
recipe = self.mixins.get_one(slug)
|
try:
|
||||||
data_service = RecipeDataService(recipe.id)
|
new_version = self.service.update_recipe_image(slug, image, extension)
|
||||||
data_service.write_image(image, extension)
|
return UpdateImageResponse(image=new_version)
|
||||||
|
except Exception as e:
|
||||||
new_version = self.recipes.update_image(slug, extension)
|
self.handle_exceptions(e)
|
||||||
return UpdateImageResponse(image=new_version)
|
return None
|
||||||
|
|
||||||
@router.post("/{slug}/assets", response_model=RecipeAsset, tags=["Recipe: Images and Assets"])
|
@router.post("/{slug}/assets", response_model=RecipeAsset, tags=["Recipe: Images and Assets"])
|
||||||
def upload_recipe_asset(
|
def upload_recipe_asset(
|
||||||
|
@ -550,7 +550,7 @@ class RecipeController(BaseRecipeController):
|
||||||
file_name = f"{file_slug}.{extension}"
|
file_name = f"{file_slug}.{extension}"
|
||||||
asset_in = RecipeAsset(name=name, icon=icon, file_name=file_name)
|
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
|
dest = recipe.asset_dir / file_name
|
||||||
|
|
||||||
|
@ -567,9 +567,9 @@ class RecipeController(BaseRecipeController):
|
||||||
if not dest.is_file():
|
if not dest.is_file():
|
||||||
raise HTTPException(status.HTTP_500_INTERNAL_SERVER_ERROR)
|
raise HTTPException(status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||||
|
|
||||||
recipe = self.mixins.get_one(slug)
|
if recipe.assets is not None:
|
||||||
recipe.assets.append(asset_in)
|
recipe.assets.append(asset_in)
|
||||||
|
|
||||||
self.mixins.update_one(recipe, slug)
|
self.service.update_one(slug, recipe)
|
||||||
|
|
||||||
return asset_in
|
return asset_in
|
||||||
|
|
|
@ -408,6 +408,16 @@ class RecipeService(RecipeServiceBase):
|
||||||
self.check_assets(new_data, recipe.slug)
|
self.check_assets(new_data, recipe.slug)
|
||||||
return new_data
|
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:
|
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)
|
||||||
|
|
||||||
|
|
|
@ -7,6 +7,7 @@ from fastapi.testclient import TestClient
|
||||||
from mealie.schema.cookbook.cookbook import SaveCookBook
|
from mealie.schema.cookbook.cookbook import SaveCookBook
|
||||||
from mealie.schema.recipe.recipe import Recipe
|
from mealie.schema.recipe.recipe import Recipe
|
||||||
from mealie.schema.recipe.recipe_category import TagSave
|
from mealie.schema.recipe.recipe_category import TagSave
|
||||||
|
from tests import data
|
||||||
from tests.utils import api_routes
|
from tests.utils import api_routes
|
||||||
from tests.utils.factories import random_string
|
from tests.utils.factories import random_string
|
||||||
from tests.utils.fixture_schemas import TestUser
|
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)
|
response = api_client.get(api_routes.recipes, params={"cookbook": h2_cookbook.slug}, headers=unique_user.token)
|
||||||
assert response.status_code == 200
|
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.repos.repository_factory import AllRepositories
|
||||||
from mealie.schema.recipe.recipe import Recipe
|
from mealie.schema.recipe.recipe import Recipe
|
||||||
from mealie.schema.recipe.recipe_settings import RecipeSettings
|
from mealie.schema.recipe.recipe_settings import RecipeSettings
|
||||||
|
from tests import data
|
||||||
from tests.utils import api_routes
|
from tests.utils import api_routes
|
||||||
from tests.utils.factories import random_string
|
from tests.utils.factories import random_string
|
||||||
from tests.utils.fixture_schemas import TestUser
|
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)
|
response = api_client.delete(api_routes.recipes_slug(slug), headers=admin_user.token)
|
||||||
assert response.status_code == 200
|
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
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue