Coverage for functions \ flipdare \ firestore \ context \ group_context.py: 63%
130 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 typing import TYPE_CHECKING, Any, override
17from flipdare.app_log import LOG
18from flipdare.app_types import DatabaseDict
19from flipdare.constants import MAX_SEARCH_GROUP_MEMBERS
20from flipdare.firestore import GroupDb
21from flipdare.firestore.context._model_context import ModelContext
22from flipdare.firestore.context._model_context_factory import ModelContextFactory
23from flipdare.generated.model.group_model import GroupModel
24from flipdare.wrapper import (
25 GroupMemberWrapper,
26 GroupWrapper,
27 UserWrapper,
28)
30if TYPE_CHECKING:
31 from flipdare.manager.db_manager import DbManager
33__all__ = ["GroupContextFactory", "GroupContext"]
36class GroupContextFactory(ModelContextFactory[GroupWrapper, "GroupContext"]):
38 def __init__(
39 self, db_manager: DbManager | None = None, limit: int = MAX_SEARCH_GROUP_MEMBERS
40 ) -> None:
41 super().__init__(db_manager=db_manager)
42 self._limit = limit
44 @property
45 def limit(self) -> int:
46 return self._limit
48 @override
49 def create(self, obj: Any) -> GroupContext | None:
50 if isinstance(obj, GroupContext):
51 return obj
52 if isinstance(obj, GroupWrapper):
53 return self._from_model(obj)
54 if isinstance(obj, str):
55 return self._from_id(obj)
56 obj_data: DatabaseDict = obj # set explicity type for type checkers.
57 return self._from_data(obj_data)
59 @override
60 def _from_id(self, doc_id: str) -> GroupContext | None:
61 group_db = self.group_db
62 group: GroupWrapper | None = None
63 try:
64 group = group_db.get(doc_id)
65 if group is None:
66 # need at least the group
67 LOG().error(f"Group {doc_id} not found in db.")
68 return None
70 owner: UserWrapper | None = self.get_user(group.model.uid)
71 return GroupContext(group=group, group_db=group_db, owner=owner, limit=self.limit)
72 except Exception:
73 LOG().error(f"Error retrieving group {doc_id} from db.")
74 return None
76 @override
77 def _from_data(self, data: DatabaseDict) -> GroupContext | None:
78 try:
79 group = GroupWrapper.from_dict(data)
80 return self._from_model(group)
81 except Exception:
82 return None
84 @override
85 def _from_model(self, model: GroupWrapper | GroupModel) -> GroupContext | None:
86 owner = self.get_user(model.uid) # Returns UserWrapper | None
88 if isinstance(model, GroupModel):
89 model = GroupWrapper.from_model(model)
91 return GroupContext(
92 group=model,
93 group_db=self.group_db,
94 owner=owner,
95 limit=self.limit,
96 )
99class GroupContext(ModelContext):
100 def __init__(
101 self,
102 group: GroupWrapper,
103 owner: UserWrapper | None = None,
104 group_db: GroupDb | None = None,
105 members: list[GroupMemberWrapper] | None = None,
106 limit: int = MAX_SEARCH_GROUP_MEMBERS,
107 ) -> None:
108 self._group = group
109 self._owner = owner
110 self._group_db = group_db
111 self._limit = limit
112 self._members = members
113 # Call super().__init__() LAST - it calls validate()
114 super().__init__()
116 @property
117 def group_db(self) -> GroupDb:
119 from flipdare.services import get_group_db
121 if self._group_db is None:
122 self._group_db = get_group_db()
123 return self._group_db
125 @property
126 def group(self) -> GroupWrapper:
127 self._require_valid("access group")
128 return self._group
130 @property
131 def owner(self) -> UserWrapper:
132 self._require_valid("access owner")
133 return self._owner # type: ignore
135 @property
136 def group_id(self) -> str:
137 self._require_valid("access group_id")
138 assert self._group is not None # for mypy
139 return self._group.doc_id
141 def members(self) -> list[GroupMemberWrapper] | None:
142 if not self.validate():
143 raise ValueError("Cannot get members from invalid GroupContext.")
145 if self._members is not None:
146 return self._members
148 try:
149 members = self.group_db.get_members(group_id=self.group_id, limit=self._limit)
150 self._members = members
151 return self._members
152 except Exception:
153 LOG().error(f"Error retrieving members for group {self.group_id} from db.")
154 return None
156 def users(self) -> list[UserWrapper] | None:
157 if not self.validate():
158 raise ValueError("Cannot get users from invalid GroupContext.")
160 try:
161 return self.group_db.get_users(group_id=self.group_id, limit=self._limit)
162 except Exception:
163 LOG().error(f"Error retrieving users for group {self.group_id} from db.")
164 return None
166 def __len__(self) -> int:
167 """Return number of members in the group."""
168 members = self.members()
169 if members is None:
170 return 0
171 return len(members)
173 def __getitem__(self, index: int) -> GroupMemberWrapper | None:
174 """Get member at given index."""
175 members = self.members()
176 if members is None:
177 return None
178 if index < 0 or index >= len(members):
179 return None
180 return members[index]
182 @property
183 @override
184 def doc_id(self) -> str:
185 return self.group.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._group) and self._is_model_valid(self._owner)
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._group, "group"):
198 errors.append(err)
199 if err := self._validate_model(self._owner, "owner"):
200 errors.append(err)
201 return errors