Coverage for functions \ flipdare \ generated \ model \ user_model.py: 97%

239 statements  

« prev     ^ index     » next       coverage.py v7.13.0, created at 2026-05-08 12:22 +1000

1#!/usr/bin/env python 

2# 

3# Copyright (c) 2026 Flipdare Pty Ltd. All rights reserved. 

4# 

5# This file is part of Flipdare's proprietary software and contains 

6# confidential and copyrighted material. Unauthorised copying, 

7# modification, distribution, or use of this file is strictly 

8# prohibited without prior written permission from Flipdare Pty Ltd. 

9# 

10# This software includes third-party components licensed under MIT, 

11# BSD, and Apache 2.0 licences. See THIRD_PARTY_NOTICES for details. 

12# 

13# NOTE: THIS FILE IS AUTO GENERATED. DO NOT EDIT. 

14# 

15# Generated by codegen_models.py 

16# 

17# Modify 'codegen_models.py' 

18# and re-run the script above to update. 

19# 

20from __future__ import annotations 

21from datetime import datetime 

22from google.cloud.firestore_v1.transforms import Sentinel 

23from flipdare.core.firestore_field import FirestoreField 

24from flipdare.util.time_util import FirestoreTime 

25from typing import Any, TypedDict, cast, Unpack 

26from enum import StrEnum 

27from pydantic import Field, ConfigDict, TypeAdapter 

28from flipdare.firestore.core.app_base_model import AppBaseModel 

29from flipdare.generated.shared.model.user.auth_type import AuthType 

30from flipdare.generated.shared.model.user.app_fee_type import AppFeeType 

31from flipdare.generated.shared.model.user.user_level_type import UserLevelType 

32from flipdare.generated.shared.model.app_visibility import AppVisibility 

33from flipdare.generated.model.internal.image_model import ImageModel, ImageDict 

34from flipdare.generated.shared.model.user.video_continue_type import VideoContinueType 

35from flipdare.generated.shared.model.user.user_cache_type import UserCacheType 

36from flipdare.generated.shared.model.user.user_archive_type import UserArchiveType 

37from flipdare.generated.model.internal.view_stats_model import ViewStatsModel, ViewStatsDict 

38from flipdare.generated.model.internal.dare_stats_model import DareStatsModel, DareStatsDict 

39from flipdare.generated.model.internal.location_model import LocationModel, LocationDict 

40from flipdare.generated.model.issue.flag_model import FlagModel, FlagDict 

41from flipdare.core.change_score import ChangeScore 

42from flipdare.util.user_util import UserUtil 

43from flipdare.app_globals import string_has_alpha 

44from typing import override, Self, Annotated 

45from flipdare.generated.model.payment.stripe_customer_model import StripeCustomerModel 

46from flipdare.generated.model.payment.stripe_account_model import StripeAccountModel 

47from flipdare.util.slug_coder import SlugCoder 

48 

49# ---- prelude ----------------------------------------- 

50 

51type StripeSettingsType = Annotated[ 

52 StripeCustomerModel | StripeAccountModel, Field(discriminator="type") 

53] 

54 

55 

56class UserKeys(StrEnum): 

57 ID = "id" 

58 CREATED_AT = "created_at" 

59 UPDATED_AT = "updated_at" 

60 AUTH_TYPE = "auth_type" 

61 EMAIL = "email" 

62 SLUG_CODE = "slug_code" 

63 REPUTATION = "reputation" 

64 FEE_TYPE = "fee_type" 

65 LEVEL = "level" 

66 VISIBILITY = "visibility" 

67 RESTRICTION_ID = "restriction_id" 

68 COMPLIANCE_ID = "compliance_id" 

69 INVITE_ID = "invite_id" 

70 FACEBOOK_TOKEN = "facebook_token" 

71 FACEBOOK_ID = "facebook_id" 

72 PASSWORD = "password" 

73 PIN_CODE = "pin_code" 

74 DELETE_CODE = "delete_code" 

75 NAME = "name" 

76 DISPLAY_NAME = "display_name" 

77 DESCRIPTION = "description" 

78 AVATAR = "avatar" 

79 WEBSITE_URI = "website_uri" 

80 EMAIL_VERIFIED = "email_verified" 

81 MUST_RESET_PASSWORD = "must_reset_password" 

82 EMAIL_NOTIFS_ENABLED = "email_notifs_enabled" 

83 ENABLE_HAPTIC = "enable_haptic" 

84 UNREAD_ACTIVITY_COUNT = "unread_activity_count" 

85 VIDEO_HISTORY_COUNT = "video_history_count" 

86 NOTIFICATION_COUNT = "notification_count" 

87 ARCHIVE_COUNT = "archive_count" 

88 AUTO_PLAY_ON_SCROLL = "auto_play_on_scroll" 

89 CONTINUE_TYPE = "continue_type" 

90 AUTO_MUTE = "auto_mute" 

91 SWIPE_LEFT_TO_ARCHIVE = "swipe_left_to_archive" 

92 PROMPT_FOR_CONFIRMATION = "prompt_for_confirmation" 

