django-nativemojo 0.1.10__py3-none-any.whl → 0.1.15__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 (120) hide show
  1. django_nativemojo-0.1.15.dist-info/METADATA +136 -0
  2. {django_nativemojo-0.1.10.dist-info → django_nativemojo-0.1.15.dist-info}/RECORD +105 -65
  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 +531 -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/models/group.py +25 -7
  10. mojo/apps/account/models/member.py +15 -4
  11. mojo/apps/account/models/user.py +197 -20
  12. mojo/apps/account/rest/group.py +1 -0
  13. mojo/apps/account/rest/user.py +6 -2
  14. mojo/apps/aws/rest/__init__.py +1 -0
  15. mojo/apps/aws/rest/s3.py +64 -0
  16. mojo/apps/fileman/README.md +8 -8
  17. mojo/apps/fileman/backends/base.py +76 -70
  18. mojo/apps/fileman/backends/filesystem.py +86 -86
  19. mojo/apps/fileman/backends/s3.py +200 -108
  20. mojo/apps/fileman/migrations/0001_initial.py +106 -0
  21. mojo/apps/fileman/migrations/0002_filemanager_parent_alter_filemanager_max_file_size.py +24 -0
  22. mojo/apps/fileman/migrations/0003_remove_file_fileman_fil_upload__c4bc35_idx_and_more.py +25 -0
  23. mojo/apps/fileman/migrations/0004_remove_file_original_filename_and_more.py +39 -0
  24. mojo/apps/fileman/migrations/0005_alter_file_upload_token.py +18 -0
  25. mojo/apps/fileman/migrations/0006_file_download_url_filemanager_forever_urls.py +23 -0
  26. mojo/apps/fileman/migrations/0007_remove_filemanager_forever_urls_and_more.py +22 -0
  27. mojo/apps/fileman/migrations/0008_file_category.py +18 -0
  28. mojo/apps/fileman/migrations/0009_rename_file_path_file_storage_file_path.py +18 -0
  29. mojo/apps/fileman/migrations/0010_filerendition.py +33 -0
  30. mojo/apps/fileman/migrations/0011_alter_filerendition_original_file.py +19 -0
  31. mojo/apps/fileman/models/__init__.py +1 -5
  32. mojo/apps/fileman/models/file.py +204 -58
  33. mojo/apps/fileman/models/manager.py +161 -31
  34. mojo/apps/fileman/models/rendition.py +118 -0
  35. mojo/apps/fileman/renderer/__init__.py +111 -0
  36. mojo/apps/fileman/renderer/audio.py +403 -0
  37. mojo/apps/fileman/renderer/base.py +205 -0
  38. mojo/apps/fileman/renderer/document.py +404 -0
  39. mojo/apps/fileman/renderer/image.py +222 -0
  40. mojo/apps/fileman/renderer/utils.py +297 -0
  41. mojo/apps/fileman/renderer/video.py +304 -0
  42. mojo/apps/fileman/rest/__init__.py +1 -18
  43. mojo/apps/fileman/rest/upload.py +22 -32
  44. mojo/apps/fileman/signals.py +58 -0
  45. mojo/apps/fileman/tasks.py +254 -0
  46. mojo/apps/fileman/utils/__init__.py +40 -16
  47. mojo/apps/incident/migrations/0005_incidenthistory.py +39 -0
  48. mojo/apps/incident/migrations/0006_alter_incident_state.py +18 -0
  49. mojo/apps/incident/models/__init__.py +1 -0
  50. mojo/apps/incident/models/history.py +36 -0
  51. mojo/apps/incident/models/incident.py +1 -1
  52. mojo/apps/incident/reporter.py +3 -1
  53. mojo/apps/incident/rest/event.py +7 -1
  54. mojo/apps/logit/migrations/0004_alter_log_level.py +18 -0
  55. mojo/apps/logit/models/log.py +4 -1
  56. mojo/apps/metrics/utils.py +2 -2
  57. mojo/apps/notify/handlers/ses/message.py +1 -1
  58. mojo/apps/notify/providers/aws.py +2 -2
  59. mojo/apps/tasks/__init__.py +34 -1
  60. mojo/apps/tasks/manager.py +200 -45
  61. mojo/apps/tasks/rest/tasks.py +24 -10
  62. mojo/apps/tasks/runner.py +283 -18
  63. mojo/apps/tasks/task.py +99 -0
  64. mojo/apps/tasks/tq_handlers.py +118 -0
  65. mojo/decorators/auth.py +6 -1
  66. mojo/decorators/http.py +7 -2
  67. mojo/helpers/aws/__init__.py +41 -0
  68. mojo/helpers/aws/ec2.py +804 -0
  69. mojo/helpers/aws/iam.py +748 -0
  70. mojo/helpers/aws/s3.py +451 -11
  71. mojo/helpers/aws/ses.py +483 -0
  72. mojo/helpers/aws/sns.py +461 -0
  73. mojo/helpers/crypto/__pycache__/hash.cpython-310.pyc +0 -0
  74. mojo/helpers/crypto/__pycache__/sign.cpython-310.pyc +0 -0
  75. mojo/helpers/crypto/__pycache__/utils.cpython-310.pyc +0 -0
  76. mojo/helpers/dates.py +18 -0
  77. mojo/helpers/response.py +6 -2
  78. mojo/helpers/settings/__init__.py +2 -0
  79. mojo/helpers/{settings.py → settings/helper.py} +1 -37
  80. mojo/helpers/settings/parser.py +132 -0
  81. mojo/middleware/logging.py +1 -1
  82. mojo/middleware/mojo.py +5 -0
  83. mojo/models/rest.py +261 -46
  84. mojo/models/secrets.py +13 -4
  85. mojo/serializers/__init__.py +100 -0
  86. mojo/serializers/advanced/README.md +363 -0
  87. mojo/serializers/advanced/__init__.py +247 -0
  88. mojo/serializers/advanced/formats/__init__.py +28 -0
  89. mojo/serializers/advanced/formats/csv.py +416 -0
  90. mojo/serializers/advanced/formats/excel.py +516 -0
  91. mojo/serializers/advanced/formats/json.py +239 -0
  92. mojo/serializers/advanced/formats/localizers.py +509 -0
  93. mojo/serializers/advanced/formats/response.py +485 -0
  94. mojo/serializers/advanced/serializer.py +568 -0
  95. mojo/serializers/manager.py +501 -0
  96. mojo/serializers/optimized.py +618 -0
  97. mojo/serializers/settings_example.py +322 -0
  98. mojo/serializers/{models.py → simple.py} +38 -15
  99. testit/helpers.py +21 -4
  100. django_nativemojo-0.1.10.dist-info/METADATA +0 -96
  101. mojo/apps/metrics/rest/db.py +0 -0
  102. mojo/helpers/aws/setup_email.py +0 -0
  103. mojo/ws4redis/README.md +0 -174
  104. mojo/ws4redis/__init__.py +0 -2
  105. mojo/ws4redis/client.py +0 -283
  106. mojo/ws4redis/connection.py +0 -327
  107. mojo/ws4redis/exceptions.py +0 -32
  108. mojo/ws4redis/redis.py +0 -183
  109. mojo/ws4redis/servers/base.py +0 -86
  110. mojo/ws4redis/servers/django.py +0 -171
  111. mojo/ws4redis/servers/uwsgi.py +0 -63
  112. mojo/ws4redis/settings.py +0 -45
  113. mojo/ws4redis/utf8validator.py +0 -128
  114. mojo/ws4redis/websocket.py +0 -403
  115. {django_nativemojo-0.1.10.dist-info → django_nativemojo-0.1.15.dist-info}/LICENSE +0 -0
  116. {django_nativemojo-0.1.10.dist-info → django_nativemojo-0.1.15.dist-info}/NOTICE +0 -0
  117. {django_nativemojo-0.1.10.dist-info → django_nativemojo-0.1.15.dist-info}/WHEEL +0 -0
  118. /mojo/{ws4redis/servers → apps/aws}/__init__.py +0 -0
  119. /mojo/apps/{fileman/models/render.py → aws/models/__init__.py} +0 -0
  120. /mojo/apps/fileman/{rest/__init__ → migrations/__init__.py} +0 -0
