Coverage for opt/mealie/lib/python3.12/site-packages/mealie/db/models/users/users.py: 83%
125 statements
« prev ^ index » next coverage.py v7.10.6, created at 2025-11-25 17:29 +0000
« prev ^ index » next coverage.py v7.10.6, created at 2025-11-25 17:29 +0000
1import enum 1a
2from datetime import datetime 1a
3from typing import TYPE_CHECKING, Optional 1a
5from pydantic import ConfigDict 1a
6from sqlalchemy import Boolean, Enum, ForeignKey, Integer, String, orm, select 1a
7from sqlalchemy.ext.associationproxy import AssociationProxy, association_proxy 1a
8from sqlalchemy.ext.hybrid import hybrid_property 1a
9from sqlalchemy.orm import Mapped, Session, mapped_column 1a
11from mealie.core.config import get_app_settings 1a
12from mealie.db.models._model_utils.auto_init import auto_init 1a
13from mealie.db.models._model_utils.datetime import NaiveDateTime 1a
14from mealie.db.models._model_utils.guid import GUID 1a
16from .._model_base import BaseMixins, SqlAlchemyBase 1a
17from .user_to_recipe import UserToRecipe 1a
19if TYPE_CHECKING: 19 ↛ 20line 19 didn't jump to line 20 because the condition on line 19 was never true1a
20 from ..group import Group
21 from ..household import Household
22 from ..household.mealplan import GroupMealPlan
23 from ..household.shopping_list import ShoppingList
24 from ..recipe import RecipeComment, RecipeModel, RecipeTimelineEvent
25 from .password_reset import PasswordResetModel
28class LongLiveToken(SqlAlchemyBase, BaseMixins): 1a
29 __tablename__ = "long_live_tokens" 1a
30 name: Mapped[str] = mapped_column(String, nullable=False) 1a
31 token: Mapped[str] = mapped_column(String, nullable=False, index=True) 1a
33 user_id: Mapped[GUID | None] = mapped_column(GUID, ForeignKey("users.id"), index=True) 1a
34 user: Mapped[Optional["User"]] = orm.relationship("User") 1a
36 group_id: AssociationProxy[GUID] = association_proxy("user", "group_id") 1a
37 household_id: AssociationProxy[GUID] = association_proxy("user", "household_id") 1a
39 def __init__(self, name, token, user_id, **_) -> None: 1a
40 self.name = name 1rsbtuvwcxyzABCDEFGHIJKLd
41 self.token = token 1rsbtuvwcxyzABCDEFGHIJKLd
42 self.user_id = user_id 1rsbtuvwcxyzABCDEFGHIJKLd
45class AuthMethod(enum.Enum): 1a
46 MEALIE = "Mealie" 1a
47 LDAP = "LDAP" 1a
48 OIDC = "OIDC" 1a
51class User(SqlAlchemyBase, BaseMixins): 1a
52 __tablename__ = "users" 1a
53 id: Mapped[GUID] = mapped_column(GUID, primary_key=True, default=GUID.generate) 1a
54 full_name: Mapped[str | None] = mapped_column(String, index=True) 1a
55 username: Mapped[str | None] = mapped_column(String, index=True, unique=True) 1a
56 email: Mapped[str | None] = mapped_column(String, unique=True, index=True) 1a
57 password: Mapped[str | None] = mapped_column(String) 1a
58 auth_method: Mapped[Enum[AuthMethod]] = mapped_column(Enum(AuthMethod), default=AuthMethod.MEALIE) 1a
59 admin: Mapped[bool | None] = mapped_column(Boolean, default=False) 1a
60 advanced: Mapped[bool | None] = mapped_column(Boolean, default=False) 1a
62 group_id: Mapped[GUID] = mapped_column(GUID, ForeignKey("groups.id"), nullable=False, index=True) 1a
63 group: Mapped["Group"] = orm.relationship("Group", back_populates="users") 1a
64 household_id: Mapped[GUID | None] = mapped_column(GUID, ForeignKey("households.id"), nullable=True, index=True) 1a
65 household: Mapped["Household"] = orm.relationship("Household", back_populates="users") 1a
67 cache_key: Mapped[str | None] = mapped_column(String, default="1234") 1a
68 login_attemps: Mapped[int | None] = mapped_column(Integer, default=0) 1a
69 locked_at: Mapped[datetime | None] = mapped_column(NaiveDateTime, default=None) 1a
71 # Group Permissions
72 can_manage_household: Mapped[bool | None] = mapped_column(Boolean, default=False) 1a
73 can_manage: Mapped[bool | None] = mapped_column(Boolean, default=False) 1a
74 can_invite: Mapped[bool | None] = mapped_column(Boolean, default=False) 1a
75 can_organize: Mapped[bool | None] = mapped_column(Boolean, default=False) 1a
77 sp_args = { 1a
78 "back_populates": "user",
79 "cascade": "all, delete, delete-orphan",
80 "single_parent": True,
81 }
83 tokens: Mapped[list[LongLiveToken]] = orm.relationship(LongLiveToken, **sp_args) 1a
84 comments: Mapped[list["RecipeComment"]] = orm.relationship("RecipeComment", **sp_args) 1a
85 recipe_timeline_events: Mapped[list["RecipeTimelineEvent"]] = orm.relationship("RecipeTimelineEvent", **sp_args) 1a
86 password_reset_tokens: Mapped[list["PasswordResetModel"]] = orm.relationship("PasswordResetModel", **sp_args) 1a
88 owned_recipes_id: Mapped[GUID | None] = mapped_column(GUID, ForeignKey("recipes.id")) 1a
89 owned_recipes: Mapped[Optional["RecipeModel"]] = orm.relationship( 1a
90 "RecipeModel", single_parent=True, foreign_keys=[owned_recipes_id]
91 )
92 mealplans: Mapped[Optional["GroupMealPlan"]] = orm.relationship( 1a
93 "GroupMealPlan", order_by="GroupMealPlan.date", **sp_args
94 )
95 shopping_lists: Mapped[Optional["ShoppingList"]] = orm.relationship("ShoppingList", **sp_args) 1a
96 rated_recipes: Mapped[list["RecipeModel"]] = orm.relationship( 1a
97 "RecipeModel",
98 secondary=UserToRecipe.__tablename__,
99 back_populates="rated_by",
100 overlaps="recipe,favorited_by,favorited_recipes",
101 )
102 favorite_recipes: Mapped[list["RecipeModel"]] = orm.relationship( 1a
103 "RecipeModel",
104 secondary=UserToRecipe.__tablename__,
105 primaryjoin="and_(User.id==UserToRecipe.user_id, UserToRecipe.is_favorite==True)",
106 back_populates="favorited_by",
107 overlaps="recipe,rated_by,rated_recipes",
108 )
109 model_config = ConfigDict( 1a
110 exclude={
111 "password",
112 "admin",
113 "can_manage_household",
114 "can_manage",
115 "can_invite",
116 "can_organize",
117 "group",
118 "household",
119 }
120 )
122 @hybrid_property 1a
123 def group_slug(self) -> str: 1a
124 return self.group.slug 2a e f M N O g P Q h R S i T U V W X Y Z 0 1 2 3 4 r 5 6 7 8 j 9 ! # $ % ' ( ) * + k , - . / : ; = l ? @ [ ] ^ _ ` { | m } ~ abbbcbdbn ebfbgbhbibjbkbo lbmbnbs b obt u v w pbc x qby z A B rbp sbC tbD E ubF vbq G H I J K wbxbL ybzbAbd
126 @hybrid_property 1a
127 def household_slug(self) -> str: 1a
128 return self.household.slug 2a e f M N O g P Q h R S i T U V W X Y Z 0 1 2 3 4 r 5 6 7 8 j 9 ! # $ % ' ( ) * + k , - . / : ; = l ? @ [ ] ^ _ ` { | m } ~ abbbcbdbn ebfbgbhbibjbkbo lbmbnbs b obt u v w pbc x qby z A B rbp sbC tbD E ubF vbq G H I J K wbxbL ybzbAbd
130 @auto_init() 1a
131 def __init__( 1a
132 self, session: Session, full_name, password, group: str | None = None, household: str | None = None, **kwargs
133 ) -> None:
134 if group is None or household is None: 134 ↛ 135line 134 didn't jump to line 135 because the condition on line 134 was never true1ae
135 settings = get_app_settings()
136 group = group or settings.DEFAULT_GROUP
137 household = household or settings.DEFAULT_HOUSEHOLD
139 from mealie.db.models.group import Group 1ae
140 from mealie.db.models.household import Household 1ae
142 self.group = session.execute(select(Group).filter(Group.name == group)).scalars().one_or_none() 1ae
143 if self.group: 143 ↛ 152line 143 didn't jump to line 152 because the condition on line 143 was always true1ae
144 self.household = ( 1ae
145 session.execute(
146 select(Household).filter(Household.name == household, Household.group_id == self.group.id)
147 )
148 .scalars()
149 .one_or_none()
150 )
151 else:
152 self.household = None
154 if self.group is None: 154 ↛ 155line 154 didn't jump to line 155 because the condition on line 154 was never true1ae
155 raise ValueError(f"Group {group} does not exist; cannot create user")
156 if self.household is None: 156 ↛ 157line 156 didn't jump to line 157 because the condition on line 156 was never true1ae
157 raise ValueError(
158 f'Household "{household}" does not exist on group '
159 f'"{self.group.name}" ({self.group.id}); cannot create user'
160 )
162 self.rated_recipes = [] 1ae
164 self.password = password 1ae
166 if self.username is None: 166 ↛ 167line 166 didn't jump to line 167 because the condition on line 166 was never true1ae
167 self.username = full_name
169 self._set_permissions(**kwargs) 1ae
171 @auto_init() 1a
172 def update(self, session: Session, full_name, email, group, household, username, **kwargs): 1a
173 self.username = username 1fghijklmnobcpqd
174 self.full_name = full_name 1fghijklmnobcpqd
175 self.email = email 1fghijklmnobcpqd
177 from mealie.db.models.group import Group 1fghijklmnobcpqd
178 from mealie.db.models.household import Household 1fghijklmnobcpqd
180 self.group = session.execute(select(Group).filter(Group.name == group)).scalars().one_or_none() 1fghijklmnobcpqd
181 if self.group: 181 ↛ 190line 181 didn't jump to line 190 because the condition on line 181 was always true1fghijklmnobcpqd
182 self.household = ( 1fghijklmnobcpqd
183 session.execute(
184 select(Household).filter(Household.name == household, Household.group_id == self.group.id)
185 )
186 .scalars()
187 .one_or_none()
188 )
189 else:
190 self.household = None
192 if self.username is None: 192 ↛ 193line 192 didn't jump to line 193 because the condition on line 192 was never true1fghijklmnobcpqd
193 self.username = full_name
195 self._set_permissions(**kwargs) 1fghijklmnobcpqd
197 def update_password(self, password): 1a
198 self.password = password
200 def _set_permissions( 1a
201 self, admin, can_manage_household=False, can_manage=False, can_invite=False, can_organize=False, **_
202 ):
203 """Set user permissions based on the admin flag and the passed in kwargs
205 Args:
206 admin (bool):
207 can_manage_household (bool):
208 can_manage (bool):
209 can_invite (bool):
210 can_organize (bool):
211 """
212 self.admin = admin 1aefghijklmnobcpqd
213 if self.admin: 1aefghijklmnobcpqd
214 self.can_manage_household = True 1a
215 self.can_manage = True 1a
216 self.can_invite = True 1a
217 self.can_organize = True 1a
218 self.advanced = True 1a
219 else:
220 self.can_manage_household = can_manage_household 1efghijklmnobcpqd
221 self.can_manage = can_manage 1efghijklmnobcpqd
222 self.can_invite = can_invite 1efghijklmnobcpqd
223 self.can_organize = can_organize 1efghijklmnobcpqd