Coverage for opt/mealie/lib/python3.12/site-packages/mealie/routes/recipe/timeline_events.py: 54%

75 statements  

« prev     ^ index     » next       coverage.py v7.10.6, created at 2025-11-25 15:48 +0000

1import shutil 1a

2from functools import cached_property 1a

3 

4from fastapi import Depends, File, Form, HTTPException 1a

5from pydantic import UUID4 1a

6 

7from mealie.repos.all_repositories import get_repositories 1a

8from mealie.routes._base import BaseCrudController, controller 1a

9from mealie.routes._base.mixins import HttpRepo 1a

10from mealie.routes._base.routers import MealieCrudRoute, UserAPIRouter 1a

11from mealie.schema.recipe.recipe_timeline_events import ( 1a

12 RecipeTimelineEventCreate, 

13 RecipeTimelineEventIn, 

14 RecipeTimelineEventOut, 

15 RecipeTimelineEventPagination, 

16 RecipeTimelineEventUpdate, 

17 TimelineEventImage, 

18) 

19from mealie.schema.recipe.request_helpers import UpdateImageResponse 1a

20from mealie.schema.response.pagination import PaginationQuery 1a

21from mealie.services import urls 1a

22from mealie.services.event_bus_service.event_types import EventOperation, EventRecipeTimelineEventData, EventTypes 1a

23from mealie.services.recipe.recipe_data_service import RecipeDataService 1a

24 

25router = UserAPIRouter(route_class=MealieCrudRoute, prefix="/timeline/events") 1a

26 

27 

28@controller(router) 1a

29class RecipeTimelineEventsController(BaseCrudController): 1a

30 @cached_property 1a

31 def repo(self): 1a

32 return self.repos.recipe_timeline_events 1dbefghnijklmc

33 

34 @cached_property 1a

35 def group_recipes(self): 1a

36 return get_repositories(self.session, group_id=self.group_id, household_id=None).recipes 1bc

37 

38 @cached_property 1a

39 def mixins(self): 1a

40 return HttpRepo[RecipeTimelineEventCreate, RecipeTimelineEventOut, RecipeTimelineEventUpdate]( 1bc

41 self.repo, 

42 self.logger, 

43 self.registered_exceptions, 

44 ) 

45 

46 @router.get("", response_model=RecipeTimelineEventPagination) 1a

47 def get_all(self, q: PaginationQuery = Depends(PaginationQuery)): 1a

48 response = self.repo.page_all( 1dbefghnijklmc

49 pagination=q, 

50 override=RecipeTimelineEventOut, 

51 ) 

52 

53 response.set_pagination_guides(router.url_path_for("get_all"), q.model_dump()) 1dbefghijklmc

54 return response 1dbefghijklmc

55 

56 @router.post("", response_model=RecipeTimelineEventOut, status_code=201) 1a

57 def create_one(self, data: RecipeTimelineEventIn): 1a

58 # if the user id is not specified, use the currently-authenticated user 

59 data.user_id = data.user_id or self.user.id 

60 

61 recipe = self.group_recipes.get_one(data.recipe_id, "id") 

62 if not recipe: 

63 raise HTTPException(status_code=404, detail="recipe not found") 

64 

65 event_data = data.cast(RecipeTimelineEventCreate) 

66 event = self.mixins.create_one(event_data) 

67 

68 self.publish_event( 

69 event_type=EventTypes.recipe_updated, 

70 document_data=EventRecipeTimelineEventData( 

71 operation=EventOperation.create, recipe_slug=recipe.slug, recipe_timeline_event_id=event.id 

72 ), 

73 group_id=recipe.group_id, 

74 household_id=recipe.household_id, 

75 message=self.t( 

76 "notifications.generic-updated-with-url", 

77 name=recipe.name, 

78 url=urls.recipe_url(self.group.slug, recipe.slug, self.settings.BASE_URL), 

79 ), 

80 ) 

81 

82 return event 

83 

84 @router.get("/{item_id}", response_model=RecipeTimelineEventOut) 1a

85 def get_one(self, item_id: UUID4): 1a

86 return self.mixins.get_one(item_id) 

87 

88 @router.put("/{item_id}", response_model=RecipeTimelineEventOut) 1a

89 def update_one(self, item_id: UUID4, data: RecipeTimelineEventUpdate): 1a

90 event = self.mixins.patch_one(data, item_id) 1bc

91 recipe = self.group_recipes.get_one(event.recipe_id, "id") 1bc

92 if recipe: 92 ↛ 107line 92 didn't jump to line 107 because the condition on line 92 was always true1bc

93 self.publish_event( 1bc

94 event_type=EventTypes.recipe_updated, 

95 document_data=EventRecipeTimelineEventData( 

96 operation=EventOperation.update, recipe_slug=recipe.slug, recipe_timeline_event_id=event.id 

97 ), 

98 group_id=recipe.group_id, 

99 household_id=recipe.household_id, 

100 message=self.t( 

101 "notifications.generic-updated-with-url", 

102 name=recipe.name, 

103 url=urls.recipe_url(self.group.slug, recipe.slug, self.settings.BASE_URL), 

104 ), 

105 ) 

106 

107 return event 1bc

108 

109 @router.delete("/{item_id}", response_model=RecipeTimelineEventOut) 1a

110 def delete_one(self, item_id: UUID4): 1a

111 event = self.mixins.delete_one(item_id) 

112 if event.image_dir.exists(): 

113 try: 

114 shutil.rmtree(event.image_dir) 

115 except FileNotFoundError: 

116 pass 

117 

118 recipe = self.group_recipes.get_one(event.recipe_id, "id") 

119 if recipe: 

120 self.publish_event( 

121 event_type=EventTypes.recipe_updated, 

122 document_data=EventRecipeTimelineEventData( 

123 operation=EventOperation.delete, recipe_slug=recipe.slug, recipe_timeline_event_id=event.id 

124 ), 

125 group_id=recipe.group_id, 

126 household_id=recipe.household_id, 

127 message=self.t( 

128 "notifications.generic-updated-with-url", 

129 name=recipe.name, 

130 url=urls.recipe_url(self.group.slug, recipe.slug, self.settings.BASE_URL), 

131 ), 

132 ) 

133 

134 return event 

135 

136 # ================================================================================================================== 

137 # Image and Assets 

138 

139 @router.put("/{item_id}/image", response_model=UpdateImageResponse) 1a

140 def update_event_image(self, item_id: UUID4, image: bytes = File(...), extension: str = Form(...)): 1a

141 event = self.mixins.get_one(item_id) 

142 data_service = RecipeDataService(event.recipe_id) 

143 data_service.write_image(image, extension, event.image_dir) 

144 

145 if event.image != TimelineEventImage.has_image.value: 

146 event.image = TimelineEventImage.has_image 

147 event = self.mixins.patch_one(event.cast(RecipeTimelineEventUpdate), event.id) 

148 recipe = self.group_recipes.get_one(event.recipe_id, "id") 

149 if recipe: 

150 self.publish_event( 

151 event_type=EventTypes.recipe_updated, 

152 document_data=EventRecipeTimelineEventData( 

153 operation=EventOperation.update, recipe_slug=recipe.slug, recipe_timeline_event_id=event.id 

154 ), 

155 group_id=recipe.group_id, 

156 household_id=recipe.household_id, 

157 message=self.t( 

158 "notifications.generic-updated-with-url", 

159 name=recipe.name, 

160 url=urls.recipe_url(self.group.slug, recipe.slug, self.settings.BASE_URL), 

161 ), 

162 ) 

163 

164 return UpdateImageResponse(image=TimelineEventImage.has_image.value)