@@ -0,0 +1,58 @@
1
+ import logging
2
+ from django.db.models.signals import post_save, post_delete
3
+ from django.dispatch import receiver
4
+ from mojo.apps.fileman.models import File
5
+ from mojo.apps.fileman.tasks import process_file_renditions, cleanup_renditions
6
+
7
+ logger = logging.getLogger(__name__)
8
+
9
+ @receiver(post_save, sender=File)
10
+ def handle_file_upload_completed(sender, instance, created, **kwargs):
11
+ """
12
+ When a file is marked as completed, automatically create renditions
13
+
14
+ This connects to the post_save signal of the File model and checks if the
15
+ file has been marked as completed. If so, it queues a task to create renditions.
16
+ """
17
+ # Only process if the file upload is completed
18
+ if instance.is_completed:
19
+ # Check if this is a status change to completed
20
+ if not created:
21
+ try:
22
+ # Get the previous instance from the database
23
+ old_instance = sender.objects.get(pk=instance.pk)
24
+ if old_instance.upload_status != instance.upload_status and instance.upload_status == File.COMPLETED:
25
+ logger.info(f"File {instance.id} ({instance.filename}) marked as completed, creating renditions")
26
+ # Queue task to create renditions
27
+ process_file_renditions.delay(instance.id)
28
+ except Exception as e:
29
+ logger.error(f"Error checking file status change: {str(e)}")
30
+ else:
31
+ # For new files that are already completed
32
+ if instance.upload_status == File.COMPLETED:
33
+ logger.info(f"New file {instance.id} ({instance.filename}) is completed, creating renditions")
34
+ # Queue task to create renditions
35
+ process_file_renditions.delay(instance.id)
36
+
37
+ @receiver(post_delete, sender=File)
38
+ def handle_file_deleted(sender, instance, **kwargs):
39
+ """
40
+ When a file is deleted, clean up its renditions
41
+
42
+ This connects to the post_delete signal of the File model and ensures that
43
+ when a file is deleted, its renditions are also removed from storage.
44
+ """
45
+ try:
46
+ # Check if we have any renditions to clean up
47
+ from mojo.apps.fileman.models import FileRendition
48
+ if FileRendition.objects.filter(original_file_id=instance.id).exists():
49
+ logger.info(f"File {instance.id} ({instance.filename}) deleted, cleaning up renditions")
50
+ # Queue task to clean up renditions
51
+ # Note: We can't use instance.id directly in the task since the object is being deleted
52
+ # So we pass the ID value
53
+ cleanup_renditions.delay(instance.id)
54
+ except Exception as e:
55
+ logger.error(f"Error queueing rendition cleanup for deleted file: {str(e)}")
56
+
57
+ # Note: No need to manually connect signals as Django will automatically
58
+ # discover and connect properly decorated signal handlers
@@ -0,0 +1,254 @@
1
+ import logging
2
+ from celery import shared_task
3
+ from mojo.apps.fileman.models import File, FileRendition
4
+ from mojo.apps.fileman.renderer import process_new_file, get_renderer_for_file
5
+
6
+ logger = logging.getLogger(__name__)
7
+
8
+ @shared_task
9
+ def process_file_renditions(file_id):
10
+ """
11
+ Process renditions for a newly uploaded file
12
+
13
+ Args:
14
+ file_id: ID of the File to process
15
+
16
+ Returns:
17
+ dict: Result information including number of renditions created
18
+ """
19
+ try:
20
+ file = File.objects.get(id=file_id)
21
+
22
+ if not file.is_completed:
23
+ logger.warning(f"Cannot process renditions for incomplete file {file_id}")
24
+ return {
25
+ 'status': 'error',
26
+ 'message': 'File upload is not complete',
27
+ 'file_id': file_id,
28
+ 'renditions_created': 0
29
+ }
30
+
31
+ renditions = process_new_file(file)
32
+
33
+ return {
34
+ 'status': 'success',
35
+ 'message': f'Created {len(renditions)} renditions',
36
+ 'file_id': file_id,
37
+ 'renditions_created': len(renditions),
38
+ 'rendition_roles': [r.role for r in renditions]
39
+ }
40
+
41
+ except File.DoesNotExist:
42
+ logger.error(f"File with ID {file_id} not found")
43
+ return {
44
+ 'status': 'error',
45
+ 'message': 'File not found',
46
+ 'file_id': file_id,
47
+ 'renditions_created': 0
48
+ }
49
+ except Exception as e:
50
+ logger.exception(f"Error processing renditions for file {file_id}: {str(e)}")
51
+ return {
52
+ 'status': 'error',
53
+ 'message': str(e),
54
+ 'file_id': file_id,
55
+ 'renditions_created': 0
56
+ }
57
+
58
+ @shared_task
59
+ def cleanup_renditions(file_id):
60
+ """
61
+ Clean up all renditions for a file
62
+
63
+ Args:
64
+ file_id: ID of the File whose renditions should be cleaned up
65
+
66
+ Returns:
67
+ dict: Result information
68
+ """
69
+ try:
70
+ # First get the file to make sure it exists
71
+ file = File.objects.get(id=file_id)
72
+
73
+ # Get all renditions
74
+ renditions = FileRendition.objects.filter(original_file_id=file_id)
75
+ count = renditions.count()
76
+
77
+ # Delete the files from storage
78
+ for rendition in renditions:
79
+ try:
80
+ if rendition.storage_path:
81
+ file.file_manager.backend.delete(rendition.storage_path)
82
+ except Exception as e:
83
+ logger.warning(f"Error deleting rendition file {rendition.id}: {str(e)}")
84
+
85
+ # Delete the database records
86
+ renditions.delete()
87
+
88
+ return {
89
+ 'status': 'success',
90
+ 'message': f'Cleaned up {count} renditions',
91
+ 'file_id': file_id,
92
+ 'renditions_deleted': count
93
+ }
94
+
95
+ except File.DoesNotExist:
96
+ logger.error(f"File with ID {file_id} not found")
97
+ return {
98
+ 'status': 'error',
99
+ 'message': 'File not found',
100
+ 'file_id': file_id,
101
+ 'renditions_deleted': 0
102
+ }
103
+ except Exception as e:
104
+ logger.exception(f"Error cleaning up renditions for file {file_id}: {str(e)}")
105
+ return {
106
+ 'status': 'error',
107
+ 'message': str(e),
108
+ 'file_id': file_id,
109
+ 'renditions_deleted': 0
110
+ }
111
+
112
+ @shared_task
113
+ def regenerate_renditions(file_id, roles=None):
114
+ """
115
+ Regenerate specific or all renditions for a file
116
+
117
+ Args:
118
+ file_id: ID of the File to process
119
+ roles: Optional list of roles to regenerate (None for all)
120
+
121
+ Returns:
122
+ dict: Result information
123
+ """
124
+ try:
125
+ file = File.objects.get(id=file_id)
126
+
127
+ if not file.is_completed:
128
+ logger.warning(f"Cannot regenerate renditions for incomplete file {file_id}")
129
+ return {
130
+ 'status': 'error',
131
+ 'message': 'File upload is not complete',
132
+ 'file_id': file_id,
133
+ 'renditions_created': 0
134
+ }
135
+
136
+ # Get renderer for this file
137
+ renderer = get_renderer_for_file(file)
138
+ if not renderer:
139
+ return {
140
+ 'status': 'error',
141
+ 'message': f'No renderer available for file type {file.category}',
142
+ 'file_id': file_id,
143
+ 'renditions_created': 0
144
+ }
145
+
146
+ # If specific roles are requested, delete and regenerate those
147
+ if roles:
148
+ # Delete existing renditions for these roles
149
+ FileRendition.objects.filter(original_file=file, role__in=roles).delete()
150
+
151
+ # Create new renditions
152
+ created_renditions = []
153
+ for role in roles:
154
+ rendition = renderer.create_rendition(role)
155
+ if rendition:
156
+ created_renditions.append(rendition)
157
+ else:
158
+ # Delete all existing renditions
159
+ FileRendition.objects.filter(original_file=file).delete()
160
+
161
+ # Create all default renditions
162
+ created_renditions = renderer.create_all_renditions()
163
+
164
+ return {
165
+ 'status': 'success',
166
+ 'message': f'Regenerated {len(created_renditions)} renditions',
167
+ 'file_id': file_id,
168
+ 'renditions_created': len(created_renditions),
169
+ 'rendition_roles': [r.role for r in created_renditions]
170
+ }
171
+
172
+ except File.DoesNotExist:
173
+ logger.error(f"File with ID {file_id} not found")
174
+ return {
175
+ 'status': 'error',
176
+ 'message': 'File not found',
177
+ 'file_id': file_id,
178
+ 'renditions_created': 0
179
+ }
180
+ except Exception as e:
181
+ logger.exception(f"Error regenerating renditions for file {file_id}: {str(e)}")
182
+ return {
183
+ 'status': 'error',
184
+ 'message': str(e),
185
+ 'file_id': file_id,
186
+ 'renditions_created': 0
187
+ }
188
+
189
+ @shared_task
190
+ def process_bulk_renditions(file_ids, roles=None):
191
+ """
192
+ Process renditions for multiple files
193
+
194
+ Args:
195
+ file_ids: List of File IDs to process
196
+ roles: Optional list of roles to generate (None for all default roles)
197
+
198
+ Returns:
199
+ dict: Result information
200
+ """
201
+ results = {
202
+ 'total': len(file_ids),
203
+ 'successful': 0,
204
+ 'failed': 0,
205
+ 'errors': []
206
+ }
207
+
208
+ for file_id in file_ids:
209
+ try:
210
+ file = File.objects.get(id=file_id)
211
+
212
+ if not file.is_completed:
213
+ results['failed'] += 1
214
+ results['errors'].append({
215
+ 'file_id': file_id,
216
+ 'error': 'File upload is not complete'
217
+ })
218
+ continue
219
+
220
+ renderer = get_renderer_for_file(file)
221
+ if not renderer:
222
+ results['failed'] += 1
223
+ results['errors'].append({
224
+ 'file_id': file_id,
225
+ 'error': f'No renderer for file type {file.category}'
226
+ })
227
+ continue
228
+
229
+ if roles:
230
+ renditions = []
231
+ for role in roles:
232
+ rendition = renderer.get_rendition(role)
233
+ if rendition:
234
+ renditions.append(rendition)
235
+ else:
236
+ renditions = renderer.create_all_renditions()
237
+
238
+ results['successful'] += 1
239
+
240
+ except File.DoesNotExist:
241
+ results['failed'] += 1
242
+ results['errors'].append({
243
+ 'file_id': file_id,
244
+ 'error': 'File not found'
245
+ })
246
+ except Exception as e:
247
+ results['failed'] += 1
248
+ results['errors'].append({
249
+ 'file_id': file_id,
250
+ 'error': str(e)
251
+ })
252
+ logger.exception(f"Error processing renditions for file {file_id}: {str(e)}")
253
+
254
+ return results
@@ -1,19 +1,43 @@
1
1
  # File upload utility functions
