diff --git a/mealie/db/models/recipe/api_extras.py b/mealie/db/models/recipe/api_extras.py index 659ea062e..eed2c2805 100644 --- a/mealie/db/models/recipe/api_extras.py +++ b/mealie/db/models/recipe/api_extras.py @@ -9,7 +9,7 @@ def api_extras(func): """Decorator function to unpack the extras into a dict; requires an "extras" column""" def wrapper(*args, **kwargs): - extras = kwargs.pop("extras") + extras = kwargs.pop("extras", None) if extras is None: extras = [] diff --git a/mealie/repos/repository_recipes.py b/mealie/repos/repository_recipes.py index 23972d024..aa7512fd4 100644 --- a/mealie/repos/repository_recipes.py +++ b/mealie/repos/repository_recipes.py @@ -201,6 +201,16 @@ class RepositoryRecipes(HouseholdRepositoryGeneric[Recipe, RecipeModel]): additional_ids = self.session.execute(sa.select(model.id).filter(model.slug.in_(slugs))).scalars().all() return ids + additional_ids + 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() + + # Handle explicit group_id injection for related items that require it + for organizer_field in ["tags", "recipe_category", "tools"]: + for organizer in new_data.get(organizer_field, []): + organizer["group_id"] = self.group_id + + return super().update(match_value, new_data) + def page_all( # type: ignore self, pagination: PaginationQuery, diff --git a/tests/unit_tests/repository_tests/test_recipe_repository.py b/tests/unit_tests/repository_tests/test_recipe_repository.py index a7cc28910..5502b05f7 100644 --- a/tests/unit_tests/repository_tests/test_recipe_repository.py +++ b/tests/unit_tests/repository_tests/test_recipe_repository.py @@ -1,5 +1,5 @@ from datetime import UTC, datetime, timedelta -from uuid import UUID +from uuid import UUID, uuid4 import pytest from sqlalchemy.orm import Session @@ -810,3 +810,48 @@ def test_order_by_rating(user_tuple: tuple[TestUser, TestUser]): assert data[0].slug == recipe_2.slug # global rating == 2.5 (avg of 4 and 1) assert data[1].slug == recipe_3.slug # global rating == 3 assert data[2].slug == recipe_1.slug # global rating == 4.25 (avg of 5 and 3.5) + + +@pytest.mark.parametrize("route", ["patch", "update"]) +def test_recipe_inject_organizer_group_id(unique_user: TestUser, route: str): + # Regression test for #6802 - Ensure explicit group_id injection works for new organizers + database = unique_user.repos + recipe = database.recipes.create( + Recipe( + user_id=unique_user.user_id, + group_id=unique_user.group_id, + name=random_string(), + ) + ) + + new_tag_name = random_string() + new_cat_name = random_string() + new_tool_name = random_string() + + patch_data = Recipe( + name=recipe.name, + tags=[new_tag_name], + recipe_category=[new_cat_name], + tools=[{"id": uuid4(), "name": new_tool_name, "slug": new_tool_name}], + ) + + # This should not raise IntegrityError or TypeError + update_func = database.recipes.patch if route == "patch" else database.recipes.update + if route == "patch": + updated_recipe = update_func(recipe.slug, patch_data.model_dump(exclude_unset=True)) + elif route == "update": + updated_recipe = update_func(recipe.slug, patch_data.model_dump(exclude_unset=True)) + + assert updated_recipe + + assert updated_recipe.tags + assert len(updated_recipe.tags) == 1 + assert updated_recipe.tags[0].name == new_tag_name + + assert updated_recipe.recipe_category + assert len(updated_recipe.recipe_category) == 1 + assert updated_recipe.recipe_category[0].name == new_cat_name + + assert updated_recipe.tools + assert len(updated_recipe.tools) == 1 + assert updated_recipe.tools[0].name == new_tool_name