geek-cafe-saas-sdk 0.6.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of geek-cafe-saas-sdk might be problematic. Click here for more details.
- geek_cafe_saas_sdk/__init__.py +9 -0
- geek_cafe_saas_sdk/core/__init__.py +11 -0
- geek_cafe_saas_sdk/core/audit_mixin.py +33 -0
- geek_cafe_saas_sdk/core/error_codes.py +132 -0
- geek_cafe_saas_sdk/core/service_errors.py +19 -0
- geek_cafe_saas_sdk/core/service_result.py +121 -0
- geek_cafe_saas_sdk/decorators/__init__.py +64 -0
- geek_cafe_saas_sdk/decorators/auth.py +373 -0
- geek_cafe_saas_sdk/decorators/core.py +358 -0
- geek_cafe_saas_sdk/domains/__init__.py +0 -0
- geek_cafe_saas_sdk/domains/analytics/__init__.py +0 -0
- geek_cafe_saas_sdk/domains/analytics/handlers/__init__.py +0 -0
- geek_cafe_saas_sdk/domains/analytics/models/__init__.py +9 -0
- geek_cafe_saas_sdk/domains/analytics/models/website_analytics.py +219 -0
- geek_cafe_saas_sdk/domains/analytics/models/website_analytics_summary.py +220 -0
- geek_cafe_saas_sdk/domains/analytics/services/__init__.py +11 -0
- geek_cafe_saas_sdk/domains/analytics/services/website_analytics_service.py +232 -0
- geek_cafe_saas_sdk/domains/analytics/services/website_analytics_summary_service.py +212 -0
- geek_cafe_saas_sdk/domains/analytics/services/website_analytics_tally_service.py +610 -0
- geek_cafe_saas_sdk/domains/auth/__init__.py +0 -0
- geek_cafe_saas_sdk/domains/auth/handlers/__init__.py +0 -0
- geek_cafe_saas_sdk/domains/auth/handlers/users/create/app.py +41 -0
- geek_cafe_saas_sdk/domains/auth/handlers/users/delete/app.py +41 -0
- geek_cafe_saas_sdk/domains/auth/handlers/users/get/app.py +39 -0
- geek_cafe_saas_sdk/domains/auth/handlers/users/list/app.py +36 -0
- geek_cafe_saas_sdk/domains/auth/handlers/users/update/app.py +44 -0
- geek_cafe_saas_sdk/domains/auth/models/__init__.py +13 -0
- geek_cafe_saas_sdk/domains/auth/models/permission.py +134 -0
- geek_cafe_saas_sdk/domains/auth/models/resource_permission.py +245 -0
- geek_cafe_saas_sdk/domains/auth/models/role.py +213 -0
- geek_cafe_saas_sdk/domains/auth/models/user.py +285 -0
- geek_cafe_saas_sdk/domains/auth/services/__init__.py +16 -0
- geek_cafe_saas_sdk/domains/auth/services/authorization_service.py +376 -0
- geek_cafe_saas_sdk/domains/auth/services/permission_registry.py +464 -0
- geek_cafe_saas_sdk/domains/auth/services/resource_permission_service.py +408 -0
- geek_cafe_saas_sdk/domains/auth/services/user_service.py +274 -0
- geek_cafe_saas_sdk/domains/communities/__init__.py +0 -0
- geek_cafe_saas_sdk/domains/communities/handlers/__init__.py +0 -0
- geek_cafe_saas_sdk/domains/communities/handlers/communities/create/app.py +41 -0
- geek_cafe_saas_sdk/domains/communities/handlers/communities/delete/app.py +41 -0
- geek_cafe_saas_sdk/domains/communities/handlers/communities/get/app.py +39 -0
- geek_cafe_saas_sdk/domains/communities/handlers/communities/list/app.py +36 -0
- geek_cafe_saas_sdk/domains/communities/handlers/communities/update/app.py +44 -0
- geek_cafe_saas_sdk/domains/communities/models/__init__.py +6 -0
- geek_cafe_saas_sdk/domains/communities/models/community.py +326 -0
- geek_cafe_saas_sdk/domains/communities/models/community_member.py +227 -0
- geek_cafe_saas_sdk/domains/communities/services/__init__.py +6 -0
- geek_cafe_saas_sdk/domains/communities/services/community_member_service.py +412 -0
- geek_cafe_saas_sdk/domains/communities/services/community_service.py +479 -0
- geek_cafe_saas_sdk/domains/events/__init__.py +0 -0
- geek_cafe_saas_sdk/domains/events/handlers/__init__.py +0 -0
- geek_cafe_saas_sdk/domains/events/handlers/attendees/app.py +67 -0
- geek_cafe_saas_sdk/domains/events/handlers/cancel/app.py +66 -0
- geek_cafe_saas_sdk/domains/events/handlers/check_in/app.py +60 -0
- geek_cafe_saas_sdk/domains/events/handlers/create/app.py +93 -0
- geek_cafe_saas_sdk/domains/events/handlers/delete/app.py +42 -0
- geek_cafe_saas_sdk/domains/events/handlers/get/app.py +39 -0
- geek_cafe_saas_sdk/domains/events/handlers/invite/app.py +98 -0
- geek_cafe_saas_sdk/domains/events/handlers/list/app.py +125 -0
- geek_cafe_saas_sdk/domains/events/handlers/publish/app.py +49 -0
- geek_cafe_saas_sdk/domains/events/handlers/rsvp/app.py +83 -0
- geek_cafe_saas_sdk/domains/events/handlers/update/app.py +44 -0
- geek_cafe_saas_sdk/domains/events/models/__init__.py +3 -0
- geek_cafe_saas_sdk/domains/events/models/event.py +681 -0
- geek_cafe_saas_sdk/domains/events/models/event_attendee.py +324 -0
- geek_cafe_saas_sdk/domains/events/services/__init__.py +9 -0
- geek_cafe_saas_sdk/domains/events/services/event_attendee_service.py +571 -0
- geek_cafe_saas_sdk/domains/events/services/event_service.py +684 -0
- geek_cafe_saas_sdk/domains/files/__init__.py +0 -0
- geek_cafe_saas_sdk/domains/files/models/__init__.py +0 -0
- geek_cafe_saas_sdk/domains/files/models/directory.py +258 -0
- geek_cafe_saas_sdk/domains/files/models/file.py +312 -0
- geek_cafe_saas_sdk/domains/files/models/file_share.py +268 -0
- geek_cafe_saas_sdk/domains/files/models/file_version.py +216 -0
- geek_cafe_saas_sdk/domains/files/services/__init__.py +0 -0
- geek_cafe_saas_sdk/domains/files/services/directory_service.py +701 -0
- geek_cafe_saas_sdk/domains/files/services/file_share_service.py +663 -0
- geek_cafe_saas_sdk/domains/files/services/file_system_service.py +575 -0
- geek_cafe_saas_sdk/domains/files/services/file_version_service.py +739 -0
- geek_cafe_saas_sdk/domains/files/services/s3_file_service.py +501 -0
- geek_cafe_saas_sdk/domains/messaging/__init__.py +0 -0
- geek_cafe_saas_sdk/domains/messaging/handlers/__init__.py +0 -0
- geek_cafe_saas_sdk/domains/messaging/handlers/chat_channels/create/app.py +86 -0
- geek_cafe_saas_sdk/domains/messaging/handlers/chat_channels/delete/app.py +65 -0
- geek_cafe_saas_sdk/domains/messaging/handlers/chat_channels/get/app.py +64 -0
- geek_cafe_saas_sdk/domains/messaging/handlers/chat_channels/list/app.py +97 -0
- geek_cafe_saas_sdk/domains/messaging/handlers/chat_channels/update/app.py +149 -0
- geek_cafe_saas_sdk/domains/messaging/handlers/chat_messages/create/app.py +67 -0
- geek_cafe_saas_sdk/domains/messaging/handlers/chat_messages/delete/app.py +65 -0
- geek_cafe_saas_sdk/domains/messaging/handlers/chat_messages/get/app.py +64 -0
- geek_cafe_saas_sdk/domains/messaging/handlers/chat_messages/list/app.py +102 -0
- geek_cafe_saas_sdk/domains/messaging/handlers/chat_messages/update/app.py +127 -0
- geek_cafe_saas_sdk/domains/messaging/handlers/contact_threads/create/app.py +94 -0
- geek_cafe_saas_sdk/domains/messaging/handlers/contact_threads/delete/app.py +66 -0
- geek_cafe_saas_sdk/domains/messaging/handlers/contact_threads/get/app.py +67 -0
- geek_cafe_saas_sdk/domains/messaging/handlers/contact_threads/list/app.py +95 -0
- geek_cafe_saas_sdk/domains/messaging/handlers/contact_threads/update/app.py +156 -0
- geek_cafe_saas_sdk/domains/messaging/models/__init__.py +13 -0
- geek_cafe_saas_sdk/domains/messaging/models/chat_channel.py +337 -0
- geek_cafe_saas_sdk/domains/messaging/models/chat_channel_member.py +180 -0
- geek_cafe_saas_sdk/domains/messaging/models/chat_message.py +426 -0
- geek_cafe_saas_sdk/domains/messaging/models/contact_thread.py +392 -0
- geek_cafe_saas_sdk/domains/messaging/services/__init__.py +11 -0
- geek_cafe_saas_sdk/domains/messaging/services/chat_channel_service.py +700 -0
- geek_cafe_saas_sdk/domains/messaging/services/chat_message_service.py +491 -0
- geek_cafe_saas_sdk/domains/messaging/services/contact_thread_service.py +497 -0
- geek_cafe_saas_sdk/domains/tenancy/__init__.py +0 -0
- geek_cafe_saas_sdk/domains/tenancy/handlers/__init__.py +0 -0
- geek_cafe_saas_sdk/domains/tenancy/handlers/subscriptions/activate/app.py +52 -0
- geek_cafe_saas_sdk/domains/tenancy/handlers/subscriptions/active/app.py +37 -0
- geek_cafe_saas_sdk/domains/tenancy/handlers/subscriptions/cancel/app.py +55 -0
- geek_cafe_saas_sdk/domains/tenancy/handlers/subscriptions/get/app.py +39 -0
- geek_cafe_saas_sdk/domains/tenancy/handlers/subscriptions/list/app.py +44 -0
- geek_cafe_saas_sdk/domains/tenancy/handlers/subscriptions/record_payment/app.py +56 -0
- geek_cafe_saas_sdk/domains/tenancy/handlers/tenants/get/app.py +39 -0
- geek_cafe_saas_sdk/domains/tenancy/handlers/tenants/me/app.py +37 -0
- geek_cafe_saas_sdk/domains/tenancy/handlers/tenants/signup/app.py +61 -0
- geek_cafe_saas_sdk/domains/tenancy/handlers/tenants/update/app.py +44 -0
- geek_cafe_saas_sdk/domains/tenancy/models/__init__.py +6 -0
- geek_cafe_saas_sdk/domains/tenancy/models/subscription.py +440 -0
- geek_cafe_saas_sdk/domains/tenancy/models/tenant.py +258 -0
- geek_cafe_saas_sdk/domains/tenancy/services/__init__.py +6 -0
- geek_cafe_saas_sdk/domains/tenancy/services/subscription_service.py +557 -0
- geek_cafe_saas_sdk/domains/tenancy/services/tenant_service.py +575 -0
- geek_cafe_saas_sdk/domains/voting/__init__.py +0 -0
- geek_cafe_saas_sdk/domains/voting/handlers/__init__.py +0 -0
- geek_cafe_saas_sdk/domains/voting/handlers/votes/create/app.py +128 -0
- geek_cafe_saas_sdk/domains/voting/handlers/votes/delete/app.py +41 -0
- geek_cafe_saas_sdk/domains/voting/handlers/votes/get/app.py +39 -0
- geek_cafe_saas_sdk/domains/voting/handlers/votes/list/app.py +38 -0
- geek_cafe_saas_sdk/domains/voting/handlers/votes/summerize/README.md +3 -0
- geek_cafe_saas_sdk/domains/voting/handlers/votes/update/app.py +44 -0
- geek_cafe_saas_sdk/domains/voting/models/__init__.py +9 -0
- geek_cafe_saas_sdk/domains/voting/models/vote.py +231 -0
- geek_cafe_saas_sdk/domains/voting/models/vote_summary.py +193 -0
- geek_cafe_saas_sdk/domains/voting/services/__init__.py +11 -0
- geek_cafe_saas_sdk/domains/voting/services/vote_service.py +264 -0
- geek_cafe_saas_sdk/domains/voting/services/vote_summary_service.py +198 -0
- geek_cafe_saas_sdk/domains/voting/services/vote_tally_service.py +533 -0
- geek_cafe_saas_sdk/lambda_handlers/README.md +404 -0
- geek_cafe_saas_sdk/lambda_handlers/__init__.py +67 -0
- geek_cafe_saas_sdk/lambda_handlers/_base/__init__.py +25 -0
- geek_cafe_saas_sdk/lambda_handlers/_base/api_key_handler.py +129 -0
- geek_cafe_saas_sdk/lambda_handlers/_base/authorized_secure_handler.py +218 -0
- geek_cafe_saas_sdk/lambda_handlers/_base/base_handler.py +185 -0
- geek_cafe_saas_sdk/lambda_handlers/_base/handler_factory.py +256 -0
- geek_cafe_saas_sdk/lambda_handlers/_base/public_handler.py +53 -0
- geek_cafe_saas_sdk/lambda_handlers/_base/secure_handler.py +89 -0
- geek_cafe_saas_sdk/lambda_handlers/_base/service_pool.py +94 -0
- geek_cafe_saas_sdk/lambda_handlers/directories/create/app.py +79 -0
- geek_cafe_saas_sdk/lambda_handlers/directories/delete/app.py +76 -0
- geek_cafe_saas_sdk/lambda_handlers/directories/get/app.py +74 -0
- geek_cafe_saas_sdk/lambda_handlers/directories/list/app.py +75 -0
- geek_cafe_saas_sdk/lambda_handlers/directories/move/app.py +79 -0
- geek_cafe_saas_sdk/lambda_handlers/files/delete/app.py +121 -0
- geek_cafe_saas_sdk/lambda_handlers/files/download/app.py +187 -0
- geek_cafe_saas_sdk/lambda_handlers/files/get/app.py +127 -0
- geek_cafe_saas_sdk/lambda_handlers/files/list/app.py +108 -0
- geek_cafe_saas_sdk/lambda_handlers/files/share/app.py +83 -0
- geek_cafe_saas_sdk/lambda_handlers/files/shares/list/app.py +84 -0
- geek_cafe_saas_sdk/lambda_handlers/files/shares/revoke/app.py +76 -0
- geek_cafe_saas_sdk/lambda_handlers/files/update/app.py +143 -0
- geek_cafe_saas_sdk/lambda_handlers/files/upload/app.py +151 -0
- geek_cafe_saas_sdk/middleware/__init__.py +36 -0
- geek_cafe_saas_sdk/middleware/auth.py +85 -0
- geek_cafe_saas_sdk/middleware/authorization.py +523 -0
- geek_cafe_saas_sdk/middleware/cors.py +63 -0
- geek_cafe_saas_sdk/middleware/error_handling.py +114 -0
- geek_cafe_saas_sdk/middleware/validation.py +80 -0
- geek_cafe_saas_sdk/models/__init__.py +20 -0
- geek_cafe_saas_sdk/models/base_model.py +233 -0
- geek_cafe_saas_sdk/services/__init__.py +18 -0
- geek_cafe_saas_sdk/services/database_service.py +441 -0
- geek_cafe_saas_sdk/utilities/__init__.py +88 -0
- geek_cafe_saas_sdk/utilities/cognito_utility.py +568 -0
- geek_cafe_saas_sdk/utilities/custom_exceptions.py +183 -0
- geek_cafe_saas_sdk/utilities/datetime_utility.py +410 -0
- geek_cafe_saas_sdk/utilities/dictionary_utility.py +78 -0
- geek_cafe_saas_sdk/utilities/dynamodb_utils.py +151 -0
- geek_cafe_saas_sdk/utilities/environment_loader.py +149 -0
- geek_cafe_saas_sdk/utilities/environment_variables.py +228 -0
- geek_cafe_saas_sdk/utilities/http_body_parameters.py +44 -0
- geek_cafe_saas_sdk/utilities/http_path_parameters.py +60 -0
- geek_cafe_saas_sdk/utilities/http_status_code.py +63 -0
- geek_cafe_saas_sdk/utilities/jwt_utility.py +234 -0
- geek_cafe_saas_sdk/utilities/lambda_event_utility.py +776 -0
- geek_cafe_saas_sdk/utilities/logging_utility.py +64 -0
- geek_cafe_saas_sdk/utilities/message_query_helper.py +340 -0
- geek_cafe_saas_sdk/utilities/response.py +209 -0
- geek_cafe_saas_sdk/utilities/string_functions.py +180 -0
- geek_cafe_saas_sdk-0.6.0.dist-info/METADATA +397 -0
- geek_cafe_saas_sdk-0.6.0.dist-info/RECORD +194 -0
- geek_cafe_saas_sdk-0.6.0.dist-info/WHEEL +4 -0
- geek_cafe_saas_sdk-0.6.0.dist-info/licenses/LICENSE +47 -0
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
|
|
2
|
+
from boto3_assist.dynamodb.dynamodb_index import DynamoDBIndex, DynamoDBKey
|
|
3
|
+
from boto3_assist.utilities.string_utility import StringUtility
|
|
4
|
+
import datetime as dt
|
|
5
|
+
from typing import Dict, Any
|
|
6
|
+
from geek_cafe_saas_sdk.models.base_model import BaseModel
|
|
7
|
+
|
|
8
|
+
class VoteSummary(BaseModel):
|
|
9
|
+
def __init__(self):
|
|
10
|
+
super().__init__()
|
|
11
|
+
self._content: Dict[str, Any] = {}
|
|
12
|
+
self._target_id: str | None = None
|
|
13
|
+
|
|
14
|
+
# Enhanced summary fields
|
|
15
|
+
self._vote_type: str = "single_choice" # Type of voting used
|
|
16
|
+
self._choice_breakdown: Dict[str, int] = {} # {"A": 150, "B": 100, "C": 50}
|
|
17
|
+
self._choice_percentages: Dict[str, float] = {} # {"A": 50.0, "B": 33.3, "C": 16.7}
|
|
18
|
+
self._choice_averages: Dict[str, float] = {} # For rating votes: {"A": 4.5, "B": 3.2}
|
|
19
|
+
self._total_participants: int = 0 # Number of people who voted
|
|
20
|
+
self._total_selections: int = 0 # For multi-select: total selections made
|
|
21
|
+
|
|
22
|
+
# Legacy fields (for backward compatibility)
|
|
23
|
+
self._total_up_votes: int = 0
|
|
24
|
+
self._total_down_votes: int = 0
|
|
25
|
+
self._total_votes: int = 0
|
|
26
|
+
|
|
27
|
+
self._setup_indexes()
|
|
28
|
+
|
|
29
|
+
def _setup_indexes(self):
|
|
30
|
+
primary: DynamoDBIndex = DynamoDBIndex()
|
|
31
|
+
primary.name = "primary"
|
|
32
|
+
primary.partition_key.attribute_name = "pk"
|
|
33
|
+
primary.partition_key.value = lambda: DynamoDBKey.build_key(
|
|
34
|
+
("vote-summary", self.id)
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
primary.sort_key.attribute_name = "sk"
|
|
38
|
+
primary.sort_key.value = lambda: DynamoDBKey.build_key(("vote-summary", self.id))
|
|
39
|
+
self.indexes.add_primary(primary)
|
|
40
|
+
|
|
41
|
+
## GSI: 1
|
|
42
|
+
# GSI: all vote summaries
|
|
43
|
+
gsi: DynamoDBIndex = DynamoDBIndex()
|
|
44
|
+
|
|
45
|
+
gsi.name = "gsi1"
|
|
46
|
+
gsi.partition_key.attribute_name = f"{gsi.name}_pk"
|
|
47
|
+
gsi.partition_key.value = lambda: DynamoDBKey.build_key(("vote-summary", "all"))
|
|
48
|
+
gsi.sort_key.attribute_name = f"{gsi.name}_sk"
|
|
49
|
+
gsi.sort_key.value = lambda: DynamoDBKey.build_key(
|
|
50
|
+
("ts", self.created_utc_ts)
|
|
51
|
+
)
|
|
52
|
+
self.indexes.add_secondary(gsi)
|
|
53
|
+
|
|
54
|
+
## GSI: 2
|
|
55
|
+
# GSI: vote summary by target_id (for quick lookup by target)
|
|
56
|
+
gsi: DynamoDBIndex = DynamoDBIndex()
|
|
57
|
+
|
|
58
|
+
gsi.name = "gsi2"
|
|
59
|
+
gsi.partition_key.attribute_name = f"{gsi.name}_pk"
|
|
60
|
+
gsi.partition_key.value = lambda: DynamoDBKey.build_key(("target", self.target_id))
|
|
61
|
+
gsi.sort_key.attribute_name = f"{gsi.name}_sk"
|
|
62
|
+
gsi.sort_key.value = lambda: DynamoDBKey.build_key(
|
|
63
|
+
("model", "vote-summary")
|
|
64
|
+
)
|
|
65
|
+
self.indexes.add_secondary(gsi)
|
|
66
|
+
|
|
67
|
+
## GSI: 3
|
|
68
|
+
# GSI: vote summaries by tenant
|
|
69
|
+
gsi: DynamoDBIndex = DynamoDBIndex()
|
|
70
|
+
|
|
71
|
+
gsi.name = "gsi3"
|
|
72
|
+
gsi.partition_key.attribute_name = f"{gsi.name}_pk"
|
|
73
|
+
gsi.partition_key.value = lambda: DynamoDBKey.build_key(("tenant", self.tenant_id))
|
|
74
|
+
gsi.sort_key.attribute_name = f"{gsi.name}_sk"
|
|
75
|
+
gsi.sort_key.value = lambda: DynamoDBKey.build_key(
|
|
76
|
+
("model", "vote-summary"),("ts", self.created_utc_ts)
|
|
77
|
+
)
|
|
78
|
+
self.indexes.add_secondary(gsi)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
@property
|
|
83
|
+
def content(self) -> Dict[str, Any]:
|
|
84
|
+
"""Get content (boto3-assist v0.30.0+ auto-converts Decimals to float)."""
|
|
85
|
+
return self._content
|
|
86
|
+
|
|
87
|
+
@content.setter
|
|
88
|
+
def content(self, value: Dict[str, Any]):
|
|
89
|
+
"""Set content (store as-is for boto3-assist compatibility)."""
|
|
90
|
+
self._content = value if value is not None else {}
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
@property
|
|
94
|
+
def total_up_votes(self) -> int:
|
|
95
|
+
return self._total_up_votes
|
|
96
|
+
|
|
97
|
+
@total_up_votes.setter
|
|
98
|
+
def total_up_votes(self, value: int):
|
|
99
|
+
self._total_up_votes = value
|
|
100
|
+
|
|
101
|
+
@property
|
|
102
|
+
def total_down_votes(self) -> int:
|
|
103
|
+
return self._total_down_votes
|
|
104
|
+
|
|
105
|
+
@total_down_votes.setter
|
|
106
|
+
def total_down_votes(self, value: int):
|
|
107
|
+
self._total_down_votes = value
|
|
108
|
+
|
|
109
|
+
@property
|
|
110
|
+
def total_votes(self) -> int:
|
|
111
|
+
return self._total_votes
|
|
112
|
+
|
|
113
|
+
@total_votes.setter
|
|
114
|
+
def total_votes(self, value: int):
|
|
115
|
+
self._total_votes = value
|
|
116
|
+
|
|
117
|
+
@property
|
|
118
|
+
def target_id(self) -> str | None:
|
|
119
|
+
return self._target_id
|
|
120
|
+
|
|
121
|
+
@target_id.setter
|
|
122
|
+
def target_id(self, value: str | None):
|
|
123
|
+
self._target_id = value
|
|
124
|
+
|
|
125
|
+
@property
|
|
126
|
+
def vote_type(self) -> str:
|
|
127
|
+
return self._vote_type
|
|
128
|
+
|
|
129
|
+
@vote_type.setter
|
|
130
|
+
def vote_type(self, value: str):
|
|
131
|
+
self._vote_type = value
|
|
132
|
+
|
|
133
|
+
@property
|
|
134
|
+
def choice_breakdown(self) -> Dict[str, int]:
|
|
135
|
+
return self._choice_breakdown
|
|
136
|
+
|
|
137
|
+
@choice_breakdown.setter
|
|
138
|
+
def choice_breakdown(self, value: Dict[str, int]):
|
|
139
|
+
self._choice_breakdown = value
|
|
140
|
+
|
|
141
|
+
@property
|
|
142
|
+
def choice_percentages(self) -> Dict[str, float]:
|
|
143
|
+
return self._choice_percentages
|
|
144
|
+
|
|
145
|
+
@choice_percentages.setter
|
|
146
|
+
def choice_percentages(self, value: Dict[str, float]):
|
|
147
|
+
self._choice_percentages = value
|
|
148
|
+
|
|
149
|
+
@property
|
|
150
|
+
def choice_averages(self) -> Dict[str, float]:
|
|
151
|
+
"""Average ratings for rating-type votes (boto3-assist v0.30.0+ auto-converts Decimals)."""
|
|
152
|
+
return self._choice_averages
|
|
153
|
+
|
|
154
|
+
@choice_averages.setter
|
|
155
|
+
def choice_averages(self, value: Dict[str, float]):
|
|
156
|
+
self._choice_averages = value
|
|
157
|
+
|
|
158
|
+
@property
|
|
159
|
+
def total_participants(self) -> int:
|
|
160
|
+
return self._total_participants
|
|
161
|
+
|
|
162
|
+
@total_participants.setter
|
|
163
|
+
def total_participants(self, value: int):
|
|
164
|
+
self._total_participants = value
|
|
165
|
+
|
|
166
|
+
@property
|
|
167
|
+
def total_selections(self) -> int:
|
|
168
|
+
return self._total_selections
|
|
169
|
+
|
|
170
|
+
@total_selections.setter
|
|
171
|
+
def total_selections(self, value: int):
|
|
172
|
+
self._total_selections = value
|
|
173
|
+
|
|
174
|
+
# Helper methods
|
|
175
|
+
def calculate_percentages(self):
|
|
176
|
+
"""Calculate percentages from choice breakdown."""
|
|
177
|
+
if self.total_participants > 0:
|
|
178
|
+
self.choice_percentages = {
|
|
179
|
+
choice: (count / self.total_participants * 100)
|
|
180
|
+
for choice, count in self.choice_breakdown.items()
|
|
181
|
+
}
|
|
182
|
+
else:
|
|
183
|
+
self.choice_percentages = {}
|
|
184
|
+
|
|
185
|
+
def get_winning_choice(self) -> str | None:
|
|
186
|
+
"""Get the choice with the most votes."""
|
|
187
|
+
if not self.choice_breakdown:
|
|
188
|
+
return None
|
|
189
|
+
return max(self.choice_breakdown.items(), key=lambda x: x[1])[0]
|
|
190
|
+
|
|
191
|
+
def get_choice_percentage(self, choice_id: str) -> float:
|
|
192
|
+
"""Get percentage for a specific choice."""
|
|
193
|
+
return self.choice_percentages.get(choice_id, 0.0)
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
# Vote Service
|
|
2
|
+
|
|
3
|
+
from typing import Dict, Any
|
|
4
|
+
from boto3_assist.dynamodb.dynamodb import DynamoDB
|
|
5
|
+
from geek_cafe_saas_sdk.services.database_service import DatabaseService
|
|
6
|
+
from geek_cafe_saas_sdk.core.service_result import ServiceResult
|
|
7
|
+
from geek_cafe_saas_sdk.core.service_errors import ValidationError, NotFoundError
|
|
8
|
+
from geek_cafe_saas_sdk.domains.voting.models import Vote
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class VoteService(DatabaseService[Vote]):
|
|
12
|
+
"""Service for Vote database operations."""
|
|
13
|
+
|
|
14
|
+
def __init__(self, *, dynamodb: DynamoDB = None, table_name: str = None):
|
|
15
|
+
super().__init__(dynamodb=dynamodb, table_name=table_name)
|
|
16
|
+
|
|
17
|
+
def create(self, tenant_id: str, user_id: str, **kwargs) -> ServiceResult[Vote]:
|
|
18
|
+
"""Create or update (upsert) a vote for a target by a user."""
|
|
19
|
+
try:
|
|
20
|
+
# Validate required fields
|
|
21
|
+
required_fields = ['target_id']
|
|
22
|
+
self._validate_required_fields(kwargs, required_fields)
|
|
23
|
+
|
|
24
|
+
# First check if a vote already exists for this user+target
|
|
25
|
+
existing = self._get_by_user_and_target(user_id, kwargs.get('target_id'))
|
|
26
|
+
if existing:
|
|
27
|
+
# Update the existing vote with new data
|
|
28
|
+
return self._update_existing_vote(existing, tenant_id, user_id, **kwargs)
|
|
29
|
+
|
|
30
|
+
# Create new vote instance
|
|
31
|
+
vote = Vote()
|
|
32
|
+
vote.tenant_id = tenant_id
|
|
33
|
+
vote.user_id = user_id
|
|
34
|
+
vote.target_id = kwargs.get('target_id')
|
|
35
|
+
vote.created_by_id = user_id
|
|
36
|
+
|
|
37
|
+
# Set vote data based on type
|
|
38
|
+
self._set_vote_data(vote, **kwargs)
|
|
39
|
+
|
|
40
|
+
# Prepare for save (sets ID and timestamps)
|
|
41
|
+
vote.prep_for_save()
|
|
42
|
+
|
|
43
|
+
# Save to database
|
|
44
|
+
return self._save_model(vote)
|
|
45
|
+
|
|
46
|
+
except Exception as e:
|
|
47
|
+
return self._handle_service_exception(e, 'create_vote', tenant_id=tenant_id, user_id=user_id)
|
|
48
|
+
|
|
49
|
+
def _update_existing_vote(self, existing_vote: Vote, tenant_id: str, user_id: str, **kwargs) -> ServiceResult[Vote]:
|
|
50
|
+
"""Update an existing vote with new data."""
|
|
51
|
+
# Set new vote data
|
|
52
|
+
self._set_vote_data(existing_vote, **kwargs)
|
|
53
|
+
|
|
54
|
+
# Update metadata
|
|
55
|
+
existing_vote.updated_by_id = user_id
|
|
56
|
+
existing_vote.prep_for_save() # Updates timestamp
|
|
57
|
+
|
|
58
|
+
# Save updated vote
|
|
59
|
+
return self._save_model(existing_vote)
|
|
60
|
+
|
|
61
|
+
def _set_vote_data(self, vote: Vote, **kwargs):
|
|
62
|
+
"""Set vote data based on the voting pattern."""
|
|
63
|
+
# Check if this is a legacy binary vote (has up_vote or down_vote but no vote_type)
|
|
64
|
+
has_legacy_fields = 'up_vote' in kwargs or 'down_vote' in kwargs
|
|
65
|
+
has_vote_type = 'vote_type' in kwargs
|
|
66
|
+
|
|
67
|
+
if has_legacy_fields and not has_vote_type:
|
|
68
|
+
# This is a legacy binary vote
|
|
69
|
+
vote.vote_type = 'legacy'
|
|
70
|
+
vote.up_vote = int(kwargs.get('up_vote', 0) or 0)
|
|
71
|
+
vote.down_vote = int(kwargs.get('down_vote', 0) or 0)
|
|
72
|
+
vote.choices = kwargs.get('choices', {})
|
|
73
|
+
vote.content = kwargs.get('content', {})
|
|
74
|
+
return
|
|
75
|
+
|
|
76
|
+
# Enhanced voting patterns
|
|
77
|
+
vote_type = kwargs.get('vote_type', 'single_choice')
|
|
78
|
+
vote.vote_type = vote_type
|
|
79
|
+
vote.content = kwargs.get('content', {})
|
|
80
|
+
|
|
81
|
+
if vote_type == 'single_choice':
|
|
82
|
+
choice_id = kwargs.get('choice_id') or kwargs.get('selected_choice')
|
|
83
|
+
available_choices = kwargs.get('available_choices', [])
|
|
84
|
+
if choice_id:
|
|
85
|
+
vote.set_single_choice(choice_id, available_choices)
|
|
86
|
+
|
|
87
|
+
elif vote_type == 'multi_select':
|
|
88
|
+
selected_choices = kwargs.get('selected_choices', [])
|
|
89
|
+
available_choices = kwargs.get('available_choices', [])
|
|
90
|
+
max_selections = kwargs.get('max_selections')
|
|
91
|
+
vote.set_multi_select(selected_choices, available_choices, max_selections)
|
|
92
|
+
|
|
93
|
+
elif vote_type == 'ranking':
|
|
94
|
+
ranked_choices = kwargs.get('ranked_choices', [])
|
|
95
|
+
vote.set_ranking(ranked_choices)
|
|
96
|
+
|
|
97
|
+
elif vote_type == 'rating':
|
|
98
|
+
ratings = kwargs.get('ratings', {})
|
|
99
|
+
vote.set_rating(ratings)
|
|
100
|
+
|
|
101
|
+
elif vote_type == 'legacy':
|
|
102
|
+
# Explicit legacy support
|
|
103
|
+
vote.up_vote = int(kwargs.get('up_vote', 0) or 0)
|
|
104
|
+
vote.down_vote = int(kwargs.get('down_vote', 0) or 0)
|
|
105
|
+
vote.choices = kwargs.get('choices', {})
|
|
106
|
+
|
|
107
|
+
else:
|
|
108
|
+
# Default to single choice if unknown type
|
|
109
|
+
vote.up_vote = int(kwargs.get('up_vote', 0) or 0)
|
|
110
|
+
vote.down_vote = int(kwargs.get('down_vote', 0) or 0)
|
|
111
|
+
vote.choices = kwargs.get('choices', {})
|
|
112
|
+
|
|
113
|
+
# Enhanced creation methods for specific vote types
|
|
114
|
+
def create_single_choice_vote(self, tenant_id: str, user_id: str, target_id: str,
|
|
115
|
+
choice_id: str, available_choices: list[str] = None,
|
|
116
|
+
content: Dict[str, Any] = None) -> ServiceResult[Vote]:
|
|
117
|
+
"""Create a single choice vote (A/B/C/D test)."""
|
|
118
|
+
return self.create(
|
|
119
|
+
tenant_id=tenant_id,
|
|
120
|
+
user_id=user_id,
|
|
121
|
+
target_id=target_id,
|
|
122
|
+
vote_type='single_choice',
|
|
123
|
+
choice_id=choice_id,
|
|
124
|
+
available_choices=available_choices or [],
|
|
125
|
+
content=content or {}
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
def create_multi_select_vote(self, tenant_id: str, user_id: str, target_id: str,
|
|
129
|
+
selected_choices: list[str], available_choices: list[str] = None,
|
|
130
|
+
max_selections: int = None, content: Dict[str, Any] = None) -> ServiceResult[Vote]:
|
|
131
|
+
"""Create a multi-select vote."""
|
|
132
|
+
return self.create(
|
|
133
|
+
tenant_id=tenant_id,
|
|
134
|
+
user_id=user_id,
|
|
135
|
+
target_id=target_id,
|
|
136
|
+
vote_type='multi_select',
|
|
137
|
+
selected_choices=selected_choices,
|
|
138
|
+
available_choices=available_choices or [],
|
|
139
|
+
max_selections=max_selections,
|
|
140
|
+
content=content or {}
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
def create_ranking_vote(self, tenant_id: str, user_id: str, target_id: str,
|
|
144
|
+
ranked_choices: list[str], content: Dict[str, Any] = None) -> ServiceResult[Vote]:
|
|
145
|
+
"""Create a ranking vote."""
|
|
146
|
+
return self.create(
|
|
147
|
+
tenant_id=tenant_id,
|
|
148
|
+
user_id=user_id,
|
|
149
|
+
target_id=target_id,
|
|
150
|
+
vote_type='ranking',
|
|
151
|
+
ranked_choices=ranked_choices,
|
|
152
|
+
content=content or {}
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
def create_rating_vote(self, tenant_id: str, user_id: str, target_id: str,
|
|
156
|
+
ratings: Dict[str, float], content: Dict[str, Any] = None) -> ServiceResult[Vote]:
|
|
157
|
+
"""Create a rating vote."""
|
|
158
|
+
return self.create(
|
|
159
|
+
tenant_id=tenant_id,
|
|
160
|
+
user_id=user_id,
|
|
161
|
+
target_id=target_id,
|
|
162
|
+
vote_type='rating',
|
|
163
|
+
ratings=ratings,
|
|
164
|
+
content=content or {}
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
def _get_by_user_and_target(self, user_id: str, target_id: str) -> Vote | None:
|
|
168
|
+
"""Helper: get a vote by user and target via GSI4."""
|
|
169
|
+
model = Vote()
|
|
170
|
+
model.user_id = user_id
|
|
171
|
+
model.target_id = target_id
|
|
172
|
+
result = self._query_by_index(model, "gsi4")
|
|
173
|
+
if result.success and result.data:
|
|
174
|
+
return result.data[0]
|
|
175
|
+
return None
|
|
176
|
+
|
|
177
|
+
def get_by_id(self, resource_id: str, tenant_id: str, user_id: str) -> ServiceResult[Vote]:
|
|
178
|
+
"""Get vote by ID with access control."""
|
|
179
|
+
try:
|
|
180
|
+
vote = self._get_model_by_id(resource_id, Vote)
|
|
181
|
+
|
|
182
|
+
if not vote:
|
|
183
|
+
raise NotFoundError(f"Vote with ID {resource_id} not found")
|
|
184
|
+
|
|
185
|
+
# Validate tenant access
|
|
186
|
+
if hasattr(vote, 'tenant_id'):
|
|
187
|
+
self._validate_tenant_access(vote.tenant_id, tenant_id)
|
|
188
|
+
|
|
189
|
+
return ServiceResult.success_result(vote)
|
|
190
|
+
|
|
191
|
+
except Exception as e:
|
|
192
|
+
return self._handle_service_exception(e, 'get_vote', resource_id=resource_id, tenant_id=tenant_id)
|
|
193
|
+
|
|
194
|
+
def update(self, resource_id: str, tenant_id: str, user_id: str,
|
|
195
|
+
updates: Dict[str, Any]) -> ServiceResult[Vote]:
|
|
196
|
+
"""Update vote with access control."""
|
|
197
|
+
try:
|
|
198
|
+
# Get existing vote
|
|
199
|
+
vote = self._get_model_by_id(resource_id, Vote)
|
|
200
|
+
|
|
201
|
+
if not vote:
|
|
202
|
+
raise NotFoundError(f"Vote with ID {resource_id} not found")
|
|
203
|
+
|
|
204
|
+
# Validate tenant access
|
|
205
|
+
if hasattr(vote, 'tenant_id'):
|
|
206
|
+
self._validate_tenant_access(vote.tenant_id, tenant_id)
|
|
207
|
+
|
|
208
|
+
# Apply updates
|
|
209
|
+
for field, value in updates.items():
|
|
210
|
+
if hasattr(vote, field) and field not in ['id', 'created_utc_ts', 'tenant_id']:
|
|
211
|
+
setattr(vote, field, value)
|
|
212
|
+
|
|
213
|
+
# Update metadata
|
|
214
|
+
vote.updated_by_id = user_id
|
|
215
|
+
vote.prep_for_save() # Updates timestamp
|
|
216
|
+
|
|
217
|
+
# Save updated vote
|
|
218
|
+
return self._save_model(vote)
|
|
219
|
+
|
|
220
|
+
except Exception as e:
|
|
221
|
+
return self._handle_service_exception(e, 'update_vote', resource_id=resource_id, tenant_id=tenant_id)
|
|
222
|
+
|
|
223
|
+
def delete(self, resource_id: str, tenant_id: str, user_id: str) -> ServiceResult[bool]:
|
|
224
|
+
"""Delete vote with access control."""
|
|
225
|
+
try:
|
|
226
|
+
vote = self._get_model_by_id(resource_id, Vote)
|
|
227
|
+
|
|
228
|
+
if not vote:
|
|
229
|
+
raise NotFoundError(f"Vote with ID {resource_id} not found")
|
|
230
|
+
|
|
231
|
+
if hasattr(vote, 'tenant_id'):
|
|
232
|
+
self._validate_tenant_access(vote.tenant_id, tenant_id)
|
|
233
|
+
|
|
234
|
+
return self._delete_model(vote)
|
|
235
|
+
|
|
236
|
+
except Exception as e:
|
|
237
|
+
return self._handle_service_exception(e, 'delete_vote', resource_id=resource_id, tenant_id=tenant_id)
|
|
238
|
+
|
|
239
|
+
def list_by_user(self, user_id: str, ascending: bool = False) -> ServiceResult[list[Vote]]:
|
|
240
|
+
"""List votes by user."""
|
|
241
|
+
try:
|
|
242
|
+
model = Vote()
|
|
243
|
+
model.user_id = user_id
|
|
244
|
+
return self._query_by_index(model, "gsi2", ascending=ascending)
|
|
245
|
+
except Exception as e:
|
|
246
|
+
return self._handle_service_exception(e, 'list_votes', user_id=user_id)
|
|
247
|
+
|
|
248
|
+
def list_by_tenant(self, tenant_id: str) -> ServiceResult[list[Vote]]:
|
|
249
|
+
"""List votes by tenant."""
|
|
250
|
+
try:
|
|
251
|
+
model = Vote()
|
|
252
|
+
model.tenant_id = tenant_id
|
|
253
|
+
return self._query_by_index(model, "gsi3")
|
|
254
|
+
except Exception as e:
|
|
255
|
+
return self._handle_service_exception(e, 'list_votes', tenant_id=tenant_id)
|
|
256
|
+
|
|
257
|
+
def list_by_target(self, target_id: str, *, start_key: dict = None, limit: int = None) -> ServiceResult[list[Vote]]:
|
|
258
|
+
"""List votes by target with optional pagination."""
|
|
259
|
+
try:
|
|
260
|
+
model = Vote()
|
|
261
|
+
model.target_id = target_id
|
|
262
|
+
return self._query_by_index(model, "gsi5", start_key=start_key, limit=limit)
|
|
263
|
+
except Exception as e:
|
|
264
|
+
return self._handle_service_exception(e, 'list_votes', target_id=target_id)
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
# Vote Summary Service
|
|
2
|
+
|
|
3
|
+
from typing import Dict, Any
|
|
4
|
+
from boto3_assist.dynamodb.dynamodb import DynamoDB
|
|
5
|
+
from geek_cafe_saas_sdk.services.database_service import DatabaseService
|
|
6
|
+
from geek_cafe_saas_sdk.core.service_result import ServiceResult
|
|
7
|
+
from geek_cafe_saas_sdk.core.service_errors import ValidationError, NotFoundError
|
|
8
|
+
from geek_cafe_saas_sdk.domains.voting.models import VoteSummary
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class VoteSummaryService(DatabaseService[VoteSummary]):
|
|
12
|
+
"""Service for VoteSummary database operations."""
|
|
13
|
+
|
|
14
|
+
def __init__(self, *, dynamodb: DynamoDB = None, table_name: str = None):
|
|
15
|
+
super().__init__(dynamodb=dynamodb, table_name=table_name)
|
|
16
|
+
|
|
17
|
+
def create(self, tenant_id: str, user_id: str, **kwargs) -> ServiceResult[VoteSummary]:
|
|
18
|
+
"""Create or update (upsert) a vote summary for a target."""
|
|
19
|
+
try:
|
|
20
|
+
# Validate required fields
|
|
21
|
+
required_fields = ['target_id']
|
|
22
|
+
self._validate_required_fields(kwargs, required_fields)
|
|
23
|
+
|
|
24
|
+
# First check if a summary already exists for this target
|
|
25
|
+
existing = self._get_by_target_id(kwargs.get('target_id'))
|
|
26
|
+
if existing:
|
|
27
|
+
# Update the existing summary with all new data
|
|
28
|
+
return self._update_existing_summary(existing, tenant_id, user_id, **kwargs)
|
|
29
|
+
|
|
30
|
+
# Create new vote summary instance
|
|
31
|
+
summary = VoteSummary()
|
|
32
|
+
summary.tenant_id = tenant_id
|
|
33
|
+
summary.user_id = user_id
|
|
34
|
+
summary.target_id = kwargs.get('target_id')
|
|
35
|
+
summary.created_by_id = user_id
|
|
36
|
+
|
|
37
|
+
# Set enhanced fields
|
|
38
|
+
summary.vote_type = kwargs.get('vote_type', 'single_choice')
|
|
39
|
+
summary.choice_breakdown = kwargs.get('choice_breakdown', {})
|
|
40
|
+
summary.choice_percentages = kwargs.get('choice_percentages', {})
|
|
41
|
+
summary.choice_averages = kwargs.get('choice_averages', {}) # For rating votes
|
|
42
|
+
summary.total_participants = int(kwargs.get('total_participants', 0) or 0)
|
|
43
|
+
summary.total_selections = int(kwargs.get('total_selections', 0) or 0)
|
|
44
|
+
|
|
45
|
+
# Set legacy fields for backward compatibility
|
|
46
|
+
summary.total_up_votes = int(kwargs.get('total_up_votes', 0) or 0)
|
|
47
|
+
summary.total_down_votes = int(kwargs.get('total_down_votes', 0) or 0)
|
|
48
|
+
summary.total_votes = int(kwargs.get('total_votes', 0) or 0)
|
|
49
|
+
|
|
50
|
+
# Set content
|
|
51
|
+
summary.content = kwargs.get('content', {})
|
|
52
|
+
|
|
53
|
+
# Calculate percentages if not provided
|
|
54
|
+
if not summary.choice_percentages and summary.choice_breakdown:
|
|
55
|
+
summary.calculate_percentages()
|
|
56
|
+
|
|
57
|
+
# Prepare for save (sets ID and timestamps)
|
|
58
|
+
summary.prep_for_save()
|
|
59
|
+
|
|
60
|
+
# Save to database
|
|
61
|
+
return self._save_model(summary)
|
|
62
|
+
|
|
63
|
+
except Exception as e:
|
|
64
|
+
return self._handle_service_exception(e, 'create_vote_summary', tenant_id=tenant_id, user_id=user_id)
|
|
65
|
+
|
|
66
|
+
def _update_existing_summary(self, existing_summary: VoteSummary, tenant_id: str, user_id: str, **kwargs) -> ServiceResult[VoteSummary]:
|
|
67
|
+
"""Update an existing summary with new data."""
|
|
68
|
+
# Update enhanced fields
|
|
69
|
+
existing_summary.vote_type = kwargs.get('vote_type', existing_summary.vote_type)
|
|
70
|
+
existing_summary.choice_breakdown = kwargs.get('choice_breakdown', existing_summary.choice_breakdown)
|
|
71
|
+
existing_summary.choice_percentages = kwargs.get('choice_percentages', existing_summary.choice_percentages)
|
|
72
|
+
existing_summary.choice_averages = kwargs.get('choice_averages', existing_summary.choice_averages)
|
|
73
|
+
existing_summary.total_participants = int(kwargs.get('total_participants', existing_summary.total_participants) or 0)
|
|
74
|
+
existing_summary.total_selections = int(kwargs.get('total_selections', existing_summary.total_selections) or 0)
|
|
75
|
+
|
|
76
|
+
# Update legacy fields
|
|
77
|
+
existing_summary.total_up_votes = int(kwargs.get('total_up_votes', existing_summary.total_up_votes) or 0)
|
|
78
|
+
existing_summary.total_down_votes = int(kwargs.get('total_down_votes', existing_summary.total_down_votes) or 0)
|
|
79
|
+
existing_summary.total_votes = int(kwargs.get('total_votes', existing_summary.total_votes) or 0)
|
|
80
|
+
|
|
81
|
+
# Update content
|
|
82
|
+
existing_summary.content = kwargs.get('content', existing_summary.content or {})
|
|
83
|
+
|
|
84
|
+
# Recalculate percentages if needed
|
|
85
|
+
if not existing_summary.choice_percentages and existing_summary.choice_breakdown:
|
|
86
|
+
existing_summary.calculate_percentages()
|
|
87
|
+
|
|
88
|
+
# Update metadata
|
|
89
|
+
existing_summary.updated_by_id = user_id
|
|
90
|
+
existing_summary.prep_for_save() # Updates timestamp
|
|
91
|
+
|
|
92
|
+
# Save updated summary
|
|
93
|
+
return self._save_model(existing_summary)
|
|
94
|
+
|
|
95
|
+
def _get_by_target_id(self, target_id: str) -> VoteSummary | None:
|
|
96
|
+
"""Helper: get a vote summary by target_id via GSI2."""
|
|
97
|
+
model = VoteSummary()
|
|
98
|
+
model.target_id = target_id
|
|
99
|
+
result = self._query_by_index(model, "gsi2")
|
|
100
|
+
if result.success and result.data:
|
|
101
|
+
return result.data[0]
|
|
102
|
+
return None
|
|
103
|
+
|
|
104
|
+
def get_by_id(self, resource_id: str, tenant_id: str, user_id: str) -> ServiceResult[VoteSummary]:
|
|
105
|
+
"""Get vote summary by ID with access control."""
|
|
106
|
+
try:
|
|
107
|
+
summary = self._get_model_by_id(resource_id, VoteSummary)
|
|
108
|
+
|
|
109
|
+
if not summary:
|
|
110
|
+
raise NotFoundError(f"VoteSummary with ID {resource_id} not found")
|
|
111
|
+
|
|
112
|
+
# Validate tenant access
|
|
113
|
+
if hasattr(summary, 'tenant_id'):
|
|
114
|
+
self._validate_tenant_access(summary.tenant_id, tenant_id)
|
|
115
|
+
|
|
116
|
+
return ServiceResult.success_result(summary)
|
|
117
|
+
|
|
118
|
+
except Exception as e:
|
|
119
|
+
return self._handle_service_exception(e, 'get_vote_summary', resource_id=resource_id, tenant_id=tenant_id)
|
|
120
|
+
|
|
121
|
+
def get_by_target_id(self, target_id: str, tenant_id: str, user_id: str) -> ServiceResult[VoteSummary]:
|
|
122
|
+
"""Get vote summary by target_id with access control."""
|
|
123
|
+
try:
|
|
124
|
+
summary = self._get_by_target_id(target_id)
|
|
125
|
+
|
|
126
|
+
if not summary:
|
|
127
|
+
raise NotFoundError(f"VoteSummary for target {target_id} not found")
|
|
128
|
+
|
|
129
|
+
# Validate tenant access
|
|
130
|
+
if hasattr(summary, 'tenant_id'):
|
|
131
|
+
self._validate_tenant_access(summary.tenant_id, tenant_id)
|
|
132
|
+
|
|
133
|
+
return ServiceResult.success_result(summary)
|
|
134
|
+
|
|
135
|
+
except Exception as e:
|
|
136
|
+
return self._handle_service_exception(e, 'get_vote_summary_by_target', target_id=target_id, tenant_id=tenant_id)
|
|
137
|
+
|
|
138
|
+
def update(self, resource_id: str, tenant_id: str, user_id: str,
|
|
139
|
+
updates: Dict[str, Any]) -> ServiceResult[VoteSummary]:
|
|
140
|
+
"""Update vote summary with access control."""
|
|
141
|
+
try:
|
|
142
|
+
# Get existing summary
|
|
143
|
+
summary = self._get_model_by_id(resource_id, VoteSummary)
|
|
144
|
+
|
|
145
|
+
if not summary:
|
|
146
|
+
raise NotFoundError(f"VoteSummary with ID {resource_id} not found")
|
|
147
|
+
|
|
148
|
+
# Validate tenant access
|
|
149
|
+
if hasattr(summary, 'tenant_id'):
|
|
150
|
+
self._validate_tenant_access(summary.tenant_id, tenant_id)
|
|
151
|
+
|
|
152
|
+
# Apply updates
|
|
153
|
+
for field, value in updates.items():
|
|
154
|
+
if hasattr(summary, field) and field not in ['id', 'created_utc_ts', 'tenant_id']:
|
|
155
|
+
setattr(summary, field, value)
|
|
156
|
+
|
|
157
|
+
# Update metadata
|
|
158
|
+
summary.updated_by_id = user_id
|
|
159
|
+
summary.prep_for_save() # Updates timestamp
|
|
160
|
+
|
|
161
|
+
# Save updated summary
|
|
162
|
+
return self._save_model(summary)
|
|
163
|
+
|
|
164
|
+
except Exception as e:
|
|
165
|
+
return self._handle_service_exception(e, 'update_vote_summary', resource_id=resource_id, tenant_id=tenant_id)
|
|
166
|
+
|
|
167
|
+
def delete(self, resource_id: str, tenant_id: str, user_id: str) -> ServiceResult[bool]:
|
|
168
|
+
"""Delete vote summary with access control."""
|
|
169
|
+
try:
|
|
170
|
+
summary = self._get_model_by_id(resource_id, VoteSummary)
|
|
171
|
+
|
|
172
|
+
if not summary:
|
|
173
|
+
raise NotFoundError(f"VoteSummary with ID {resource_id} not found")
|
|
174
|
+
|
|
175
|
+
if hasattr(summary, 'tenant_id'):
|
|
176
|
+
self._validate_tenant_access(summary.tenant_id, tenant_id)
|
|
177
|
+
|
|
178
|
+
return self._delete_model(summary)
|
|
179
|
+
|
|
180
|
+
except Exception as e:
|
|
181
|
+
return self._handle_service_exception(e, 'delete_vote_summary', resource_id=resource_id, tenant_id=tenant_id)
|
|
182
|
+
|
|
183
|
+
def list_by_tenant(self, tenant_id: str) -> ServiceResult[list[VoteSummary]]:
|
|
184
|
+
"""List vote summaries by tenant."""
|
|
185
|
+
try:
|
|
186
|
+
model = VoteSummary()
|
|
187
|
+
model.tenant_id = tenant_id
|
|
188
|
+
return self._query_by_index(model, "gsi3")
|
|
189
|
+
except Exception as e:
|
|
190
|
+
return self._handle_service_exception(e, 'list_vote_summaries', tenant_id=tenant_id)
|
|
191
|
+
|
|
192
|
+
def list_all(self) -> ServiceResult[list[VoteSummary]]:
|
|
193
|
+
"""List all vote summaries."""
|
|
194
|
+
try:
|
|
195
|
+
model = VoteSummary()
|
|
196
|
+
return self._query_by_index(model, "gsi1")
|
|
197
|
+
except Exception as e:
|
|
198
|
+
return self._handle_service_exception(e, 'list_all_vote_summaries')
|