44from datetime import UTC , datetime
55from pathlib import Path
66from shutil import copytree , rmtree
7+ from textwrap import dedent
78from typing import Any
89from uuid import UUID , uuid4
910from zipfile import ZipFile
1011
12+ import sqlalchemy as sa
1113from fastapi import UploadFile
1214
1315from mealie .core import exceptions
@@ -64,31 +66,57 @@ def _get_recipe(self, data: str | UUID, key: str | None = None) -> Recipe:
6466 raise exceptions .NoEntryFound ("Recipe not found." )
6567 return recipe
6668
67- def can_delete (self , recipe : Recipe ) -> bool :
69+ def can_delete (self , recipe_slugs : list [ str ] ) -> bool :
6870 if self .user .admin :
6971 return True
7072 else :
71- return self .can_update (recipe )
72-
73- def can_update (self , recipe : Recipe ) -> bool :
74- if recipe .settings is None :
75- raise exceptions .UnexpectedNone ("Recipe Settings is None" )
76-
77- # Check if this user owns the recipe
78- if self .user .id == recipe .user_id :
79- return True
73+ return self .can_update (recipe_slugs )
74+
75+ def can_update (self , recipe_slugs : list [str ]) -> bool :
76+ sql = dedent (
77+ """
78+ SELECT
79+ CASE
80+ WHEN COUNT(*) = SUM(
81+ CASE
82+ -- User owns the recipe
83+ WHEN r.user_id = :user_id THEN 1
84+
85+ -- Not owner: check if recipe is locked
86+ WHEN COALESCE(rs.locked, TRUE) = TRUE THEN 0
87+
88+ -- Different household: check household policy
89+ WHEN
90+ u.household_id != :household_id
91+ AND COALESCE(hp.lock_recipe_edits_from_other_households, TRUE) = TRUE
92+ THEN 0
93+
94+ -- All other cases: can update
95+ ELSE 1
96+ END
97+ ) THEN 1
98+ ELSE 0
99+ END AS all_can_update
100+ FROM recipes r
101+ LEFT JOIN recipe_settings rs ON rs.recipe_id = r.id
102+ LEFT JOIN users u ON u.id = r.user_id
103+ LEFT JOIN households h ON h.id = u.household_id
104+ LEFT JOIN household_preferences hp ON hp.household_id = h.id
105+ WHERE r.slug IN :recipe_slugs AND r.group_id = :group_id;
106+ """
107+ )
80108
81- # Check if this user has permission to edit this recipe
82- if self . household . id != recipe . household_id :
83- other_household = self . repos . households . get_one ( recipe . household_id )
84- if not ( other_household and other_household . preferences ):
85- return False
86- if other_household . preferences . lock_recipe_edits_from_other_households :
87- return False
88- if recipe . settings . locked :
89- return False
109+ result = self . repos . session . execute (
110+ sa . text ( sql ). bindparams ( sa . bindparam ( "recipe_slugs" , expanding = True )),
111+ params = {
112+ "user_id" : self . repos . uuid_to_str ( self . user . id ),
113+ "household_id" : self . repos . uuid_to_str ( self . household . id ),
114+ "group_id" : self . repos . uuid_to_str ( self . user . group_id ),
115+ "recipe_slugs" : recipe_slugs ,
116+ },
117+ ). scalar ()
90118
91- return True
119+ return bool ( result )
92120
93121 def can_lock_unlock (self , recipe : Recipe ) -> bool :
94122 return recipe .user_id == self .user .id
@@ -423,7 +451,7 @@ def _pre_update_check(self, slug_or_id: str | UUID, new_data: Recipe) -> Recipe:
423451 if recipe is None or recipe .settings is None :
424452 raise exceptions .NoEntryFound ("Recipe not found." )
425453
426- if not self .can_update (recipe ):
454+ if not self .can_update ([ recipe . slug ] ):
427455 raise exceptions .PermissionDenied ("You do not have permission to edit this recipe." )
428456
429457 setting_lock = new_data .settings is not None and recipe .settings .locked != new_data .settings .locked
@@ -444,7 +472,7 @@ def update_one(self, slug_or_id: str | UUID, update_data: Recipe) -> Recipe:
444472
445473 def update_recipe_image (self , slug : str , image : bytes , extension : str ):
446474 recipe = self .get_one (slug )
447- if not self .can_update (recipe ):
475+ if not self .can_update ([ recipe . slug ] ):
448476 raise exceptions .PermissionDenied ("You do not have permission to edit this recipe." )
449477
450478 data_service = RecipeDataService (recipe .id )
@@ -454,7 +482,7 @@ def update_recipe_image(self, slug: str, image: bytes, extension: str):
454482
455483 def delete_recipe_image (self , slug : str ) -> None :
456484 recipe = self .get_one (slug )
457- if not self .can_update (recipe ):
485+ if not self .can_update ([ recipe . slug ] ):
458486 raise exceptions .PermissionDenied ("You do not have permission to edit this recipe." )
459487
460488 data_service = RecipeDataService (recipe .id )
@@ -482,12 +510,24 @@ def update_last_made(self, slug_or_id: str | UUID, timestamp: datetime) -> Recip
482510
483511 def delete_one (self , slug_or_id : str | UUID ) -> Recipe :
484512 recipe = self .get_one (slug_or_id )
513+ resp = self .delete_many ([recipe .slug ])
514+ return resp [0 ]
485515
486- if not self .can_delete (recipe ):
487- raise exceptions .PermissionDenied ("You do not have permission to delete this recipe." )
516+ def delete_many (self , recipe_slugs : list [str ]) -> list [Recipe ]:
517+ if not self .can_delete (recipe_slugs ):
518+ if len (recipe_slugs ) == 1 :
519+ msg = "You do not have permission to delete this recipe."
520+ else :
521+ msg = "You do not have permission to delete all of these recipes."
522+ raise exceptions .PermissionDenied (msg )
523+
524+ data = self .group_recipes .delete_many (recipe_slugs )
525+ for r in data :
526+ try :
527+ self .delete_assets (r )
528+ except Exception :
529+ self .logger .exception (f"Failed to delete recipe assets for { r .slug } " )
488530
489- data = self .group_recipes .delete (recipe .id , "id" )
490- self .delete_assets (data )
491531 return data
492532
493533 # =================================================================
0 commit comments