django-nativemojo 0.1.15__py3-none-any.whl → 0.1.17__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 (221) hide show
  1. {django_nativemojo-0.1.15.dist-info → django_nativemojo-0.1.17.dist-info}/METADATA +3 -2
  2. django_nativemojo-0.1.17.dist-info/RECORD +302 -0
  3. mojo/__init__.py +1 -1
  4. mojo/apps/account/management/commands/serializer_admin.py +121 -1
  5. mojo/apps/account/migrations/0006_add_device_tracking_models.py +72 -0
  6. mojo/apps/account/migrations/0007_delete_userdevicelocation.py +16 -0
  7. mojo/apps/account/migrations/0008_userdevicelocation.py +33 -0
  8. mojo/apps/account/migrations/0009_geolocatedip_subnet.py +18 -0
  9. mojo/apps/account/migrations/0010_group_avatar.py +20 -0
  10. mojo/apps/account/migrations/0011_user_org_registereddevice_pushconfig_and_more.py +118 -0
  11. mojo/apps/account/migrations/0012_remove_pushconfig_apns_key_file_and_more.py +21 -0
  12. mojo/apps/account/migrations/0013_pushconfig_test_mode_alter_pushconfig_apns_enabled_and_more.py +28 -0
  13. mojo/apps/account/migrations/0014_notificationdelivery_data_payload_and_more.py +48 -0
  14. mojo/apps/account/models/__init__.py +2 -0
  15. mojo/apps/account/models/device.py +279 -0
  16. mojo/apps/account/models/group.py +294 -8
  17. mojo/apps/account/models/member.py +14 -1
  18. mojo/apps/account/models/push/__init__.py +4 -0
  19. mojo/apps/account/models/push/config.py +112 -0
  20. mojo/apps/account/models/push/delivery.py +93 -0
  21. mojo/apps/account/models/push/device.py +66 -0
  22. mojo/apps/account/models/push/template.py +99 -0
  23. mojo/apps/account/models/user.py +190 -17
  24. mojo/apps/account/rest/__init__.py +2 -0
  25. mojo/apps/account/rest/device.py +39 -0
  26. mojo/apps/account/rest/group.py +8 -0
  27. mojo/apps/account/rest/push.py +187 -0
  28. mojo/apps/account/rest/user.py +95 -5
  29. mojo/apps/account/services/__init__.py +1 -0
  30. mojo/apps/account/services/push.py +363 -0
  31. mojo/apps/aws/migrations/0001_initial.py +206 -0
  32. mojo/apps/aws/migrations/0002_emaildomain_can_recv_emaildomain_can_send_and_more.py +28 -0
  33. mojo/apps/aws/migrations/0003_mailbox_is_domain_default_mailbox_is_system_default_and_more.py +31 -0
  34. mojo/apps/aws/migrations/0004_s3bucket.py +39 -0
  35. mojo/apps/aws/migrations/0005_alter_emaildomain_region_delete_s3bucket.py +21 -0
  36. mojo/apps/aws/models/__init__.py +19 -0
  37. mojo/apps/aws/models/email_attachment.py +99 -0
  38. mojo/apps/aws/models/email_domain.py +218 -0
  39. mojo/apps/aws/models/email_template.py +132 -0
  40. mojo/apps/aws/models/incoming_email.py +197 -0
  41. mojo/apps/aws/models/mailbox.py +288 -0
  42. mojo/apps/aws/models/sent_message.py +175 -0
  43. mojo/apps/aws/rest/__init__.py +6 -0
  44. mojo/apps/aws/rest/email.py +33 -0
  45. mojo/apps/aws/rest/email_ops.py +183 -0
  46. mojo/apps/aws/rest/messages.py +32 -0
  47. mojo/apps/aws/rest/send.py +101 -0
  48. mojo/apps/aws/rest/sns.py +403 -0
  49. mojo/apps/aws/rest/templates.py +19 -0
  50. mojo/apps/aws/services/__init__.py +32 -0
  51. mojo/apps/aws/services/email.py +390 -0
  52. mojo/apps/aws/services/email_ops.py +548 -0
  53. mojo/apps/docit/__init__.py +6 -0
  54. mojo/apps/docit/markdown_plugins/syntax_highlight.py +25 -0
  55. mojo/apps/docit/markdown_plugins/toc.py +12 -0
  56. mojo/apps/docit/migrations/0001_initial.py +113 -0
  57. mojo/apps/docit/migrations/0002_alter_book_modified_by_alter_page_modified_by.py +26 -0
  58. mojo/apps/docit/migrations/0003_alter_book_group.py +20 -0
  59. mojo/apps/docit/models/__init__.py +17 -0
  60. mojo/apps/docit/models/asset.py +231 -0
  61. mojo/apps/docit/models/book.py +227 -0
  62. mojo/apps/docit/models/page.py +319 -0
  63. mojo/apps/docit/models/page_revision.py +203 -0
  64. mojo/apps/docit/rest/__init__.py +10 -0
  65. mojo/apps/docit/rest/asset.py +17 -0
  66. mojo/apps/docit/rest/book.py +22 -0
  67. mojo/apps/docit/rest/page.py +22 -0
  68. mojo/apps/docit/rest/page_revision.py +17 -0
  69. mojo/apps/docit/services/__init__.py +11 -0
  70. mojo/apps/docit/services/docit.py +315 -0
  71. mojo/apps/docit/services/markdown.py +44 -0
  72. mojo/apps/fileman/backends/s3.py +209 -0
  73. mojo/apps/fileman/models/file.py +45 -9
  74. mojo/apps/fileman/models/manager.py +269 -3
  75. mojo/apps/incident/migrations/0007_event_uid.py +18 -0
  76. mojo/apps/incident/migrations/0008_ticket_ticketnote.py +55 -0
  77. mojo/apps/incident/migrations/0009_incident_status.py +18 -0
  78. mojo/apps/incident/migrations/0010_event_country_code.py +18 -0
  79. mojo/apps/incident/migrations/0011_incident_country_code.py +18 -0
  80. mojo/apps/incident/migrations/0012_alter_incident_status.py +18 -0
  81. mojo/apps/incident/models/__init__.py +1 -0
  82. mojo/apps/incident/models/event.py +35 -0
  83. mojo/apps/incident/models/incident.py +2 -0
  84. mojo/apps/incident/models/ticket.py +62 -0
  85. mojo/apps/incident/reporter.py +21 -3
  86. mojo/apps/incident/rest/__init__.py +1 -0
  87. mojo/apps/incident/rest/ticket.py +43 -0
  88. mojo/apps/jobs/__init__.py +489 -0
  89. mojo/apps/jobs/adapters.py +24 -0
  90. mojo/apps/jobs/cli.py +616 -0
  91. mojo/apps/jobs/daemon.py +370 -0
  92. mojo/apps/jobs/examples/sample_jobs.py +376 -0
  93. mojo/apps/jobs/examples/webhook_examples.py +203 -0
  94. mojo/apps/jobs/handlers/__init__.py +5 -0
  95. mojo/apps/jobs/handlers/webhook.py +317 -0
  96. mojo/apps/jobs/job_engine.py +734 -0
  97. mojo/apps/jobs/keys.py +203 -0
  98. mojo/apps/jobs/local_queue.py +363 -0
  99. mojo/apps/jobs/management/__init__.py +3 -0
  100. mojo/apps/jobs/management/commands/__init__.py +3 -0
  101. mojo/apps/jobs/manager.py +1327 -0
  102. mojo/apps/jobs/migrations/0001_initial.py +97 -0
  103. mojo/apps/jobs/migrations/0002_alter_job_max_retries_joblog.py +39 -0
  104. mojo/apps/jobs/models/__init__.py +6 -0
  105. mojo/apps/jobs/models/job.py +441 -0
  106. mojo/apps/jobs/rest/__init__.py +2 -0
  107. mojo/apps/jobs/rest/control.py +466 -0
  108. mojo/apps/jobs/rest/jobs.py +421 -0
  109. mojo/apps/jobs/scheduler.py +571 -0
  110. mojo/apps/jobs/services/__init__.py +6 -0
  111. mojo/apps/jobs/services/job_actions.py +465 -0
  112. mojo/apps/jobs/settings.py +209 -0
  113. mojo/apps/logit/models/log.py +3 -0
  114. mojo/apps/metrics/__init__.py +8 -1
  115. mojo/apps/metrics/redis_metrics.py +198 -0
  116. mojo/apps/metrics/rest/__init__.py +3 -0
  117. mojo/apps/metrics/rest/categories.py +266 -0
  118. mojo/apps/metrics/rest/helpers.py +48 -0
  119. mojo/apps/metrics/rest/permissions.py +99 -0
  120. mojo/apps/metrics/rest/values.py +277 -0
  121. mojo/apps/metrics/utils.py +17 -0
  122. mojo/decorators/http.py +40 -1
  123. mojo/helpers/aws/__init__.py +11 -7
  124. mojo/helpers/aws/inbound_email.py +309 -0
  125. mojo/helpers/aws/kms.py +413 -0
  126. mojo/helpers/aws/ses_domain.py +959 -0
  127. mojo/helpers/crypto/__init__.py +1 -1
  128. mojo/helpers/crypto/utils.py +15 -0
  129. mojo/helpers/location/__init__.py +2 -0
  130. mojo/helpers/location/countries.py +262 -0
  131. mojo/helpers/location/geolocation.py +196 -0
  132. mojo/helpers/logit.py +37 -0
  133. mojo/helpers/redis/__init__.py +2 -0
  134. mojo/helpers/redis/adapter.py +606 -0
  135. mojo/helpers/redis/client.py +48 -0
  136. mojo/helpers/redis/pool.py +225 -0
  137. mojo/helpers/request.py +8 -0
  138. mojo/helpers/response.py +8 -0
  139. mojo/middleware/auth.py +1 -1
  140. mojo/middleware/cors.py +40 -0
  141. mojo/middleware/logging.py +131 -12
  142. mojo/middleware/mojo.py +5 -0
  143. mojo/models/rest.py +271 -57
  144. mojo/models/secrets.py +86 -0
  145. mojo/serializers/__init__.py +16 -10
  146. mojo/serializers/core/__init__.py +90 -0
  147. mojo/serializers/core/cache/__init__.py +121 -0
  148. mojo/serializers/core/cache/backends.py +518 -0
  149. mojo/serializers/core/cache/base.py +102 -0
  150. mojo/serializers/core/cache/disabled.py +181 -0
  151. mojo/serializers/core/cache/memory.py +287 -0
  152. mojo/serializers/core/cache/redis.py +533 -0
  153. mojo/serializers/core/cache/utils.py +454 -0
  154. mojo/serializers/{manager.py → core/manager.py} +53 -4
  155. mojo/serializers/core/serializer.py +475 -0
  156. mojo/serializers/{advanced/formats → formats}/csv.py +116 -139
  157. mojo/serializers/suggested_improvements.md +388 -0
  158. testit/client.py +1 -1
  159. testit/helpers.py +14 -0
  160. testit/runner.py +23 -6
  161. django_nativemojo-0.1.15.dist-info/RECORD +0 -234
  162. mojo/apps/notify/README.md +0 -91
  163. mojo/apps/notify/README_NOTIFICATIONS.md +0 -566
  164. mojo/apps/notify/admin.py +0 -52
  165. mojo/apps/notify/handlers/example_handlers.py +0 -516
  166. mojo/apps/notify/handlers/ses/__init__.py +0 -25
  167. mojo/apps/notify/handlers/ses/complaint.py +0 -25
  168. mojo/apps/notify/handlers/ses/message.py +0 -86
  169. mojo/apps/notify/management/commands/__init__.py +0 -1
  170. mojo/apps/notify/management/commands/process_notifications.py +0 -370
  171. mojo/apps/notify/mod +0 -0
  172. mojo/apps/notify/models/__init__.py +0 -12
  173. mojo/apps/notify/models/account.py +0 -128
  174. mojo/apps/notify/models/attachment.py +0 -24
  175. mojo/apps/notify/models/bounce.py +0 -68
  176. mojo/apps/notify/models/complaint.py +0 -40
  177. mojo/apps/notify/models/inbox.py +0 -113
  178. mojo/apps/notify/models/inbox_message.py +0 -173
  179. mojo/apps/notify/models/outbox.py +0 -129
  180. mojo/apps/notify/models/outbox_message.py +0 -288
  181. mojo/apps/notify/models/template.py +0 -30
  182. mojo/apps/notify/providers/aws.py +0 -73
  183. mojo/apps/notify/rest/ses.py +0 -0
  184. mojo/apps/notify/utils/__init__.py +0 -2
  185. mojo/apps/notify/utils/notifications.py +0 -404
  186. mojo/apps/notify/utils/parsing.py +0 -202
  187. mojo/apps/notify/utils/render.py +0 -144
  188. mojo/apps/tasks/README.md +0 -118
  189. mojo/apps/tasks/__init__.py +0 -44
  190. mojo/apps/tasks/manager.py +0 -644
  191. mojo/apps/tasks/rest/__init__.py +0 -2
  192. mojo/apps/tasks/rest/hooks.py +0 -0
  193. mojo/apps/tasks/rest/tasks.py +0 -76
  194. mojo/apps/tasks/runner.py +0 -439
  195. mojo/apps/tasks/task.py +0 -99
  196. mojo/apps/tasks/tq_handlers.py +0 -132
  197. mojo/helpers/crypto/__pycache__/hash.cpython-310.pyc +0 -0
  198. mojo/helpers/crypto/__pycache__/sign.cpython-310.pyc +0 -0
  199. mojo/helpers/crypto/__pycache__/utils.cpython-310.pyc +0 -0
  200. mojo/helpers/redis.py +0 -10
  201. mojo/models/meta.py +0 -262
  202. mojo/serializers/advanced/README.md +0 -363
  203. mojo/serializers/advanced/__init__.py +0 -247
  204. mojo/serializers/advanced/formats/__init__.py +0 -28
  205. mojo/serializers/advanced/formats/excel.py +0 -516
  206. mojo/serializers/advanced/formats/json.py +0 -239
  207. mojo/serializers/advanced/formats/response.py +0 -485
  208. mojo/serializers/advanced/serializer.py +0 -568
  209. mojo/serializers/optimized.py +0 -618
  210. {django_nativemojo-0.1.15.dist-info → django_nativemojo-0.1.17.dist-info}/LICENSE +0 -0
  211. {django_nativemojo-0.1.15.dist-info → django_nativemojo-0.1.17.dist-info}/NOTICE +0 -0
  212. {django_nativemojo-0.1.15.dist-info → django_nativemojo-0.1.17.dist-info}/WHEEL +0 -0
  213. /mojo/apps/{notify → aws/migrations}/__init__.py +0 -0
  214. /mojo/apps/{notify/handlers → docit/markdown_plugins}/__init__.py +0 -0
  215. /mojo/apps/{notify/management → docit/migrations}/__init__.py +0 -0
  216. /mojo/apps/{notify/providers → jobs/examples}/__init__.py +0 -0
  217. /mojo/apps/{notify/rest → jobs/migrations}/__init__.py +0 -0
  218. /mojo/{serializers → rest}/openapi.py +0 -0
  219. /mojo/serializers/{settings_example.py → examples/settings.py} +0 -0
  220. /mojo/{apps/notify/handlers/ses/bounce.py → serializers/formats/__init__.py} +0 -0
  221. /mojo/serializers/{advanced/formats → formats}/localizers.py +0 -0