93 SHOW_SYSTEM_NOTIFICATIONS = "show_system_notifications" 

94 CACHE_SIZE = "cache_size" 

95 ARCHIVE_TIME = "archive_time" 

96 VIEW_STATS = "view_stats" 

97 DARE_STATS = "dare_stats" 

98 STRIPE_SETTINGS = "stripe_settings" 

99 LOCATION = "location" 

100 TZ_STR = "tz_str" 

101 FLAGGED = "flagged" 

102 VERSION = "version" 

103 PROCESSED = "processed" 

104 ERROR_COUNT = "error_count" 

105 INVITE_PROCESSED = "invite_processed" 

106 CONTEXT_CREATED = "context_created" 

107 SEARCH_INDEXED = "search_indexed" 

108 

109 

110# !! IMPORTANT !! 

111# !! 

112# !! this should only be used in the database to query. 

113# !! 

114class UserInternalKeys(StrEnum): 

115 CREATED_AT = "created_at" 

116 UPDATED_AT = "updated_at" 

117 VERSION = "VERSION" 

118 PROCESSED = "INT_P" 

119 ERROR_COUNT = "INT_E" 

120 INVITE_PROCESSED = "INT_U_IP" 

121 CONTEXT_CREATED = "INT_U_CC" 

122 SEARCH_INDEXED = "INT_U_I" 

123 

124 

125class UserModel(AppBaseModel): 

126 """Represents a user in the system, including authentication details etc.""" 

127 

128 model_config = ConfigDict(populate_by_name=True, arbitrary_types_allowed=True) 

129 

130 id: str | None = Field(None, alias="id") 

131 created_at: FirestoreField = Field( 

132 default_factory=cast("Any", lambda: FirestoreTime.server_timestamp()) 

133 ) 

134 updated_at: FirestoreField = Field( 

135 default_factory=cast("Any", lambda: FirestoreTime.server_timestamp()) 

136 ) 

137 auth_type: AuthType 

138 email: str 

139 slug_code: str 

140 reputation: int = Field(default=50) 

141 fee_type: AppFeeType = Field(default=AppFeeType.STANDARD) 

142 level: UserLevelType = Field(default=UserLevelType.ROOKIE) 

143 visibility: AppVisibility = Field(default=AppVisibility.PUBLIC) 

144 restriction_id: str | None = None 

145 compliance_id: str | None = None 

146 invite_id: str | None = None 

147 facebook_token: str | None = None 

148 facebook_id: str | None = None 

149 password: str | None = None 

150 pin_code: str | None = None 

151 delete_code: str | None = None 

152 name: str | None = None 

153 display_name: str | None = None 

154 description: str | None = None 

155 avatar: ImageModel | None = None 

156 website_uri: str | None = None 

157 email_verified: bool = Field(default=False) 

158 must_reset_password: bool = Field(default=False) 

159 email_notifs_enabled: bool = Field(default=True) 

160 enable_haptic: bool = Field(default=True) 

161 unread_activity_count: int = Field(default=0) 

162 video_history_count: int = Field(default=0) 

163 notification_count: int = Field(default=0) 

164 archive_count: int = Field(default=0) 

165 auto_play_on_scroll: bool = Field(default=True) 

166 continue_type: VideoContinueType = Field(default=VideoContinueType.NONE) 

167 auto_mute: bool = Field(default=True) 

168 swipe_left_to_archive: bool = Field(default=True) 

169 prompt_for_confirmation: bool = Field(default=True) 

170 show_system_notifications: bool = Field(default=True) 

171 cache_size: UserCacheType = Field(default=UserCacheType.TEN_MB) 

172 archive_time: UserArchiveType = Field(default=UserArchiveType.ONE_WEEK) 

173 view_stats: ViewStatsModel = Field(default_factory=lambda: ViewStatsModel()) 

174 dare_stats: DareStatsModel = Field(default_factory=lambda: DareStatsModel()) 

175 stripe_settings: StripeSettingsType | None = None 

176 location: LocationModel | None = None 

177 tz_str: str | None = None 

178 flagged: FlagModel | None = None 

179 # Version (base internal field) 

180 version: int = Field(default=1, alias="VERSION") 

181 # Processed (base internal field) 

182 processed: bool = Field(default=False, alias="INT_P") 

183 # Error Count (base internal field) 

184 error_count: int = Field(default=0, alias="INT_E") 

185 # Invite Processed (internal field) 

186 invite_processed: bool = Field(default=False, alias="INT_U_IP") 

187 # Context Created (internal field) 

188 context_created: bool = Field(default=False, alias="INT_U_CC") 

189 # Search Indexed (internal field) 

190 search_indexed: bool = Field(default=False, alias="INT_U_I") 

191 

192 @classmethod 

193 def validate_partial(cls, **data: Unpack[UserDict]) -> dict[str, Any]: 

194 """ 

195 Uses Unpack to give you autocomplete and static warnings 

196 if you pass an invalid key or type in your code. 

197 

198 Returns a dict with Firestore field names (aliases) for use with batch.update(). 

199 """ 

