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.
- django_nativemojo-0.1.15.dist-info/METADATA +136 -0
- {django_nativemojo-0.1.10.dist-info → django_nativemojo-0.1.15.dist-info}/RECORD +105 -65
- mojo/__init__.py +1 -1
- mojo/apps/account/management/__init__.py +5 -0
- mojo/apps/account/management/commands/__init__.py +6 -0
- mojo/apps/account/management/commands/serializer_admin.py +531 -0
- mojo/apps/account/migrations/0004_user_avatar.py +20 -0
- mojo/apps/account/migrations/0005_group_last_activity.py +18 -0
- mojo/apps/account/models/group.py +25 -7
- mojo/apps/account/models/member.py +15 -4
- mojo/apps/account/models/user.py +197 -20
- mojo/apps/account/rest/group.py +1 -0
- mojo/apps/account/rest/user.py +6 -2
- mojo/apps/aws/rest/__init__.py +1 -0
- mojo/apps/aws/rest/s3.py +64 -0
- mojo/apps/fileman/README.md +8 -8
- mojo/apps/fileman/backends/base.py +76 -70
- mojo/apps/fileman/backends/filesystem.py +86 -86
- mojo/apps/fileman/backends/s3.py +200 -108
- mojo/apps/fileman/migrations/0001_initial.py +106 -0
- mojo/apps/fileman/migrations/0002_filemanager_parent_alter_filemanager_max_file_size.py +24 -0
- mojo/apps/fileman/migrations/0003_remove_file_fileman_fil_upload__c4bc35_idx_and_more.py +25 -0
- mojo/apps/fileman/migrations/0004_remove_file_original_filename_and_more.py +39 -0
- mojo/apps/fileman/migrations/0005_alter_file_upload_token.py +18 -0
- mojo/apps/fileman/migrations/0006_file_download_url_filemanager_forever_urls.py +23 -0
- mojo/apps/fileman/migrations/0007_remove_filemanager_forever_urls_and_more.py +22 -0
- mojo/apps/fileman/migrations/0008_file_category.py +18 -0
- mojo/apps/fileman/migrations/0009_rename_file_path_file_storage_file_path.py +18 -0
- mojo/apps/fileman/migrations/0010_filerendition.py +33 -0
- mojo/apps/fileman/migrations/0011_alter_filerendition_original_file.py +19 -0
- mojo/apps/fileman/models/__init__.py +1 -5
- mojo/apps/fileman/models/file.py +204 -58
- mojo/apps/fileman/models/manager.py +161 -31
- mojo/apps/fileman/models/rendition.py +118 -0
- mojo/apps/fileman/renderer/__init__.py +111 -0
- mojo/apps/fileman/renderer/audio.py +403 -0
- mojo/apps/fileman/renderer/base.py +205 -0
- mojo/apps/fileman/renderer/document.py +404 -0
- mojo/apps/fileman/renderer/image.py +222 -0
- mojo/apps/fileman/renderer/utils.py +297 -0
- mojo/apps/fileman/renderer/video.py +304 -0
- mojo/apps/fileman/rest/__init__.py +1 -18
- mojo/apps/fileman/rest/upload.py +22 -32
- mojo/apps/fileman/signals.py +58 -0
- mojo/apps/fileman/tasks.py +254 -0
- mojo/apps/fileman/utils/__init__.py +40 -16
- mojo/apps/incident/migrations/0005_incidenthistory.py +39 -0
- mojo/apps/incident/migrations/0006_alter_incident_state.py +18 -0
- mojo/apps/incident/models/__init__.py +1 -0
- mojo/apps/incident/models/history.py +36 -0
- mojo/apps/incident/models/incident.py +1 -1
- mojo/apps/incident/reporter.py +3 -1
- mojo/apps/incident/rest/event.py +7 -1
- mojo/apps/logit/migrations/0004_alter_log_level.py +18 -0
- mojo/apps/logit/models/log.py +4 -1
- mojo/apps/metrics/utils.py +2 -2
- mojo/apps/notify/handlers/ses/message.py +1 -1
- mojo/apps/notify/providers/aws.py +2 -2
- mojo/apps/tasks/__init__.py +34 -1
- mojo/apps/tasks/manager.py +200 -45
- mojo/apps/tasks/rest/tasks.py +24 -10
- mojo/apps/tasks/runner.py +283 -18
- mojo/apps/tasks/task.py +99 -0
- mojo/apps/tasks/tq_handlers.py +118 -0
- mojo/decorators/auth.py +6 -1
- mojo/decorators/http.py +7 -2
- mojo/helpers/aws/__init__.py +41 -0
- mojo/helpers/aws/ec2.py +804 -0
- mojo/helpers/aws/iam.py +748 -0
- mojo/helpers/aws/s3.py +451 -11
- mojo/helpers/aws/ses.py +483 -0
- mojo/helpers/aws/sns.py +461 -0
- mojo/helpers/crypto/__pycache__/hash.cpython-310.pyc +0 -0
- mojo/helpers/crypto/__pycache__/sign.cpython-310.pyc +0 -0
- mojo/helpers/crypto/__pycache__/utils.cpython-310.pyc +0 -0
- mojo/helpers/dates.py +18 -0
- mojo/helpers/response.py +6 -2
- mojo/helpers/settings/__init__.py +2 -0
- mojo/helpers/{settings.py → settings/helper.py} +1 -37
- mojo/helpers/settings/parser.py +132 -0
- mojo/middleware/logging.py +1 -1
- mojo/middleware/mojo.py +5 -0
- mojo/models/rest.py +261 -46
- mojo/models/secrets.py +13 -4
- mojo/serializers/__init__.py +100 -0
- mojo/serializers/advanced/README.md +363 -0
- mojo/serializers/advanced/__init__.py +247 -0
- mojo/serializers/advanced/formats/__init__.py +28 -0
- mojo/serializers/advanced/formats/csv.py +416 -0
- mojo/serializers/advanced/formats/excel.py +516 -0
- mojo/serializers/advanced/formats/json.py +239 -0
- mojo/serializers/advanced/formats/localizers.py +509 -0
- mojo/serializers/advanced/formats/response.py +485 -0
- mojo/serializers/advanced/serializer.py +568 -0
- mojo/serializers/manager.py +501 -0
- mojo/serializers/optimized.py +618 -0
- mojo/serializers/settings_example.py +322 -0
- mojo/serializers/{models.py → simple.py} +38 -15
- testit/helpers.py +21 -4
- django_nativemojo-0.1.10.dist-info/METADATA +0 -96
- mojo/apps/metrics/rest/db.py +0 -0
- mojo/helpers/aws/setup_email.py +0 -0
- mojo/ws4redis/README.md +0 -174
- mojo/ws4redis/__init__.py +0 -2
- mojo/ws4redis/client.py +0 -283
- mojo/ws4redis/connection.py +0 -327
- mojo/ws4redis/exceptions.py +0 -32
- mojo/ws4redis/redis.py +0 -183
- mojo/ws4redis/servers/base.py +0 -86
- mojo/ws4redis/servers/django.py +0 -171
- mojo/ws4redis/servers/uwsgi.py +0 -63
- mojo/ws4redis/settings.py +0 -45
- mojo/ws4redis/utf8validator.py +0 -128
- mojo/ws4redis/websocket.py +0 -403
- {django_nativemojo-0.1.10.dist-info → django_nativemojo-0.1.15.dist-info}/LICENSE +0 -0
- {django_nativemojo-0.1.10.dist-info → django_nativemojo-0.1.15.dist-info}/NOTICE +0 -0
- {django_nativemojo-0.1.10.dist-info → django_nativemojo-0.1.15.dist-info}/WHEEL +0 -0
- /mojo/{ws4redis/servers → apps/aws}/__init__.py +0 -0
- /mojo/apps/{fileman/models/render.py → aws/models/__init__.py} +0 -0
- /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
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
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
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
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
|
+
]
|
@@ -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.
|
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)
|
mojo/apps/incident/reporter.py
CHANGED
@@ -1,5 +1,7 @@
|
|
1
1
|
|
2
|
-
|
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)
|
mojo/apps/incident/rest/event.py
CHANGED
@@ -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
|
+
]
|
mojo/apps/logit/models/log.py
CHANGED
@@ -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=
|
57
|
+
user_agent=user_agent,
|
55
58
|
model_name=model_name,
|
56
59
|
model_id=model_id
|
57
60
|
)
|
mojo/apps/metrics/utils.py
CHANGED
@@ -34,8 +34,8 @@ GRANULARITY_OFFSET_MAP = {
|
|
34
34
|
|
35
35
|
GRANULARITY_END_MAP = {
|
36
36
|
'minutes': timedelta(minutes=29),
|
37
|
-
'hours': timedelta(hours=
|
38
|
-
'days': timedelta(days=
|
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="
|
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="
|
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="
|
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:
|
mojo/apps/tasks/__init__.py
CHANGED
@@ -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.
|
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
|