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.
Files changed (276) hide show
  1. django_nativemojo-0.1.16.dist-info/METADATA +138 -0
  2. django_nativemojo-0.1.16.dist-info/RECORD +302 -0
  3. mojo/__init__.py +1 -1
  4. mojo/apps/account/management/__init__.py +5 -0
  5. mojo/apps/account/management/commands/__init__.py +6 -0
  6. mojo/apps/account/management/commands/serializer_admin.py +651 -0
  7. mojo/apps/account/migrations/0004_user_avatar.py +20 -0
  8. mojo/apps/account/migrations/0005_group_last_activity.py +18 -0
  9. mojo/apps/account/migrations/0006_add_device_tracking_models.py +72 -0
  10. mojo/apps/account/migrations/0007_delete_userdevicelocation.py +16 -0
  11. mojo/apps/account/migrations/0008_userdevicelocation.py +33 -0
  12. mojo/apps/account/migrations/0009_geolocatedip_subnet.py +18 -0
  13. mojo/apps/account/migrations/0010_group_avatar.py +20 -0
  14. mojo/apps/account/migrations/0011_user_org_registereddevice_pushconfig_and_more.py +118 -0
  15. mojo/apps/account/migrations/0012_remove_pushconfig_apns_key_file_and_more.py +21 -0
  16. mojo/apps/account/migrations/0013_pushconfig_test_mode_alter_pushconfig_apns_enabled_and_more.py +28 -0
  17. mojo/apps/account/migrations/0014_notificationdelivery_data_payload_and_more.py +48 -0
  18. mojo/apps/account/models/__init__.py +2 -0
  19. mojo/apps/account/models/device.py +281 -0
  20. mojo/apps/account/models/group.py +319 -15
  21. mojo/apps/account/models/member.py +29 -5
  22. mojo/apps/account/models/push/__init__.py +4 -0
  23. mojo/apps/account/models/push/config.py +112 -0
  24. mojo/apps/account/models/push/delivery.py +93 -0
  25. mojo/apps/account/models/push/device.py +66 -0
  26. mojo/apps/account/models/push/template.py +99 -0
  27. mojo/apps/account/models/user.py +369 -19
  28. mojo/apps/account/rest/__init__.py +2 -0
  29. mojo/apps/account/rest/device.py +39 -0
  30. mojo/apps/account/rest/group.py +9 -0
  31. mojo/apps/account/rest/push.py +187 -0
  32. mojo/apps/account/rest/user.py +100 -6
  33. mojo/apps/account/services/__init__.py +1 -0
  34. mojo/apps/account/services/push.py +363 -0
  35. mojo/apps/aws/migrations/0001_initial.py +206 -0
  36. mojo/apps/aws/migrations/0002_emaildomain_can_recv_emaildomain_can_send_and_more.py +28 -0
  37. mojo/apps/aws/migrations/0003_mailbox_is_domain_default_mailbox_is_system_default_and_more.py +31 -0
  38. mojo/apps/aws/migrations/0004_s3bucket.py +39 -0
  39. mojo/apps/aws/migrations/0005_alter_emaildomain_region_delete_s3bucket.py +21 -0
  40. mojo/apps/aws/models/__init__.py +19 -0
  41. mojo/apps/aws/models/email_attachment.py +99 -0
  42. mojo/apps/aws/models/email_domain.py +218 -0
  43. mojo/apps/aws/models/email_template.py +132 -0
  44. mojo/apps/aws/models/incoming_email.py +197 -0
  45. mojo/apps/aws/models/mailbox.py +288 -0
  46. mojo/apps/aws/models/sent_message.py +175 -0
  47. mojo/apps/aws/rest/__init__.py +7 -0
  48. mojo/apps/aws/rest/email.py +33 -0
  49. mojo/apps/aws/rest/email_ops.py +183 -0
  50. mojo/apps/aws/rest/messages.py +32 -0
  51. mojo/apps/aws/rest/s3.py +64 -0
  52. mojo/apps/aws/rest/send.py +101 -0
  53. mojo/apps/aws/rest/sns.py +403 -0
  54. mojo/apps/aws/rest/templates.py +19 -0
  55. mojo/apps/aws/services/__init__.py +32 -0
  56. mojo/apps/aws/services/email.py +390 -0
  57. mojo/apps/aws/services/email_ops.py +548 -0
  58. mojo/apps/docit/__init__.py +6 -0
  59. mojo/apps/docit/markdown_plugins/syntax_highlight.py +25 -0
  60. mojo/apps/docit/markdown_plugins/toc.py +12 -0
  61. mojo/apps/docit/migrations/0001_initial.py +113 -0
  62. mojo/apps/docit/migrations/0002_alter_book_modified_by_alter_page_modified_by.py +26 -0
  63. mojo/apps/docit/migrations/0003_alter_book_group.py +20 -0
  64. mojo/apps/docit/models/__init__.py +17 -0
  65. mojo/apps/docit/models/asset.py +231 -0
  66. mojo/apps/docit/models/book.py +227 -0
  67. mojo/apps/docit/models/page.py +319 -0
  68. mojo/apps/docit/models/page_revision.py +203 -0
  69. mojo/apps/docit/rest/__init__.py +10 -0
  70. mojo/apps/docit/rest/asset.py +17 -0
  71. mojo/apps/docit/rest/book.py +22 -0
  72. mojo/apps/docit/rest/page.py +22 -0
  73. mojo/apps/docit/rest/page_revision.py +17 -0
  74. mojo/apps/docit/services/__init__.py +11 -0
  75. mojo/apps/docit/services/docit.py +315 -0
  76. mojo/apps/docit/services/markdown.py +44 -0
  77. mojo/apps/fileman/README.md +8 -8
  78. mojo/apps/fileman/backends/base.py +76 -70
  79. mojo/apps/fileman/backends/filesystem.py +86 -86
  80. mojo/apps/fileman/backends/s3.py +409 -108
  81. mojo/apps/fileman/migrations/0001_initial.py +106 -0
  82. mojo/apps/fileman/migrations/0002_filemanager_parent_alter_filemanager_max_file_size.py +24 -0
  83. mojo/apps/fileman/migrations/0003_remove_file_fileman_fil_upload__c4bc35_idx_and_more.py +25 -0
  84. mojo/apps/fileman/migrations/0004_remove_file_original_filename_and_more.py +39 -0
  85. mojo/apps/fileman/migrations/0005_alter_file_upload_token.py +18 -0
  86. mojo/apps/fileman/migrations/0006_file_download_url_filemanager_forever_urls.py +23 -0
  87. mojo/apps/fileman/migrations/0007_remove_filemanager_forever_urls_and_more.py +22 -0
  88. mojo/apps/fileman/migrations/0008_file_category.py +18 -0
  89. mojo/apps/fileman/migrations/0009_rename_file_path_file_storage_file_path.py +18 -0
  90. mojo/apps/fileman/migrations/0010_filerendition.py +33 -0
  91. mojo/apps/fileman/migrations/0011_alter_filerendition_original_file.py +19 -0
  92. mojo/apps/fileman/models/__init__.py +1 -5
  93. mojo/apps/fileman/models/file.py +240 -58
  94. mojo/apps/fileman/models/manager.py +427 -31
  95. mojo/apps/fileman/models/rendition.py +118 -0
  96. mojo/apps/fileman/renderer/__init__.py +111 -0
  97. mojo/apps/fileman/renderer/audio.py +403 -0
  98. mojo/apps/fileman/renderer/base.py +205 -0
  99. mojo/apps/fileman/renderer/document.py +404 -0
  100. mojo/apps/fileman/renderer/image.py +222 -0
  101. mojo/apps/fileman/renderer/utils.py +297 -0
  102. mojo/apps/fileman/renderer/video.py +304 -0
  103. mojo/apps/fileman/rest/__init__.py +1 -18
  104. mojo/apps/fileman/rest/upload.py +22 -32
  105. mojo/apps/fileman/signals.py +58 -0
  106. mojo/apps/fileman/tasks.py +254 -0
  107. mojo/apps/fileman/utils/__init__.py +40 -16
  108. mojo/apps/incident/migrations/0005_incidenthistory.py +39 -0
  109. mojo/apps/incident/migrations/0006_alter_incident_state.py +18 -0
  110. mojo/apps/incident/migrations/0007_event_uid.py +18 -0
  111. mojo/apps/incident/migrations/0008_ticket_ticketnote.py +55 -0
  112. mojo/apps/incident/migrations/0009_incident_status.py +18 -0
  113. mojo/apps/incident/migrations/0010_event_country_code.py +18 -0
  114. mojo/apps/incident/migrations/0011_incident_country_code.py +18 -0
  115. mojo/apps/incident/migrations/0012_alter_incident_status.py +18 -0
  116. mojo/apps/incident/models/__init__.py +2 -0
  117. mojo/apps/incident/models/event.py +35 -0
  118. mojo/apps/incident/models/history.py +36 -0
  119. mojo/apps/incident/models/incident.py +3 -1
  120. mojo/apps/incident/models/ticket.py +62 -0
  121. mojo/apps/incident/reporter.py +21 -1
  122. mojo/apps/incident/rest/__init__.py +1 -0
  123. mojo/apps/incident/rest/event.py +7 -1
  124. mojo/apps/incident/rest/ticket.py +43 -0
  125. mojo/apps/jobs/__init__.py +489 -0
  126. mojo/apps/jobs/adapters.py +24 -0
  127. mojo/apps/jobs/cli.py +616 -0
  128. mojo/apps/jobs/daemon.py +370 -0
  129. mojo/apps/jobs/examples/sample_jobs.py +376 -0
  130. mojo/apps/jobs/examples/webhook_examples.py +203 -0
  131. mojo/apps/jobs/handlers/__init__.py +5 -0
  132. mojo/apps/jobs/handlers/webhook.py +317 -0
  133. mojo/apps/jobs/job_engine.py +734 -0
  134. mojo/apps/jobs/keys.py +203 -0
  135. mojo/apps/jobs/local_queue.py +363 -0
  136. mojo/apps/jobs/management/__init__.py +3 -0
  137. mojo/apps/jobs/management/commands/__init__.py +3 -0
  138. mojo/apps/jobs/manager.py +1327 -0
  139. mojo/apps/jobs/migrations/0001_initial.py +97 -0
  140. mojo/apps/jobs/migrations/0002_alter_job_max_retries_joblog.py +39 -0
  141. mojo/apps/jobs/models/__init__.py +6 -0
  142. mojo/apps/jobs/models/job.py +441 -0
  143. mojo/apps/jobs/rest/__init__.py +2 -0
  144. mojo/apps/jobs/rest/control.py +466 -0
  145. mojo/apps/jobs/rest/jobs.py +421 -0
  146. mojo/apps/jobs/scheduler.py +571 -0
  147. mojo/apps/jobs/services/__init__.py +6 -0
  148. mojo/apps/jobs/services/job_actions.py +465 -0
  149. mojo/apps/jobs/settings.py +209 -0
  150. mojo/apps/logit/migrations/0004_alter_log_level.py +18 -0
  151. mojo/apps/logit/models/log.py +7 -1
  152. mojo/apps/metrics/__init__.py +8 -1
  153. mojo/apps/metrics/redis_metrics.py +198 -0
  154. mojo/apps/metrics/rest/__init__.py +3 -0
  155. mojo/apps/metrics/rest/categories.py +266 -0
  156. mojo/apps/metrics/rest/helpers.py +48 -0
  157. mojo/apps/metrics/rest/permissions.py +99 -0
  158. mojo/apps/metrics/rest/values.py +277 -0
  159. mojo/apps/metrics/utils.py +19 -2
  160. mojo/decorators/auth.py +6 -1
  161. mojo/decorators/http.py +47 -3
  162. mojo/helpers/aws/__init__.py +45 -0
  163. mojo/helpers/aws/ec2.py +804 -0
  164. mojo/helpers/aws/iam.py +748 -0
  165. mojo/helpers/aws/inbound_email.py +309 -0
  166. mojo/helpers/aws/kms.py +413 -0
  167. mojo/helpers/aws/s3.py +451 -11
  168. mojo/helpers/aws/ses.py +483 -0
  169. mojo/helpers/aws/ses_domain.py +959 -0
  170. mojo/helpers/aws/sns.py +461 -0
  171. mojo/helpers/crypto/__init__.py +1 -1
  172. mojo/helpers/crypto/utils.py +15 -0
  173. mojo/helpers/dates.py +18 -0
  174. mojo/helpers/location/__init__.py +2 -0
  175. mojo/helpers/location/countries.py +262 -0
  176. mojo/helpers/location/geolocation.py +196 -0
  177. mojo/helpers/logit.py +37 -0
  178. mojo/helpers/redis/__init__.py +2 -0
  179. mojo/helpers/redis/adapter.py +606 -0
  180. mojo/helpers/redis/client.py +48 -0
  181. mojo/helpers/redis/pool.py +225 -0
  182. mojo/helpers/request.py +8 -0
  183. mojo/helpers/response.py +14 -2
  184. mojo/helpers/settings/__init__.py +2 -0
  185. mojo/helpers/{settings.py → settings/helper.py} +1 -37
  186. mojo/helpers/settings/parser.py +132 -0
  187. mojo/middleware/auth.py +1 -1
  188. mojo/middleware/cors.py +40 -0
  189. mojo/middleware/logging.py +131 -12
  190. mojo/middleware/mojo.py +10 -0
  191. mojo/models/rest.py +494 -65
  192. mojo/models/secrets.py +98 -3
  193. mojo/serializers/__init__.py +106 -0
  194. mojo/serializers/core/__init__.py +90 -0
  195. mojo/serializers/core/cache/__init__.py +121 -0
  196. mojo/serializers/core/cache/backends.py +518 -0
  197. mojo/serializers/core/cache/base.py +102 -0
  198. mojo/serializers/core/cache/disabled.py +181 -0
  199. mojo/serializers/core/cache/memory.py +287 -0
  200. mojo/serializers/core/cache/redis.py +533 -0
  201. mojo/serializers/core/cache/utils.py +454 -0
  202. mojo/serializers/core/manager.py +550 -0
  203. mojo/serializers/core/serializer.py +475 -0
  204. mojo/serializers/examples/settings.py +322 -0
  205. mojo/serializers/formats/csv.py +393 -0
  206. mojo/serializers/formats/localizers.py +509 -0
  207. mojo/serializers/{models.py → simple.py} +38 -15
  208. mojo/serializers/suggested_improvements.md +388 -0
  209. testit/client.py +1 -1
  210. testit/helpers.py +35 -4
  211. testit/runner.py +23 -6
  212. django_nativemojo-0.1.10.dist-info/METADATA +0 -96
  213. django_nativemojo-0.1.10.dist-info/RECORD +0 -194
  214. mojo/apps/metrics/rest/db.py +0 -0
  215. mojo/apps/notify/README.md +0 -91
  216. mojo/apps/notify/README_NOTIFICATIONS.md +0 -566
  217. mojo/apps/notify/admin.py +0 -52
  218. mojo/apps/notify/handlers/example_handlers.py +0 -516
  219. mojo/apps/notify/handlers/ses/__init__.py +0 -25
  220. mojo/apps/notify/handlers/ses/bounce.py +0 -0
  221. mojo/apps/notify/handlers/ses/complaint.py +0 -25
  222. mojo/apps/notify/handlers/ses/message.py +0 -86
  223. mojo/apps/notify/management/commands/__init__.py +0 -1
  224. mojo/apps/notify/management/commands/process_notifications.py +0 -370
  225. mojo/apps/notify/mod +0 -0
  226. mojo/apps/notify/models/__init__.py +0 -12
  227. mojo/apps/notify/models/account.py +0 -128
  228. mojo/apps/notify/models/attachment.py +0 -24
  229. mojo/apps/notify/models/bounce.py +0 -68
  230. mojo/apps/notify/models/complaint.py +0 -40
  231. mojo/apps/notify/models/inbox.py +0 -113
  232. mojo/apps/notify/models/inbox_message.py +0 -173
  233. mojo/apps/notify/models/outbox.py +0 -129
  234. mojo/apps/notify/models/outbox_message.py +0 -288
  235. mojo/apps/notify/models/template.py +0 -30
  236. mojo/apps/notify/providers/aws.py +0 -73
  237. mojo/apps/notify/rest/ses.py +0 -0
  238. mojo/apps/notify/utils/__init__.py +0 -2
  239. mojo/apps/notify/utils/notifications.py +0 -404
  240. mojo/apps/notify/utils/parsing.py +0 -202
  241. mojo/apps/notify/utils/render.py +0 -144
  242. mojo/apps/tasks/README.md +0 -118
  243. mojo/apps/tasks/__init__.py +0 -11
  244. mojo/apps/tasks/manager.py +0 -489
  245. mojo/apps/tasks/rest/__init__.py +0 -2
  246. mojo/apps/tasks/rest/hooks.py +0 -0
  247. mojo/apps/tasks/rest/tasks.py +0 -62
  248. mojo/apps/tasks/runner.py +0 -174
  249. mojo/apps/tasks/tq_handlers.py +0 -14
  250. mojo/helpers/aws/setup_email.py +0 -0
  251. mojo/helpers/redis.py +0 -10
  252. mojo/models/meta.py +0 -262
  253. mojo/ws4redis/README.md +0 -174
  254. mojo/ws4redis/__init__.py +0 -2
  255. mojo/ws4redis/client.py +0 -283
  256. mojo/ws4redis/connection.py +0 -327
  257. mojo/ws4redis/exceptions.py +0 -32
  258. mojo/ws4redis/redis.py +0 -183
  259. mojo/ws4redis/servers/base.py +0 -86
  260. mojo/ws4redis/servers/django.py +0 -171
  261. mojo/ws4redis/servers/uwsgi.py +0 -63
  262. mojo/ws4redis/settings.py +0 -45
  263. mojo/ws4redis/utf8validator.py +0 -128
  264. mojo/ws4redis/websocket.py +0 -403
  265. {django_nativemojo-0.1.10.dist-info → django_nativemojo-0.1.16.dist-info}/LICENSE +0 -0
  266. {django_nativemojo-0.1.10.dist-info → django_nativemojo-0.1.16.dist-info}/NOTICE +0 -0
  267. {django_nativemojo-0.1.10.dist-info → django_nativemojo-0.1.16.dist-info}/WHEEL +0 -0
  268. /mojo/apps/{notify → aws}/__init__.py +0 -0
  269. /mojo/apps/{notify/handlers → aws/migrations}/__init__.py +0 -0
  270. /mojo/apps/{notify/management → docit/markdown_plugins}/__init__.py +0 -0
  271. /mojo/apps/{notify/providers → docit/migrations}/__init__.py +0 -0
  272. /mojo/apps/{notify/rest → fileman/migrations}/__init__.py +0 -0
  273. /mojo/{ws4redis/servers → apps/jobs/examples}/__init__.py +0 -0
  274. /mojo/apps/{fileman/models/render.py → jobs/migrations/__init__.py} +0 -0
  275. /mojo/{serializers → rest}/openapi.py +0 -0
  276. /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
+ ]