2
2
 
3
- from .upload import (
4
- get_file_manager,
5
- validate_file_request,
6
- initiate_upload,
7
- finalize_upload,
8
- direct_upload,
9
- get_download_url
10
- )
3
+ # from .upload import (
4
+ # get_file_manager,
5
+ # validate_file_request,
6
+ # initiate_upload,
7
+ # finalize_upload,
8
+ # direct_upload,
9
+ # get_download_url
10
+ # )
11
11
 
12
- __all__ = [
13
- 'get_file_manager',
14
- 'validate_file_request',
15
- 'initiate_upload',
16
- 'finalize_upload',
17
- 'direct_upload',
18
- 'get_download_url'
19
- ]
12
+ def get_file_category(content_type: str) -> str:
13
+ if not content_type:
14
+ return "unknown"
15
+
16
+ content_type = content_type.lower()
17
+
18
+ if content_type.startswith("image/"):
19
+ return "image"
20
+ elif content_type.startswith("video/"):
21
+ return "video"
22
+ elif content_type.startswith("audio/"):
23
+ return "audio"
24
+ elif content_type == "application/pdf":
25
+ return "pdf"
26
+ elif content_type in ["text/csv", "application/csv"]:
27
+ return "csv"
28
+ elif content_type in [
29
+ "application/vnd.ms-excel",
30
+ "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
31
+ ]:
32
+ return "spreadsheet"
33
+ elif content_type in [
34
+ "application/msword",
35
+ "application/vnd.openxmlformats-officedocument.wordprocessingml.document"
36
+ ]:
37
+ return "document"
38
+ elif content_type in ["application/zip", "application/x-zip-compressed"]:
39
+ return "archive"
40
+ elif content_type.startswith("text/"):
41
+ return "text"
42
+ else:
43
+ return "other"
@@ -0,0 +1,39 @@
1
+ # Generated by Django 4.2.21 on 2025-06-07 13:32
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
+ ('account', '0003_group_mojo_secrets_user_mojo_secrets'),
13
+ ('fileman', '0001_initial'),
14
+ migrations.swappable_dependency(settings.AUTH_USER_MODEL),
15
+ ('incident', '0004_alter_incident_model_id'),
16
+ ]
17
+
18
+ operations = [
19
+ migrations.CreateModel(
20
+ name='IncidentHistory',
21
+ fields=[
22
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
23
+ ('created', models.DateTimeField(auto_now_add=True)),
24
+ ('kind', models.CharField(blank=True, db_index=True, default=None, max_length=80, null=True)),
25
+ ('state', models.IntegerField(default=0)),
26
+ ('priority', models.IntegerField(default=0)),
27
+ ('note', models.TextField(blank=True, default=None, null=True)),
28
+ ('by', models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='+', to=settings.AUTH_USER_MODEL)),
29
+ ('group', models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='+', to='account.group')),
30
+ ('media', models.ForeignKey(default=None, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='+', to='fileman.file')),
31
+ ('parent', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='history', to='incident.incident')),
32
+ ('to', models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='+', to=settings.AUTH_USER_MODEL)),
33
+ ],
34
+ options={
35
+ 'ordering': ['-created'],
36
+ },
37
+ bases=(models.Model, mojo.models.rest.MojoModel),
38
+ ),
39
+ ]
@@ -0,0 +1,18 @@
1
+ # Generated by Django 4.2.21 on 2025-06-07 14:13
2
+
3
+ from django.db import migrations, models
4
+
5
+
6
+ class Migration(migrations.Migration):
7
+
8
+ dependencies = [
9
+ ('incident', '0005_incidenthistory'),
10
+ ]
11
+
12
+ operations = [
13
+ migrations.AlterField(
14
+ model_name='incident',
15
+ name='state',
16
+ field=models.CharField(db_index=True, default=0, max_length=24),
17
+ ),
18
+ ]
@@ -1,3 +1,4 @@
1
1
  from .event import Event
