mirror of
https://github.com/hay-kot/mealie.git
synced 2025-07-06 04:52:25 -07:00
fix: truncate slugs when too long (#5633)
This commit is contained in:
parent
e794c6b525
commit
c9e22892a6
4 changed files with 69 additions and 8 deletions
|
@ -7,7 +7,6 @@ from uuid import UUID
|
||||||
import sqlalchemy as sa
|
import sqlalchemy as sa
|
||||||
from fastapi import HTTPException
|
from fastapi import HTTPException
|
||||||
from pydantic import UUID4
|
from pydantic import UUID4
|
||||||
from slugify import slugify
|
|
||||||
from sqlalchemy import orm
|
from sqlalchemy import orm
|
||||||
from sqlalchemy.exc import IntegrityError
|
from sqlalchemy.exc import IntegrityError
|
||||||
|
|
||||||
|
@ -22,7 +21,7 @@ from mealie.db.models.users.user_to_recipe import UserToRecipe
|
||||||
from mealie.db.models.users.users import User
|
from mealie.db.models.users.users import User
|
||||||
from mealie.schema.cookbook.cookbook import ReadCookBook
|
from mealie.schema.cookbook.cookbook import ReadCookBook
|
||||||
from mealie.schema.recipe import Recipe
|
from mealie.schema.recipe import Recipe
|
||||||
from mealie.schema.recipe.recipe import RecipeCategory, RecipePagination, RecipeSummary
|
from mealie.schema.recipe.recipe import RecipeCategory, RecipePagination, RecipeSummary, create_recipe_slug
|
||||||
from mealie.schema.recipe.recipe_ingredient import IngredientFood
|
from mealie.schema.recipe.recipe_ingredient import IngredientFood
|
||||||
from mealie.schema.recipe.recipe_suggestion import RecipeSuggestionQuery, RecipeSuggestionResponseItem
|
from mealie.schema.recipe.recipe_suggestion import RecipeSuggestionQuery, RecipeSuggestionResponseItem
|
||||||
from mealie.schema.recipe.recipe_tool import RecipeToolOut
|
from mealie.schema.recipe.recipe_tool import RecipeToolOut
|
||||||
|
@ -98,7 +97,7 @@ class RepositoryRecipes(HouseholdRepositoryGeneric[Recipe, RecipeModel]):
|
||||||
except IntegrityError:
|
except IntegrityError:
|
||||||
self.session.rollback()
|
self.session.rollback()
|
||||||
document.name = f"{original_name} ({i})"
|
document.name = f"{original_name} ({i})"
|
||||||
document.slug = slugify(document.name)
|
document.slug = create_recipe_slug(document.name)
|
||||||
|
|
||||||
if i >= max_retries:
|
if i >= max_retries:
|
||||||
raise
|
raise
|
||||||
|
|
|
@ -36,6 +36,22 @@ from .recipe_step import RecipeStep
|
||||||
app_dirs = get_app_dirs()
|
app_dirs = get_app_dirs()
|
||||||
|
|
||||||
|
|
||||||
|
def create_recipe_slug(name: str, max_length: int = 250) -> str:
|
||||||
|
"""Generate a slug from a recipe name, truncating to a reasonable length.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
name: The recipe name to create a slug from
|
||||||
|
max_length: Maximum length for the slug (default: 250)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A truncated slug string
|
||||||
|
"""
|
||||||
|
generated_slug = slugify(name)
|
||||||
|
if len(generated_slug) > max_length:
|
||||||
|
generated_slug = generated_slug[:max_length]
|
||||||
|
return generated_slug
|
||||||
|
|
||||||
|
|
||||||
class RecipeTag(MealieModel):
|
class RecipeTag(MealieModel):
|
||||||
id: UUID4 | None = None
|
id: UUID4 | None = None
|
||||||
group_id: UUID4 | None = None
|
group_id: UUID4 | None = None
|
||||||
|
@ -229,7 +245,7 @@ class Recipe(RecipeSummary):
|
||||||
if not info.data.get("name"):
|
if not info.data.get("name"):
|
||||||
return slug
|
return slug
|
||||||
|
|
||||||
return slugify(info.data["name"])
|
return create_recipe_slug(info.data["name"])
|
||||||
|
|
||||||
@field_validator("recipe_ingredient", mode="before")
|
@field_validator("recipe_ingredient", mode="before")
|
||||||
def validate_ingredients(recipe_ingredient):
|
def validate_ingredients(recipe_ingredient):
|
||||||
|
|
|
@ -9,7 +9,6 @@ from uuid import UUID, uuid4
|
||||||
from zipfile import ZipFile
|
from zipfile import ZipFile
|
||||||
|
|
||||||
from fastapi import UploadFile
|
from fastapi import UploadFile
|
||||||
from slugify import slugify
|
|
||||||
|
|
||||||
from mealie.core import exceptions
|
from mealie.core import exceptions
|
||||||
from mealie.core.config import get_app_settings
|
from mealie.core.config import get_app_settings
|
||||||
|
@ -21,7 +20,7 @@ from mealie.repos.repository_factory import AllRepositories
|
||||||
from mealie.repos.repository_generic import RepositoryGeneric
|
from mealie.repos.repository_generic import RepositoryGeneric
|
||||||
from mealie.schema.household.household import HouseholdInDB, HouseholdRecipeUpdate
|
from mealie.schema.household.household import HouseholdInDB, HouseholdRecipeUpdate
|
||||||
from mealie.schema.openai.recipe import OpenAIRecipe
|
from mealie.schema.openai.recipe import OpenAIRecipe
|
||||||
from mealie.schema.recipe.recipe import CreateRecipe, Recipe
|
from mealie.schema.recipe.recipe import CreateRecipe, Recipe, create_recipe_slug
|
||||||
from mealie.schema.recipe.recipe_ingredient import RecipeIngredient
|
from mealie.schema.recipe.recipe_ingredient import RecipeIngredient
|
||||||
from mealie.schema.recipe.recipe_notes import RecipeNote
|
from mealie.schema.recipe.recipe_notes import RecipeNote
|
||||||
from mealie.schema.recipe.recipe_settings import RecipeSettings
|
from mealie.schema.recipe.recipe_settings import RecipeSettings
|
||||||
|
@ -332,7 +331,7 @@ class RecipeService(RecipeServiceBase):
|
||||||
|
|
||||||
new_name = dup_data.name if dup_data.name else old_recipe.name or ""
|
new_name = dup_data.name if dup_data.name else old_recipe.name or ""
|
||||||
new_recipe.id = uuid4()
|
new_recipe.id = uuid4()
|
||||||
new_recipe.slug = slugify(new_name)
|
new_recipe.slug = create_recipe_slug(new_name)
|
||||||
new_recipe.image = cache.cache_key.new_key() if old_recipe.image else None
|
new_recipe.image = cache.cache_key.new_key() if old_recipe.image else None
|
||||||
new_recipe.recipe_instructions = (
|
new_recipe.recipe_instructions = (
|
||||||
None
|
None
|
||||||
|
@ -447,7 +446,7 @@ class OpenAIRecipeService(RecipeServiceBase):
|
||||||
group_id=self.user.group_id,
|
group_id=self.user.group_id,
|
||||||
household_id=self.household.id,
|
household_id=self.household.id,
|
||||||
name=openai_recipe.name,
|
name=openai_recipe.name,
|
||||||
slug=slugify(openai_recipe.name),
|
slug=create_recipe_slug(openai_recipe.name),
|
||||||
description=openai_recipe.description,
|
description=openai_recipe.description,
|
||||||
recipe_yield=openai_recipe.recipe_yield,
|
recipe_yield=openai_recipe.recipe_yield,
|
||||||
total_time=openai_recipe.total_time,
|
total_time=openai_recipe.total_time,
|
||||||
|
|
|
@ -900,3 +900,50 @@ def test_get_cookbook_recipes(api_client: TestClient, unique_user: utils.TestUse
|
||||||
assert recipe.id in fetched_recipe_ids
|
assert recipe.id in fetched_recipe_ids
|
||||||
for recipe in other_recipes:
|
for recipe in other_recipes:
|
||||||
assert recipe.id not in fetched_recipe_ids
|
assert recipe.id not in fetched_recipe_ids
|
||||||
|
|
||||||
|
|
||||||
|
def test_create_recipe_with_extremely_long_slug(api_client: TestClient, unique_user: TestUser):
|
||||||
|
"""Test creating a recipe with an extremely long name that would generate a very long slug.
|
||||||
|
This reproduces the issue where long slugs cause 500 internal server errors.
|
||||||
|
"""
|
||||||
|
# Create a recipe name that's extremely long like the one in the GitHub issue
|
||||||
|
long_recipe_name = "giallozafferano-on-instagram-il-piatto-vincente-di-simone-barlaam-medaglia-d-oro-e-d-argento-a-parigi-2024-paccheri-tricolore-se-ve-li-siete-persi-dovete-assolutamente-rimediare-lulugargari-ingredienti-paccheri-320-gr-spinacini-500-gr-nocciole-50-gr-ricotta-350-gr-olio-evo-q-b-limone-non-trattato-con-buccia-edibile-q-b-menta-q-b-peperoncino-fresco-q-b-10-pomodorini-ciliegino-preparazione-saltiamo-gli-spinaci-in-padella-lasciamo-raffreddare-e-frulliamo-insieme-a-ricotta-olio-sale-pepe-e-peperoncino-fresco-cuociamo-la-pasta-al-dente-e-mantechiamo-fuori-dal-fuoco-con-la-crema-tostiamo-a-parte-noci-o-nocciole-e-frulliamo-con-scorza-di-limone-impiattiamo-i-paccheri-con-qualche-spinacino-fresco-ciuffetti-di-ricotta-pomodorini-tagliati-in-4-e-la-polvere-di-nocciole-e-limone-buon-appetito-dmtc-pr-finp-nuotoparalimpico-giallozafferano-ricette-olimpiadi-paralimpiadi-atleti-simonebarlaam-cucina-paccheri-pasta-spinaci" # noqa: E501
|
||||||
|
|
||||||
|
# Create the recipe
|
||||||
|
response = api_client.post(api_routes.recipes, json={"name": long_recipe_name}, headers=unique_user.token)
|
||||||
|
assert response.status_code == 201
|
||||||
|
created_slug = json.loads(response.text)
|
||||||
|
|
||||||
|
assert created_slug is not None
|
||||||
|
assert len(created_slug) > 0
|
||||||
|
|
||||||
|
new_name = "Pasta"
|
||||||
|
response = api_client.patch(
|
||||||
|
api_routes.recipes_slug(created_slug), json={"name": new_name}, headers=unique_user.token
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status_code == 200
|
||||||
|
|
||||||
|
updated_recipe = json.loads(response.text)
|
||||||
|
assert updated_recipe["name"] == new_name
|
||||||
|
assert updated_recipe["slug"] == slugify(new_name)
|
||||||
|
|
||||||
|
|
||||||
|
def test_create_recipe_slug_length_validation(api_client: TestClient, unique_user: TestUser):
|
||||||
|
"""Test that recipe slugs are properly truncated to a reasonable length."""
|
||||||
|
very_long_name = "A" * 500 # 500 character name
|
||||||
|
|
||||||
|
response = api_client.post(api_routes.recipes, json={"name": very_long_name}, headers=unique_user.token)
|
||||||
|
assert response.status_code == 201
|
||||||
|
|
||||||
|
created_slug = json.loads(response.text)
|
||||||
|
|
||||||
|
# The slug should be truncated to a reasonable length
|
||||||
|
# Using 250 characters as a reasonable limit, leaving room for collision suffixes
|
||||||
|
assert len(created_slug) <= 250
|
||||||
|
|
||||||
|
assert created_slug is not None
|
||||||
|
assert len(created_slug) > 0
|
||||||
|
|
||||||
|
response = api_client.get(api_routes.recipes_slug(created_slug), headers=unique_user.token)
|
||||||
|
assert response.status_code == 200
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue