django-nativemojo 0.1.10__py3-none-any.whl → 0.1.16__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.
- django_nativemojo-0.1.16.dist-info/METADATA +138 -0
- django_nativemojo-0.1.16.dist-info/RECORD +302 -0
- mojo/__init__.py +1 -1
- mojo/apps/account/management/__init__.py +5 -0
- mojo/apps/account/management/commands/__init__.py +6 -0
- mojo/apps/account/management/commands/serializer_admin.py +651 -0
- mojo/apps/account/migrations/0004_user_avatar.py +20 -0
- mojo/apps/account/migrations/0005_group_last_activity.py +18 -0
- mojo/apps/account/migrations/0006_add_device_tracking_models.py +72 -0
- mojo/apps/account/migrations/0007_delete_userdevicelocation.py +16 -0
- mojo/apps/account/migrations/0008_userdevicelocation.py +33 -0
- mojo/apps/account/migrations/0009_geolocatedip_subnet.py +18 -0
- mojo/apps/account/migrations/0010_group_avatar.py +20 -0
- mojo/apps/account/migrations/0011_user_org_registereddevice_pushconfig_and_more.py +118 -0
- mojo/apps/account/migrations/0012_remove_pushconfig_apns_key_file_and_more.py +21 -0
- mojo/apps/account/migrations/0013_pushconfig_test_mode_alter_pushconfig_apns_enabled_and_more.py +28 -0
- mojo/apps/account/migrations/0014_notificationdelivery_data_payload_and_more.py +48 -0
- mojo/apps/account/models/__init__.py +2 -0
- mojo/apps/account/models/device.py +281 -0
- mojo/apps/account/models/group.py +319 -15
- mojo/apps/account/models/member.py +29 -5
- mojo/apps/account/models/push/__init__.py +4 -0
- mojo/apps/account/models/push/config.py +112 -0
- mojo/apps/account/models/push/delivery.py +93 -0
- mojo/apps/account/models/push/device.py +66 -0
- mojo/apps/account/models/push/template.py +99 -0
- mojo/apps/account/models/user.py +369 -19
- mojo/apps/account/rest/__init__.py +2 -0
- mojo/apps/account/rest/device.py +39 -0
- mojo/apps/account/rest/group.py +9 -0
- mojo/apps/account/rest/push.py +187 -0
- mojo/apps/account/rest/user.py +100 -6
- mojo/apps/account/services/__init__.py +1 -0
- mojo/apps/account/services/push.py +363 -0
- mojo/apps/aws/migrations/0001_initial.py +206 -0
- mojo/apps/aws/migrations/0002_emaildomain_can_recv_emaildomain_can_send_and_more.py +28 -0
- mojo/apps/aws/migrations/0003_mailbox_is_domain_default_mailbox_is_system_default_and_more.py +31 -0
- mojo/apps/aws/migrations/0004_s3bucket.py +39 -0
- mojo/apps/aws/migrations/0005_alter_emaildomain_region_delete_s3bucket.py +21 -0
- mojo/apps/aws/models/__init__.py +19 -0
- mojo/apps/aws/models/email_attachment.py +99 -0
- mojo/apps/aws/models/email_domain.py +218 -0
- mojo/apps/aws/models/email_template.py +132 -0
- mojo/apps/aws/models/incoming_email.py +197 -0
- mojo/apps/aws/models/mailbox.py +288 -0
- mojo/apps/aws/models/sent_message.py +175 -0
- mojo/apps/aws/rest/__init__.py +7 -0
- mojo/apps/aws/rest/email.py +33 -0
- mojo/apps/aws/rest/email_ops.py +183 -0
- mojo/apps/aws/rest/messages.py +32 -0
- mojo/apps/aws/rest/s3.py +64 -0
- mojo/apps/aws/rest/send.py +101 -0
- mojo/apps/aws/rest/sns.py +403 -0
- mojo/apps/aws/rest/templates.py +19 -0
- mojo/apps/aws/services/__init__.py +32 -0
- mojo/apps/aws/services/email.py +390 -0
- mojo/apps/aws/services/email_ops.py +548 -0
- mojo/apps/docit/__init__.py +6 -0
- mojo/apps/docit/markdown_plugins/syntax_highlight.py +25 -0
- mojo/apps/docit/markdown_plugins/toc.py +12 -0
- mojo/apps/docit/migrations/0001_initial.py +113 -0
- mojo/apps/docit/migrations/0002_alter_book_modified_by_alter_page_modified_by.py +26 -0
- mojo/apps/docit/migrations/0003_alter_book_group.py +20 -0
- mojo/apps/docit/models/__init__.py +17 -0
- mojo/apps/docit/models/asset.py +231 -0
- mojo/apps/docit/models/book.py +227 -0
- mojo/apps/docit/models/page.py +319 -0
- mojo/apps/docit/models/page_revision.py +203 -0
- mojo/apps/docit/rest/__init__.py +10 -0
- mojo/apps/docit/rest/asset.py +17 -0
- mojo/apps/docit/rest/book.py +22 -0
- mojo/apps/docit/rest/page.py +22 -0
- mojo/apps/docit/rest/page_revision.py +17 -0
- mojo/apps/docit/services/__init__.py +11 -0
- mojo/apps/docit/services/docit.py +315 -0
- mojo/apps/docit/services/markdown.py +44 -0
- mojo/apps/fileman/README.md +8 -8
- mojo/apps/fileman/backends/base.py +76 -70
- mojo/apps/fileman/backends/filesystem.py +86 -86
- mojo/apps/fileman/backends/s3.py +409 -108
- mojo/apps/fileman/migrations/0001_initial.py +106 -0
- mojo/apps/fileman/migrations/0002_filemanager_parent_alter_filemanager_max_file_size.py +24 -0
- mojo/apps/fileman/migrations/0003_remove_file_fileman_fil_upload__c4bc35_idx_and_more.py +25 -0
- mojo/apps/fileman/migrations/0004_remove_file_original_filename_and_more.py +39 -0
- mojo/apps/fileman/migrations/0005_alter_file_upload_token.py +18 -0
- mojo/apps/fileman/migrations/0006_file_download_url_filemanager_forever_urls.py +23 -0
- mojo/apps/fileman/migrations/0007_remove_filemanager_forever_urls_and_more.py +22 -0
- mojo/apps/fileman/migrations/0008_file_category.py +18 -0
- mojo/apps/fileman/migrations/0009_rename_file_path_file_storage_file_path.py +18 -0
- mojo/apps/fileman/migrations/0010_filerendition.py +33 -0
- mojo/apps/fileman/migrations/0011_alter_filerendition_original_file.py +19 -0
- mojo/apps/fileman/models/__init__.py +1 -5
- mojo/apps/fileman/models/file.py +240 -58
- mojo/apps/fileman/models/manager.py +427 -31
- mojo/apps/fileman/models/rendition.py +118 -0
- mojo/apps/fileman/renderer/__init__.py +111 -0
- mojo/apps/fileman/renderer/audio.py +403 -0
- mojo/apps/fileman/renderer/base.py +205 -0
- mojo/apps/fileman/renderer/document.py +404 -0
- mojo/apps/fileman/renderer/image.py +222 -0
- mojo/apps/fileman/renderer/utils.py +297 -0
- mojo/apps/fileman/renderer/video.py +304 -0
- mojo/apps/fileman/rest/__init__.py +1 -18
- mojo/apps/fileman/rest/upload.py +22 -32
- mojo/apps/fileman/signals.py +58 -0
- mojo/apps/fileman/tasks.py +254 -0
- mojo/apps/fileman/utils/__init__.py +40 -16
- mojo/apps/incident/migrations/0005_incidenthistory.py +39 -0
- mojo/apps/incident/migrations/0006_alter_incident_state.py +18 -0
- mojo/apps/incident/migrations/0007_event_uid.py +18 -0
- mojo/apps/incident/migrations/0008_ticket_ticketnote.py +55 -0
- mojo/apps/incident/migrations/0009_incident_status.py +18 -0
- mojo/apps/incident/migrations/0010_event_country_code.py +18 -0
- mojo/apps/incident/migrations/0011_incident_country_code.py +18 -0
- mojo/apps/incident/migrations/0012_alter_incident_status.py +18 -0
- mojo/apps/incident/models/__init__.py +2 -0
- mojo/apps/incident/models/event.py +35 -0
- mojo/apps/incident/models/history.py +36 -0
- mojo/apps/incident/models/incident.py +3 -1
- mojo/apps/incident/models/ticket.py +62 -0
- mojo/apps/incident/reporter.py +21 -1
- mojo/apps/incident/rest/__init__.py +1 -0
- mojo/apps/incident/rest/event.py +7 -1
- mojo/apps/incident/rest/ticket.py +43 -0
- mojo/apps/jobs/__init__.py +489 -0
- mojo/apps/jobs/adapters.py +24 -0
- mojo/apps/jobs/cli.py +616 -0
- mojo/apps/jobs/daemon.py +370 -0
- mojo/apps/jobs/examples/sample_jobs.py +376 -0
- mojo/apps/jobs/examples/webhook_examples.py +203 -0
- mojo/apps/jobs/handlers/__init__.py +5 -0
- mojo/apps/jobs/handlers/webhook.py +317 -0
- mojo/apps/jobs/job_engine.py +734 -0
- mojo/apps/jobs/keys.py +203 -0
- mojo/apps/jobs/local_queue.py +363 -0
- mojo/apps/jobs/management/__init__.py +3 -0
- mojo/apps/jobs/management/commands/__init__.py +3 -0
- mojo/apps/jobs/manager.py +1327 -0
- mojo/apps/jobs/migrations/0001_initial.py +97 -0
- mojo/apps/jobs/migrations/0002_alter_job_max_retries_joblog.py +39 -0
- mojo/apps/jobs/models/__init__.py +6 -0
- mojo/apps/jobs/models/job.py +441 -0
- mojo/apps/jobs/rest/__init__.py +2 -0
- mojo/apps/jobs/rest/control.py +466 -0
- mojo/apps/jobs/rest/jobs.py +421 -0
- mojo/apps/jobs/scheduler.py +571 -0
- mojo/apps/jobs/services/__init__.py +6 -0
- mojo/apps/jobs/services/job_actions.py +465 -0
- mojo/apps/jobs/settings.py +209 -0
- mojo/apps/logit/migrations/0004_alter_log_level.py +18 -0
- mojo/apps/logit/models/log.py +7 -1
- mojo/apps/metrics/__init__.py +8 -1
- mojo/apps/metrics/redis_metrics.py +198 -0
- mojo/apps/metrics/rest/__init__.py +3 -0
- mojo/apps/metrics/rest/categories.py +266 -0
- mojo/apps/metrics/rest/helpers.py +48 -0
- mojo/apps/metrics/rest/permissions.py +99 -0
- mojo/apps/metrics/rest/values.py +277 -0
- mojo/apps/metrics/utils.py +19 -2
- mojo/decorators/auth.py +6 -1
- mojo/decorators/http.py +47 -3
- mojo/helpers/aws/__init__.py +45 -0
- mojo/helpers/aws/ec2.py +804 -0
- mojo/helpers/aws/iam.py +748 -0
- mojo/helpers/aws/inbound_email.py +309 -0
- mojo/helpers/aws/kms.py +413 -0
- mojo/helpers/aws/s3.py +451 -11
- mojo/helpers/aws/ses.py +483 -0
- mojo/helpers/aws/ses_domain.py +959 -0
- mojo/helpers/aws/sns.py +461 -0
- mojo/helpers/crypto/__init__.py +1 -1
- mojo/helpers/crypto/utils.py +15 -0
- mojo/helpers/dates.py +18 -0
- mojo/helpers/location/__init__.py +2 -0
- mojo/helpers/location/countries.py +262 -0
- mojo/helpers/location/geolocation.py +196 -0
- mojo/helpers/logit.py +37 -0
- mojo/helpers/redis/__init__.py +2 -0
- mojo/helpers/redis/adapter.py +606 -0
- mojo/helpers/redis/client.py +48 -0
- mojo/helpers/redis/pool.py +225 -0
- mojo/helpers/request.py +8 -0
- mojo/helpers/response.py +14 -2
- mojo/helpers/settings/__init__.py +2 -0
- mojo/helpers/{settings.py → settings/helper.py} +1 -37
- mojo/helpers/settings/parser.py +132 -0
- mojo/middleware/auth.py +1 -1
- mojo/middleware/cors.py +40 -0
- mojo/middleware/logging.py +131 -12
- mojo/middleware/mojo.py +10 -0
- mojo/models/rest.py +494 -65
- mojo/models/secrets.py +98 -3
- mojo/serializers/__init__.py +106 -0
- mojo/serializers/core/__init__.py +90 -0
- mojo/serializers/core/cache/__init__.py +121 -0
- mojo/serializers/core/cache/backends.py +518 -0
- mojo/serializers/core/cache/base.py +102 -0
- mojo/serializers/core/cache/disabled.py +181 -0
- mojo/serializers/core/cache/memory.py +287 -0
- mojo/serializers/core/cache/redis.py +533 -0
- mojo/serializers/core/cache/utils.py +454 -0
- mojo/serializers/core/manager.py +550 -0
- mojo/serializers/core/serializer.py +475 -0
- mojo/serializers/examples/settings.py +322 -0
- mojo/serializers/formats/csv.py +393 -0
- mojo/serializers/formats/localizers.py +509 -0
- mojo/serializers/{models.py → simple.py} +38 -15
- mojo/serializers/suggested_improvements.md +388 -0
- testit/client.py +1 -1
- testit/helpers.py +35 -4
- testit/runner.py +23 -6
- django_nativemojo-0.1.10.dist-info/METADATA +0 -96
- django_nativemojo-0.1.10.dist-info/RECORD +0 -194
- mojo/apps/metrics/rest/db.py +0 -0
- mojo/apps/notify/README.md +0 -91
- mojo/apps/notify/README_NOTIFICATIONS.md +0 -566
- mojo/apps/notify/admin.py +0 -52
- mojo/apps/notify/handlers/example_handlers.py +0 -516
- mojo/apps/notify/handlers/ses/__init__.py +0 -25
- mojo/apps/notify/handlers/ses/bounce.py +0 -0
- mojo/apps/notify/handlers/ses/complaint.py +0 -25
- mojo/apps/notify/handlers/ses/message.py +0 -86
- mojo/apps/notify/management/commands/__init__.py +0 -1
- mojo/apps/notify/management/commands/process_notifications.py +0 -370
- mojo/apps/notify/mod +0 -0
- mojo/apps/notify/models/__init__.py +0 -12
- mojo/apps/notify/models/account.py +0 -128
- mojo/apps/notify/models/attachment.py +0 -24
- mojo/apps/notify/models/bounce.py +0 -68
- mojo/apps/notify/models/complaint.py +0 -40
- mojo/apps/notify/models/inbox.py +0 -113
- mojo/apps/notify/models/inbox_message.py +0 -173
- mojo/apps/notify/models/outbox.py +0 -129
- mojo/apps/notify/models/outbox_message.py +0 -288
- mojo/apps/notify/models/template.py +0 -30
- mojo/apps/notify/providers/aws.py +0 -73
- mojo/apps/notify/rest/ses.py +0 -0
- mojo/apps/notify/utils/__init__.py +0 -2
- mojo/apps/notify/utils/notifications.py +0 -404
- mojo/apps/notify/utils/parsing.py +0 -202
- mojo/apps/notify/utils/render.py +0 -144
- mojo/apps/tasks/README.md +0 -118
- mojo/apps/tasks/__init__.py +0 -11
- mojo/apps/tasks/manager.py +0 -489
- mojo/apps/tasks/rest/__init__.py +0 -2
- mojo/apps/tasks/rest/hooks.py +0 -0
- mojo/apps/tasks/rest/tasks.py +0 -62
- mojo/apps/tasks/runner.py +0 -174
- mojo/apps/tasks/tq_handlers.py +0 -14
- mojo/helpers/aws/setup_email.py +0 -0
- mojo/helpers/redis.py +0 -10
- mojo/models/meta.py +0 -262
- mojo/ws4redis/README.md +0 -174
- mojo/ws4redis/__init__.py +0 -2
- mojo/ws4redis/client.py +0 -283
- mojo/ws4redis/connection.py +0 -327
- mojo/ws4redis/exceptions.py +0 -32
- mojo/ws4redis/redis.py +0 -183
- mojo/ws4redis/servers/base.py +0 -86
- mojo/ws4redis/servers/django.py +0 -171
- mojo/ws4redis/servers/uwsgi.py +0 -63
- mojo/ws4redis/settings.py +0 -45
- mojo/ws4redis/utf8validator.py +0 -128
- mojo/ws4redis/websocket.py +0 -403
- {django_nativemojo-0.1.10.dist-info → django_nativemojo-0.1.16.dist-info}/LICENSE +0 -0
- {django_nativemojo-0.1.10.dist-info → django_nativemojo-0.1.16.dist-info}/NOTICE +0 -0
- {django_nativemojo-0.1.10.dist-info → django_nativemojo-0.1.16.dist-info}/WHEEL +0 -0
- /mojo/apps/{notify → aws}/__init__.py +0 -0
- /mojo/apps/{notify/handlers → aws/migrations}/__init__.py +0 -0
- /mojo/apps/{notify/management → docit/markdown_plugins}/__init__.py +0 -0
- /mojo/apps/{notify/providers → docit/migrations}/__init__.py +0 -0
- /mojo/apps/{notify/rest → fileman/migrations}/__init__.py +0 -0
- /mojo/{ws4redis/servers → apps/jobs/examples}/__init__.py +0 -0
- /mojo/apps/{fileman/models/render.py → jobs/migrations/__init__.py} +0 -0
- /mojo/{serializers → rest}/openapi.py +0 -0
- /mojo/{apps/fileman/rest/__init__ → serializers/formats/__init__.py} +0 -0
@@ -0,0 +1,363 @@
|
|
1
|
+
from mojo.helpers.settings import settings
|
2
|
+
from mojo.helpers import logit, dates
|
3
|
+
from mojo.apps.account.models import (
|
4
|
+
PushConfig, RegisteredDevice, NotificationTemplate,
|
5
|
+
NotificationDelivery, User
|
6
|
+
)
|
7
|
+
|
8
|
+
|
9
|
+
|
10
|
+
# Optional imports - will be imported only if needed
|
11
|
+
try:
|
12
|
+
from pyfcm import FCMNotification
|
13
|
+
HAS_FCM = True
|
14
|
+
except ImportError:
|
15
|
+
HAS_FCM = False
|
16
|
+
logit.warn("pyfcm not installed - FCM notifications disabled")
|
17
|
+
|
18
|
+
try:
|
19
|
+
from apns2.client import APNsClient
|
20
|
+
from apns2.payload import Payload
|
21
|
+
from apns2.credentials import TokenCredentials
|
22
|
+
HAS_APNS = True
|
23
|
+
except ImportError:
|
24
|
+
HAS_APNS = False
|
25
|
+
logit.warn("apns2 not installed - APNS notifications disabled")
|
26
|
+
|
27
|
+
|
28
|
+
class PushNotificationService:
|
29
|
+
"""
|
30
|
+
Central push notification service for account-specific push functionality.
|
31
|
+
|
32
|
+
FCM is the primary service supporting both iOS and Android platforms.
|
33
|
+
APNS is available for iOS-specific requirements but rarely needed.
|
34
|
+
Test mode allows fake notifications for development and testing.
|
35
|
+
"""
|
36
|
+
|
37
|
+
def __init__(self, user):
|
38
|
+
self.user = user
|
39
|
+
self.config = self._get_push_config()
|
40
|
+
|
41
|
+
def _get_push_config(self):
|
42
|
+
"""Get push config for user's organization or system default."""
|
43
|
+
return PushConfig.get_for_user(self.user)
|
44
|
+
|
45
|
+
def send_notification(self, template_name=None, context=None, devices=None, user_ids=None,
|
46
|
+
title=None, body=None, category="general", action_url=None, data=None):
|
47
|
+
"""
|
48
|
+
Send notification using template or direct content.
|
49
|
+
|
50
|
+
Args:
|
51
|
+
template_name: Name of notification template (for templated sending)
|
52
|
+
context: Variables for template rendering
|
53
|
+
devices: Specific RegisteredDevice queryset/list
|
54
|
+
user_ids: List of user IDs to send to (uses their active devices)
|
55
|
+
title: Direct title (for non-templated sending)
|
56
|
+
body: Direct body (for non-templated sending)
|
57
|
+
category: Notification category
|
58
|
+
action_url: Direct action URL
|
59
|
+
data: Custom data payload dict
|
60
|
+
|
61
|
+
Returns:
|
62
|
+
List of NotificationDelivery objects
|
63
|
+
"""
|
64
|
+
if not self.config:
|
65
|
+
logit.info(f"No push config available for user {self.user.username}")
|
66
|
+
return []
|
67
|
+
|
68
|
+
# Support both templated and direct sending
|
69
|
+
template = None
|
70
|
+
if template_name:
|
71
|
+
template = self._get_template(template_name)
|
72
|
+
if not template:
|
73
|
+
logit.error(f"Template {template_name} not found")
|
74
|
+
return []
|
75
|
+
elif not (title or body or data):
|
76
|
+
logit.error("Must provide either template_name, title/body, or data payload")
|
77
|
+
return []
|
78
|
+
|
79
|
+
target_devices = self._resolve_devices(devices, user_ids)
|
80
|
+
if not target_devices:
|
81
|
+
logit.info(f"No devices to send to for template {template_name or 'direct'}")
|
82
|
+
return []
|
83
|
+
|
84
|
+
results = []
|
85
|
+
for device in target_devices:
|
86
|
+
notification_category = template.category if template else category
|
87
|
+
if self._should_send_to_device(device, notification_category):
|
88
|
+
if template:
|
89
|
+
result = self._send_to_device(device, template, context or {}, data)
|
90
|
+
else:
|
91
|
+
result = self._send_direct(device, title, body, notification_category, action_url, data)
|
92
|
+
results.append(result)
|
93
|
+
|
94
|
+
return results
|
95
|
+
|
96
|
+
def _get_template(self, template_name):
|
97
|
+
"""Get template by name, preferring user's org templates."""
|
98
|
+
# Try user's org first
|
99
|
+
if self.user.org:
|
100
|
+
template = NotificationTemplate.objects.filter(
|
101
|
+
group=self.user.org, name=template_name, is_active=True
|
102
|
+
).first()
|
103
|
+
if template:
|
104
|
+
return template
|
105
|
+
|
106
|
+
# Fallback to system templates
|
107
|
+
return NotificationTemplate.objects.filter(
|
108
|
+
group__isnull=True, name=template_name, is_active=True
|
109
|
+
).first()
|
110
|
+
|
111
|
+
def _resolve_devices(self, devices, user_ids):
|
112
|
+
"""Resolve target devices from various inputs."""
|
113
|
+
if devices is not None:
|
114
|
+
return devices
|
115
|
+
|
116
|
+
if user_ids:
|
117
|
+
users = User.objects.filter(id__in=user_ids)
|
118
|
+
return RegisteredDevice.objects.filter(
|
119
|
+
user__in=users, is_active=True, push_enabled=True
|
120
|
+
)
|
121
|
+
|
122
|
+
# Default to current user's devices
|
123
|
+
return self.user.registered_devices.filter(
|
124
|
+
is_active=True, push_enabled=True
|
125
|
+
)
|
126
|
+
|
127
|
+
def _should_send_to_device(self, device, category):
|
128
|
+
"""Check if device should receive this category of notification."""
|
129
|
+
preferences = device.push_preferences or {}
|
130
|
+
return preferences.get(category, True) # Default to enabled
|
131
|
+
|
132
|
+
def _send_to_device(self, device, template, context, custom_data=None):
|
133
|
+
"""Send notification to a specific device using template."""
|
134
|
+
title, body, action_url, template_data = template.render(context)
|
135
|
+
|
136
|
+
# Merge template data with custom data (custom data takes precedence)
|
137
|
+
merged_data = template_data.copy() if template_data else {}
|
138
|
+
if custom_data:
|
139
|
+
merged_data.update(custom_data)
|
140
|
+
|
141
|
+
delivery = NotificationDelivery.objects.create(
|
142
|
+
user=device.user,
|
143
|
+
device=device,
|
144
|
+
template=template,
|
145
|
+
title=title,
|
146
|
+
body=body,
|
147
|
+
category=template.category,
|
148
|
+
action_url=action_url,
|
149
|
+
data_payload=merged_data
|
150
|
+
)
|
151
|
+
|
152
|
+
self._attempt_delivery(delivery, device, title, body, template)
|
153
|
+
return delivery
|
154
|
+
|
155
|
+
def _send_direct(self, device, title, body, category, action_url=None, data=None):
|
156
|
+
"""Send direct notification without template."""
|
157
|
+
delivery = NotificationDelivery.objects.create(
|
158
|
+
user=device.user,
|
159
|
+
device=device,
|
160
|
+
title=title,
|
161
|
+
body=body,
|
162
|
+
category=category,
|
163
|
+
action_url=action_url,
|
164
|
+
data_payload=data or {}
|
165
|
+
)
|
166
|
+
|
167
|
+
self._attempt_delivery(delivery, device, title, body, None)
|
168
|
+
return delivery
|
169
|
+
|
170
|
+
def _attempt_delivery(self, delivery, device, title, body, template):
|
171
|
+
"""Attempt to deliver notification to device."""
|
172
|
+
try:
|
173
|
+
success = False
|
174
|
+
|
175
|
+
# Test mode - fake delivery for development/testing
|
176
|
+
if self.config.test_mode:
|
177
|
+
success = self._send_test(delivery, device, title, body, template)
|
178
|
+
|
179
|
+
# FCM is primary - supports both iOS and Android
|
180
|
+
elif self.config.fcm_enabled:
|
181
|
+
success = self._send_fcm(delivery, device, title, body, template)
|
182
|
+
|
183
|
+
# APNS fallback for iOS only (rarely needed)
|
184
|
+
elif device.platform == 'ios' and self.config.apns_enabled:
|
185
|
+
success = self._send_apns(delivery, device, title, body, template)
|
186
|
+
|
187
|
+
else:
|
188
|
+
error_msg = "No push service configured"
|
189
|
+
if not self.config.fcm_enabled and not self.config.apns_enabled:
|
190
|
+
error_msg = "No push services enabled in config"
|
191
|
+
elif device.platform not in ['ios', 'android', 'web']:
|
192
|
+
error_msg = f"Unsupported platform: {device.platform}"
|
193
|
+
delivery.mark_failed(error_msg)
|
194
|
+
return
|
195
|
+
|
196
|
+
if success:
|
197
|
+
delivery.mark_sent()
|
198
|
+
else:
|
199
|
+
delivery.mark_failed("Platform delivery failed")
|
200
|
+
|
201
|
+
except Exception as e:
|
202
|
+
error_msg = f"Push notification failed: {str(e)}"
|
203
|
+
logit.error(error_msg)
|
204
|
+
delivery.mark_failed(error_msg)
|
205
|
+
|
206
|
+
def _send_apns(self, delivery, device, title, body, template):
|
207
|
+
"""Send APNS notification to iOS device."""
|
208
|
+
if not HAS_APNS:
|
209
|
+
logit.error("APNS support not available - apns2 package not installed")
|
210
|
+
return False
|
211
|
+
|
212
|
+
try:
|
213
|
+
credentials = TokenCredentials(
|
214
|
+
auth_key=self.config.get_decrypted_apns_key(),
|
215
|
+
auth_key_id=self.config.apns_key_id,
|
216
|
+
team_id=self.config.apns_team_id
|
217
|
+
)
|
218
|
+
|
219
|
+
client = APNsClient(credentials=credentials,
|
220
|
+
use_sandbox=self.config.apns_use_sandbox)
|
221
|
+
|
222
|
+
# Build payload - handle optional title/body for silent notifications
|
223
|
+
if title or body:
|
224
|
+
alert = {}
|
225
|
+
if title:
|
226
|
+
alert['title'] = title
|
227
|
+
if body:
|
228
|
+
alert['body'] = body
|
229
|
+
payload = Payload(
|
230
|
+
alert=alert,
|
231
|
+
sound=self.config.default_sound,
|
232
|
+
badge=self.config.default_badge_count
|
233
|
+
)
|
234
|
+
else:
|
235
|
+
# Silent notification - no alert, sound, or badge
|
236
|
+
payload = Payload(
|
237
|
+
content_available=True
|
238
|
+
)
|
239
|
+
|
240
|
+
# Build custom data payload - merge custom data with action_url
|
241
|
+
custom_data = delivery.data_payload.copy() if delivery.data_payload else {}
|
242
|
+
if delivery.action_url:
|
243
|
+
custom_data['action_url'] = delivery.action_url
|
244
|
+
|
245
|
+
if custom_data:
|
246
|
+
payload.custom = custom_data
|
247
|
+
|
248
|
+
# Send notification
|
249
|
+
response = client.send_notification(
|
250
|
+
device.device_token,
|
251
|
+
payload,
|
252
|
+
self.config.apns_bundle_id
|
253
|
+
)
|
254
|
+
|
255
|
+
# Store platform response data
|
256
|
+
delivery.platform_data = {
|
257
|
+
'apns_id': response.id if hasattr(response, 'id') else None,
|
258
|
+
'status': response.status if hasattr(response, 'status') else 'sent'
|
259
|
+
}
|
260
|
+
delivery.save(update_fields=['platform_data'])
|
261
|
+
|
262
|
+
return True
|
263
|
+
|
264
|
+
except Exception as e:
|
265
|
+
logit.error(f"APNS send failed: {e}")
|
266
|
+
return False
|
267
|
+
|
268
|
+
def _send_fcm(self, delivery, device, title, body, template):
|
269
|
+
"""Send FCM notification to device (supports both iOS and Android)."""
|
270
|
+
if not HAS_FCM:
|
271
|
+
logit.error("FCM support not available - pyfcm package not installed")
|
272
|
+
return False
|
273
|
+
|
274
|
+
try:
|
275
|
+
push_service = FCMNotification(api_key=self.config.get_decrypted_fcm_key())
|
276
|
+
|
277
|
+
# Build data payload - merge custom data with action_url
|
278
|
+
data_message = delivery.data_payload.copy() if delivery.data_payload else {}
|
279
|
+
if delivery.action_url:
|
280
|
+
data_message['action_url'] = delivery.action_url
|
281
|
+
|
282
|
+
result = push_service.notify_single_device(
|
283
|
+
registration_id=device.device_token,
|
284
|
+
message_title=title,
|
285
|
+
message_body=body,
|
286
|
+
sound=self.config.default_sound if (title or body) else None,
|
287
|
+
data_message=data_message if data_message else None
|
288
|
+
)
|
289
|
+
|
290
|
+
# Store platform response data
|
291
|
+
delivery.platform_data = {
|
292
|
+
'multicast_id': result.get('multicast_id'),
|
293
|
+
'success': result.get('success', 0),
|
294
|
+
'failure': result.get('failure', 0),
|
295
|
+
'results': result.get('results', [])
|
296
|
+
}
|
297
|
+
delivery.save(update_fields=['platform_data'])
|
298
|
+
|
299
|
+
return result.get('success', 0) > 0
|
300
|
+
|
301
|
+
except Exception as e:
|
302
|
+
logit.error(f"FCM send failed: {e}")
|
303
|
+
return False
|
304
|
+
|
305
|
+
def _send_test(self, delivery, device, title, body, template):
|
306
|
+
"""Send fake notification for testing - always succeeds."""
|
307
|
+
# Build log message with optional title/body and data payload
|
308
|
+
log_parts = []
|
309
|
+
if title:
|
310
|
+
log_parts.append(f"Title: {title}")
|
311
|
+
if body:
|
312
|
+
log_parts.append(f"Body: {body}")
|
313
|
+
if delivery.data_payload:
|
314
|
+
log_parts.append(f"Data: {delivery.data_payload}")
|
315
|
+
|
316
|
+
log_message = f"TEST MODE: Fake notification to {device.platform} device '{device.device_name}'"
|
317
|
+
if log_parts:
|
318
|
+
log_message += f" - {' | '.join(log_parts)}"
|
319
|
+
|
320
|
+
logit.info(log_message)
|
321
|
+
|
322
|
+
# Store fake test data including data payload
|
323
|
+
delivery.platform_data = {
|
324
|
+
'test_mode': True,
|
325
|
+
'platform': device.platform,
|
326
|
+
'device_name': device.device_name,
|
327
|
+
'timestamp': dates.utcnow().isoformat(),
|
328
|
+
'fake_delivery': 'success',
|
329
|
+
'data_payload': delivery.data_payload or {}
|
330
|
+
}
|
331
|
+
delivery.save(update_fields=['platform_data'])
|
332
|
+
|
333
|
+
return True
|
334
|
+
|
335
|
+
|
336
|
+
# Convenience functions for easy usage
|
337
|
+
def send_push_notification(user, template_name, context=None, devices=None, user_ids=None, data=None, delay=None):
|
338
|
+
"""
|
339
|
+
Send templated push notification.
|
340
|
+
|
341
|
+
Usage:
|
342
|
+
send_push_notification(user, 'welcome', {'name': user.display_name})
|
343
|
+
send_push_notification(user, 'alert', user_ids=[1, 2, 3])
|
344
|
+
send_push_notification(user, 'order_update', {'order_id': '123'}, data={'action': 'view_order'})
|
345
|
+
"""
|
346
|
+
service = PushNotificationService(user)
|
347
|
+
return service.send_notification(template_name=template_name, context=context,
|
348
|
+
devices=devices, user_ids=user_ids, data=data)
|
349
|
+
|
350
|
+
|
351
|
+
def send_direct_notification(user, title=None, body=None, category="general", action_url=None,
|
352
|
+
data=None, devices=None, user_ids=None, delay=None):
|
353
|
+
"""
|
354
|
+
Send direct push notification without template.
|
355
|
+
|
356
|
+
Usage:
|
357
|
+
send_direct_notification(user, "Hello!", "Your order is ready", "orders")
|
358
|
+
send_direct_notification(user, "Alert", "System maintenance", user_ids=[1, 2, 3])
|
359
|
+
send_direct_notification(user, data={"action": "sync", "silent": True})
|
360
|
+
"""
|
361
|
+
service = PushNotificationService(user)
|
362
|
+
return service.send_notification(title=title, body=body, category=category,
|
363
|
+
action_url=action_url, data=data, devices=devices, user_ids=user_ids)
|
@@ -0,0 +1,206 @@
|
|
1
|
+
# Generated by Django 4.2.21 on 2025-08-27 18:23
|
2
|
+
|
3
|
+
from django.db import migrations, models
|
4
|
+
import django.db.models.deletion
|
5
|
+
import mojo.models.rest
|
6
|
+
|
7
|
+
|
8
|
+
class Migration(migrations.Migration):
|
9
|
+
|
10
|
+
initial = True
|
11
|
+
|
12
|
+
dependencies = [
|
13
|
+
]
|
14
|
+
|
15
|
+
operations = [
|
16
|
+
migrations.CreateModel(
|
17
|
+
name='EmailAttachment',
|
18
|
+
fields=[
|
19
|
+
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
20
|
+
('created', models.DateTimeField(auto_now_add=True)),
|
21
|
+
('modified', models.DateTimeField(auto_now=True, db_index=True)),
|
22
|
+
('filename', models.CharField(blank=True, help_text='Original filename (if provided by the sender)', max_length=512, null=True)),
|
23
|
+
('content_type', models.CharField(blank=True, help_text='MIME content type (e.g., application/pdf)', max_length=255, null=True)),
|
24
|
+
('size_bytes', models.IntegerField(default=0, help_text='Size of the stored attachment in bytes (approximate)')),
|
25
|
+
('stored_as', models.CharField(help_text='Storage reference (e.g., s3://bucket/key)', max_length=512)),
|
26
|
+
('metadata', models.JSONField(blank=True, default=dict, help_text='Arbitrary metadata (e.g., content-id, part headers)')),
|
27
|
+
],
|
28
|
+
options={
|
29
|
+
'db_table': 'aws_email_attachment',
|
30
|
+
'ordering': ['-created', 'id'],
|
31
|
+
},
|
32
|
+
bases=(models.Model, mojo.models.rest.MojoModel),
|
33
|
+
),
|
34
|
+
migrations.CreateModel(
|
35
|
+
name='EmailDomain',
|
36
|
+
fields=[
|
37
|
+
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
38
|
+
('mojo_secrets', models.TextField(blank=True, default=None, null=True)),
|
39
|
+
('created', models.DateTimeField(auto_now_add=True)),
|
40
|
+
('modified', models.DateTimeField(auto_now=True, db_index=True)),
|
41
|
+
('name', models.CharField(db_index=True, max_length=255, unique=True)),
|
42
|
+
('region', models.CharField(default='us-west-2', help_text='AWS region for SES operations', max_length=64)),
|
43
|
+
('status', models.CharField(db_index=True, default='pending', help_text='High-level status: pending, verified, error (free-form)', max_length=32)),
|
44
|
+
('receiving_enabled', models.BooleanField(default=False, help_text='When true, domain-level catch-all receiving is enabled via SES receipt rules')),
|
45
|
+
('s3_inbound_bucket', models.CharField(blank=True, help_text='S3 bucket for inbound emails (required if receiving_enabled)', max_length=255, null=True)),
|
46
|
+
('s3_inbound_prefix', models.CharField(blank=True, default='', help_text='S3 prefix for inbound emails (e.g., inbound/example.com/)', max_length=255)),
|
47
|
+
('dns_mode', models.CharField(default='manual', help_text='DNS automation mode: manual | route53 | godaddy', max_length=32)),
|
48
|
+
('sns_topic_bounce_arn', models.CharField(blank=True, help_text='SNS topic ARN for SES bounce notifications', max_length=512, null=True)),
|
49
|
+
('sns_topic_complaint_arn', models.CharField(blank=True, help_text='SNS topic ARN for SES complaint notifications', max_length=512, null=True)),
|
50
|
+
('sns_topic_delivery_arn', models.CharField(blank=True, help_text='SNS topic ARN for SES delivery notifications', max_length=512, null=True)),
|
51
|
+
('sns_topic_inbound_arn', models.CharField(blank=True, help_text='SNS topic ARN for SES inbound notifications', max_length=512, null=True)),
|
52
|
+
('metadata', models.JSONField(blank=True, default=dict)),
|
53
|
+
],
|
54
|
+
options={
|
55
|
+
'db_table': 'aws_email_domain',
|
56
|
+
'ordering': ['name'],
|
57
|
+
},
|
58
|
+
bases=(models.Model, mojo.models.rest.MojoModel),
|
59
|
+
),
|
60
|
+
migrations.CreateModel(
|
61
|
+
name='Mailbox',
|
62
|
+
fields=[
|
63
|
+
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
64
|
+
('created', models.DateTimeField(auto_now_add=True)),
|
65
|
+
('modified', models.DateTimeField(auto_now=True, db_index=True)),
|
66
|
+
('email', models.EmailField(db_index=True, help_text='Full email address for this mailbox (e.g., support@example.com)', max_length=254, unique=True)),
|
67
|
+
('allow_inbound', models.BooleanField(default=True, help_text='If true, inbound messages addressed to this mailbox will be processed')),
|
68
|
+
('allow_outbound', models.BooleanField(default=True, help_text='If true, outbound messages can be sent from this mailbox')),
|
69
|
+
('async_handler', models.CharField(blank=True, help_text="Dotted path to async handler: 'package.module:function'", max_length=255, null=True)),
|
70
|
+
('metadata', models.JSONField(blank=True, default=dict)),
|
71
|
+
('domain', models.ForeignKey(help_text='Owning email domain (SES identity)', on_delete=django.db.models.deletion.CASCADE, related_name='mailboxes', to='aws.emaildomain')),
|
72
|
+
],
|
73
|
+
options={
|
74
|
+
'db_table': 'aws_mailbox',
|
75
|
+
'ordering': ['email'],
|
76
|
+
},
|
77
|
+
bases=(models.Model, mojo.models.rest.MojoModel),
|
78
|
+
),
|
79
|
+
migrations.CreateModel(
|
80
|
+
name='SentMessage',
|
81
|
+
fields=[
|
82
|
+
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
83
|
+
('created', models.DateTimeField(auto_now_add=True)),
|
84
|
+
('modified', models.DateTimeField(auto_now=True, db_index=True)),
|
85
|
+
('ses_message_id', models.CharField(blank=True, db_index=True, help_text='AWS SES MessageId returned after a successful send', max_length=255, null=True)),
|
86
|
+
('to_addresses', models.JSONField(blank=True, default=list, help_text='List of recipient addresses (To)')),
|
87
|
+
('cc_addresses', models.JSONField(blank=True, default=list, help_text='List of recipient addresses (Cc)')),
|
88
|
+
('bcc_addresses', models.JSONField(blank=True, default=list, help_text='List of recipient addresses (Bcc)')),
|
89
|
+
('subject', models.CharField(blank=True, help_text='Email subject', max_length=512, null=True)),
|
90
|
+
('body_text', models.TextField(blank=True, help_text='Plain text body', null=True)),
|
91
|
+
('body_html', models.TextField(blank=True, help_text='HTML body', null=True)),
|
92
|
+
('template_name', models.CharField(blank=True, help_text='Optional EmailTemplate name used to render this message', max_length=255, null=True)),
|
93
|
+
('template_context', models.JSONField(blank=True, default=dict, help_text='Context used when rendering a template')),
|
94
|
+
('status', models.CharField(choices=[('queued', 'Queued'), ('sending', 'Sending'), ('delivered', 'Delivered'), ('bounced', 'Bounced'), ('complained', 'Complained'), ('failed', 'Failed'), ('unknown', 'Unknown')], db_index=True, default='queued', help_text='Current delivery status', max_length=32)),
|
95
|
+
('status_reason', models.TextField(blank=True, help_text='Details or raw payload for bounces/complaints/errors', null=True)),
|
96
|
+
('metadata', models.JSONField(blank=True, default=dict, help_text='Arbitrary metadata for downstream processing/auditing')),
|
97
|
+
('mailbox', models.ForeignKey(help_text='Mailbox used as the sender (envelope MAIL FROM = mailbox.email)', on_delete=django.db.models.deletion.CASCADE, related_name='sent_messages', to='aws.mailbox')),
|
98
|
+
],
|
99
|
+
options={
|
100
|
+
'db_table': 'aws_sent_message',
|
101
|
+
'ordering': ['-created', 'id'],
|
102
|
+
},
|
103
|
+
bases=(models.Model, mojo.models.rest.MojoModel),
|
104
|
+
),
|
105
|
+
migrations.CreateModel(
|
106
|
+
name='IncomingEmail',
|
107
|
+
fields=[
|
108
|
+
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
109
|
+
('created', models.DateTimeField(auto_now_add=True)),
|
110
|
+
('modified', models.DateTimeField(auto_now=True, db_index=True)),
|
111
|
+
('s3_object_url', models.CharField(help_text='S3 URL for the raw MIME message (e.g., s3://bucket/key)', max_length=512)),
|
112
|
+
('message_id', models.CharField(blank=True, db_index=True, help_text='SMTP Message-ID header (if present)', max_length=255, null=True)),
|
113
|
+
('from_address', models.CharField(blank=True, help_text='Raw From header address (may include name)', max_length=512, null=True)),
|
114
|
+
('to_addresses', models.JSONField(blank=True, default=list, help_text='List of recipient addresses from To header')),
|
115
|
+
('cc_addresses', models.JSONField(blank=True, default=list, help_text='List of recipient addresses from Cc header')),
|
116
|
+
('subject', models.CharField(blank=True, help_text='Email subject', max_length=512, null=True)),
|
117
|
+
('date_header', models.DateTimeField(blank=True, help_text='Parsed Date header from the message', null=True)),
|
118
|
+
('headers', models.JSONField(blank=True, default=dict, help_text='All headers as a JSON object (flattened)')),
|
119
|
+
('text_body', models.TextField(blank=True, help_text='Extracted plain text body (if available)', null=True)),
|
120
|
+
('html_body', models.TextField(blank=True, help_text='Extracted HTML body (if available)', null=True)),
|
121
|
+
('size_bytes', models.IntegerField(default=0, help_text='Approximate size of the raw message in bytes')),
|
122
|
+
('received_at', models.DateTimeField(blank=True, db_index=True, help_text='Time message was received (from SNS/S3 event or set by parser)', null=True)),
|
123
|
+
('processed', models.BooleanField(default=False, help_text='True if post-receive processing completed')),
|
124
|
+
('process_status', models.CharField(db_index=True, default='pending', help_text='Processing status: pending | success | error', max_length=32)),
|
125
|
+
('process_error', models.TextField(blank=True, help_text='Error details if processing failed', null=True)),
|
126
|
+
('mailbox', models.ForeignKey(blank=True, help_text='Associated mailbox if any recipient matches', null=True, on_delete=django.db.models.deletion.CASCADE, related_name='incoming_emails', to='aws.mailbox')),
|
127
|
+
],
|
128
|
+
options={
|
129
|
+
'db_table': 'aws_incoming_email',
|
130
|
+
'ordering': ['-received_at', '-created'],
|
131
|
+
},
|
132
|
+
bases=(models.Model, mojo.models.rest.MojoModel),
|
133
|
+
),
|
134
|
+
migrations.CreateModel(
|
135
|
+
name='EmailTemplate',
|
136
|
+
fields=[
|
137
|
+
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
138
|
+
('created', models.DateTimeField(auto_now_add=True)),
|
139
|
+
('modified', models.DateTimeField(auto_now=True, db_index=True)),
|
140
|
+
('name', models.CharField(db_index=True, help_text='Unique template name (used by callers to reference this template)', max_length=255, unique=True)),
|
141
|
+
('subject_template', models.TextField(blank=True, default='', help_text='Django template string for the email subject')),
|
142
|
+
('html_template', models.TextField(blank=True, default='', help_text='Django template string for the HTML body')),
|
143
|
+
('text_template', models.TextField(blank=True, default='', help_text='Django template string for the plain text body')),
|
144
|
+
('metadata', models.JSONField(blank=True, default=dict, help_text='Arbitrary metadata for this template (e.g., description, tags)')),
|
145
|
+
],
|
146
|
+
options={
|
147
|
+
'db_table': 'aws_email_template',
|
148
|
+
'ordering': ['name'],
|
149
|
+
'indexes': [models.Index(fields=['modified'], name='aws_email_t_modifie_cf76d6_idx'), models.Index(fields=['name'], name='aws_email_t_name_d1662a_idx')],
|
150
|
+
},
|
151
|
+
bases=(models.Model, mojo.models.rest.MojoModel),
|
152
|
+
),
|
153
|
+
migrations.AddIndex(
|
154
|
+
model_name='emaildomain',
|
155
|
+
index=models.Index(fields=['status'], name='aws_email_d_status_398945_idx'),
|
156
|
+
),
|
157
|
+
migrations.AddIndex(
|
158
|
+
model_name='emaildomain',
|
159
|
+
index=models.Index(fields=['modified'], name='aws_email_d_modifie_66ca75_idx'),
|
160
|
+
),
|
161
|
+
migrations.AddField(
|
162
|
+
model_name='emailattachment',
|
163
|
+
name='incoming_email',
|
164
|
+
field=models.ForeignKey(help_text='The inbound email this attachment belongs to', on_delete=django.db.models.deletion.CASCADE, related_name='attachments', to='aws.incomingemail'),
|
165
|
+
),
|
166
|
+
migrations.AddIndex(
|
167
|
+
model_name='sentmessage',
|
168
|
+
index=models.Index(fields=['modified'], name='aws_sent_me_modifie_a26352_idx'),
|
169
|
+
),
|
170
|
+
migrations.AddIndex(
|
171
|
+
model_name='sentmessage',
|
172
|
+
index=models.Index(fields=['status'], name='aws_sent_me_status_63e619_idx'),
|
173
|
+
),
|
174
|
+
migrations.AddIndex(
|
175
|
+
model_name='sentmessage',
|
176
|
+
index=models.Index(fields=['ses_message_id'], name='aws_sent_me_ses_mes_b72855_idx'),
|
177
|
+
),
|
178
|
+
migrations.AddIndex(
|
179
|
+
model_name='mailbox',
|
180
|
+
index=models.Index(fields=['modified'], name='aws_mailbox_modifie_597f5d_idx'),
|
181
|
+
),
|
182
|
+
migrations.AddIndex(
|
183
|
+
model_name='mailbox',
|
184
|
+
index=models.Index(fields=['email'], name='aws_mailbox_email_e2c6d1_idx'),
|
185
|
+
),
|
186
|
+
migrations.AddIndex(
|
187
|
+
model_name='incomingemail',
|
188
|
+
index=models.Index(fields=['modified'], name='aws_incomin_modifie_8e68a9_idx'),
|
189
|
+
),
|
190
|
+
migrations.AddIndex(
|
191
|
+
model_name='incomingemail',
|
192
|
+
index=models.Index(fields=['received_at'], name='aws_incomin_receive_7b6d93_idx'),
|
193
|
+
),
|
194
|
+
migrations.AddIndex(
|
195
|
+
model_name='incomingemail',
|
196
|
+
index=models.Index(fields=['message_id'], name='aws_incomin_message_d8ca7f_idx'),
|
197
|
+
),
|
198
|
+
migrations.AddIndex(
|
199
|
+
model_name='emailattachment',
|
200
|
+
index=models.Index(fields=['modified'], name='aws_email_a_modifie_b283df_idx'),
|
201
|
+
),
|
202
|
+
migrations.AddIndex(
|
203
|
+
model_name='emailattachment',
|
204
|
+
index=models.Index(fields=['filename'], name='aws_email_a_filenam_da3c89_idx'),
|
205
|
+
),
|
206
|
+
]
|
@@ -0,0 +1,28 @@
|
|
1
|
+
# Generated by Django 4.2.21 on 2025-08-27 21:38
|
2
|
+
|
3
|
+
from django.db import migrations, models
|
4
|
+
|
5
|
+
|
6
|
+
class Migration(migrations.Migration):
|
7
|
+
|
8
|
+
dependencies = [
|
9
|
+
('aws', '0001_initial'),
|
10
|
+
]
|
11
|
+
|
12
|
+
operations = [
|
13
|
+
migrations.AddField(
|
14
|
+
model_name='emaildomain',
|
15
|
+
name='can_recv',
|
16
|
+
field=models.BooleanField(default=False, help_text='True if inbound receiving is ready per last audit'),
|
17
|
+
),
|
18
|
+
migrations.AddField(
|
19
|
+
model_name='emaildomain',
|
20
|
+
name='can_send',
|
21
|
+
field=models.BooleanField(default=False, help_text='True if outbound sending is ready per last audit'),
|
22
|
+
),
|
23
|
+
migrations.AlterField(
|
24
|
+
model_name='emaildomain',
|
25
|
+
name='status',
|
26
|
+
field=models.CharField(db_index=True, default='pending', help_text='Domain status: "pending" (created), "ready" (audit passed), or "missing" (audit failed)', max_length=32),
|
27
|
+
),
|
28
|
+
]
|
@@ -0,0 +1,31 @@
|
|
1
|
+
# Generated by Django 4.2.21 on 2025-08-31 23:09
|
2
|
+
|
3
|
+
from django.db import migrations, models
|
4
|
+
|
5
|
+
|
6
|
+
class Migration(migrations.Migration):
|
7
|
+
|
8
|
+
dependencies = [
|
9
|
+
('aws', '0002_emaildomain_can_recv_emaildomain_can_send_and_more'),
|
10
|
+
]
|
11
|
+
|
12
|
+
operations = [
|
13
|
+
migrations.AddField(
|
14
|
+
model_name='mailbox',
|
15
|
+
name='is_domain_default',
|
16
|
+
field=models.BooleanField(db_index=True, default=False, help_text='Default mailbox for this domain (one per domain)'),
|
17
|
+
),
|
18
|
+
migrations.AddField(
|
19
|
+
model_name='mailbox',
|
20
|
+
name='is_system_default',
|
21
|
+
field=models.BooleanField(db_index=True, default=False, help_text='System-wide default mailbox (only one allowed)'),
|
22
|
+
),
|
23
|
+
migrations.AddIndex(
|
24
|
+
model_name='mailbox',
|
25
|
+
index=models.Index(fields=['is_system_default'], name='aws_mailbox_is_syst_9911c7_idx'),
|
26
|
+
),
|
27
|
+
migrations.AddIndex(
|
28
|
+
model_name='mailbox',
|
29
|
+
index=models.Index(fields=['is_domain_default', 'domain'], name='aws_mailbox_is_doma_189efb_idx'),
|
30
|
+
),
|
31
|
+
]
|
@@ -0,0 +1,39 @@
|
|
1
|
+
# Generated by Django 4.2.23 on 2025-09-06 00:00
|
2
|
+
|
3
|
+
from django.conf import settings
|
4
|
+
from django.db import migrations, models
|
5
|
+
import django.db.models.deletion
|
6
|
+
import mojo.models.rest
|
7
|
+
|
8
|
+
|
9
|
+
class Migration(migrations.Migration):
|
10
|
+
|
11
|
+
dependencies = [
|
12
|
+
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
13
|
+
('account', '0014_notificationdelivery_data_payload_and_more'),
|
14
|
+
('aws', '0003_mailbox_is_domain_default_mailbox_is_system_default_and_more'),
|
15
|
+
]
|
16
|
+
|
17
|
+
operations = [
|
18
|
+
migrations.CreateModel(
|
19
|
+
name='S3Bucket',
|
20
|
+
fields=[
|
21
|
+
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
22
|
+
('mojo_secrets', models.TextField(blank=True, default=None, null=True)),
|
23
|
+
('created', models.DateTimeField(auto_now_add=True)),
|
24
|
+
('modified', models.DateTimeField(auto_now=True, db_index=True)),
|
25
|
+
('name', models.CharField(db_index=True, max_length=255, unique=True)),
|
26
|
+
('region', models.CharField(default='us-west-2', help_text='AWS region for SES operations', max_length=64)),
|
27
|
+
('metadata', models.JSONField(blank=True, default=dict)),
|
28
|
+
('is_active', models.BooleanField(db_index=True, default=False)),
|
29
|
+
('is_system_default', models.BooleanField(db_index=True, default=False)),
|
30
|
+
('is_group_default', models.BooleanField(db_index=True, default=False)),
|
31
|
+
('group', models.ForeignKey(default=None, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='s3_buckets', to='account.group')),
|
32
|
+
('user', models.ForeignKey(default=None, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='s3_buckets', to=settings.AUTH_USER_MODEL)),
|
33
|
+
],
|
34
|
+
options={
|
35
|
+
'abstract': False,
|
36
|
+
},
|
37
|
+
bases=(models.Model, mojo.models.rest.MojoModel),
|
38
|
+
),
|
39
|
+
]
|