From c9e22892a66b977efcf492a6e069145d0af3e8b0 Mon Sep 17 00:00:00 2001 From: Hayden <64056131+hay-kot@users.noreply.github.com> Date: Fri, 4 Jul 2025 15:43:53 -0500 Subject: [PATCH 1/7] fix: truncate slugs when too long (#5633) --- mealie/repos/repository_recipes.py | 5 +- mealie/schema/recipe/recipe.py | 18 ++++++- mealie/services/recipe/recipe_service.py | 7 ++- .../user_recipe_tests/test_recipe_crud.py | 47 +++++++++++++++++++ 4 files changed, 69 insertions(+), 8 deletions(-) diff --git a/mealie/repos/repository_recipes.py b/mealie/repos/repository_recipes.py index 64a153c83..7250e681b 100644 --- a/mealie/repos/repository_recipes.py +++ b/mealie/repos/repository_recipes.py @@ -7,7 +7,6 @@ from uuid import UUID import sqlalchemy as sa from fastapi import HTTPException from pydantic import UUID4 -from slugify import slugify from sqlalchemy import orm 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.schema.cookbook.cookbook import ReadCookBook 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_suggestion import RecipeSuggestionQuery, RecipeSuggestionResponseItem from mealie.schema.recipe.recipe_tool import RecipeToolOut @@ -98,7 +97,7 @@ class RepositoryRecipes(HouseholdRepositoryGeneric[Recipe, RecipeModel]): except IntegrityError: self.session.rollback() document.name = f"{original_name} ({i})" - document.slug = slugify(document.name) + document.slug = create_recipe_slug(document.name) if i >= max_retries: raise diff --git a/mealie/schema/recipe/recipe.py b/mealie/schema/recipe/recipe.py index 9cf1db0d5..b6c96e098 100644 --- a/mealie/schema/recipe/recipe.py +++ b/mealie/schema/recipe/recipe.py @@ -36,6 +36,22 @@ from .recipe_step import RecipeStep 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): id: UUID4 | None = None group_id: UUID4 | None = None @@ -229,7 +245,7 @@ class Recipe(RecipeSummary): if not info.data.get("name"): return slug - return slugify(info.data["name"]) + return create_recipe_slug(info.data["name"]) @field_validator("recipe_ingredient", mode="before") def validate_ingredients(recipe_ingredient): diff --git a/mealie/services/recipe/recipe_service.py b/mealie/services/recipe/recipe_service.py index 0e995bab7..7ca2bb150 100644 --- a/mealie/services/recipe/recipe_service.py +++ b/mealie/services/recipe/recipe_service.py @@ -9,7 +9,6 @@ from uuid import UUID, uuid4 from zipfile import ZipFile from fastapi import UploadFile -from slugify import slugify from mealie.core import exceptions 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.schema.household.household import HouseholdInDB, HouseholdRecipeUpdate 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_notes import RecipeNote 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_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.recipe_instructions = ( None @@ -447,7 +446,7 @@ class OpenAIRecipeService(RecipeServiceBase): group_id=self.user.group_id, household_id=self.household.id, name=openai_recipe.name, - slug=slugify(openai_recipe.name), + slug=create_recipe_slug(openai_recipe.name), description=openai_recipe.description, recipe_yield=openai_recipe.recipe_yield, total_time=openai_recipe.total_time, diff --git a/tests/integration_tests/user_recipe_tests/test_recipe_crud.py b/tests/integration_tests/user_recipe_tests/test_recipe_crud.py index f675d6da2..b87200fc6 100644 --- a/tests/integration_tests/user_recipe_tests/test_recipe_crud.py +++ b/tests/integration_tests/user_recipe_tests/test_recipe_crud.py @@ -900,3 +900,50 @@ def test_get_cookbook_recipes(api_client: TestClient, unique_user: utils.TestUse assert recipe.id in fetched_recipe_ids for recipe in other_recipes: 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 From 9cce0f65aa2a06f0c44579759b5a5504b6cc46f3 Mon Sep 17 00:00:00 2001 From: Hayden <64056131+hay-kot@users.noreply.github.com> Date: Fri, 4 Jul 2025 19:00:23 -0500 Subject: [PATCH 2/7] chore: automatic crowdin sync via gh actions (#5630) --- .github/workflows/locale-sync.yml | 103 ++++++++++++++++++++++++++ Taskfile.yml | 2 +- dev/code-generation/gen_ts_locales.py | 16 ++-- dev/code-generation/main.py | 41 ++++++++-- dev/code-generation/utils/template.py | 20 +++-- 5 files changed, 161 insertions(+), 21 deletions(-) create mode 100644 .github/workflows/locale-sync.yml diff --git a/.github/workflows/locale-sync.yml b/.github/workflows/locale-sync.yml new file mode 100644 index 000000000..f76d1e4bd --- /dev/null +++ b/.github/workflows/locale-sync.yml @@ -0,0 +1,103 @@ +name: Automatic Locale Sync + +on: + schedule: + # Run every Sunday at 2 AM UTC + - cron: '0 2 * * 0' + workflow_dispatch: + # Allow manual triggering from the GitHub UI + +jobs: + sync-locales: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + token: ${{ secrets.GITHUB_TOKEN }} + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.12' + + - name: Install Poetry + uses: snok/install-poetry@v1 + with: + virtualenvs-create: true + virtualenvs-in-project: true + + - name: Load cached venv + id: cached-poetry-dependencies + uses: actions/cache@v4 + with: + path: .venv + key: venv-${{ runner.os }}-${{ hashFiles('**/poetry.lock') }} + + - name: Check venv cache + id: cache-validate + if: steps.cached-poetry-dependencies.outputs.cache-hit == 'true' + run: | + echo "import fastapi;print('venv good?')" > test.py && poetry run python test.py && echo "cache-hit-success=true" >> $GITHUB_OUTPUT + rm test.py + continue-on-error: true + + - name: Install dependencies + run: | + sudo apt-get update + sudo apt-get install libsasl2-dev libldap2-dev libssl-dev + poetry install + if: steps.cached-poetry-dependencies.outputs.cache-hit != 'true' + + - name: Run locale generation + run: | + cd dev/code-generation + poetry run python main.py locales + env: + CROWDIN_API_KEY: ${{ secrets.CROWDIN_API_KEY }} + + - name: Check for changes + id: changes + run: | + if git diff --quiet; then + echo "has_changes=false" >> $GITHUB_OUTPUT + else + echo "has_changes=true" >> $GITHUB_OUTPUT + fi + + - name: Commit and create PR + if: steps.changes.outputs.has_changes == 'true' + run: | + # Configure git + git config --local user.email "action@github.com" + git config --local user.name "GitHub Action" + + # Create a new branch + BRANCH_NAME="auto-locale-sync-$(date +%Y%m%d-%H%M%S)" + git checkout -b "$BRANCH_NAME" + + # Add and commit changes + git add . + git commit -m "chore: automatic locale sync" + + # Push the branch + git push origin "$BRANCH_NAME" + + # Create PR using GitHub CLI + gh pr create --title "chore: automatic locale sync" --body "## Summary + + Automatically generated locale updates from the weekly sync job. + + ## Changes + - Updated frontend locale files + - Generated from latest translation sources + + ## Test plan + - [ ] Verify locale files are properly formatted + - [ ] Test that translations load correctly in the frontend" --base dev --head "$BRANCH_NAME" + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: No changes detected + if: steps.changes.outputs.has_changes == 'false' + run: echo "No locale changes detected, skipping PR creation" diff --git a/Taskfile.yml b/Taskfile.yml index ccff66fd6..c54c075db 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -70,7 +70,7 @@ tasks: dev:generate: desc: run code generators cmds: - - poetry run python dev/code-generation/main.py + - poetry run python dev/code-generation/main.py {{ .CLI_ARGS }} - task: py:format dev:services: diff --git a/dev/code-generation/gen_ts_locales.py b/dev/code-generation/gen_ts_locales.py index 6ff441d22..0d7e207ef 100644 --- a/dev/code-generation/gen_ts_locales.py +++ b/dev/code-generation/gen_ts_locales.py @@ -23,19 +23,22 @@ class LocaleData: LOCALE_DATA: dict[str, LocaleData] = { - "en-US": LocaleData(name="American English"), - "en-GB": LocaleData(name="British English"), "af-ZA": LocaleData(name="Afrikaans (Afrikaans)"), "ar-SA": LocaleData(name="العربية (Arabic)", dir="rtl"), + "bg-BG": LocaleData(name="Български (Bulgarian)"), "ca-ES": LocaleData(name="Català (Catalan)"), "cs-CZ": LocaleData(name="Čeština (Czech)"), "da-DK": LocaleData(name="Dansk (Danish)"), "de-DE": LocaleData(name="Deutsch (German)"), "el-GR": LocaleData(name="Ελληνικά (Greek)"), + "en-GB": LocaleData(name="British English"), + "en-US": LocaleData(name="American English"), "es-ES": LocaleData(name="Español (Spanish)"), + "et-EE": LocaleData(name="Eesti (Estonian)"), "fi-FI": LocaleData(name="Suomi (Finnish)"), - "fr-FR": LocaleData(name="Français (French)"), "fr-BE": LocaleData(name="Belge (Belgian)"), + "fr-CA": LocaleData(name="Français canadien (Canadian French)"), + "fr-FR": LocaleData(name="Français (French)"), "gl-ES": LocaleData(name="Galego (Galician)"), "he-IL": LocaleData(name="עברית (Hebrew)", dir="rtl"), "hr-HR": LocaleData(name="Hrvatski (Croatian)"), @@ -53,6 +56,7 @@ LOCALE_DATA: dict[str, LocaleData] = { "pt-PT": LocaleData(name="Português (Portuguese)"), "ro-RO": LocaleData(name="Română (Romanian)"), "ru-RU": LocaleData(name="Pусский (Russian)"), + "sk-SK": LocaleData(name="Slovenčina (Slovak)"), "sl-SI": LocaleData(name="Slovenščina (Slovenian)"), "sr-SP": LocaleData(name="српски (Serbian)"), "sv-SE": LocaleData(name="Svenska (Swedish)"), @@ -93,8 +97,8 @@ class CrowdinApi: project_id = "451976" api_key = API_KEY - def __init__(self, api_key: str): - api_key = api_key + def __init__(self, api_key: str | None): + self.api_key = api_key or API_KEY @property def headers(self) -> dict: @@ -196,7 +200,7 @@ def inject_registration_validation_values(): def generate_locales_ts_file(): - api = CrowdinApi("") + api = CrowdinApi(None) models = api.get_languages() tmpl = Template(LOCALE_TEMPLATE) rendered = tmpl.render(locales=models) diff --git a/dev/code-generation/main.py b/dev/code-generation/main.py index 2fd542e7b..b2d0f2264 100644 --- a/dev/code-generation/main.py +++ b/dev/code-generation/main.py @@ -1,3 +1,4 @@ +import argparse from pathlib import Path import gen_py_pytest_data_paths @@ -11,15 +12,39 @@ CWD = Path(__file__).parent def main(): - items = [ - (gen_py_schema_exports.main, "schema exports"), - (gen_ts_types.main, "frontend types"), - (gen_ts_locales.main, "locales"), - (gen_py_pytest_data_paths.main, "test data paths"), - (gen_py_pytest_routes.main, "pytest routes"), - ] + parser = argparse.ArgumentParser(description="Run code generators") + parser.add_argument( + "generators", + nargs="*", + help="Specific generators to run (schema, types, locales, data-paths, routes). If none specified, all will run.", # noqa: E501 - long line + ) + args = parser.parse_args() - for func, name in items: + # Define all available generators + all_generators = { + "schema": (gen_py_schema_exports.main, "schema exports"), + "types": (gen_ts_types.main, "frontend types"), + "locales": (gen_ts_locales.main, "locales"), + "data-paths": (gen_py_pytest_data_paths.main, "test data paths"), + "routes": (gen_py_pytest_routes.main, "pytest routes"), + } + + # Determine which generators to run + if args.generators: + # Validate requested generators + invalid_generators = [g for g in args.generators if g not in all_generators] + if invalid_generators: + log.error(f"Invalid generator(s): {', '.join(invalid_generators)}") + log.info(f"Available generators: {', '.join(all_generators.keys())}") + return + + generators_to_run = [(all_generators[g][0], all_generators[g][1]) for g in args.generators] + else: + # Run all generators (default behavior) + generators_to_run = list(all_generators.values()) + + # Run the selected generators + for func, name in generators_to_run: log.info(f"Generating {name}...") func() diff --git a/dev/code-generation/utils/template.py b/dev/code-generation/utils/template.py index 6312426e2..32ecf9c47 100644 --- a/dev/code-generation/utils/template.py +++ b/dev/code-generation/utils/template.py @@ -1,5 +1,4 @@ import logging -import re import subprocess from dataclasses import dataclass from pathlib import Path @@ -35,7 +34,7 @@ class CodeSlicer: start: int end: int - indentation: str + indentation: str | None text: list[str] _next_line = None @@ -47,15 +46,24 @@ class CodeSlicer: def push_line(self, string: str) -> None: self._next_line = self._next_line or self.start + 1 - self.text.insert(self._next_line, self.indentation + string + "\n") + self.text.insert(self._next_line, (self.indentation or "") + string + "\n") self._next_line += 1 -def get_indentation_of_string(line: str, comment_char: str = "//|#") -> str: - return re.sub(rf"{comment_char}.*", "", line).removesuffix("\n") +def get_indentation_of_string(line: str) -> str: + # Extract everything before the comment + if "//" in line: + indentation = line.split("//")[0] + elif "#" in line: + indentation = line.split("#")[0] + else: + indentation = line + + # Keep only the whitespace, remove any non-whitespace characters + return "".join(c for c in indentation if c.isspace()) -def find_start_end(file_text: list[str], gen_id: str) -> tuple[int, int, str]: +def find_start_end(file_text: list[str], gen_id: str) -> tuple[int, int, str | None]: start = None end = None indentation = None From 0c534ad9d4e98da5ac69b4d1f47b932cdf2b6b25 Mon Sep 17 00:00:00 2001 From: Hayden <64056131+hay-kot@users.noreply.github.com> Date: Fri, 4 Jul 2025 19:08:50 -0500 Subject: [PATCH 3/7] fix: load from env if available vs file (#5635) --- dev/code-generation/gen_ts_locales.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/dev/code-generation/gen_ts_locales.py b/dev/code-generation/gen_ts_locales.py index 0d7e207ef..465f01b8d 100644 --- a/dev/code-generation/gen_ts_locales.py +++ b/dev/code-generation/gen_ts_locales.py @@ -1,3 +1,4 @@ +import os import pathlib from dataclasses import dataclass from pathlib import Path @@ -13,7 +14,7 @@ from mealie.schema._mealie import MealieModel BASE = pathlib.Path(__file__).parent.parent.parent -API_KEY = dotenv.get_key(BASE / ".env", "CROWDIN_API_KEY") +API_KEY = dotenv.get_key(BASE / ".env", "CROWDIN_API_KEY") or os.environ.get("CROWDIN_API_KEY", "") @dataclass From f72ebed0dcf70ef3506a029e28f277f765731b73 Mon Sep 17 00:00:00 2001 From: Hayden <64056131+hay-kot@users.noreply.github.com> Date: Fri, 4 Jul 2025 19:19:25 -0500 Subject: [PATCH 4/7] fix: workflow permissions (#5636) --- .github/workflows/locale-sync.yml | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/.github/workflows/locale-sync.yml b/.github/workflows/locale-sync.yml index f76d1e4bd..3b78a9fc3 100644 --- a/.github/workflows/locale-sync.yml +++ b/.github/workflows/locale-sync.yml @@ -3,10 +3,14 @@ name: Automatic Locale Sync on: schedule: # Run every Sunday at 2 AM UTC - - cron: '0 2 * * 0' + - cron: "0 2 * * 0" workflow_dispatch: # Allow manual triggering from the GitHub UI +permissions: + contents: write # To checkout, commit, and push changes + pull-requests: write # To create pull requests + jobs: sync-locales: runs-on: ubuntu-latest @@ -19,7 +23,7 @@ jobs: - name: Set up Python uses: actions/setup-python@v5 with: - python-version: '3.12' + python-version: "3.12" - name: Install Poetry uses: snok/install-poetry@v1 @@ -83,6 +87,8 @@ jobs: # Push the branch git push origin "$BRANCH_NAME" + sleep 2 + # Create PR using GitHub CLI gh pr create --title "chore: automatic locale sync" --body "## Summary From 50a986f331e326b02c9059d0dcdc5acc19a7ff0d Mon Sep 17 00:00:00 2001 From: Hayden <64056131+hay-kot@users.noreply.github.com> Date: Fri, 4 Jul 2025 19:34:44 -0500 Subject: [PATCH 5/7] fix: workflow branch target/base (#5637) --- .github/workflows/locale-sync.yml | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/.github/workflows/locale-sync.yml b/.github/workflows/locale-sync.yml index 3b78a9fc3..a5b4999df 100644 --- a/.github/workflows/locale-sync.yml +++ b/.github/workflows/locale-sync.yml @@ -76,7 +76,11 @@ jobs: git config --local user.email "action@github.com" git config --local user.name "GitHub Action" - # Create a new branch + # Use the current branch as the base + BASE_BRANCH="${{ github.ref_name }}" + echo "Using base branch: $BASE_BRANCH" + + # Create a new branch from the base branch BRANCH_NAME="auto-locale-sync-$(date +%Y%m%d-%H%M%S)" git checkout -b "$BRANCH_NAME" @@ -89,18 +93,19 @@ jobs: sleep 2 - # Create PR using GitHub CLI - gh pr create --title "chore: automatic locale sync" --body "## Summary + # Create PR using GitHub CLI with explicit repository + gh pr create \ + --repo "${{ github.repository }}" \ + --title "chore: automatic locale sync" \ + --base "$BASE_BRANCH" \ + --head "$BRANCH_NAME" \ + --body "## Summary Automatically generated locale updates from the weekly sync job. ## Changes - Updated frontend locale files - - Generated from latest translation sources - - ## Test plan - - [ ] Verify locale files are properly formatted - - [ ] Test that translations load correctly in the frontend" --base dev --head "$BRANCH_NAME" + - Generated from latest translation sources" \ env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} From da3271f33ffae9a96b31b075a5adff4069785cab Mon Sep 17 00:00:00 2001 From: Hayden <64056131+hay-kot@users.noreply.github.com> Date: Fri, 4 Jul 2025 19:45:56 -0500 Subject: [PATCH 6/7] chore: remove unused jinja export option (#5631) --- dev/data/templates/recipes.md | 24 ---------- mealie/assets/templates/__init__.py | 5 --- mealie/assets/templates/recipes.md | 24 ---------- mealie/core/settings/directories.py | 9 ---- mealie/lang/providers.py | 2 +- mealie/routes/recipe/_base.py | 1 - mealie/routes/users/images.py | 6 ++- mealie/services/recipe/template_service.py | 44 +------------------ .../test_recipe_export_as.py | 29 ------------ tests/unit_tests/test_recipe_export_types.py | 1 - 10 files changed, 8 insertions(+), 137 deletions(-) delete mode 100644 dev/data/templates/recipes.md delete mode 100644 mealie/assets/templates/__init__.py delete mode 100644 mealie/assets/templates/recipes.md diff --git a/dev/data/templates/recipes.md b/dev/data/templates/recipes.md deleted file mode 100644 index eda4c6e14..000000000 --- a/dev/data/templates/recipes.md +++ /dev/null @@ -1,24 +0,0 @@ - - -![Recipe Image](../../images/{{ recipe.slug }}/original.jpg) - -# {{ recipe.name }} -{{ recipe.description }} - -## Ingredients -{% for ingredient in recipe.recipeIngredient %} -- [ ] {{ ingredient }} {% endfor %} - -## Instructions -{% for step in recipe.recipeInstructions %} -- [ ] {{ step.text }} {% endfor %} - -{% for note in recipe.notes %} -**{{ note.title }}:** {{ note.text }} -{% endfor %} - ---- - -Tags: {{ recipe.tags }} -Categories: {{ recipe.categories }} -Original URL: {{ recipe.orgURL }} diff --git a/mealie/assets/templates/__init__.py b/mealie/assets/templates/__init__.py deleted file mode 100644 index 7c3db204f..000000000 --- a/mealie/assets/templates/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -from pathlib import Path - -CWD = Path(__file__).parent - -recipes_markdown = CWD / "recipes.md" diff --git a/mealie/assets/templates/recipes.md b/mealie/assets/templates/recipes.md deleted file mode 100644 index eda4c6e14..000000000 --- a/mealie/assets/templates/recipes.md +++ /dev/null @@ -1,24 +0,0 @@ - - -![Recipe Image](../../images/{{ recipe.slug }}/original.jpg) - -# {{ recipe.name }} -{{ recipe.description }} - -## Ingredients -{% for ingredient in recipe.recipeIngredient %} -- [ ] {{ ingredient }} {% endfor %} - -## Instructions -{% for step in recipe.recipeInstructions %} -- [ ] {{ step.text }} {% endfor %} - -{% for note in recipe.notes %} -**{{ note.title }}:** {{ note.text }} -{% endfor %} - ---- - -Tags: {{ recipe.tags }} -Categories: {{ recipe.categories }} -Original URL: {{ recipe.orgURL }} diff --git a/mealie/core/settings/directories.py b/mealie/core/settings/directories.py index f55032444..86a4dc1dc 100644 --- a/mealie/core/settings/directories.py +++ b/mealie/core/settings/directories.py @@ -1,8 +1,5 @@ -import shutil from pathlib import Path -from mealie.assets import templates - class AppDirectories: def __init__(self, data_dir: Path) -> None: @@ -38,9 +35,3 @@ class AppDirectories: for dir in required_dirs: dir.mkdir(parents=True, exist_ok=True) - - # Bootstrap Templates - markdown_template = self.TEMPLATE_DIR.joinpath("recipes.md") - - if not markdown_template.exists(): - shutil.copyfile(templates.recipes_markdown, markdown_template) diff --git a/mealie/lang/providers.py b/mealie/lang/providers.py index 106a89403..67d1471f7 100644 --- a/mealie/lang/providers.py +++ b/mealie/lang/providers.py @@ -13,7 +13,7 @@ TRANSLATIONS = CWD / "messages" class Translator(Protocol): @abstractmethod - def t(self, key, default=None, **kwargs): + def t(self, key, default=None, **kwargs) -> str: pass diff --git a/mealie/routes/recipe/_base.py b/mealie/routes/recipe/_base.py index 146cbe2bb..8a48a8744 100644 --- a/mealie/routes/recipe/_base.py +++ b/mealie/routes/recipe/_base.py @@ -32,7 +32,6 @@ class JSONBytes(JSONResponse): class FormatResponse(BaseModel): jjson: list[str] = Field(..., alias="json") zip: list[str] - jinja2: list[str] class BaseRecipeController(BaseCrudController): diff --git a/mealie/routes/users/images.py b/mealie/routes/users/images.py index b472e4b1f..c2606c6f6 100644 --- a/mealie/routes/users/images.py +++ b/mealie/routes/users/images.py @@ -1,4 +1,5 @@ import shutil +from uuid import uuid4 from fastapi import File, HTTPException, UploadFile, status from pydantic import UUID4 @@ -24,7 +25,10 @@ class UserImageController(BaseUserController): """Updates a User Image""" with get_temporary_path() as temp_path: assert_user_change_allowed(id, self.user, self.user) - temp_img = temp_path.joinpath(profile.filename) + + # use a generated uuid and ignore the filename so we don't + # need to worry about sanitizing user inputs. + temp_img = temp_path.joinpath(str(uuid4())) with temp_img.open("wb") as buffer: shutil.copyfileobj(profile.file, buffer) diff --git a/mealie/services/recipe/template_service.py b/mealie/services/recipe/template_service.py index 9038ca2f1..504adeb23 100644 --- a/mealie/services/recipe/template_service.py +++ b/mealie/services/recipe/template_service.py @@ -2,8 +2,6 @@ import enum from pathlib import Path from zipfile import ZipFile -from jinja2 import Template - from mealie.schema.recipe import Recipe from mealie.schema.recipe.recipe_image_types import RecipeImageTypes from mealie.services._base_service import BaseService @@ -11,7 +9,6 @@ from mealie.services._base_service import BaseService class TemplateType(str, enum.Enum): json = "json" - jinja2 = "jinja2" zip = "zip" @@ -32,7 +29,6 @@ class TemplateService(BaseService): Returns a list of all templates available to render. """ return { - TemplateType.jinja2.value: [x.name for x in self.directories.TEMPLATE_DIR.iterdir() if x.is_file()], TemplateType.json.value: ["raw"], TemplateType.zip.value: ["zip"], } @@ -65,16 +61,13 @@ class TemplateService(BaseService): Args: t_type (TemplateType): The type of template to render recipe (Recipe): The recipe to render - template (str): The template to render **Required for Jinja2 Templates** + template (str): The template to render """ t_type = self.template_type(template) if t_type == TemplateType.json: return self._render_json(recipe) - if t_type == TemplateType.jinja2: - return self._render_jinja2(recipe, template) - if t_type == TemplateType.zip: return self._render_zip(recipe) @@ -96,41 +89,8 @@ class TemplateService(BaseService): return save_path - def _render_jinja2(self, recipe: Recipe, j2_template: str | None = None) -> Path: - """ - Renders a Jinja2 Template in a temporary directory and returns - the path to the file. - """ - self.__check_temp(self._render_jinja2) - - if j2_template is None: - raise ValueError("Template must be provided for method _render_jinja2") - - j2_path: Path = self.directories.TEMPLATE_DIR / j2_template - - if not j2_path.is_file(): - raise FileNotFoundError(f"Template '{j2_path}' not found.") - - with open(j2_path) as f: - template_text = f.read() - - template = Template(template_text) - rendered_text = template.render(recipe=recipe.model_dump(by_alias=True)) - - save_name = f"{recipe.slug}{j2_path.suffix}" - - if self.temp is None: - raise ValueError("Temporary directory must be provided for method _render_jinja2") - - save_path = self.temp.joinpath(save_name) - - with open(save_path, "w") as f: - f.write(rendered_text) - - return save_path - def _render_zip(self, recipe: Recipe) -> Path: - self.__check_temp(self._render_jinja2) + self.__check_temp(self._render_zip) image_asset = recipe.image_dir.joinpath(RecipeImageTypes.original.value) diff --git a/tests/integration_tests/user_recipe_tests/test_recipe_export_as.py b/tests/integration_tests/user_recipe_tests/test_recipe_export_as.py index 7729a437e..37e25d44a 100644 --- a/tests/integration_tests/user_recipe_tests/test_recipe_export_as.py +++ b/tests/integration_tests/user_recipe_tests/test_recipe_export_as.py @@ -18,28 +18,9 @@ def test_get_available_exports(api_client: TestClient, unique_user: TestUser) -> as_json = response.json() - assert "recipes.md" in as_json["jinja2"] assert "raw" in as_json["json"] -def test_render_jinja_template(api_client: TestClient, unique_user: TestUser) -> None: - # Create Recipe - recipe_name = random_string() - response = api_client.post(api_routes.recipes, json={"name": recipe_name}, headers=unique_user.token) - assert response.status_code == 201 - slug = response.json() - - # Render Template - response = api_client.get( - api_routes.recipes_slug_exports(slug) + "?template_name=recipes.md", headers=unique_user.token - ) - assert response.status_code == 200 - - # Assert Template is Rendered Correctly - # TODO: More robust test - assert f"# {recipe_name}" in response.text - - def test_get_recipe_as_zip(api_client: TestClient, unique_user: TestUser) -> None: # Create Recipe recipe_name = random_string() @@ -61,13 +42,3 @@ def test_get_recipe_as_zip(api_client: TestClient, unique_user: TestUser) -> Non with zipfile.ZipFile(zip_file, "r") as zip_fp: with zip_fp.open(f"{slug}.json") as json_fp: assert json.loads(json_fp.read())["name"] == recipe_name - - -# TODO: Allow users to upload templates to their own directory -# def test_upload_template(api_client: TestClient, unique_user: TestUser) -> None: -# assert False - - -# # TODO: Allow users to upload templates to their own directory -# def test_delete_template(api_client: TestClient, unique_user: TestUser) -> None: -# assert False diff --git a/tests/unit_tests/test_recipe_export_types.py b/tests/unit_tests/test_recipe_export_types.py index 5c4003147..475c5d6f0 100644 --- a/tests/unit_tests/test_recipe_export_types.py +++ b/tests/unit_tests/test_recipe_export_types.py @@ -3,6 +3,5 @@ from mealie.services.recipe.template_service import TemplateService, TemplateTyp def test_recipe_export_types() -> None: ts = TemplateService() - assert ts.template_type("recipes.md") == TemplateType.jinja2.value assert ts.template_type("raw") == TemplateType.json.value assert ts.template_type("zip") == TemplateType.zip.value From ca41bc8d5c3be8405a62673bd38945394308d7e2 Mon Sep 17 00:00:00 2001 From: Kuchenpirat <24235032+Kuchenpirat@users.noreply.github.com> Date: Sat, 5 Jul 2025 03:37:42 +0200 Subject: [PATCH 7/7] fix: 500 error on recipe share link (#5627) --- .../components/Domain/Recipe/RecipeAssets.vue | 223 ++++++++---------- 1 file changed, 104 insertions(+), 119 deletions(-) diff --git a/frontend/components/Domain/Recipe/RecipeAssets.vue b/frontend/components/Domain/Recipe/RecipeAssets.vue index 8e4f742ed..b95ff417e 100644 --- a/frontend/components/Domain/Recipe/RecipeAssets.vue +++ b/frontend/components/Domain/Recipe/RecipeAssets.vue @@ -1,23 +1,23 @@