2
2
  from .rule import RuleSet, Rule
3
3
  from .incident import Incident
4
+ from .history import IncidentHistory
@@ -0,0 +1,36 @@
1
+ from django.db import models
2
+ from mojo.models import MojoModel
3
+
4
+ class IncidentHistory(models.Model, MojoModel):
5
+ class Meta:
6
+ ordering = ['-created']
7
+
8
+ class RestMeta:
9
+ GRAPHS = {
10
+ "default": {
11
+ "extra": [
12
+ ("get_state_display", "state_display"),
13
+ ("get_priority_display", "priority_display"),
14
+ ],
15
+ "graphs": {
16
+ "by": "basic",
17
+ "to": "basic",
18
+ "media": "basic"
19
+ }
20
+ },
21
+ }
22
+ parent = models.ForeignKey("incident.Incident", related_name="history", on_delete=models.CASCADE)
23
+ created = models.DateTimeField(auto_now_add=True, editable=False)
24
+
25
+ group = models.ForeignKey("account.Group", blank=True, null=True, default=None, related_name="+", on_delete=models.CASCADE)
26
+
27
+ kind = models.CharField(max_length=80, blank=True, null=True, default=None, db_index=True)
28
+
29
+ to = models.ForeignKey("account.User", blank=True, null=True, default=None, related_name="+", on_delete=models.CASCADE)
30
+ by = models.ForeignKey("account.User", blank=True, null=True, default=None, related_name="+", on_delete=models.CASCADE)
31
+
32
+ state = models.IntegerField(default=0)
33
+ priority = models.IntegerField(default=0)
34
+
35
+ note = models.TextField(blank=True, null=True, default=None)
36
+ media = models.ForeignKey("fileman.File", related_name="+", null=True, default=None, on_delete=models.CASCADE)
@@ -9,7 +9,7 @@ class Incident(models.Model, MojoModel):
9
9
  created = models.DateTimeField(auto_now_add=True, editable=False, db_index=True)
