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,684 @@
|
|
|
1
|
+
# Event Service (Refactored for location-based discovery and timestamps)
|
|
2
|
+
|
|
3
|
+
from typing import Dict, Any, Optional, List, Tuple
|
|
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, AccessDeniedError
|
|
8
|
+
from geek_cafe_saas_sdk.domains.events.models import Event, EventAttendee
|
|
9
|
+
from geek_cafe_saas_sdk.utilities.dynamodb_utils import build_projection_with_reserved_keywords
|
|
10
|
+
import datetime as dt
|
|
11
|
+
import math
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class EventService(DatabaseService[Event]):
|
|
15
|
+
"""
|
|
16
|
+
Service for Event database operations (refactored for location-based discovery).
|
|
17
|
+
|
|
18
|
+
Features:
|
|
19
|
+
- Timestamp-based date fields (float UTC timestamps)
|
|
20
|
+
- Location-based queries (city, state, geo)
|
|
21
|
+
- Automatic EventAttendee creation for owner
|
|
22
|
+
- Capacity management
|
|
23
|
+
- Multi-host support via EventAttendee
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
def __init__(self, *, dynamodb: DynamoDB = None, table_name: str = None):
|
|
27
|
+
super().__init__(dynamodb=dynamodb, table_name=table_name)
|
|
28
|
+
|
|
29
|
+
def create(self, tenant_id: str, user_id: str, create_organizer_attendee: bool = True,
|
|
30
|
+
**kwargs) -> ServiceResult[Event]:
|
|
31
|
+
"""
|
|
32
|
+
Create a new event.
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
tenant_id: Tenant ID
|
|
36
|
+
user_id: User ID (becomes owner_id)
|
|
37
|
+
create_organizer_attendee: Auto-create EventAttendee for organizer
|
|
38
|
+
**kwargs: Event fields
|
|
39
|
+
|
|
40
|
+
Required kwargs: title, start_utc_ts (or start_datetime)
|
|
41
|
+
"""
|
|
42
|
+
try:
|
|
43
|
+
# Validate required fields
|
|
44
|
+
if not kwargs.get('title'):
|
|
45
|
+
raise ValidationError("Title is required")
|
|
46
|
+
|
|
47
|
+
# Handle datetime conversion (support multiple field names)
|
|
48
|
+
start_utc_ts = kwargs.get('start_utc_ts')
|
|
49
|
+
if not start_utc_ts and kwargs.get('start_datetime'):
|
|
50
|
+
start_utc_ts = Event.datetime_to_utc_ts(kwargs['start_datetime'])
|
|
51
|
+
kwargs['start_utc_ts'] = start_utc_ts
|
|
52
|
+
elif not start_utc_ts and kwargs.get('date'):
|
|
53
|
+
# Support legacy 'date' field
|
|
54
|
+
start_utc_ts = Event.datetime_to_utc_ts(kwargs['date'])
|
|
55
|
+
kwargs['start_utc_ts'] = start_utc_ts
|
|
56
|
+
|
|
57
|
+
if not start_utc_ts:
|
|
58
|
+
raise ValidationError("start_utc_ts or start_datetime or date is required")
|
|
59
|
+
|
|
60
|
+
# Validate date is in future (only skip for explicitly draft status)
|
|
61
|
+
status = kwargs.get('status')
|
|
62
|
+
# Only allow past dates if status is explicitly set to 'draft'
|
|
63
|
+
if status != 'draft' and start_utc_ts < dt.datetime.now(dt.UTC).timestamp():
|
|
64
|
+
raise ValidationError("Event start time must be in the future")
|
|
65
|
+
|
|
66
|
+
# Create event instance
|
|
67
|
+
event = Event().map(kwargs)
|
|
68
|
+
event.owner_id = user_id # Primary owner
|
|
69
|
+
event.tenant_id = tenant_id
|
|
70
|
+
event.user_id = user_id
|
|
71
|
+
event.created_by_id = user_id
|
|
72
|
+
|
|
73
|
+
# Set defaults
|
|
74
|
+
if not event.status:
|
|
75
|
+
event.status = 'draft'
|
|
76
|
+
if not event.visibility:
|
|
77
|
+
event.visibility = 'public'
|
|
78
|
+
|
|
79
|
+
# Prepare for save
|
|
80
|
+
event.prep_for_save()
|
|
81
|
+
|
|
82
|
+
# Save to database
|
|
83
|
+
save_result = self._save_model(event)
|
|
84
|
+
|
|
85
|
+
if save_result.success and create_organizer_attendee:
|
|
86
|
+
# Create EventAttendee record for organizer
|
|
87
|
+
from .event_attendee_service import EventAttendeeService
|
|
88
|
+
attendee_service = EventAttendeeService(dynamodb=self.dynamodb, table_name=self.table_name)
|
|
89
|
+
|
|
90
|
+
attendee_result = attendee_service.invite(
|
|
91
|
+
event_id=event.id,
|
|
92
|
+
user_id=user_id,
|
|
93
|
+
tenant_id=tenant_id,
|
|
94
|
+
invited_by_user_id=user_id,
|
|
95
|
+
role='organizer',
|
|
96
|
+
rsvp_status='accepted'
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
# Don't fail event creation if attendee creation fails
|
|
100
|
+
if not attendee_result.success:
|
|
101
|
+
print(f"Warning: Failed to create attendee record for organizer: {attendee_result.error}")
|
|
102
|
+
|
|
103
|
+
return save_result
|
|
104
|
+
|
|
105
|
+
except Exception as e:
|
|
106
|
+
return self._handle_service_exception(e, 'create_event', tenant_id=tenant_id, user_id=user_id)
|
|
107
|
+
|
|
108
|
+
def get_by_id(self, resource_id: str, tenant_id: str, user_id: str) -> ServiceResult[Event]:
|
|
109
|
+
"""Get event by ID with access control."""
|
|
110
|
+
try:
|
|
111
|
+
event = self._get_model_by_id(resource_id, Event)
|
|
112
|
+
|
|
113
|
+
if not event:
|
|
114
|
+
raise NotFoundError(f"Event with ID {resource_id} not found")
|
|
115
|
+
|
|
116
|
+
# Check if deleted
|
|
117
|
+
if event.is_deleted():
|
|
118
|
+
raise NotFoundError(f"Event with ID {resource_id} not found")
|
|
119
|
+
|
|
120
|
+
# Validate tenant access
|
|
121
|
+
if hasattr(event, 'tenant_id'):
|
|
122
|
+
self._validate_tenant_access(event.tenant_id, tenant_id)
|
|
123
|
+
|
|
124
|
+
return ServiceResult.success_result(event)
|
|
125
|
+
|
|
126
|
+
except Exception as e:
|
|
127
|
+
return self._handle_service_exception(e, 'get_event', resource_id=resource_id, tenant_id=tenant_id)
|
|
128
|
+
|
|
129
|
+
def list_by_owner(self, owner_id: str, tenant_id: str, limit: int = 50) -> ServiceResult[List[Event]]:
|
|
130
|
+
"""
|
|
131
|
+
Get events by owner (primary organizer) using GSI1.
|
|
132
|
+
|
|
133
|
+
Args:
|
|
134
|
+
owner_id: Owner user ID
|
|
135
|
+
tenant_id: Tenant ID
|
|
136
|
+
limit: Max results
|
|
137
|
+
"""
|
|
138
|
+
try:
|
|
139
|
+
temp_event = Event()
|
|
140
|
+
temp_event.owner_id = owner_id
|
|
141
|
+
|
|
142
|
+
result = self._query_by_index(
|
|
143
|
+
temp_event,
|
|
144
|
+
"gsi1",
|
|
145
|
+
ascending=False, # Most recent first
|
|
146
|
+
limit=limit
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
if not result.success:
|
|
150
|
+
return result
|
|
151
|
+
|
|
152
|
+
# Filter out deleted events and validate tenant
|
|
153
|
+
active_events = []
|
|
154
|
+
for event in result.data:
|
|
155
|
+
if not event.is_deleted() and event.tenant_id == tenant_id:
|
|
156
|
+
active_events.append(event)
|
|
157
|
+
|
|
158
|
+
return ServiceResult.success_result(active_events)
|
|
159
|
+
|
|
160
|
+
except Exception as e:
|
|
161
|
+
return self._handle_service_exception(e, 'list_by_owner', owner_id=owner_id)
|
|
162
|
+
|
|
163
|
+
def list_by_city(self, city: str, state: str, country: str, tenant_id: str,
|
|
164
|
+
start_date: float = None, end_date: float = None,
|
|
165
|
+
limit: int = 50) -> ServiceResult[List[Event]]:
|
|
166
|
+
"""
|
|
167
|
+
Get events by city using GSI2 (city#state#country).
|
|
168
|
+
|
|
169
|
+
Args:
|
|
170
|
+
city: City name
|
|
171
|
+
state: State/Province/Region
|
|
172
|
+
country: Country code
|
|
173
|
+
tenant_id: Tenant ID
|
|
174
|
+
start_date: Optional start date filter (UTC timestamp)
|
|
175
|
+
end_date: Optional end date filter (UTC timestamp)
|
|
176
|
+
limit: Max results
|
|
177
|
+
"""
|
|
178
|
+
try:
|
|
179
|
+
temp_event = Event()
|
|
180
|
+
temp_event.location_city = city
|
|
181
|
+
temp_event.location_state = state
|
|
182
|
+
temp_event.location_country = country
|
|
183
|
+
|
|
184
|
+
# Use helper method for query
|
|
185
|
+
result = self._query_by_index(
|
|
186
|
+
model=temp_event,
|
|
187
|
+
index_name="gsi2",
|
|
188
|
+
ascending=True, # Upcoming events first
|
|
189
|
+
limit=limit
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
if not result.success:
|
|
193
|
+
return result
|
|
194
|
+
|
|
195
|
+
# Post-query filtering
|
|
196
|
+
valid_events = []
|
|
197
|
+
for event in result.data:
|
|
198
|
+
# Tenant isolation and deleted check
|
|
199
|
+
if event.tenant_id != tenant_id or event.is_deleted():
|
|
200
|
+
continue
|
|
201
|
+
|
|
202
|
+
# Date range filter if specified
|
|
203
|
+
if start_date and end_date:
|
|
204
|
+
if not (start_date <= event.start_utc_ts <= end_date):
|
|
205
|
+
continue
|
|
206
|
+
|
|
207
|
+
valid_events.append(event)
|
|
208
|
+
|
|
209
|
+
return ServiceResult.success_result(valid_events)
|
|
210
|
+
|
|
211
|
+
except Exception as e:
|
|
212
|
+
return self._handle_service_exception(e, 'list_by_city', city=city, state=state, country=country)
|
|
213
|
+
|
|
214
|
+
def list_by_state(self, state: str, country: str, tenant_id: str,
|
|
215
|
+
limit: int = 50) -> ServiceResult[List[Event]]:
|
|
216
|
+
"""
|
|
217
|
+
Get events by state/region using GSI3.
|
|
218
|
+
|
|
219
|
+
Args:
|
|
220
|
+
state: State/Province/Region
|
|
221
|
+
country: Country code
|
|
222
|
+
tenant_id: Tenant ID
|
|
223
|
+
limit: Max results
|
|
224
|
+
"""
|
|
225
|
+
try:
|
|
226
|
+
temp_event = Event()
|
|
227
|
+
temp_event.location_state = state
|
|
228
|
+
temp_event.location_country = country
|
|
229
|
+
|
|
230
|
+
# Use helper method for query
|
|
231
|
+
result = self._query_by_index(
|
|
232
|
+
model=temp_event,
|
|
233
|
+
index_name="gsi3",
|
|
234
|
+
ascending=True,
|
|
235
|
+
limit=limit
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
if not result.success:
|
|
239
|
+
return result
|
|
240
|
+
|
|
241
|
+
# Post-query filtering
|
|
242
|
+
valid_events = [
|
|
243
|
+
event for event in result.data
|
|
244
|
+
if event.tenant_id == tenant_id and not event.is_deleted()
|
|
245
|
+
]
|
|
246
|
+
|
|
247
|
+
return ServiceResult.success_result(valid_events)
|
|
248
|
+
|
|
249
|
+
except Exception as e:
|
|
250
|
+
return self._handle_service_exception(e, 'list_by_state', state=state, country=country)
|
|
251
|
+
|
|
252
|
+
def list_nearby(self, latitude: float, longitude: float, radius_km: float,
|
|
253
|
+
tenant_id: str, limit: int = 50) -> ServiceResult[List[Event]]:
|
|
254
|
+
"""
|
|
255
|
+
Get events near a location using GSI4 (geo-location).
|
|
256
|
+
|
|
257
|
+
Uses geohash for approximate location then filters by actual distance.
|
|
258
|
+
|
|
259
|
+
Args:
|
|
260
|
+
latitude: Latitude
|
|
261
|
+
longitude: Longitude
|
|
262
|
+
radius_km: Search radius in kilometers
|
|
263
|
+
tenant_id: Tenant ID
|
|
264
|
+
limit: Max results
|
|
265
|
+
"""
|
|
266
|
+
try:
|
|
267
|
+
# Create temp event to get geohash
|
|
268
|
+
temp_event = Event()
|
|
269
|
+
temp_event.location_latitude = latitude
|
|
270
|
+
temp_event.location_longitude = longitude
|
|
271
|
+
|
|
272
|
+
geohash = temp_event._get_geohash()
|
|
273
|
+
if not geohash:
|
|
274
|
+
return ServiceResult.error_result("Invalid coordinates", "INVALID_COORDINATES")
|
|
275
|
+
|
|
276
|
+
# Use helper method for query (get more results for distance filtering)
|
|
277
|
+
result = self._query_by_index(
|
|
278
|
+
model=temp_event,
|
|
279
|
+
index_name="gsi4",
|
|
280
|
+
ascending=True,
|
|
281
|
+
limit=limit * 3 # Get extra to filter by distance
|
|
282
|
+
)
|
|
283
|
+
|
|
284
|
+
if not result.success:
|
|
285
|
+
return result
|
|
286
|
+
|
|
287
|
+
# Filter by actual distance and tenant
|
|
288
|
+
nearby_events = []
|
|
289
|
+
for event in result.data:
|
|
290
|
+
# Tenant isolation and deleted check
|
|
291
|
+
if event.tenant_id != tenant_id or event.is_deleted():
|
|
292
|
+
continue
|
|
293
|
+
|
|
294
|
+
if event.has_location_coordinates():
|
|
295
|
+
distance = self._calculate_distance(
|
|
296
|
+
latitude, longitude,
|
|
297
|
+
event.location_latitude, event.location_longitude
|
|
298
|
+
)
|
|
299
|
+
if distance <= radius_km:
|
|
300
|
+
nearby_events.append(event)
|
|
301
|
+
if len(nearby_events) >= limit:
|
|
302
|
+
break
|
|
303
|
+
|
|
304
|
+
return ServiceResult.success_result(nearby_events)
|
|
305
|
+
|
|
306
|
+
except Exception as e:
|
|
307
|
+
return self._handle_service_exception(e, 'list_nearby', latitude=latitude, longitude=longitude)
|
|
308
|
+
|
|
309
|
+
def list_by_type(self, event_type: str, tenant_id: str, status: str = 'published',
|
|
310
|
+
limit: int = 50) -> ServiceResult[List[Event]]:
|
|
311
|
+
"""
|
|
312
|
+
Get events by type using GSI6.
|
|
313
|
+
|
|
314
|
+
Args:
|
|
315
|
+
event_type: Event type (meetup, conference, etc.)
|
|
316
|
+
tenant_id: Tenant ID
|
|
317
|
+
status: Event status filter
|
|
318
|
+
limit: Max results
|
|
319
|
+
"""
|
|
320
|
+
try:
|
|
321
|
+
temp_event = Event()
|
|
322
|
+
temp_event.event_type = event_type
|
|
323
|
+
temp_event.status = status
|
|
324
|
+
|
|
325
|
+
# Use helper method for query
|
|
326
|
+
result = self._query_by_index(
|
|
327
|
+
model=temp_event,
|
|
328
|
+
index_name="gsi6",
|
|
329
|
+
ascending=True,
|
|
330
|
+
limit=limit
|
|
331
|
+
)
|
|
332
|
+
|
|
333
|
+
if not result.success:
|
|
334
|
+
return result
|
|
335
|
+
|
|
336
|
+
# Post-query filtering
|
|
337
|
+
valid_events = [
|
|
338
|
+
event for event in result.data
|
|
339
|
+
if event.tenant_id == tenant_id and not event.is_deleted()
|
|
340
|
+
]
|
|
341
|
+
|
|
342
|
+
return ServiceResult.success_result(valid_events)
|
|
343
|
+
|
|
344
|
+
except Exception as e:
|
|
345
|
+
return self._handle_service_exception(e, 'list_by_type', event_type=event_type)
|
|
346
|
+
|
|
347
|
+
def list_by_group(self, group_id: str, tenant_id: str, limit: int = 50) -> ServiceResult[List[Event]]:
|
|
348
|
+
"""
|
|
349
|
+
Get events for a group using GSI7.
|
|
350
|
+
|
|
351
|
+
Args:
|
|
352
|
+
group_id: Group ID
|
|
353
|
+
tenant_id: Tenant ID
|
|
354
|
+
limit: Max results
|
|
355
|
+
"""
|
|
356
|
+
try:
|
|
357
|
+
temp_event = Event()
|
|
358
|
+
temp_event.group_id = group_id
|
|
359
|
+
|
|
360
|
+
result = self._query_by_index(
|
|
361
|
+
temp_event,
|
|
362
|
+
"gsi7",
|
|
363
|
+
ascending=False,
|
|
364
|
+
limit=limit
|
|
365
|
+
)
|
|
366
|
+
|
|
367
|
+
if not result.success:
|
|
368
|
+
return result
|
|
369
|
+
|
|
370
|
+
# Filter tenant and deleted
|
|
371
|
+
active_events = []
|
|
372
|
+
for event in result.data:
|
|
373
|
+
if not event.is_deleted() and event.tenant_id == tenant_id:
|
|
374
|
+
active_events.append(event)
|
|
375
|
+
|
|
376
|
+
return ServiceResult.success_result(active_events)
|
|
377
|
+
|
|
378
|
+
except Exception as e:
|
|
379
|
+
return self._handle_service_exception(e, 'list_by_group', group_id=group_id)
|
|
380
|
+
|
|
381
|
+
def list_public_events(self, tenant_id: str, visibility: str = 'public',
|
|
382
|
+
status: str = 'published', limit: int = 50) -> ServiceResult[List[Event]]:
|
|
383
|
+
"""
|
|
384
|
+
Get public published events using GSI8 (discovery feed).
|
|
385
|
+
|
|
386
|
+
Args:
|
|
387
|
+
tenant_id: Tenant ID
|
|
388
|
+
visibility: Visibility filter (public, private, members_only)
|
|
389
|
+
status: Status filter (published, draft, etc.)
|
|
390
|
+
limit: Max results
|
|
391
|
+
"""
|
|
392
|
+
try:
|
|
393
|
+
temp_event = Event()
|
|
394
|
+
temp_event.visibility = visibility
|
|
395
|
+
temp_event.status = status
|
|
396
|
+
|
|
397
|
+
# Use helper method for query
|
|
398
|
+
result = self._query_by_index(
|
|
399
|
+
model=temp_event,
|
|
400
|
+
index_name="gsi8",
|
|
401
|
+
ascending=True, # Upcoming first
|
|
402
|
+
limit=limit
|
|
403
|
+
)
|
|
404
|
+
|
|
405
|
+
if not result.success:
|
|
406
|
+
return result
|
|
407
|
+
|
|
408
|
+
# Post-query filtering
|
|
409
|
+
valid_events = [
|
|
410
|
+
event for event in result.data
|
|
411
|
+
if event.tenant_id == tenant_id and not event.is_deleted()
|
|
412
|
+
]
|
|
413
|
+
|
|
414
|
+
return ServiceResult.success_result(valid_events)
|
|
415
|
+
|
|
416
|
+
except Exception as e:
|
|
417
|
+
return self._handle_service_exception(e, 'list_public_events', visibility=visibility, status=status)
|
|
418
|
+
|
|
419
|
+
def list_by_tenant(self, tenant_id: str, user_id: str = None, limit: int = 50) -> ServiceResult[List[Event]]:
|
|
420
|
+
"""
|
|
421
|
+
Get all events for a tenant using GSI5.
|
|
422
|
+
|
|
423
|
+
Args:
|
|
424
|
+
tenant_id: Tenant ID
|
|
425
|
+
user_id: User ID (optional, for access control)
|
|
426
|
+
limit: Max results
|
|
427
|
+
"""
|
|
428
|
+
try:
|
|
429
|
+
temp_event = Event()
|
|
430
|
+
temp_event.tenant_id = tenant_id
|
|
431
|
+
|
|
432
|
+
result = self._query_by_index(
|
|
433
|
+
temp_event,
|
|
434
|
+
"gsi5",
|
|
435
|
+
ascending=False,
|
|
436
|
+
limit=limit
|
|
437
|
+
)
|
|
438
|
+
|
|
439
|
+
if not result.success:
|
|
440
|
+
return result
|
|
441
|
+
|
|
442
|
+
# Filter deleted
|
|
443
|
+
active_events = []
|
|
444
|
+
for event in result.data:
|
|
445
|
+
if not event.is_deleted():
|
|
446
|
+
active_events.append(event)
|
|
447
|
+
|
|
448
|
+
return ServiceResult.success_result(active_events)
|
|
449
|
+
|
|
450
|
+
except Exception as e:
|
|
451
|
+
return self._handle_service_exception(e, 'list_by_tenant', tenant_id=tenant_id)
|
|
452
|
+
|
|
453
|
+
def update(self, resource_id: str, tenant_id: str, user_id: str,
|
|
454
|
+
updates: Dict[str, Any]) -> ServiceResult[Event]:
|
|
455
|
+
"""
|
|
456
|
+
Update event with access control.
|
|
457
|
+
|
|
458
|
+
Args:
|
|
459
|
+
resource_id: Event ID
|
|
460
|
+
tenant_id: Tenant ID
|
|
461
|
+
user_id: User making update
|
|
462
|
+
updates: Fields to update
|
|
463
|
+
"""
|
|
464
|
+
try:
|
|
465
|
+
# Get existing event
|
|
466
|
+
event = self._get_model_by_id(resource_id, Event)
|
|
467
|
+
|
|
468
|
+
if not event:
|
|
469
|
+
raise NotFoundError(f"Event with ID {resource_id} not found")
|
|
470
|
+
|
|
471
|
+
# Validate tenant access
|
|
472
|
+
if hasattr(event, 'tenant_id'):
|
|
473
|
+
self._validate_tenant_access(event.tenant_id, tenant_id)
|
|
474
|
+
|
|
475
|
+
# Check permissions (owner or host)
|
|
476
|
+
if not (event.owner_id == user_id or self._is_host(resource_id, user_id, tenant_id)):
|
|
477
|
+
raise AccessDeniedError("Access denied: only event owner/hosts can update")
|
|
478
|
+
|
|
479
|
+
# Handle datetime conversions
|
|
480
|
+
if 'start_datetime' in updates and 'start_utc_ts' not in updates:
|
|
481
|
+
updates['start_utc_ts'] = Event.datetime_to_utc_ts(updates['start_datetime'])
|
|
482
|
+
if 'end_datetime' in updates and 'end_utc_ts' not in updates:
|
|
483
|
+
updates['end_utc_ts'] = Event.datetime_to_utc_ts(updates['end_datetime'])
|
|
484
|
+
|
|
485
|
+
# Validate dates if updating
|
|
486
|
+
if 'start_utc_ts' in updates:
|
|
487
|
+
if event.status != 'draft' and updates['start_utc_ts'] < dt.datetime.now(dt.UTC).timestamp():
|
|
488
|
+
raise ValidationError("Event start time must be in the future")
|
|
489
|
+
|
|
490
|
+
# Block group_id changes after creation
|
|
491
|
+
if 'group_id' in updates and event.group_id and updates['group_id'] != event.group_id:
|
|
492
|
+
raise ValidationError("Cannot change group_id after event creation")
|
|
493
|
+
|
|
494
|
+
# Apply updates using map()
|
|
495
|
+
event = event.map(updates)
|
|
496
|
+
|
|
497
|
+
# Update metadata
|
|
498
|
+
event.updated_by_id = user_id
|
|
499
|
+
event.prep_for_save()
|
|
500
|
+
|
|
501
|
+
# Save
|
|
502
|
+
return self._save_model(event)
|
|
503
|
+
|
|
504
|
+
except Exception as e:
|
|
505
|
+
return self._handle_service_exception(e, 'update_event', resource_id=resource_id)
|
|
506
|
+
|
|
507
|
+
def delete(self, resource_id: str, tenant_id: str, user_id: str) -> ServiceResult[bool]:
|
|
508
|
+
"""
|
|
509
|
+
Soft delete event with access control.
|
|
510
|
+
|
|
511
|
+
Args:
|
|
512
|
+
resource_id: Event ID
|
|
513
|
+
tenant_id: Tenant ID
|
|
514
|
+
user_id: User deleting event
|
|
515
|
+
"""
|
|
516
|
+
try:
|
|
517
|
+
event = self._get_model_by_id(resource_id, Event)
|
|
518
|
+
|
|
519
|
+
if not event:
|
|
520
|
+
raise NotFoundError(f"Event with ID {resource_id} not found")
|
|
521
|
+
|
|
522
|
+
if event.is_deleted():
|
|
523
|
+
return ServiceResult.success_result(True)
|
|
524
|
+
|
|
525
|
+
# Validate tenant
|
|
526
|
+
if hasattr(event, 'tenant_id'):
|
|
527
|
+
self._validate_tenant_access(event.tenant_id, tenant_id)
|
|
528
|
+
|
|
529
|
+
# Check permissions
|
|
530
|
+
if not (event.owner_id == user_id or self._is_host(resource_id, user_id, tenant_id)):
|
|
531
|
+
raise AccessDeniedError("Access denied: only event owner/hosts can delete")
|
|
532
|
+
|
|
533
|
+
# Soft delete
|
|
534
|
+
event.deleted_utc_ts = dt.datetime.now(dt.UTC).timestamp()
|
|
535
|
+
event.deleted_by_id = user_id
|
|
536
|
+
event.prep_for_save()
|
|
537
|
+
|
|
538
|
+
save_result = self._save_model(event)
|
|
539
|
+
return ServiceResult.success_result(save_result.success) if save_result.success else save_result
|
|
540
|
+
|
|
541
|
+
except Exception as e:
|
|
542
|
+
return self._handle_service_exception(e, 'delete_event', resource_id=resource_id)
|
|
543
|
+
|
|
544
|
+
def publish(self, resource_id: str, tenant_id: str, user_id: str) -> ServiceResult[Event]:
|
|
545
|
+
"""
|
|
546
|
+
Publish a draft event.
|
|
547
|
+
|
|
548
|
+
Args:
|
|
549
|
+
resource_id: Event ID
|
|
550
|
+
tenant_id: Tenant ID
|
|
551
|
+
user_id: User publishing
|
|
552
|
+
"""
|
|
553
|
+
try:
|
|
554
|
+
return self.update(resource_id, tenant_id, user_id, {'status': 'published'})
|
|
555
|
+
except Exception as e:
|
|
556
|
+
return self._handle_service_exception(e, 'publish_event', resource_id=resource_id)
|
|
557
|
+
|
|
558
|
+
def cancel(self, resource_id: str, tenant_id: str, user_id: str,
|
|
559
|
+
cancellation_reason: str = None) -> ServiceResult[Event]:
|
|
560
|
+
"""
|
|
561
|
+
Cancel an event.
|
|
562
|
+
|
|
563
|
+
Args:
|
|
564
|
+
resource_id: Event ID
|
|
565
|
+
tenant_id: Tenant ID
|
|
566
|
+
user_id: User cancelling
|
|
567
|
+
cancellation_reason: Reason for cancellation
|
|
568
|
+
"""
|
|
569
|
+
try:
|
|
570
|
+
updates = {'status': 'cancelled'}
|
|
571
|
+
if cancellation_reason:
|
|
572
|
+
updates['cancellation_reason'] = cancellation_reason
|
|
573
|
+
|
|
574
|
+
return self.update(resource_id, tenant_id, user_id, updates)
|
|
575
|
+
except Exception as e:
|
|
576
|
+
return self._handle_service_exception(e, 'cancel_event', resource_id=resource_id)
|
|
577
|
+
|
|
578
|
+
def create_draft(self, tenant_id: str, user_id: str, **kwargs) -> ServiceResult[Event]:
|
|
579
|
+
"""
|
|
580
|
+
Create a draft event with minimal required fields.
|
|
581
|
+
Auto-generates a future date if not provided.
|
|
582
|
+
|
|
583
|
+
Args:
|
|
584
|
+
tenant_id: Tenant ID
|
|
585
|
+
user_id: User ID
|
|
586
|
+
**kwargs: Optional event fields
|
|
587
|
+
"""
|
|
588
|
+
# Ensure status is draft
|
|
589
|
+
kwargs['status'] = 'draft'
|
|
590
|
+
|
|
591
|
+
# Auto-generate start date if not provided (1 week from now)
|
|
592
|
+
if not kwargs.get('start_utc_ts') and not kwargs.get('start_datetime') and not kwargs.get('date'):
|
|
593
|
+
future_date = dt.datetime.now(dt.UTC) + dt.timedelta(days=7)
|
|
594
|
+
kwargs['start_utc_ts'] = future_date.timestamp()
|
|
595
|
+
|
|
596
|
+
# Use the standard create method (draft events don't require date validation)
|
|
597
|
+
return self.create(tenant_id, user_id, **kwargs)
|
|
598
|
+
|
|
599
|
+
def get_events_by_organizer(self, organizer_id: str, tenant_id: str, user_id: str,
|
|
600
|
+
limit: int = 50) -> ServiceResult[List[Event]]:
|
|
601
|
+
"""
|
|
602
|
+
Get events by organizer ID (uses owner_id field).
|
|
603
|
+
Alias for list_by_owner for backward compatibility.
|
|
604
|
+
|
|
605
|
+
Args:
|
|
606
|
+
organizer_id: Organizer user ID
|
|
607
|
+
tenant_id: Tenant ID
|
|
608
|
+
user_id: Requesting user ID
|
|
609
|
+
limit: Max results
|
|
610
|
+
"""
|
|
611
|
+
return self.list_by_owner(organizer_id, tenant_id, limit)
|
|
612
|
+
|
|
613
|
+
def get_events_by_group(self, group_id: str, tenant_id: str, user_id: str,
|
|
614
|
+
limit: int = 50) -> ServiceResult[List[Event]]:
|
|
615
|
+
"""
|
|
616
|
+
Get events by group ID.
|
|
617
|
+
Wrapper for list_by_group for backward compatibility.
|
|
618
|
+
|
|
619
|
+
Args:
|
|
620
|
+
group_id: Group ID
|
|
621
|
+
tenant_id: Tenant ID
|
|
622
|
+
user_id: Requesting user ID
|
|
623
|
+
limit: Max results
|
|
624
|
+
"""
|
|
625
|
+
return self.list_by_group(group_id, tenant_id, limit)
|
|
626
|
+
|
|
627
|
+
def get_upcoming_events(self, tenant_id: str, user_id: str, limit: int = 50) -> ServiceResult[List[Event]]:
|
|
628
|
+
"""
|
|
629
|
+
Get upcoming events for a tenant (events starting in the future).
|
|
630
|
+
|
|
631
|
+
Args:
|
|
632
|
+
tenant_id: Tenant ID
|
|
633
|
+
user_id: Requesting user ID
|
|
634
|
+
limit: Max results
|
|
635
|
+
"""
|
|
636
|
+
try:
|
|
637
|
+
# Get all events for tenant
|
|
638
|
+
result = self.list_by_tenant(tenant_id, limit=limit)
|
|
639
|
+
|
|
640
|
+
if not result.success:
|
|
641
|
+
return result
|
|
642
|
+
|
|
643
|
+
# Filter to only upcoming events
|
|
644
|
+
now_ts = dt.datetime.now(dt.UTC).timestamp()
|
|
645
|
+
upcoming_events = [
|
|
646
|
+
event for event in result.data
|
|
647
|
+
if event.start_utc_ts and event.start_utc_ts > now_ts
|
|
648
|
+
]
|
|
649
|
+
|
|
650
|
+
# Sort by start date (soonest first)
|
|
651
|
+
upcoming_events.sort(key=lambda e: e.start_utc_ts or 0)
|
|
652
|
+
|
|
653
|
+
return ServiceResult.success_result(upcoming_events)
|
|
654
|
+
|
|
655
|
+
except Exception as e:
|
|
656
|
+
return self._handle_service_exception(e, 'get_upcoming_events', tenant_id=tenant_id)
|
|
657
|
+
|
|
658
|
+
# Helper Methods
|
|
659
|
+
def _is_host(self, event_id: str, user_id: str, tenant_id: str) -> bool:
|
|
660
|
+
"""Check if user is a host (organizer or co-host) for event."""
|
|
661
|
+
try:
|
|
662
|
+
from .event_attendee_service import EventAttendeeService
|
|
663
|
+
attendee_service = EventAttendeeService(dynamodb=self.dynamodb, table_name=self.table_name)
|
|
664
|
+
|
|
665
|
+
result = attendee_service.get_attendee(event_id, user_id, tenant_id)
|
|
666
|
+
if result.success:
|
|
667
|
+
return result.data.is_host()
|
|
668
|
+
return False
|
|
669
|
+
except:
|
|
670
|
+
return False
|
|
671
|
+
|
|
672
|
+
def _calculate_distance(self, lat1: float, lng1: float, lat2: float, lng2: float) -> float:
|
|
673
|
+
"""Calculate distance between two coordinates in kilometers (Haversine formula)."""
|
|
674
|
+
R = 6371 # Earth's radius in km
|
|
675
|
+
|
|
676
|
+
lat1_rad = math.radians(lat1)
|
|
677
|
+
lat2_rad = math.radians(lat2)
|
|
678
|
+
delta_lat = math.radians(lat2 - lat1)
|
|
679
|
+
delta_lng = math.radians(lng2 - lng1)
|
|
680
|
+
|
|
681
|
+
a = math.sin(delta_lat / 2) ** 2 + math.cos(lat1_rad) * math.cos(lat2_rad) * math.sin(delta_lng / 2) ** 2
|
|
682
|
+
c = 2 * math.atan2(math.sqrt(a), math.sqrt(1 - a))
|
|
683
|
+
|
|
684
|
+
return R * c
|
|
File without changes
|
|
File without changes
|