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 15:48 +0000

1import enum 1a

2from datetime import datetime 1a

3from typing import TYPE_CHECKING, Optional 1a

4 

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

10 

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

15 

16from .._model_base import BaseMixins, SqlAlchemyBase 1a

17from .user_to_recipe import UserToRecipe 1a

18 

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 

26 

27 

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

32 

33 user_id: Mapped[GUID | None] = mapped_column(GUID, ForeignKey("users.id"), index=True) 1a

34 user: Mapped[Optional["User"]] = orm.relationship("User") 1a

35 

36 group_id: AssociationProxy[GUID] = association_proxy("user", "group_id") 1a

37 household_id: AssociationProxy[GUID] = association_proxy("user", "household_id") 1a

38 

39 def __init__(self, name, token, user_id, **_) -> None: 1a

40 self.name = name 1ghbijklmnopcqr

41 self.token = token 1ghbijklmnopcqr

42 self.user_id = user_id 1ghbijklmnopcqr

43 

44 

45class AuthMethod(enum.Enum): 1a

46 MEALIE = "Mealie" 1a

47 LDAP = "LDAP" 1a

48 OIDC = "OIDC" 1a

49 

50 

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

61 

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

66 

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

70 

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

76 

77 sp_args = { 1a

78 "back_populates": "user", 

79 "cascade": "all, delete, delete-orphan", 

80 "single_parent": True, 

81 } 

82 

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

87 

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 ) 

121 

122 @hybrid_property 1a

123 def group_slug(self) -> str: 1a

124 return self.group.slug 1adestuvwxfyzABCghbijklmnopcqDr

125 

126 @hybrid_property 1a

127 def household_slug(self) -> str: 1a

128 return self.household.slug 1adestuvwxfyzABCghbijklmnopcqDr

129 

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 true1ad

135 settings = get_app_settings() 

136 group = group or settings.DEFAULT_GROUP 

137 household = household or settings.DEFAULT_HOUSEHOLD 

138 

139 from mealie.db.models.group import Group 1ad

140 from mealie.db.models.household import Household 1ad

141 

142 self.group = session.execute(select(Group).filter(Group.name == group)).scalars().one_or_none() 1ad

143 if self.group: 143 ↛ 152line 143 didn't jump to line 152 because the condition on line 143 was always true1ad

144 self.household = ( 1ad

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 

153 

154 if self.group is None: 154 ↛ 155line 154 didn't jump to line 155 because the condition on line 154 was never true1ad

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 true1ad

157 raise ValueError( 

158 f'Household "{household}" does not exist on group ' 

159 f'"{self.group.name}" ({self.group.id}); cannot create user' 

160 ) 

161 

162 self.rated_recipes = [] 1ad

163 

164 self.password = password 1ad

165 

166 if self.username is None: 166 ↛ 167line 166 didn't jump to line 167 because the condition on line 166 was never true1ad

167 self.username = full_name 

168 

169 self._set_permissions(**kwargs) 1ad

170 

171 @auto_init() 1a

172 def update(self, session: Session, full_name, email, group, household, username, **kwargs): 1a

173 self.username = username 1efbc

174 self.full_name = full_name 1efbc

175 self.email = email 1efbc

176 

177 from mealie.db.models.group import Group 1efbc

178 from mealie.db.models.household import Household 1efbc

179 

180 self.group = session.execute(select(Group).filter(Group.name == group)).scalars().one_or_none() 1efbc

181 if self.group: 181 ↛ 190line 181 didn't jump to line 190 because the condition on line 181 was always true1efbc

182 self.household = ( 1efbc

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 

191 

192 if self.username is None: 192 ↛ 193line 192 didn't jump to line 193 because the condition on line 192 was never true1efbc

193 self.username = full_name 

194 

195 self._set_permissions(**kwargs) 1efbc

196 

197 def update_password(self, password): 1a

198 self.password = password 

199 

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 

204 

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 1adefbc

213 if self.admin: 1adefbc

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 1defbc

221 self.can_manage = can_manage 1defbc

222 self.can_invite = can_invite 1defbc

223 self.can_organize = can_organize 1defbc