10
10
 
11
11
  priority = models.IntegerField(default=0, db_index=True)
12
- state = models.IntegerField(default=0, db_index=True)
12
+ state = models.CharField(max_length=24, default=0, db_index=True)
13
13
  category = models.CharField(max_length=124, db_index=True)
14
14
  title = models.TextField(default=None, null=True)
15
15
  details = models.TextField(default=None, null=True)
@@ -1,5 +1,7 @@
1
1
 
2
- def report_event(details, title=None, category="api_error", level=1, request=None, **kwargs):
2
+
3
+ # TODO make this async using our task queue
4
+ async def report_event(details, title=None, category="api_error", level=1, request=None, **kwargs):
3
5
  from .models import Event
4
6
  event_data = _create_event_dict(details, title, category, level, request, **kwargs)
5
7
  event = Event(**event_data)
@@ -1,5 +1,5 @@
1
1
  from mojo import decorators as md
2
- from mojo.apps.incident.models import Incident, Event, RuleSet, Rule
2
+ from mojo.apps.incident.models import Incident, IncidentHistory, Event, RuleSet, Rule
3
3
 
4
4
 
5
5
  @md.URL('incident')
@@ -7,6 +7,12 @@ from mojo.apps.incident.models import Incident, Event, RuleSet, Rule
7
7
  def on_incident(request, pk=None):
