Coverage for opt/mealie/lib/python3.12/site-packages/mealie/schema/user/user.py: 92%
205 statements
« prev ^ index » next coverage.py v7.10.6, created at 2025-11-25 15:48 +0000
« prev ^ index » next coverage.py v7.10.6, created at 2025-11-25 15:48 +0000
1from datetime import UTC, datetime, timedelta 1a
2from pathlib import Path 1a
3from typing import Annotated, Any 1a
4from uuid import UUID 1a
6from pydantic import UUID4, BaseModel, ConfigDict, Field, StringConstraints, field_validator 1a
7from sqlalchemy.orm import joinedload, selectinload 1a
8from sqlalchemy.orm.interfaces import LoaderOption 1a
10from mealie.core.config import get_app_dirs, get_app_settings 1a
11from mealie.db.models.recipe.recipe import RecipeModel 1a
12from mealie.db.models.users import User 1a
13from mealie.db.models.users.user_to_recipe import UserToRecipe 1a
14from mealie.db.models.users.users import AuthMethod, LongLiveToken 1a
15from mealie.schema._mealie import MealieModel 1a
16from mealie.schema.group.group_preferences import ReadGroupPreferences 1a
17from mealie.schema.household.webhook import CreateWebhook, ReadWebhook 1a
18from mealie.schema.response.pagination import PaginationBase 1a
20from ...db.models.group import Group 1a
21from ..recipe import CategoryBase 1a
23DEFAULT_INTEGRATION_ID = "generic" 1a
24settings = get_app_settings() 1a
27class LongLiveTokenIn(MealieModel): 1a
28 name: str 1a
29 integration_id: str = DEFAULT_INTEGRATION_ID 1a
32class LongLiveTokenOut(MealieModel): 1a
33 name: str 1a
34 id: int 1a
35 created_at: datetime | None = None 1a
36 model_config = ConfigDict(from_attributes=True) 1a
38 @classmethod 1a
39 def loader_options(cls) -> list[LoaderOption]: 1a
40 return [joinedload(LongLiveToken.user)]
43class LongLiveTokenCreateResponse(LongLiveTokenOut): 1a
44 """Should ONLY be used when creating a new token, as the token field is sensitive"""
46 token: str 1a
49class CreateToken(LongLiveTokenIn): 1a
50 user_id: UUID4 1a
51 token: str 1a
52 model_config = ConfigDict(from_attributes=True) 1a
55class DeleteTokenResponse(MealieModel): 1a
56 token_delete: str 1a
57 model_config = ConfigDict(from_attributes=True) 1a
60class ChangePassword(MealieModel): 1a
61 current_password: str = "" 1a
62 new_password: str = Field(..., min_length=8) 1a
65class GroupBase(MealieModel): 1a
66 name: Annotated[str, StringConstraints(strip_whitespace=True, min_length=1)] 1a
67 model_config = ConfigDict(from_attributes=True) 1a
70class UserRatingSummary(MealieModel): 1a
71 recipe_id: UUID4 1a
72 rating: float | None = None 1a
73 is_favorite: Annotated[bool, Field(validate_default=True)] = False 1a
75 model_config = ConfigDict(from_attributes=True) 1a
77 @field_validator("is_favorite", mode="before") 1a
78 def convert_is_favorite(cls, v: Any) -> bool: 1a
79 if v is None:
80 return False
81 else:
82 return v
85class UserRatingCreate(UserRatingSummary): 1a
86 user_id: UUID4 1a
89class UserRatingUpdate(MealieModel): 1a
90 rating: float | None = None 1a
91 is_favorite: bool | None = None 1a
94class UserRatingOut(UserRatingCreate): 1a
95 id: UUID4 1a
97 @classmethod 1a
98 def loader_options(cls) -> list[LoaderOption]: 1a
99 return [
100 joinedload(UserToRecipe.recipe).joinedload(RecipeModel.user).load_only(User.household_id, User.group_id)
101 ]
104class UserRatings[DataT: BaseModel](BaseModel): 1a
105 ratings: list[DataT] 1a
108class UserBase(MealieModel): 1a
109 id: UUID4 | None = None 1a
110 username: str | None = None 1a
111 full_name: str | None = None 1a
112 email: Annotated[str, StringConstraints(to_lower=True, strip_whitespace=True)] 1a
113 auth_method: AuthMethod = AuthMethod.MEALIE 1a
114 admin: bool = False 1a
115 group: str | None = None 1a
116 household: str | None = None 1a
117 advanced: bool = False 1a
119 can_invite: bool = False 1a
120 can_manage: bool = False 1a
121 can_manage_household: bool = False 1a
122 can_organize: bool = False 1a
123 model_config = ConfigDict( 1a
124 from_attributes=True,
125 json_schema_extra={
126 "example": {
127 "username": "ChangeMe",
128 "fullName": "Change Me",
129 "email": "changeme@example.com",
130 "group": settings.DEFAULT_GROUP,
131 "household": settings.DEFAULT_HOUSEHOLD,
132 "admin": "false",
133 }
134 },
135 )
137 @field_validator("group", mode="before") 1a
138 def convert_group_to_name(cls, v): 1a
139 if not v or isinstance(v, str): 1adcpqvwrsgxyzABtheijklmnCufoDb
140 return v
142 try: 1adcpqvwrsgxyzABtheijklmnCufoDb
143 return v.name 1adcpqvwrsgxyzABtheijklmnCufoDb
144 except AttributeError:
145 return v
147 @field_validator("household", mode="before") 1a
148 def convert_household_to_name(cls, v): 1a
149 if not v or isinstance(v, str): 1adcpqvwrsgxyzABtheijklmnCufoDb
150 return v
152 try: 1adcpqvwrsgxyzABtheijklmnCufoDb
153 return v.name 1adcpqvwrsgxyzABtheijklmnCufoDb
154 except AttributeError:
155 return v
158class UserIn(UserBase): 1a
159 username: str 1a
160 full_name: str 1a
161 password: str 1a
164class UserOut(UserBase): 1a
165 id: UUID4 1a
166 group: str 1a
167 group_id: UUID4 1a
168 group_slug: str 1a
169 household: str 1a
170 household_id: UUID4 1a
171 household_slug: str 1a
172 tokens: list[LongLiveTokenOut] | None = None 1a
173 cache_key: str 1a
174 model_config = ConfigDict(from_attributes=True) 1a
176 @property 1a
177 def is_default_user(self) -> bool: 1a
178 return self.email == settings._DEFAULT_EMAIL.strip().lower()
180 @classmethod 1a
181 def loader_options(cls) -> list[LoaderOption]: 1a
182 return [joinedload(User.group), joinedload(User.household), joinedload(User.tokens)] 1heijklmnfob
185class UserSummary(MealieModel): 1a
186 id: UUID4 1a
187 group_id: UUID4 1a
188 household_id: UUID4 1a
189 username: str 1a
190 full_name: str 1a
191 model_config = ConfigDict(from_attributes=True) 1a
194class UserPagination(PaginationBase): 1a
195 items: list[UserOut] 1a
198class UserSummaryPagination(PaginationBase): 1a
199 items: list[UserSummary] 1a
202class PrivateUser(UserOut): 1a
203 password: str 1a
204 login_attemps: int = 0 1a
205 locked_at: datetime | None = None 1a
206 model_config = ConfigDict(from_attributes=True) 1a
208 @field_validator("login_attemps", mode="before") 1a
209 @classmethod 1a
210 def none_to_zero(cls, v): 1a
211 return 0 if v is None else v 1adcpqvwrsgxyzABtheijklmnCufoDb
213 @staticmethod 1a
214 def get_directory(user_id: UUID4 | str) -> Path: 1a
215 user_dir = get_app_dirs().USER_DIR / str(user_id) 1ad
216 user_dir.mkdir(parents=True, exist_ok=True) 1ad
217 return user_dir 1ad
219 @property 1a
220 def is_locked(self) -> bool: 1a
221 if self.locked_at is None: 221 ↛ 224line 221 didn't jump to line 224 because the condition on line 221 was always true1cgef
222 return False 1cgef
224 lockout_expires_at = self.locked_at + timedelta(hours=get_app_settings().SECURITY_USER_LOCKOUT_TIME)
225 return lockout_expires_at > datetime.now(UTC)
227 def directory(self) -> Path: 1a
228 return PrivateUser.get_directory(self.id) 1ad
230 @classmethod 1a
231 def loader_options(cls) -> list[LoaderOption]: 1a
232 return [joinedload(User.group), joinedload(User.household), joinedload(User.tokens)] 1adcpqvwrsgxyzABtheijklmnCufoDb
235class UpdateGroup(GroupBase): 1a
236 id: UUID4 1a
237 name: str 1a
238 slug: str 1a
239 categories: list[CategoryBase] | None = [] 1a
241 webhooks: list[CreateWebhook] = [] 1a
244class GroupHouseholdSummary(MealieModel): 1a
245 id: UUID4 1a
246 name: str 1a
247 model_config = ConfigDict(from_attributes=True) 1a
250class GroupInDB(UpdateGroup): 1a
251 households: list[GroupHouseholdSummary] | None = None 1a
252 users: list[UserSummary] | None = None 1a
253 preferences: ReadGroupPreferences | None = None 1a
254 webhooks: list[ReadWebhook] = [] 1a
256 model_config = ConfigDict(from_attributes=True) 1a
258 @staticmethod 1a
259 def get_directory(id: UUID4) -> Path: 1a
260 group_dir = get_app_dirs().GROUPS_DIR / str(id) 1cb
261 group_dir.mkdir(parents=True, exist_ok=True) 1cb
262 return group_dir 1cb
264 @staticmethod 1a
265 def get_export_directory(id: UUID) -> Path: 1a
266 export_dir = GroupInDB.get_directory(id) / "export"
267 export_dir.mkdir(parents=True, exist_ok=True)
268 return export_dir
270 @property 1a
271 def directory(self) -> Path: 1a
272 return GroupInDB.get_directory(self.id) 1cb
274 @property 1a
275 def exports(self) -> Path: 1a
276 return GroupInDB.get_export_directory(self.id)
278 @classmethod 1a
279 def loader_options(cls) -> list[LoaderOption]: 1a
280 return [ 1cpqrsgtheijklmnufob
281 joinedload(Group.categories),
282 joinedload(Group.webhooks),
283 joinedload(Group.preferences),
284 joinedload(Group.households),
285 selectinload(Group.users).joinedload(User.group),
286 selectinload(Group.users).joinedload(User.tokens),
287 ]
290class GroupSummary(GroupBase): 1a
291 id: UUID4 1a
292 name: str 1a
293 slug: str 1a
294 preferences: ReadGroupPreferences | None = None 1a
296 @classmethod 1a
297 def loader_options(cls) -> list[LoaderOption]: 1a
298 return [
299 joinedload(Group.preferences),
300 ]
303class GroupPagination(PaginationBase): 1a
304 items: list[GroupInDB] 1a
307class LongLiveTokenInDB(CreateToken): 1a
308 id: int 1a
309 user: PrivateUser 1a
310 model_config = ConfigDict(from_attributes=True) 1a