mirror of
https://github.com/mealie-recipes/mealie.git
synced 2026-04-05 12:35:35 -04:00
fix: preserve stored recipe slugs during hydration (#7294)
Co-authored-by: Michael Genson <71845777+michael-genson@users.noreply.github.com> Co-authored-by: Michael Genson <genson.michael@gmail.com>
This commit is contained in:
@@ -203,13 +203,19 @@ class RepositoryRecipes(HouseholdRepositoryGeneric[Recipe, RecipeModel]):
|
|||||||
|
|
||||||
def update(self, match_value: str | int | UUID4, new_data: dict | Recipe) -> Recipe:
|
def update(self, match_value: str | int | UUID4, new_data: dict | Recipe) -> Recipe:
|
||||||
new_data = new_data if isinstance(new_data, dict) else new_data.model_dump()
|
new_data = new_data if isinstance(new_data, dict) else new_data.model_dump()
|
||||||
|
entry = self._query_one(match_value=match_value)
|
||||||
|
|
||||||
|
if new_name := new_data.get("name"):
|
||||||
|
new_data["slug"] = entry.slug if new_name == entry.name else create_recipe_slug(new_name)
|
||||||
|
|
||||||
# Handle explicit group_id injection for related items that require it
|
# Handle explicit group_id injection for related items that require it
|
||||||
for organizer_field in ["tags", "recipe_category", "tools"]:
|
for organizer_field in ["tags", "recipe_category", "tools"]:
|
||||||
for organizer in new_data.get(organizer_field, []):
|
for organizer in new_data.get(organizer_field, []):
|
||||||
organizer["group_id"] = self.group_id
|
organizer["group_id"] = self.group_id
|
||||||
|
|
||||||
return super().update(match_value, new_data)
|
entry.update(session=self.session, **new_data)
|
||||||
|
self.session.commit()
|
||||||
|
return self.schema.model_validate(entry)
|
||||||
|
|
||||||
def page_all( # type: ignore
|
def page_all( # type: ignore
|
||||||
self,
|
self,
|
||||||
|
|||||||
@@ -240,7 +240,7 @@ class Recipe(RecipeSummary):
|
|||||||
|
|
||||||
@field_validator("slug", mode="before")
|
@field_validator("slug", mode="before")
|
||||||
def validate_slug(slug: str, info: ValidationInfo):
|
def validate_slug(slug: str, info: ValidationInfo):
|
||||||
if not info.data.get("name"):
|
if slug or not info.data.get("name"):
|
||||||
return slug
|
return slug
|
||||||
|
|
||||||
return create_recipe_slug(info.data["name"])
|
return create_recipe_slug(info.data["name"])
|
||||||
|
|||||||
@@ -98,8 +98,8 @@ def test_get_all_public_recipes(
|
|||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
"query_filter, recipe_data, should_fetch",
|
"query_filter, recipe_data, should_fetch",
|
||||||
[
|
[
|
||||||
('slug = "mypublicslug"', {"slug": "mypublicslug"}, True),
|
('slug = "mypublicslug"', {"slug": "mypublicslug", "name": "mypublicslug"}, True),
|
||||||
('slug = "mypublicslug"', {"slug": "notmypublicslug"}, False),
|
('slug = "mypublicslug"', {"slug": "notmypublicslug", "name": "notmypublicslug"}, False),
|
||||||
("settings.public = FALSE", {}, False),
|
("settings.public = FALSE", {}, False),
|
||||||
("settings.public <> TRUE", {}, False),
|
("settings.public <> TRUE", {}, False),
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -1357,6 +1357,60 @@ def test_recipe_crud_404(api_client: TestClient, unique_user: TestUser):
|
|||||||
assert response.status_code == 404
|
assert response.status_code == 404
|
||||||
|
|
||||||
|
|
||||||
|
def test_patch_recipe_after_name_changes_without_slug_update(api_client: TestClient, unique_user: TestUser):
|
||||||
|
original_name = "Nourish Bowls (Zuppa Copycat)"
|
||||||
|
translated_name = "Bols nourrissants (copie de Zuppa)"
|
||||||
|
|
||||||
|
response = api_client.post(api_routes.recipes, json={"name": original_name}, headers=unique_user.token)
|
||||||
|
assert response.status_code == 201
|
||||||
|
original_slug = response.json()
|
||||||
|
|
||||||
|
session = unique_user.repos.session
|
||||||
|
recipe = session.query(RecipeModel).filter(RecipeModel.slug == original_slug).one()
|
||||||
|
recipe.name = translated_name
|
||||||
|
session.commit()
|
||||||
|
|
||||||
|
response = api_client.get(api_routes.recipes_slug(original_slug), headers=unique_user.token)
|
||||||
|
assert response.status_code == 200
|
||||||
|
|
||||||
|
recipe_payload = response.json()
|
||||||
|
assert recipe_payload["name"] == translated_name
|
||||||
|
assert recipe_payload["slug"] == original_slug
|
||||||
|
|
||||||
|
response = api_client.patch(
|
||||||
|
api_routes.recipes_slug(original_slug),
|
||||||
|
json={"description": "Translated without changing the stored slug"},
|
||||||
|
headers=unique_user.token,
|
||||||
|
)
|
||||||
|
assert response.status_code == 200
|
||||||
|
|
||||||
|
patched_recipe = response.json()
|
||||||
|
assert patched_recipe["slug"] == original_slug
|
||||||
|
assert patched_recipe["description"] == "Translated without changing the stored slug"
|
||||||
|
|
||||||
|
|
||||||
|
def test_put_recipe_name_change_updates_slug(api_client: TestClient, unique_user: TestUser):
|
||||||
|
original_name = "Original Recipe Name"
|
||||||
|
renamed_name = "Renamed Recipe Name"
|
||||||
|
|
||||||
|
response = api_client.post(api_routes.recipes, json={"name": original_name}, headers=unique_user.token)
|
||||||
|
assert response.status_code == 201
|
||||||
|
original_slug = response.json()
|
||||||
|
|
||||||
|
response = api_client.get(api_routes.recipes_slug(original_slug), headers=unique_user.token)
|
||||||
|
assert response.status_code == 200
|
||||||
|
|
||||||
|
recipe_payload = response.json()
|
||||||
|
recipe_payload["name"] = renamed_name
|
||||||
|
|
||||||
|
response = api_client.put(api_routes.recipes_slug(original_slug), json=recipe_payload, headers=unique_user.token)
|
||||||
|
assert response.status_code == 200
|
||||||
|
|
||||||
|
renamed_recipe = response.json()
|
||||||
|
assert renamed_recipe["slug"] == slugify(renamed_name)
|
||||||
|
assert renamed_recipe["name"] == renamed_name
|
||||||
|
|
||||||
|
|
||||||
def test_create_recipe_same_name(api_client: TestClient, unique_user: TestUser):
|
def test_create_recipe_same_name(api_client: TestClient, unique_user: TestUser):
|
||||||
slug = random_string(10)
|
slug = random_string(10)
|
||||||
|
|
||||||
|
|||||||
@@ -61,3 +61,16 @@ def test_recipe_string_sanitation(field: str, val: Any, expected: Any):
|
|||||||
)
|
)
|
||||||
|
|
||||||
assert getattr(recipe, field) == expected
|
assert getattr(recipe, field) == expected
|
||||||
|
|
||||||
|
|
||||||
|
def test_recipe_preserves_existing_slug():
|
||||||
|
recipe = RecipeSummary(
|
||||||
|
id=uuid4(),
|
||||||
|
user_id=uuid4(),
|
||||||
|
household_id=uuid4(),
|
||||||
|
group_id=uuid4(),
|
||||||
|
name="Bols nourrissants (copie de Zuppa)",
|
||||||
|
slug="nourish-bowls-zuppa-copycat",
|
||||||
|
)
|
||||||
|
|
||||||
|
assert recipe.slug == "nourish-bowls-zuppa-copycat"
|
||||||
|
|||||||
Reference in New Issue
Block a user