8
8
  return Incident.on_rest_request(request, pk)
9
9
 
10
+ @md.URL('incident/history')
11
+ @md.URL('incident/<int:pk>/history')
12
+ def on_incident_history(request, pk=None):
13
+ return IncidentHistory.on_rest_request(request, pk)
14
+
15
+
10
16
  @md.URL('event')
11
17
  @md.URL('event/<int:pk>')
12
18
  def on_event(request, pk=None):
@@ -0,0 +1,18 @@
1
+ # Generated by Django 4.2.21 on 2025-08-26 16:05
2
+
3
+ from django.db import migrations, models
4
+
5
+
6
+ class Migration(migrations.Migration):
7
+
8
+ dependencies = [
9
+ ('logit', '0003_log_level'),
10
+ ]
11
+
12
+ operations = [
13
+ migrations.AlterField(
14
+ model_name='log',
15
+ name='level',
16
+ field=models.CharField(db_index=True, default='info', max_length=12),
17
+ ),
18
+ ]
@@ -29,6 +29,7 @@ class Log(dm.Model, MojoModel):
29
29
  log = logit.mask_sensitive_data(log)
30
30
 
31
31
  uid, username, ip_address, path, method, duid = 0, None, None, None, None, None
32
+ user_agent = "system"
32
33
  if request:
