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,324 @@
|
|
|
1
|
+
from boto3_assist.dynamodb.dynamodb_index import DynamoDBIndex, DynamoDBKey
|
|
2
|
+
from typing import List, Optional, Dict, Any
|
|
3
|
+
from geek_cafe_saas_sdk.models.base_model import BaseModel
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class EventAttendee(BaseModel):
|
|
7
|
+
"""
|
|
8
|
+
Event attendee/invitation record (adjacent record pattern).
|
|
9
|
+
|
|
10
|
+
Similar to ChatChannelMember, this enables:
|
|
11
|
+
- Unlimited attendees (no DynamoDB item size limit)
|
|
12
|
+
- Full RSVP tracking with history
|
|
13
|
+
- Multiple hosts/co-organizers per event
|
|
14
|
+
- Guest +1 support
|
|
15
|
+
- Custom registration data
|
|
16
|
+
- Check-in tracking
|
|
17
|
+
|
|
18
|
+
Each attendee gets their own record with their RSVP status and role.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
def __init__(self):
|
|
22
|
+
super().__init__()
|
|
23
|
+
# Mark as multi-record (adjacent to Event)
|
|
24
|
+
self.is_multi_record = True
|
|
25
|
+
|
|
26
|
+
# Relationship
|
|
27
|
+
self._event_id: str | None = None
|
|
28
|
+
self._user_id: str | None = None # Also in BaseModel, but key for this model
|
|
29
|
+
|
|
30
|
+
# RSVP Status
|
|
31
|
+
self._rsvp_status: str | None = None # invited, accepted, declined, tentative, waitlist (defaults to "invited" when saved)
|
|
32
|
+
self._invited_at_utc_ts: float | None = None
|
|
33
|
+
self._responded_at_utc_ts: float | None = None
|
|
34
|
+
self._invited_by_user_id: str | None = None
|
|
35
|
+
|
|
36
|
+
# Role (enables multiple hosts)
|
|
37
|
+
self._role: str | None = None # organizer, co_host, attendee, speaker, volunteer (defaults to "attendee" when saved)
|
|
38
|
+
|
|
39
|
+
# Guest +1
|
|
40
|
+
self._plus_one_count: int = 0
|
|
41
|
+
self._plus_one_names: List[str] = []
|
|
42
|
+
|
|
43
|
+
# Check-in
|
|
44
|
+
self._checked_in: bool = False
|
|
45
|
+
self._checked_in_at_utc_ts: float | None = None
|
|
46
|
+
self._checked_in_by_user_id: str | None = None
|
|
47
|
+
|
|
48
|
+
# Custom Registration
|
|
49
|
+
self._registration_data: Dict[str, Any] = {} # Answers to custom fields
|
|
50
|
+
self._registration_notes: str | None = None
|
|
51
|
+
|
|
52
|
+
# Notifications
|
|
53
|
+
self._notification_preferences: Dict[str, bool] = {
|
|
54
|
+
"event_updates": True,
|
|
55
|
+
"reminders": True,
|
|
56
|
+
"cancellations": True
|
|
57
|
+
}
|
|
58
|
+
self._reminder_sent: bool = False
|
|
59
|
+
self._reminder_sent_at_utc_ts: float | None = None
|
|
60
|
+
|
|
61
|
+
self._setup_indexes()
|
|
62
|
+
|
|
63
|
+
def _setup_indexes(self):
|
|
64
|
+
"""Setup DynamoDB indexes for event attendee queries."""
|
|
65
|
+
|
|
66
|
+
# Primary index: attendee by composite ID
|
|
67
|
+
primary: DynamoDBIndex = DynamoDBIndex()
|
|
68
|
+
primary.name = "primary"
|
|
69
|
+
primary.partition_key.attribute_name = "pk"
|
|
70
|
+
primary.partition_key.value = lambda: DynamoDBKey.build_key(("event", self.event_id), ("user", self.user_id))
|
|
71
|
+
primary.sort_key.attribute_name = "sk"
|
|
72
|
+
primary.sort_key.value = lambda: DynamoDBKey.build_key(("attendee", self.id))
|
|
73
|
+
self.indexes.add_primary(primary)
|
|
74
|
+
|
|
75
|
+
## GSI1: Attendees by event (most common query)
|
|
76
|
+
gsi: DynamoDBIndex = DynamoDBIndex()
|
|
77
|
+
gsi.name = "gsi1"
|
|
78
|
+
gsi.partition_key.attribute_name = f"{gsi.name}_pk"
|
|
79
|
+
gsi.partition_key.value = lambda: DynamoDBKey.build_key(("event", self.event_id))
|
|
80
|
+
gsi.sort_key.attribute_name = f"{gsi.name}_sk"
|
|
81
|
+
gsi.sort_key.value = lambda: DynamoDBKey.build_key(
|
|
82
|
+
("role", self.role),
|
|
83
|
+
("status", self.rsvp_status),
|
|
84
|
+
("ts", self.invited_at_utc_ts)
|
|
85
|
+
)
|
|
86
|
+
self.indexes.add_secondary(gsi)
|
|
87
|
+
|
|
88
|
+
## GSI2: User's events (my RSVPs)
|
|
89
|
+
gsi: DynamoDBIndex = DynamoDBIndex()
|
|
90
|
+
gsi.name = "gsi2"
|
|
91
|
+
gsi.partition_key.attribute_name = f"{gsi.name}_pk"
|
|
92
|
+
gsi.partition_key.value = lambda: DynamoDBKey.build_key(("user", self.user_id))
|
|
93
|
+
gsi.sort_key.attribute_name = f"{gsi.name}_sk"
|
|
94
|
+
gsi.sort_key.value = lambda: DynamoDBKey.build_key(
|
|
95
|
+
("status", self.rsvp_status),
|
|
96
|
+
("ts", self.invited_at_utc_ts)
|
|
97
|
+
)
|
|
98
|
+
self.indexes.add_secondary(gsi)
|
|
99
|
+
|
|
100
|
+
## GSI3: Event hosts (organizers and co-hosts)
|
|
101
|
+
gsi: DynamoDBIndex = DynamoDBIndex()
|
|
102
|
+
gsi.name = "gsi3"
|
|
103
|
+
gsi.partition_key.attribute_name = f"{gsi.name}_pk"
|
|
104
|
+
gsi.partition_key.value = lambda: DynamoDBKey.build_key(
|
|
105
|
+
("event", self.event_id),
|
|
106
|
+
("role", self.role if self.role in ["organizer", "co_host"] else None)
|
|
107
|
+
)
|
|
108
|
+
gsi.sort_key.attribute_name = f"{gsi.name}_sk"
|
|
109
|
+
gsi.sort_key.value = lambda: DynamoDBKey.build_key(("ts", self.invited_at_utc_ts))
|
|
110
|
+
self.indexes.add_secondary(gsi)
|
|
111
|
+
|
|
112
|
+
## GSI4: Confirmed attendees (for display)
|
|
113
|
+
gsi: DynamoDBIndex = DynamoDBIndex()
|
|
114
|
+
gsi.name = "gsi4"
|
|
115
|
+
gsi.partition_key.attribute_name = f"{gsi.name}_pk"
|
|
116
|
+
gsi.partition_key.value = lambda: DynamoDBKey.build_key(
|
|
117
|
+
("event", self.event_id),
|
|
118
|
+
("status", self.rsvp_status if self.rsvp_status == "accepted" else None)
|
|
119
|
+
)
|
|
120
|
+
gsi.sort_key.attribute_name = f"{gsi.name}_sk"
|
|
121
|
+
gsi.sort_key.value = lambda: DynamoDBKey.build_key(("ts", self.responded_at_utc_ts))
|
|
122
|
+
self.indexes.add_secondary(gsi)
|
|
123
|
+
|
|
124
|
+
## GSI5: Who invited this user (networking)
|
|
125
|
+
gsi: DynamoDBIndex = DynamoDBIndex()
|
|
126
|
+
gsi.name = "gsi5"
|
|
127
|
+
gsi.partition_key.attribute_name = f"{gsi.name}_pk"
|
|
128
|
+
gsi.partition_key.value = lambda: DynamoDBKey.build_key(("inviter", self.invited_by_user_id))
|
|
129
|
+
gsi.sort_key.attribute_name = f"{gsi.name}_sk"
|
|
130
|
+
gsi.sort_key.value = lambda: DynamoDBKey.build_key(("ts", self.invited_at_utc_ts))
|
|
131
|
+
self.indexes.add_secondary(gsi)
|
|
132
|
+
|
|
133
|
+
# Properties - Relationship
|
|
134
|
+
@property
|
|
135
|
+
def event_id(self) -> str | None:
|
|
136
|
+
"""Event ID."""
|
|
137
|
+
return self._event_id
|
|
138
|
+
|
|
139
|
+
@event_id.setter
|
|
140
|
+
def event_id(self, value: str | None):
|
|
141
|
+
self._event_id = value
|
|
142
|
+
|
|
143
|
+
# user_id is inherited from BaseModel but we need it for this model
|
|
144
|
+
# No need to override, just use self.user_id from BaseModel
|
|
145
|
+
|
|
146
|
+
# Properties - RSVP Status
|
|
147
|
+
@property
|
|
148
|
+
def rsvp_status(self) -> str:
|
|
149
|
+
"""RSVP status: invited, accepted, declined, tentative, waitlist."""
|
|
150
|
+
return self._rsvp_status
|
|
151
|
+
|
|
152
|
+
@rsvp_status.setter
|
|
153
|
+
def rsvp_status(self, value: str | None):
|
|
154
|
+
if value is None or value in ["invited", "accepted", "declined", "tentative", "waitlist"]:
|
|
155
|
+
self._rsvp_status = value
|
|
156
|
+
|
|
157
|
+
@property
|
|
158
|
+
def invited_at_utc_ts(self) -> float | None:
|
|
159
|
+
"""When invitation was sent (UTC timestamp)."""
|
|
160
|
+
return self._invited_at_utc_ts
|
|
161
|
+
|
|
162
|
+
@invited_at_utc_ts.setter
|
|
163
|
+
def invited_at_utc_ts(self, value: float | None):
|
|
164
|
+
self._invited_at_utc_ts = value
|
|
165
|
+
|
|
166
|
+
@property
|
|
167
|
+
def responded_at_utc_ts(self) -> float | None:
|
|
168
|
+
"""When user responded to RSVP (UTC timestamp)."""
|
|
169
|
+
return self._responded_at_utc_ts
|
|
170
|
+
|
|
171
|
+
@responded_at_utc_ts.setter
|
|
172
|
+
def responded_at_utc_ts(self, value: float | None):
|
|
173
|
+
self._responded_at_utc_ts = value
|
|
174
|
+
|
|
175
|
+
@property
|
|
176
|
+
def invited_by_user_id(self) -> str | None:
|
|
177
|
+
"""Who invited this attendee."""
|
|
178
|
+
return self._invited_by_user_id
|
|
179
|
+
|
|
180
|
+
@invited_by_user_id.setter
|
|
181
|
+
def invited_by_user_id(self, value: str | None):
|
|
182
|
+
self._invited_by_user_id = value
|
|
183
|
+
|
|
184
|
+
# Properties - Role
|
|
185
|
+
@property
|
|
186
|
+
def role(self) -> str:
|
|
187
|
+
"""Attendee role: organizer, co_host, attendee, speaker, volunteer."""
|
|
188
|
+
return self._role
|
|
189
|
+
|
|
190
|
+
@role.setter
|
|
191
|
+
def role(self, value: str | None):
|
|
192
|
+
if value is None or value in ["organizer", "co_host", "attendee", "speaker", "volunteer"]:
|
|
193
|
+
self._role = value
|
|
194
|
+
|
|
195
|
+
# Properties - Guest +1
|
|
196
|
+
@property
|
|
197
|
+
def plus_one_count(self) -> int:
|
|
198
|
+
"""Number of +1 guests."""
|
|
199
|
+
return self._plus_one_count
|
|
200
|
+
|
|
201
|
+
@plus_one_count.setter
|
|
202
|
+
def plus_one_count(self, value: int):
|
|
203
|
+
self._plus_one_count = max(0, value)
|
|
204
|
+
|
|
205
|
+
@property
|
|
206
|
+
def plus_one_names(self) -> List[str]:
|
|
207
|
+
"""Names of +1 guests."""
|
|
208
|
+
return self._plus_one_names
|
|
209
|
+
|
|
210
|
+
@plus_one_names.setter
|
|
211
|
+
def plus_one_names(self, value: List[str] | None):
|
|
212
|
+
self._plus_one_names = value if isinstance(value, list) else []
|
|
213
|
+
|
|
214
|
+
# Properties - Check-in
|
|
215
|
+
@property
|
|
216
|
+
def checked_in(self) -> bool:
|
|
217
|
+
"""Has attendee checked in."""
|
|
218
|
+
return self._checked_in
|
|
219
|
+
|
|
220
|
+
@checked_in.setter
|
|
221
|
+
def checked_in(self, value: bool):
|
|
222
|
+
self._checked_in = bool(value)
|
|
223
|
+
|
|
224
|
+
@property
|
|
225
|
+
def checked_in_at_utc_ts(self) -> float | None:
|
|
226
|
+
"""When attendee checked in (UTC timestamp)."""
|
|
227
|
+
return self._checked_in_at_utc_ts
|
|
228
|
+
|
|
229
|
+
@checked_in_at_utc_ts.setter
|
|
230
|
+
def checked_in_at_utc_ts(self, value: float | None):
|
|
231
|
+
self._checked_in_at_utc_ts = value
|
|
232
|
+
|
|
233
|
+
@property
|
|
234
|
+
def checked_in_by_user_id(self) -> str | None:
|
|
235
|
+
"""Who checked in this attendee."""
|
|
236
|
+
return self._checked_in_by_user_id
|
|
237
|
+
|
|
238
|
+
@checked_in_by_user_id.setter
|
|
239
|
+
def checked_in_by_user_id(self, value: str | None):
|
|
240
|
+
self._checked_in_by_user_id = value
|
|
241
|
+
|
|
242
|
+
# Properties - Custom Registration
|
|
243
|
+
@property
|
|
244
|
+
def registration_data(self) -> Dict[str, Any]:
|
|
245
|
+
"""Custom registration field answers."""
|
|
246
|
+
return self._registration_data
|
|
247
|
+
|
|
248
|
+
@registration_data.setter
|
|
249
|
+
def registration_data(self, value: Dict[str, Any] | None):
|
|
250
|
+
self._registration_data = value if isinstance(value, dict) else {}
|
|
251
|
+
|
|
252
|
+
@property
|
|
253
|
+
def registration_notes(self) -> str | None:
|
|
254
|
+
"""Additional registration notes."""
|
|
255
|
+
return self._registration_notes
|
|
256
|
+
|
|
257
|
+
@registration_notes.setter
|
|
258
|
+
def registration_notes(self, value: str | None):
|
|
259
|
+
self._registration_notes = value
|
|
260
|
+
|
|
261
|
+
# Properties - Notifications
|
|
262
|
+
@property
|
|
263
|
+
def notification_preferences(self) -> Dict[str, bool]:
|
|
264
|
+
"""Notification preferences."""
|
|
265
|
+
return self._notification_preferences
|
|
266
|
+
|
|
267
|
+
@notification_preferences.setter
|
|
268
|
+
def notification_preferences(self, value: Dict[str, bool] | None):
|
|
269
|
+
self._notification_preferences = value if isinstance(value, dict) else {}
|
|
270
|
+
|
|
271
|
+
@property
|
|
272
|
+
def reminder_sent(self) -> bool:
|
|
273
|
+
"""Has reminder been sent."""
|
|
274
|
+
return self._reminder_sent
|
|
275
|
+
|
|
276
|
+
@reminder_sent.setter
|
|
277
|
+
def reminder_sent(self, value: bool):
|
|
278
|
+
self._reminder_sent = bool(value)
|
|
279
|
+
|
|
280
|
+
@property
|
|
281
|
+
def reminder_sent_at_utc_ts(self) -> float | None:
|
|
282
|
+
"""When reminder was sent (UTC timestamp)."""
|
|
283
|
+
return self._reminder_sent_at_utc_ts
|
|
284
|
+
|
|
285
|
+
@reminder_sent_at_utc_ts.setter
|
|
286
|
+
def reminder_sent_at_utc_ts(self, value: float | None):
|
|
287
|
+
self._reminder_sent_at_utc_ts = value
|
|
288
|
+
|
|
289
|
+
# Helper Methods
|
|
290
|
+
def is_organizer(self) -> bool:
|
|
291
|
+
"""Check if this attendee is an organizer."""
|
|
292
|
+
return self._role == "organizer"
|
|
293
|
+
|
|
294
|
+
def is_host(self) -> bool:
|
|
295
|
+
"""Check if this attendee is organizer or co-host."""
|
|
296
|
+
return self._role in ["organizer", "co_host"]
|
|
297
|
+
|
|
298
|
+
def has_accepted(self) -> bool:
|
|
299
|
+
"""Check if attendee accepted invitation."""
|
|
300
|
+
return self._rsvp_status == "accepted"
|
|
301
|
+
|
|
302
|
+
def has_declined(self) -> bool:
|
|
303
|
+
"""Check if attendee declined invitation."""
|
|
304
|
+
return self._rsvp_status == "declined"
|
|
305
|
+
|
|
306
|
+
def is_on_waitlist(self) -> bool:
|
|
307
|
+
"""Check if attendee is on waitlist."""
|
|
308
|
+
return self._rsvp_status == "waitlist"
|
|
309
|
+
|
|
310
|
+
def has_responded(self) -> bool:
|
|
311
|
+
"""Check if attendee has responded to invitation."""
|
|
312
|
+
return self._rsvp_status != "invited"
|
|
313
|
+
|
|
314
|
+
def has_plus_ones(self) -> bool:
|
|
315
|
+
"""Check if attendee is bringing +1 guests."""
|
|
316
|
+
return self._plus_one_count > 0
|
|
317
|
+
|
|
318
|
+
def total_attendee_count(self) -> int:
|
|
319
|
+
"""Total number of people attending (including +1s)."""
|
|
320
|
+
return 1 + (self._plus_one_count or 0)
|
|
321
|
+
|
|
322
|
+
def can_check_in(self) -> bool:
|
|
323
|
+
"""Check if attendee can check in (must be accepted)."""
|
|
324
|
+
return self.has_accepted() and not self._checked_in
|