Coverage for functions \ flipdare \ firestore \ context \ dare_context.py: 68%
180 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# Copyright (c) 2026 Flipdare Pty Ltd. All rights reserved.
3#
4# This file is part of Flipdare's proprietary software and contains
5# confidential and copyrighted material. Unauthorised copying,
6# modification, distribution, or use of this file is strictly
7# prohibited without prior written permission from Flipdare Pty Ltd.
8#
9# This software includes third-party components licensed under MIT,
10# BSD, and Apache 2.0 licences. See THIRD_PARTY_NOTICES for details.
11#
13from __future__ import annotations
15from abc import abstractmethod
16from typing import TYPE_CHECKING, Any, TypeVar, override
18from flipdare.app_log import LOG
19from flipdare.app_types import DatabaseDict
20from flipdare.firestore.context._model_context import ModelContext
21from flipdare.firestore.context._model_context_factory import ModelContextFactory
22from flipdare.generated.model.dare_model import DareModel
23from flipdare.wrapper import (
24 DareWrapper,
25 GroupWrapper,
26 UserWrapper,
27)
29if TYPE_CHECKING:
30 from flipdare.manager.db_manager import DbManager
32__all__ = ["DareContextFactory", "DareContext", "UserDareContext", "GroupDareContext"]
34T = TypeVar("T", bound="DareContext")
37class DareContextFactory(ModelContextFactory[DareWrapper, "DareContext"]):
38 """Factory for creating DareContext instances (UserDareContext or GroupDareContext)."""
40 def __init__(self, db_manager: DbManager | None = None) -> None:
41 super().__init__(db_manager=db_manager)
43 @override
44 def create(self, obj: Any) -> DareContext | None:
45 if isinstance(obj, DareContext):
46 return obj
47 if isinstance(obj, DareWrapper):
48 return self._from_model(obj)
49 if isinstance(obj, str):
50 return self._from_id(obj)
51 obj_data: DatabaseDict = obj # set explicity type for type checkers.
52 return self._from_data(obj_data)
54 @override
55 def _from_id(self, doc_id: str) -> DareContext | None:
56 dare_db = self.dare_db
57 try:
58 dare = dare_db.get(doc_id)
59 if dare is None:
60 LOG().error(f"Dare {doc_id} not found in db.")
61 return None
63 return self._from_model(dare.model) # Unwrap before passing to _from_model
64 except Exception:
65 LOG().error(f"Error retrieving dare {doc_id} from db.")
66 return None
68 @override
69 def _from_data(self, data: DatabaseDict) -> DareContext | None:
70 wrapper: DareWrapper | None = None
71 try:
72 wrapper = DareWrapper.from_dict(data)
73 except Exception:
74 LOG().error(f"Error parsing dare data into model: {data}")
75 return None
77 return self._from_model(wrapper)
79 @override
80 def _from_model(self, model: DareWrapper | DareModel) -> DareContext | None:
81 user_db = self.user_db
82 group_db = self.group_db
84 from_uid = model.from_uid
85 obj_id = model.obj_id
87 doc_id = model.doc_id if isinstance(model, DareWrapper) else model.id
88 if doc_id is None:
89 LOG().error(f"Dare model is missing doc_id: {model}")
90 return None
92 # db.get() already returns UserWrapper | None
93 from_user = user_db.get(from_uid)
94 if isinstance(model, DareModel):
95 model = DareWrapper.from_model(model)
97 to_user = user_db.get(obj_id)
99 if not model.is_group_dare:
100 if to_user is None:
101 msg = f"To user {obj_id} for dare {model.doc_id} not found in db."
102 LOG().error(msg)
103 return None
105 return UserDareContext(from_user=from_user, to_user=to_user, dare=model)
106 else:
107 group_model = group_db.get(obj_id)
108 if group_model is None:
109 msg = f"To group {obj_id} for dare {model.doc_id} not found in db."
110 LOG().error(msg)
111 return None
113 return GroupDareContext(
114 from_user=from_user,
115 to_group=group_model,
116 dare=model,
117 to_user=to_user,
118 )
121class DareContext(ModelContext):
122 """Abstract base context for dare relationships (user-to-user or user-to-group)."""
124 def __init__(
125 self,
126 from_user: UserWrapper | None = None,
127 dare: DareWrapper | None = None,
128 ) -> None:
129 self._from_user = from_user
130 self._dare = dare
131 # Call super().__init__() LAST - it calls validate()
132 super().__init__()
134 @property
135 @override
136 def doc_id(self) -> str:
137 return self.dare.doc_id
139 @property
140 @abstractmethod
141 def to_id(self) -> str: ...
143 @property
144 @abstractmethod
145 def to_obj(self) -> UserWrapper | GroupWrapper: ...
147 @property
148 def is_group_dare(self) -> bool:
149 self._require_valid("determine is_group_dare")
150 return self.dare.model.is_group_dare
152 @property
153 def short_description(self) -> str:
154 self._require_valid("generate short description")
155 to_str: str
156 if isinstance(self.to_obj, GroupWrapper):
157 to_str = self.to_obj.name
158 else:
159 to_str = self.to_obj.model.safe_name
161 return f"Dare '{self.dare.model.title}' from {self.from_user.model.safe_name} to {to_str}"
163 @property
164 def from_user(self) -> UserWrapper:
165 self._require_valid("access from_user")
166 assert self._from_user is not None # narrowing
167 return self._from_user
169 @property
170 def from_id(self) -> str:
171 self._require_valid("access from_id")
172 assert self._from_user is not None # narrowing
173 return self._from_user.doc_id
175 @property
176 def dare(self) -> DareWrapper:
177 self._require_valid("access dare")
178 assert self._dare is not None # narrowing
179 return self._dare
181 @property
182 def dare_id(self) -> str:
183 self._require_valid("access dare_id")
184 assert self._dare is not None # narrowing
185 return self._dare.doc_id
187 @override
188 def validate(self) -> bool:
189 """Validate that all required models exist and have doc_ids."""
190 return self._is_model_valid(self._from_user) and self._is_model_valid(self._dare)
192 @property
193 @override
194 def _error_messages(self) -> list[str]:
195 """Build list of validation errors."""
196 errors: list[str] = []
197 if err := self._validate_model(self._from_user, "from_user"):
198 errors.append(err)
199 if err := self._validate_model(self._dare, "dare"):
200 errors.append(err)
201 return errors
204class UserDareContext(DareContext):
205 """Context for user-to-user dare relationships."""
207 def __init__(
208 self,
209 from_user: UserWrapper | None = None,
210 to_user: UserWrapper | None = None,
211 dare: DareWrapper | None = None,
212 ) -> None:
213 self._to_user = to_user
214 # Call parent __init__ after setting _to_user so validation can check it
215 super().__init__(from_user=from_user, dare=dare)
217 @property
218 @override
219 def to_id(self) -> str:
220 self._require_valid("access to_id")
221 assert self._to_user is not None # narrowing
222 return self._to_user.doc_id
224 @property
225 @override
226 def to_obj(self) -> UserWrapper:
227 self._require_valid("access to_user")
228 assert self._to_user is not None # narrowing
229 return self._to_user
231 @override
232 def validate(self) -> bool:
233 """Validate that all required models exist and have doc_ids."""
234 return super().validate() and self._is_model_valid(self._to_user)
236 @property
237 @override
238 def _error_messages(self) -> list[str]:
239 """Build list of validation errors."""
240 errors = super()._error_messages
241 if err := self._validate_model(self._to_user, "to_user"):
242 errors.append(err)
243 return errors
246class GroupDareContext(DareContext):
247 """Context for user-to-group dare relationships."""
249 def __init__(
250 self,
251 from_user: UserWrapper | None = None,
252 to_group: GroupWrapper | None = None,
253 # i.e. the user that has accepted/completed the group dare
254 to_user: UserWrapper | None = None,
255 dare: DareWrapper | None = None,
256 ) -> None:
257 self._to_group = to_group
258 self._to_user = to_user
260 # Call parent __init__ after setting _to_group so validation can check it
261 super().__init__(from_user=from_user, dare=dare)
263 @property
264 @override
265 def to_id(self) -> str:
266 self._require_valid("access to_id")
267 assert self._to_group is not None # narrowing
268 return self._to_group.doc_id
270 @property
271 @override
272 def to_obj(self) -> GroupWrapper:
273 self._require_valid("access to_group")
274 assert self._to_group is not None # narrowing
275 return self._to_group
277 @property
278 def to_user(self) -> UserWrapper | None:
279 """The user that has accepted/completed the group dare, if applicable."""
280 self._require_valid("access to_user")
281 return self._to_user
283 @override
284 def validate(self) -> bool:
285 """Validate that all required models exist and have doc_ids."""
286 return super().validate() and self._is_model_valid(self._to_group)
288 @property
289 @override
290 def _error_messages(self) -> list[str]:
291 """Build list of validation errors."""
292 errors = super()._error_messages
293 if err := self._validate_model(self._to_group, "to_group"):
294 errors.append(err)
295 return errors