33
34
  username = request.user.username if request.user.is_authenticated else None
34
35
  uid = request.user.pk if request.user.is_authenticated else 0
@@ -36,6 +37,7 @@ class Log(dm.Model, MojoModel):
36
37
  duid = request.duid
37
38
  ip_address = request.ip
38
39
  method = request.method
40
+ user_agent = request.user_agent
39
41
 
40
42
  path = kwargs.get("path", path)
41
43
  method = kwargs.get("method", method)
@@ -46,12 +48,13 @@ class Log(dm.Model, MojoModel):
46
48
  kind=kind,
47
49
  method=method,
48
50
  path=path,
51
+ payload=kwargs.get("payload", None),
49
52
  ip=ip_address,
50
53
  uid=uid,
51
54
  duid=duid,
52
55
  username=username,
53
56
  log=log,
54
- user_agent=request.user_agent,
57
+ user_agent=user_agent,
55
58
  model_name=model_name,
56
59
  model_id=model_id
57
60
  )
@@ -34,8 +34,8 @@ GRANULARITY_OFFSET_MAP = {
34
34
 
35
35
  GRANULARITY_END_MAP = {
36
36
  'minutes': timedelta(minutes=29),
37
- 'hours': timedelta(hours=11),
38
- 'days': timedelta(days=11),
37
+ 'hours': timedelta(hours=24),
38
+ 'days': timedelta(days=30),
39
39
  'weeks': timedelta(weeks=11),
40
40
  'months': timedelta(days=12*30),
41
41
  'years': timedelta(days=11*360)
@@ -40,7 +40,7 @@ def on_raw_email(request, imsg, msg_data):
40
40
  to_email = imsg.receipt.recipients[0]
41
41
  # logger.info("parsed", msg_data)
42
42
  msg = createMessage(to_email, msg_data)
43
- metrics.metric("emails_received", category="email", min_granularity="hourly")
43
+ metrics.metric("emails_received", category="email", min_granularity="hours")
44
44
 
45
45
  attachments = []
46
46
  for msg_atch in msg_data.attachments:
@@ -31,11 +31,11 @@ def send_mail_via_ses(msg, sender, recipients, fail_silently=True):
31
31
  RawMessage={'Data': msg.as_string()}
32
32
  )
33
33
  if EMAIL_METRICS:
34
- metrics.record("emails_sent", category="email", min_granularity="hourly")
34
+ metrics.record("emails_sent", category="email", min_granularity="hours")
35
35
  return True
36
36
  except Exception as err:
37
37
  if EMAIL_METRICS:
38
- metrics.record("email_errors", category="email", min_granularity="hourly")
38
+ metrics.record("email_errors", category="email", min_granularity="hours")
39
39
  EMAIL_LOGGER.exception(err)
40
40
  EMAIL_LOGGER.error(msg.as_string())
41
41
  if not fail_silently:
@@ -1,11 +1,44 @@
1
1
  from mojo.helpers.settings import settings
2
+ from mojo.helpers import modules
3
+ import functools
2
4
 
3
5
 
4
6
  def get_manager():
5
7
  from .manager import TaskManager
6
- return TaskManager(settings.TASKIT_CHANNELS)
8
+ return TaskManager(settings.TASK_CHANNELS)
7
9
 
8
10
 
9
11
  def publish(channel, function, data, expires=1800):
10
12
  man = get_manager()
11
13
  return man.publish(function, data, channel=channel, expires=expires)
14
+
15
+
16
+ def async_task(channel="bg_tasks", expires=1800):
17
+ def decorator(func):
18
+ @functools.wraps(func)
19
+ def wrapper(*args, **kwargs):
20
+ # Check if this is being called from the task queue
21
+ # If '_from_task_queue' is in kwargs and True, execute directly without publishing
22
+ from_task_queue = kwargs.pop('_from_task_queue', False)
23
+
24
+ if from_task_queue:
25
+ # Execute the original function directly when called from task queue
26
+ from mojo.helpers import logit
27
+ logit.get_logger("debug", "debug.log").info(f"executing directly {args} {kwargs}")
28
+ return func(*args, **kwargs)
29
+ else:
30
+ # Generate function string as "<module_name>.<function_name>"
31
+ function_string = f"{func.__module__}.{func.__name__}"
32
+
33
+ # Generate data from args and kwargs
34
+ data = {
35
+ 'args': list(args),
36
+ 'kwargs': {**kwargs, '_from_task_queue': True} # Add flag for task queue
37
+ }
38
+
39
+ # Publish the task
40
+ publish(channel=channel, function=function_string, data=data, expires=expires)
41
+
42
+ return True
43
+ return wrapper
44
+ return decorator