200 result: dict[str, Any] = {} 

201 for k, v in data.items(): 

202 if k in cls.__pydantic_fields__: 

203 field_info = cls.__pydantic_fields__[k] 

204 validated_value = cast( 

205 "Any", 

206 TypeAdapter(field_info.annotation).validate_python(v), 

207 ) 

208 # Use alias if defined, otherwise use field name 

209 output_key = field_info.alias or k 

210 result[output_key] = validated_value 

211 return result 

212 

213 # ---- Convenience factories ----------------------------------------- 

214 

215 @classmethod 

216 def create_invite( 

217 cls, 

218 email: str, 

219 invite_id: str, 

220 pin_code: str, 

221 name: str | None = None, 

222 ) -> UserModel: 

223 """ 

224 Create user from invitation. 

225 

226 This is a specialized factory for the invite flow where users 

227 are created with minimal information. 

228 """ 

229 from flipdare.generated.model.internal.view_stats_model import ViewStatsModel 

230 from flipdare.generated.model.internal.dare_stats_model import DareStatsModel 

231 from flipdare.generated.shared.model.user.auth_type import AuthType 

232 

233 return cls( 

234 id=None, 

235 slug_code=SlugCoder().from_user_info(email=email, name=name), 

236 email=email, 

237 name=name, 

238 invite_id=invite_id, 

239 pin_code=pin_code, 

240 auth_type=AuthType.EMAIL, 

241 view_stats=ViewStatsModel(), # Empty stats 

242 dare_stats=DareStatsModel(), # Empty stats with explicit id 

243 ) 

244 

245 # ---- Convenience predicates ----------------------------------------- 

246 @property 

247 def can_share(self) -> bool: 

248 return self.visibility == AppVisibility.PUBLIC and self.flagged is None 

249 

250 @property 

251 def contact_name(self) -> str: 

252 # NOTE: this SHOULD NOT be used in search, because it can return 

253 # NOTE: an full email address which is should not be PUBLICLY searchable 

254 return UserUtil.contact_name(self.email, self.display_name, self.name) 

255 

256 @property 

257 def safe_name(self) -> str: 

258 # NOTE: this should be used in search 

259 return UserUtil.safe_name(self.email, self.display_name, self.name) 

260 

261 @property 

262 def searchable_names(self) -> list[str]: 

263 """Get safe name for search results (never shows email).""" 

264 values: list[str] = [] 

265 

266 if self.display_name and string_has_alpha(self.display_name): 

267 values.append(self.display_name) 

268 

269 if self.name and string_has_alpha(self.name): 

270 values.append(self.name) 

271 

272 return values 

273 

274 @property 

275 @override 

276 def searchable_values(self) -> list[str]: 

277 values = self.searchable_names 

278 if self.description and string_has_alpha(self.description): 

279 values.append(self.description) 

280 return values 

281 

282 @override 

283 def calculate_change_score(self, other: Self) -> float: 

284 return ChangeScore(self, other, USER_FIELD_NAMES).score 

285 

286 

287USER_FIELD_NAMES: list[str] = list(UserModel.model_fields.keys()) 

288 

289 

290class UserDict(TypedDict, total=False): 

291 id: str | None 

292 created_at: Sentinel | datetime | str 

293 updated_at: Sentinel | datetime | str 

294 auth_type: AuthType 

295 email: str 

296 slug_code: str 

297 reputation: int | None 

298 fee_type: AppFeeType | None 

299 level: UserLevelType | None 

300 visibility: AppVisibility | None 

301 restriction_id: str | None 

302 compliance_id: str | None 

303 invite_id: str | None 

304 facebook_token: str | None 

305 facebook_id: str | None 

306 password: str | None 

307 pin_code: str | None 

308 delete_code: str | None 

309 name: str | None 

310 display_name: str | None 

311 description: str | None 

312 avatar: ImageDict | None 

313 website_uri: str | None 

314 email_verified: bool | None 

315 must_reset_password: bool | None 

316 email_notifs_enabled: bool | None 

317 enable_haptic: bool | None 

318 unread_activity_count: int | None 

319 video_history_count: int | None 

320 notification_count: int | None 

321 archive_count: int | None 

322 auto_play_on_scroll: bool | None 

323 continue_type: VideoContinueType | None 

324 auto_mute: bool | None 

325 swipe_left_to_archive: bool | None 

326 prompt_for_confirmation: bool | None 

327 show_system_notifications: bool | None 

328 cache_size: UserCacheType | None 

329 archive_time: UserArchiveType | None 

330 view_stats: ViewStatsDict 

331 dare_stats: DareStatsDict 

332 stripe_settings: StripeSettingsType | None 

333 location: LocationDict | None 

334 tz_str: str | None 

335 flagged: FlagDict | None 

336 VERSION: int | None 

337 INT_P: bool | None 

338 INT_E: int | None 

339 INT_U_IP: bool | None 

340 INT_U_CC: bool | None 

341 INT_U_I: bool | None