Coverage for opt/mealie/lib/python3.12/site-packages/mealie/routes/organizers/controller_categories.py: 97%
61 statements
« prev ^ index » next coverage.py v7.15.2, created at 2026-10-07 03:04 +0000
« prev ^ index » next coverage.py v7.15.2, created at 2026-10-07 03:04 +0000
1from functools import cached_property
3from fastapi import APIRouter, Depends
4from pydantic import UUID4, BaseModel, ConfigDict
6from mealie.routes._base import BaseCrudController, controller
7from mealie.routes._base.mixins import HttpRepo
8from mealie.schema import mapper
9from mealie.schema.recipe import CategoryIn, RecipeCategoryResponse
10from mealie.schema.recipe.recipe import RecipeCategory, RecipeCategoryPagination
11from mealie.schema.recipe.recipe_category import CategoryBase, CategoryOut, CategorySave
12from mealie.schema.response.pagination import PaginationQuery
13from mealie.services import urls
14from mealie.services.event_bus_service.event_types import EventCategoryData, EventOperation, EventTypes
16router = APIRouter(prefix="/categories", tags=["Organizer: Categories"])
19class CategorySummary(BaseModel):
20 id: UUID4
21 slug: str
22 name: str
23 model_config = ConfigDict(from_attributes=True)
26@controller(router)
27class RecipeCategoryController(BaseCrudController):
28 # =========================================================================
29 # CRUD Operations
30 @cached_property
31 def repo(self):
32 return self.repos.categories
34 @cached_property
35 def mixins(self):
36 return HttpRepo[CategorySave, CategoryOut, CategorySave](self.repo, self.logger)
38 @router.get("", response_model=RecipeCategoryPagination)
39 def get_all(self, q: PaginationQuery = Depends(PaginationQuery), search: str | None = None):
40 """Returns a list of available categories in the database"""
41 response = self.repo.page_all(
42 pagination=q,
43 override=RecipeCategory,
44 search=search,
45 )
47 response.set_pagination_guides(router.url_path_for("get_all"), q.model_dump())
48 return response
50 @router.post("", status_code=201)
51 def create_one(self, category: CategoryIn):
52 """Creates a Category in the database"""
53 save_data = mapper.cast(category, CategorySave, group_id=self.group_id)
54 new_category = self.mixins.create_one(save_data)
55 if new_category: 55 ↛ 68line 55 didn't jump to line 68 because the condition on line 55 was always true
56 self.publish_event(
57 event_type=EventTypes.category_created,
58 document_data=EventCategoryData(operation=EventOperation.create, category_id=new_category.id),
59 group_id=new_category.group_id,
60 household_id=None,
61 message=self.t(
62 "notifications.generic-created-with-url",
63 name=new_category.name,
64 url=urls.category_url(new_category.slug, self.settings.BASE_URL),
65 ),
66 )
68 return new_category
70 @router.get("/empty", response_model=list[CategoryBase])
71 def get_all_empty(self):
72 """Returns a list of categories that do not contain any recipes"""
73 return self.repos.categories.get_empty()
75 @router.get("/{item_id}", response_model=CategorySummary)
76 def get_one(self, item_id: UUID4):
77 """Returns a list of recipes associated with the provided category."""
78 category_obj = self.mixins.get_one(item_id)
79 category_obj = CategorySummary.model_validate(category_obj)
80 return category_obj
82 @router.put("/{item_id}", response_model=CategorySummary)
83 def update_one(self, item_id: UUID4, update_data: CategoryIn):
84 """Updates an existing Tag in the database"""
85 save_data = mapper.cast(update_data, CategorySave, group_id=self.group_id)
86 category = self.mixins.update_one(save_data, item_id)
88 if category: 88 ↛ 101line 88 didn't jump to line 101 because the condition on line 88 was always true
89 self.publish_event(
90 event_type=EventTypes.category_updated,
91 document_data=EventCategoryData(operation=EventOperation.update, category_id=category.id),
92 group_id=category.group_id,
93 household_id=None,
94 message=self.t(
95 "notifications.generic-updated-with-url",
96 name=category.name,
97 url=urls.category_url(category.slug, self.settings.BASE_URL),
98 ),
99 )
101 return category
103 @router.delete("/{item_id}")
104 def delete_one(self, item_id: UUID4):
105 """
106 Removes a recipe category from the database. Deleting a
107 category does not impact a recipe. The category will be removed
108 from any recipes that contain it
109 """
110 if category := self.mixins.delete_one(item_id):
111 self.publish_event(
112 event_type=EventTypes.category_deleted,
113 document_data=EventCategoryData(operation=EventOperation.delete, category_id=category.id),
114 group_id=category.group_id,
115 household_id=None,
116 message=self.t("notifications.generic-deleted", name=category.name),
117 )
119 # =========================================================================
120 # Read All Operations
122 @router.get("/slug/{category_slug}")
123 def get_one_by_slug(self, category_slug: str):
124 """Returns a category object with the associated recieps relating to the category"""
125 category: RecipeCategory = self.mixins.get_one(category_slug, "slug")
126 return RecipeCategoryResponse.model_construct(
127 id=category.id,
128 slug=category.slug,
129 name=category.name,
130 recipes=self.repos.recipes.get_by_categories([category]),
131 )