feat: Cross-Household Recipes (#4089)

This commit is contained in:
Michael Genson
2024-08-31 21:54:10 -05:00
committed by GitHub
parent 7ef2e91ecf
commit 9acf9ec27c
16 changed files with 545 additions and 92 deletions

View File

@@ -32,6 +32,7 @@ from mealie.core.dependencies import (
from mealie.core.security import create_recipe_slug_token
from mealie.db.models.household.cookbook import CookBook
from mealie.pkgs import cache
from mealie.repos.all_repositories import get_repositories
from mealie.repos.repository_generic import RepositoryGeneric
from mealie.repos.repository_recipes import RepositoryRecipes
from mealie.routes._base import BaseCrudController, controller
@@ -94,9 +95,13 @@ class JSONBytes(JSONResponse):
class BaseRecipeController(BaseCrudController):
@cached_property
def repo(self) -> RepositoryRecipes:
def recipes(self) -> RepositoryRecipes:
return self.repos.recipes
@cached_property
def group_recipes(self) -> RepositoryRecipes:
return get_repositories(self.session, group_id=self.group_id, household_id=None).recipes
@cached_property
def cookbooks_repo(self) -> RepositoryGeneric[ReadCookBook, CookBook]:
return self.repos.cookbooks
@@ -107,7 +112,7 @@ class BaseRecipeController(BaseCrudController):
@cached_property
def mixins(self):
return HttpRepo[CreateRecipe, Recipe, Recipe](self.repo, self.logger)
return HttpRepo[CreateRecipe, Recipe, Recipe](self.recipes, self.logger)
class FormatResponse(BaseModel):
@@ -331,8 +336,9 @@ class RecipeController(BaseRecipeController):
if cookbook_data is None:
raise HTTPException(status_code=404, detail="cookbook not found")
# we use the repo by user so we can sort favorites correctly
pagination_response = self.repos.recipes.by_user(self.user.id).page_all(
# We use "group_recipes" here so we can return all recipes regardless of household. The query filter can include
# a household_id to filter by household. We use the "by_user" so we can sort favorites correctly.
pagination_response = self.group_recipes.by_user(self.user.id).page_all(
pagination=q,
cookbook=cookbook_data,
categories=categories,
@@ -362,7 +368,7 @@ class RecipeController(BaseRecipeController):
def get_one(self, slug: str = Path(..., description="A recipe's slug or id")):
"""Takes in a recipe's slug or id and returns all data for a recipe"""
try:
recipe = self.service.get_one_by_slug_or_id(slug)
recipe = self.service.get_one(slug)
except Exception as e:
self.handle_exceptions(e)
return None
@@ -534,7 +540,7 @@ class RecipeController(BaseRecipeController):
data_service = RecipeDataService(recipe.id)
data_service.write_image(image, extension)
new_version = self.repo.update_image(slug, extension)
new_version = self.recipes.update_image(slug, extension)
return UpdateImageResponse(image=new_version)
@router.post("/{slug}/assets", response_model=RecipeAsset, tags=["Recipe: Images and Assets"])

View File

@@ -4,6 +4,7 @@ from functools import cached_property
from fastapi import Depends, File, Form, HTTPException
from pydantic import UUID4
from mealie.repos.all_repositories import get_repositories
from mealie.routes._base import BaseCrudController, controller
from mealie.routes._base.mixins import HttpRepo
from mealie.routes._base.routers import MealieCrudRoute, UserAPIRouter
@@ -31,8 +32,8 @@ class RecipeTimelineEventsController(BaseCrudController):
return self.repos.recipe_timeline_events
@cached_property
def recipes_repo(self):
return self.repos.recipes
def group_recipes(self):
return get_repositories(self.session, group_id=self.group_id, household_id=None).recipes
@cached_property
def mixins(self):
@@ -57,7 +58,7 @@ class RecipeTimelineEventsController(BaseCrudController):
# if the user id is not specified, use the currently-authenticated user
data.user_id = data.user_id or self.user.id
recipe = self.recipes_repo.get_one(data.recipe_id, "id")
recipe = self.group_recipes.get_one(data.recipe_id, "id")
if not recipe:
raise HTTPException(status_code=404, detail="recipe not found")
@@ -87,7 +88,7 @@ class RecipeTimelineEventsController(BaseCrudController):
@events_router.put("/{item_id}", response_model=RecipeTimelineEventOut)
def update_one(self, item_id: UUID4, data: RecipeTimelineEventUpdate):
event = self.mixins.patch_one(data, item_id)
recipe = self.recipes_repo.get_one(event.recipe_id, "id")
recipe = self.group_recipes.get_one(event.recipe_id, "id")
if recipe:
self.publish_event(
event_type=EventTypes.recipe_updated,
@@ -114,7 +115,7 @@ class RecipeTimelineEventsController(BaseCrudController):
except FileNotFoundError:
pass
recipe = self.recipes_repo.get_one(event.recipe_id, "id")
recipe = self.group_recipes.get_one(event.recipe_id, "id")
if recipe:
self.publish_event(
event_type=EventTypes.recipe_updated,
@@ -144,7 +145,7 @@ class RecipeTimelineEventsController(BaseCrudController):
if event.image != TimelineEventImage.has_image.value:
event.image = TimelineEventImage.has_image
event = self.mixins.patch_one(event.cast(RecipeTimelineEventUpdate), event.id)
recipe = self.recipes_repo.get_one(event.recipe_id, "id")
recipe = self.group_recipes.get_one(event.recipe_id, "id")
if recipe:
self.publish_event(
event_type=EventTypes.recipe_updated,

View File

@@ -1,8 +1,10 @@
from functools import cached_property
from uuid import UUID
from fastapi import HTTPException, status
from pydantic import UUID4
from mealie.repos.all_repositories import get_repositories
from mealie.routes._base import BaseUserController, controller
from mealie.routes._base.routers import UserAPIRouter
from mealie.routes.users._helpers import assert_user_change_allowed
@@ -14,6 +16,10 @@ router = UserAPIRouter()
@controller(router)
class UserRatingsController(BaseUserController):
@cached_property
def group_recipes(self):
return get_repositories(self.session, group_id=self.group_id, household_id=None).recipes
def get_recipe_or_404(self, slug_or_id: str | UUID):
"""Fetches a recipe by slug or id, or raises a 404 error if not found."""
if isinstance(slug_or_id, str):
@@ -22,11 +28,10 @@ class UserRatingsController(BaseUserController):
except ValueError:
pass
recipes_repo = self.repos.recipes
if isinstance(slug_or_id, UUID):
recipe = recipes_repo.get_one(slug_or_id, key="id")
recipe = self.group_recipes.get_one(slug_or_id, key="id")
else:
recipe = recipes_repo.get_one(slug_or_id, key="slug")
recipe = self.group_recipes.get_one(slug_or_id, key="slug")
if not recipe:
raise HTTPException(

View File

@@ -16,6 +16,7 @@ from mealie.core.config import get_app_settings
from mealie.core.dependencies.dependencies import get_temporary_path
from mealie.lang.providers import Translator
from mealie.pkgs import cache
from mealie.repos.all_repositories import get_repositories
from mealie.repos.repository_factory import AllRepositories
from mealie.repos.repository_generic import RepositoryGeneric
from mealie.schema.household.household import HouseholdInDB
@@ -46,6 +47,9 @@ class RecipeServiceBase(BaseService):
if repos.household_id != user.household_id != household.id:
raise Exception("household ids do not match")
self.group_recipes = get_repositories(repos.session, group_id=repos.group_id, household_id=None).recipes
"""Recipes repo without a Household filter"""
self.translator = translator
self.t = translator.t
@@ -54,7 +58,7 @@ class RecipeServiceBase(BaseService):
class RecipeService(RecipeServiceBase):
def _get_recipe(self, data: str | UUID, key: str | None = None) -> Recipe:
recipe = self.repos.recipes.get_one(data, key)
recipe = self.group_recipes.get_one(data, key)
if recipe is None:
raise exceptions.NoEntryFound("Recipe not found.")
return recipe
@@ -62,7 +66,18 @@ class RecipeService(RecipeServiceBase):
def can_update(self, recipe: Recipe) -> bool:
if recipe.settings is None:
raise exceptions.UnexpectedNone("Recipe Settings is None")
return recipe.settings.locked is False or self.user.id == recipe.user_id
# Check if this user owns the recipe
if self.user.id == recipe.user_id:
return True
# Check if this user has permission to edit this recipe
if self.household.id != recipe.household_id:
return False
if recipe.settings.locked:
return False
return True
def can_lock_unlock(self, recipe: Recipe) -> bool:
return recipe.user_id == self.user.id
@@ -120,7 +135,7 @@ class RecipeService(RecipeServiceBase):
return Recipe(**additional_attrs)
def get_one_by_slug_or_id(self, slug_or_id: str | UUID) -> Recipe | None:
def get_one(self, slug_or_id: str | UUID) -> Recipe | None:
if isinstance(slug_or_id, str):
try:
slug_or_id = UUID(slug_or_id)
@@ -393,9 +408,10 @@ class RecipeService(RecipeServiceBase):
return new_data
def update_last_made(self, slug: str, timestamp: datetime) -> Recipe:
# we bypass the pre update check since any user can update a recipe's last made date, even if it's locked
# we bypass the pre update check since any user can update a recipe's last made date, even if it's locked,
# or if the user belongs to a different household
recipe = self._get_recipe(slug)
return self.repos.recipes.patch(recipe.slug, {"last_made": timestamp})
return self.group_recipes.patch(recipe.slug, {"last_made": timestamp})
def delete_one(self, slug) -> Recipe:
recipe = self._get_recipe(slug)