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
« 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
49# ---- prelude -----------------------------------------
51type StripeSettingsType = Annotated[
52 StripeCustomerModel | StripeAccountModel, Field(discriminator="type")
53]
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"
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"
125class UserModel(AppBaseModel):
126 """Represents a user in the system, including authentication details etc."""
128 model_config = ConfigDict(populate_by_name=True, arbitrary_types_allowed=True)
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")
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.
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
213 # ---- Convenience factories -----------------------------------------
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.
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
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 )
245 # ---- Convenience predicates -----------------------------------------
246 @property
247 def can_share(self) -> bool:
248 return self.visibility == AppVisibility.PUBLIC and self.flagged is None
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)
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)
261 @property
262 def searchable_names(self) -> list[str]:
263 """Get safe name for search results (never shows email)."""
264 values: list[str] = []
266 if self.display_name and string_has_alpha(self.display_name):
267 values.append(self.display_name)
269 if self.name and string_has_alpha(self.name):
270 values.append(self.name)
272 return values
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
282 @override
283 def calculate_change_score(self, other: Self) -> float:
284 return ChangeScore(self, other, USER_FIELD_NAMES).score
287USER_FIELD_NAMES: list[str] = list(UserModel.model_fields.keys())
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