@@ -1,113 +0,0 @@
1
- from django.db import models
2
- from mojo.models import MojoModel
3
-
4
-
5
- class Inbox(models.Model, MojoModel):
6
- """
7
- Inbox for receiving messages from various notification services
8
- """
9
-
10
- class RestMeta:
11
- CAN_SAVE = CAN_CREATE = True
12
- CAN_DELETE = True
13
- DEFAULT_SORT = "-id"
14
- VIEW_PERMS = ["view_notify"]
15
- SEARCH_FIELDS = ["address"]
16
- SEARCH_TERMS = [
17
- "address",
18
- ("account", "account__domain"),
19
- ("account_kind", "account__kind"),
20
- ("group", "account__group__name")]
21
-
22
- GRAPHS = {
23
- "default": {
24
- "graphs": {
25
- "account": "basic"
26
- }
27
- },
28
- "list": {
29
- "graphs": {
30
- "account": "basic"
31
- }
32
- }
33
- }
34
-
35
- created = models.DateTimeField(auto_now_add=True, editable=False, db_index=True)
36
- modified = models.DateTimeField(auto_now=True)
37
-
38
- account = models.ForeignKey(
39
- "notify.Account",
40
- related_name="inboxes",
41
- on_delete=models.CASCADE,
42
- help_text="Notification account this inbox belongs to"
43
- )
44
-
45
- address = models.CharField(
46
- max_length=255,
47
- db_index=True,
48
- help_text="Inbox address (e.g., support@example.com, +1234567890)"
49
- )
50
-
51
- async_handler = models.CharField(
52
- max_length=255,
53
- null=True,
54
- blank=True,
55
- default=None,
56
- help_text="Python path to async handler function (e.g., app.inbox_handler.on_message_received)"
57
- )
58
-
59
- sync_handler = models.CharField(
60
- max_length=255,
61
- null=True,
62
- blank=True,
63
- default=None,
64
- help_text="Python path to sync handler function (e.g., app.inbox_handler.on_message_received)"
65
- )
66
-
67
- is_active = models.BooleanField(
68
- default=True,
69
- help_text="Whether this inbox is active and can receive messages"
70
- )
71
-
72
- settings = models.JSONField(
73
- default=dict,
74
- blank=True,
75
- help_text="Inbox-specific configuration settings"
76
- )
77
-
78
- class Meta:
79
- unique_together = ['account', 'address']
80
- indexes = [
81
- models.Index(fields=['account', 'address']),
82
- models.Index(fields=['address', 'is_active']),
83
- ]
84
-
85
- def __str__(self):
86
- return f"{self.account.get_kind_display()} inbox: {self.address}"
87
-
88
- def get_setting(self, key, default=None):
89
- """Get a specific setting value"""
90
- return self.settings.get(key, default)
91
-
92
- def set_setting(self, key, value):
93
- """Set a specific setting value"""
94
- self.settings[key] = value
95
-
96
- @property
97
- def group(self):
98
- """Get the group this inbox belongs to through its account"""
99
- return self.account.group if self.account else None
100
-
101
- def can_receive_messages(self):
102
- """Check if this inbox can receive messages"""
103
- return self.is_active and self.account.is_active
104
-
105
- def get_handler(self, async_preferred=True):
106
- """Get the appropriate handler for message processing"""
107
- if async_preferred and self.async_handler:
108
- return self.async_handler
109
- elif self.sync_handler:
110
- return self.sync_handler
111
- elif self.async_handler:
112
- return self.async_handler
113
- return None
@@ -1,173 +0,0 @@
1
- from django.db import models
2
- from mojo.models import MojoModel
3
-
4
-
5
- class InboxMessage(models.Model, MojoModel):
6
- """
7
- Message received in an inbox from various notification services
8
- """
9
-
10
- class RestMeta:
11
- CAN_SAVE = CAN_CREATE = True
12
- CAN_DELETE = True
13
- DEFAULT_SORT = "-id"
14
- VIEW_PERMS = ["view_notify"]
15
- SEARCH_FIELDS = ["to_address", "from_address", "subject", "message"]
16
- SEARCH_TERMS = [
17
- "to_address", "from_address", "subject",
18
- ("inbox", "inbox__address"),
19
- ("account", "inbox__account__domain"),
20
- ("account_kind", "inbox__account__kind"),
21
- ("user", "user__username"),
22
- ("group", "group__name")]
23
-
24
- GRAPHS = {
25
- "default": {
26
- "graphs": {
27
- "inbox": "basic",
28
- "user": "basic",
29
- "group": "basic"
30
- }
31
- },
32
- "list": {
33
- "graphs": {
34
- "inbox": "basic",
35
- "user": "basic",
36
- "group": "basic"
37
- }
38
- }
39
- }
40
-
41
- created = models.DateTimeField(auto_now_add=True, editable=False, db_index=True)
42
- modified = models.DateTimeField(auto_now=True)
43
-
44
- inbox = models.ForeignKey(
45
- "notify.Inbox",
46
- related_name="messages",
47
- on_delete=models.CASCADE,
48
- help_text="Inbox that received this message"
49
- )
50
-
51
- user = models.ForeignKey(
52
- "account.User",
53
- related_name="inbox_messages",
54
- null=True,
55
- blank=True,
56
- default=None,
57
- on_delete=models.CASCADE,
58
- help_text="User associated with this message (if applicable)"
59
- )
60
-
61
- group = models.ForeignKey(
62
- "account.Group",
63
- related_name="inbox_messages",
64
- null=True,
65
- blank=True,
66
- default=None,
67
- on_delete=models.CASCADE,
68
- help_text="Group associated with this message (if applicable)"
69
- )
70
-
71
- to_address = models.CharField(
72
- max_length=255,
73
- db_index=True,
74
- help_text="Recipient address (e.g., inbox@example.com, +1234567890)"
75
- )
76
-
77
- from_address = models.CharField(
78
- max_length=255,
79
- db_index=True,
80
- help_text="Sender address (e.g., sender@example.com, +0987654321)"
81
- )
82
-
83
- subject = models.CharField(
84
- max_length=500,
85
- null=True,
86
- blank=True,
87
- default=None,
88
- help_text="Message subject (for email, etc.)"
89
- )
90
-
91
- message = models.TextField(
92
- help_text="Message content/body"
93
- )
94
-
95
- metadata = models.JSONField(
96
- default=dict,
97
- blank=True,
98
- help_text="Additional message metadata (headers, delivery info, etc.)"
99
- )
100
-
101
- processed = models.BooleanField(
102
- default=False,
103
- db_index=True,
104
- help_text="Whether this message has been processed by handlers"
105
- )
106
-
107
- processed_at = models.DateTimeField(
108
- null=True,
109
- blank=True,
110
- default=None,
111
- help_text="When this message was processed"
112
- )
113
-
114
- message_id = models.CharField(
115
- max_length=255,
116
- null=True,
117
- blank=True,
118
- default=None,
119
- db_index=True,
120
- help_text="External message ID from the service provider"
121
- )
122
-
123
- class Meta:
124
- indexes = [
125
- models.Index(fields=['inbox', 'created']),
126
- models.Index(fields=['from_address', 'created']),
127
- models.Index(fields=['to_address', 'created']),
128
- models.Index(fields=['processed', 'created']),
129
- models.Index(fields=['user', 'created']),
130
- models.Index(fields=['group', 'created']),
131
- ]
132
-
133
- def __str__(self):
134
- subject_preview = f" - {self.subject[:50]}..." if self.subject else ""
135
- return f"{self.inbox.account.get_kind_display()} message: {self.from_address} → {self.to_address}{subject_preview}"
136
-
137
- def get_metadata_value(self, key, default=None):
138
- """Get a specific metadata value"""
139
- return self.metadata.get(key, default)
140
-
141
- def set_metadata_value(self, key, value):
142
- """Set a specific metadata value"""
143
- self.metadata[key] = value
144
-
145
- @property
146
- def account(self):
147
- """Get the account this message was received through"""
148
- return self.inbox.account if self.inbox else None
149
-
150
- @property
151
- def message_kind(self):
152
- """Get the kind of message (email, sms, etc.)"""
153
- return self.account.kind if self.account else None
154
-
155
- @property
156
- def message_preview(self):
157
- """Get a preview of the message content"""
158
- if not self.message:
159
- return ""
160
- return self.message[:200] + "..." if len(self.message) > 200 else self.message
161
-
162
- def mark_processed(self):
163
- """Mark this message as processed"""
164
- from django.utils import timezone
165
- self.processed = True
166
- self.processed_at = timezone.now()
167
- self.save(update_fields=['processed', 'processed_at'])
168
-
169
- def is_from_user(self, user):
170
- """Check if this message is from a specific user"""
171
- if not user or not user.email:
172
- return False
173
- return self.from_address.lower() == user.email.lower()
@@ -1,129 +0,0 @@
1
- from django.db import models
2
- from mojo.models import MojoModel
3
-
4
-
5
- class Outbox(models.Model, MojoModel):
6
- """
7
- Outbox for sending messages through various notification services
8
- """
9
-
10
- class RestMeta:
11
- CAN_SAVE = CAN_CREATE = True
12
- CAN_DELETE = True
13
- DEFAULT_SORT = "-id"
14
- VIEW_PERMS = ["view_notify"]
15
- SEARCH_FIELDS = ["address"]
16
- SEARCH_TERMS = [
17
- "address",
18
- ("account", "account__domain"),
19
- ("account_kind", "account__kind"),
20
- ("group", "group__name")]
21
-
22
- GRAPHS = {
23
- "default": {
24
- "graphs": {
25
- "account": "basic",
26
- "group": "basic"
27
- }
28
- },
29
- "list": {
30
- "graphs": {
31
- "account": "basic",
32
- "group": "basic"
33
- }
34
- }
35
- }
36
-
37
- created = models.DateTimeField(auto_now_add=True, editable=False, db_index=True)
38
- modified = models.DateTimeField(auto_now=True)
39
-
40
- account = models.ForeignKey(
41
- "notify.Account",
42
- related_name="outboxes",
43
- on_delete=models.CASCADE,
44
- help_text="Notification account this outbox belongs to"
45
- )
46
-
47
- group = models.ForeignKey(
48
- "account.Group",
49
- related_name="outboxes",
50
- null=True,
51
- blank=True,
52
- default=None,
53
- on_delete=models.CASCADE,
54
- help_text="Group that owns this outbox"
55
- )
56
-
57
- address = models.CharField(
58
- max_length=255,
59
- db_index=True,
60
- help_text="Outbox address (e.g., outbox@example.com, +1234567890)"
61
- )
62
-
63
- handler = models.CharField(
64
- max_length=255,
65
- null=True,
66
- blank=True,
67
- default=None,
68
- help_text="Python path to handler function (e.g., app.outbox_handler.on_message_sent)"
69
- )
70
-
71
- is_active = models.BooleanField(
72
- default=True,
73
- help_text="Whether this outbox is active and can send messages"
74
- )
75
-
76
- settings = models.JSONField(
77
- default=dict,
78
- blank=True,
79
- help_text="Outbox-specific configuration settings"
80
- )
81
-
82
- rate_limit = models.IntegerField(
83
- null=True,
84
- blank=True,
85
- default=None,
86
- help_text="Max messages per hour (null for no limit)"
87
- )
88
-
89
- class Meta:
90
- unique_together = ['account', 'address']
91
- indexes = [
92
- models.Index(fields=['account', 'address']),
93
- models.Index(fields=['group', 'is_active']),
94
- models.Index(fields=['address', 'is_active']),
95
- ]
96
-
97
- def __str__(self):
98
- group_name = self.group.name if self.group else "No Group"
99
- return f"{self.account.get_kind_display()} outbox: {self.address} ({group_name})"
100
-
101
- def get_setting(self, key, default=None):
102
- """Get a specific setting value"""
103
- return self.settings.get(key, default)
104
-
105
- def set_setting(self, key, value):
106
- """Set a specific setting value"""
107
- self.settings[key] = value
108
-
109
- def can_send_messages(self):
110
- """Check if this outbox can send messages"""
111
- return self.is_active and self.account.is_active
112
-
113
- def check_rate_limit(self):
114
- """Check if outbox is within rate limits"""
115
- if not self.rate_limit:
116
- return True
117
-
118
- from django.utils import timezone
119
- from datetime import timedelta
120
-
121
- one_hour_ago = timezone.now() - timedelta(hours=1)
122
- recent_messages = self.messages.filter(created__gte=one_hour_ago).count()
123
-
124
- return recent_messages < self.rate_limit
125
-
126
- @property
127
- def message_kind(self):
128
- """Get the kind of messages this outbox sends"""
129
- return self.account.kind if self.account else None
@@ -1,288 +0,0 @@
1
- from django.db import models
2
- from mojo.models import MojoModel
3
-
4
-
5
- class OutboxMessage(models.Model, MojoModel):
6
- """
7
- Message to be sent or already sent through an outbox
8
- """
9
-
10
- class RestMeta:
11
- CAN_SAVE = CAN_CREATE = True
12
- CAN_DELETE = True
13
- DEFAULT_SORT = "-id"
14
- VIEW_PERMS = ["view_notify"]
15
- SEARCH_FIELDS = ["to_address", "from_address", "subject", "message"]
16
- SEARCH_TERMS = [
17
- "to_address", "from_address", "subject", "status",
18
- ("outbox", "outbox__address"),
19
- ("account", "outbox__account__domain"),
20
- ("account_kind", "outbox__account__kind"),
21
- ("user", "user__username"),
22
- ("group", "group__name")]
23
-
24
- GRAPHS = {
25
- "default": {
26
- "graphs": {
27
- "outbox": "basic",
28
- "user": "basic",
29
- "group": "basic"
30
- }
31
- },
32
- "list": {
33
- "graphs": {
34
- "outbox": "basic",
35
- "user": "basic",
36
- "group": "basic"
37
- }
38
- }
39
- }
40
-
41
- # Message status choices
42
- PENDING = 'pending'
43
- SENDING = 'sending'
44
- SENT = 'sent'
45
- FAILED = 'failed'
46
- CANCELLED = 'cancelled'
47
-
48
- STATUS_CHOICES = [
49
- (PENDING, 'Pending'),
50
- (SENDING, 'Sending'),
51
- (SENT, 'Sent'),
52
- (FAILED, 'Failed'),
53
- (CANCELLED, 'Cancelled'),
54
- ]
55
-
56
- created = models.DateTimeField(auto_now_add=True, editable=False, db_index=True)
57
- modified = models.DateTimeField(auto_now=True)
58
-
59
- outbox = models.ForeignKey(
60
- "notify.Outbox",
61
- related_name="messages",
62
- on_delete=models.CASCADE,
63
- help_text="Outbox that will send this message"
64
- )
65
-
66
- user = models.ForeignKey(
67
- "account.User",
68
- related_name="outbox_messages",
69
- null=True,
70
- blank=True,
71
- default=None,
72
- on_delete=models.CASCADE,
73
- help_text="User associated with this message (if applicable)"
74
- )
75
-
76
- group = models.ForeignKey(
77
- "account.Group",
78
- related_name="outbox_messages",
79
- null=True,
80
- blank=True,
81
- default=None,
82
- on_delete=models.CASCADE,
83
- help_text="Group associated with this message (if applicable)"
84
- )
85
-
86
- to_address = models.CharField(
87
- max_length=255,
88
- db_index=True,
89
- help_text="Recipient address (e.g., joe@example.com, +1234567890)"
90
- )
91
-
92
- from_address = models.CharField(
93
- max_length=255,
94
- db_index=True,
95
- help_text="Sender address (e.g., outbox@example.com, +0987654321)"
96
- )
97
-
98
- subject = models.CharField(
99
- max_length=500,
100
- null=True,
101
- blank=True,
102
- default=None,
103
- help_text="Message subject (for email, etc.)"
104
- )
105
-
106
- message = models.TextField(
107
- help_text="Message content/body"
108
- )
109
-
110
- metadata = models.JSONField(
111
- default=dict,
112
- blank=True,
113
- help_text="Additional message metadata (delivery options, attachments, etc.)"
114
- )
115
-
116
- status = models.CharField(
117
- max_length=32,
118
- choices=STATUS_CHOICES,
119
- default=PENDING,
120
- db_index=True,
121
- help_text="Current status of the message"
122
- )
123
-
124
- scheduled_at = models.DateTimeField(
125
- null=True,
126
- blank=True,
127
- default=None,
128
- db_index=True,
129
- help_text="When this message should be sent (null for immediate)"
130
- )
131
-
132
- sent_at = models.DateTimeField(
133
- null=True,
134
- blank=True,
135
- default=None,
136
- help_text="When this message was actually sent"
137
- )
138
-
139
- failed_at = models.DateTimeField(
140
- null=True,
141
- blank=True,
142
- default=None,
143
- help_text="When this message failed to send"
144
- )
145
-
146
- error_message = models.TextField(
147
- null=True,
148
- blank=True,
149
- default=None,
150
- help_text="Error message if sending failed"
151
- )
152
-
153
- message_id = models.CharField(
154
- max_length=255,
155
- null=True,
156
- blank=True,
157
- default=None,
158
- db_index=True,
159
- help_text="External message ID from the service provider"
160
- )
161
-
162
- retry_count = models.IntegerField(
163
- default=0,
164
- help_text="Number of times sending has been attempted"
165
- )
166
-
167
- max_retries = models.IntegerField(
168
- default=3,
169
- help_text="Maximum number of retry attempts"
170
- )
171
-
172
- class Meta:
173
- indexes = [
174
- models.Index(fields=['outbox', 'status', 'created']),
175
- models.Index(fields=['status', 'scheduled_at']),
176
- models.Index(fields=['to_address', 'created']),
177
- models.Index(fields=['from_address', 'created']),
178
- models.Index(fields=['user', 'created']),
179
- models.Index(fields=['group', 'created']),
180
- models.Index(fields=['sent_at']),
181
- ]
182
-
183
- def __str__(self):
184
- subject_preview = f" - {self.subject[:50]}..." if self.subject else ""
185
- return f"{self.outbox.account.get_kind_display()} message: {self.from_address} → {self.to_address}{subject_preview} ({self.get_status_display()})"
186
-
187
- def get_metadata_value(self, key, default=None):
188
- """Get a specific metadata value"""
189
- return self.metadata.get(key, default)
190
-
191
- def set_metadata_value(self, key, value):
192
- """Set a specific metadata value"""
193
- self.metadata[key] = value
194
-
195
- @property
196
- def account(self):
197
- """Get the account this message will be sent through"""
198
- return self.outbox.account if self.outbox else None
199
-
200
- @property
201
- def message_kind(self):
202
- """Get the kind of message (email, sms, etc.)"""
203
- return self.account.kind if self.account else None
204
-
205
- @property
206
- def message_preview(self):
207
- """Get a preview of the message content"""
208
- if not self.message:
209
- return ""
210
- return self.message[:200] + "..." if len(self.message) > 200 else self.message
211
-
212
- @property
213
- def is_pending(self):
214
- return self.status == self.PENDING
215
-
216
- @property
217
- def is_sending(self):
218
- return self.status == self.SENDING
219
-
220
- @property
221
- def is_sent(self):
222
- return self.status == self.SENT
223
-
224
- @property
225
- def is_failed(self):
226
- return self.status == self.FAILED
227
-
228
- @property
229
- def is_cancelled(self):
230
- return self.status == self.CANCELLED
231
-
232
- @property
233
- def can_retry(self):
234
- """Check if this message can be retried"""
235
- return self.is_failed and self.retry_count < self.max_retries
236
-
237
- @property
238
- def is_ready_to_send(self):
239
- """Check if this message is ready to be sent"""
240
- if not self.is_pending:
241
- return False
242
-
243
- if self.scheduled_at:
244
- from django.utils import timezone
245
- return timezone.now() >= self.scheduled_at
246
-
247
- return True
248
-
249
- def mark_sending(self):
250
- """Mark this message as currently being sent"""
251
- self.status = self.SENDING
252
- self.save(update_fields=['status'])
253
-
254
- def mark_sent(self, message_id=None):
255
- """Mark this message as successfully sent"""
256
- from django.utils import timezone
257
- self.status = self.SENT
258
- self.sent_at = timezone.now()
259
- if message_id:
260
- self.message_id = message_id
261
- self.save(update_fields=['status', 'sent_at', 'message_id'])
262
-
263
- def mark_failed(self, error_message=None):
264
- """Mark this message as failed to send"""
265
- from django.utils import timezone
266
- self.status = self.FAILED
267
- self.failed_at = timezone.now()
268
- self.retry_count += 1
269
- if error_message:
270
- self.error_message = error_message
271
- self.save(update_fields=['status', 'failed_at', 'retry_count', 'error_message'])
272
-
273
- def mark_cancelled(self):
274
- """Mark this message as cancelled"""
275
- self.status = self.CANCELLED
276
- self.save(update_fields=['status'])
277
-
278
- def reset_for_retry(self):
279
- """Reset message status for retry"""
280
- if self.can_retry:
281
- self.status = self.PENDING
282
- self.save(update_fields=['status'])
283
-
284
- def is_to_user(self, user):
285
- """Check if this message is to a specific user"""
286
- if not user or not user.email:
287
- return False
288
- return self.to_address.lower() == user.email.lower()