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.
- {django_nativemojo-0.1.15.dist-info → django_nativemojo-0.1.17.dist-info}/METADATA +3 -2
- django_nativemojo-0.1.17.dist-info/RECORD +302 -0
- mojo/__init__.py +1 -1
- mojo/apps/account/management/commands/serializer_admin.py +121 -1
- mojo/apps/account/migrations/0006_add_device_tracking_models.py +72 -0
- mojo/apps/account/migrations/0007_delete_userdevicelocation.py +16 -0
- mojo/apps/account/migrations/0008_userdevicelocation.py +33 -0
- mojo/apps/account/migrations/0009_geolocatedip_subnet.py +18 -0
- mojo/apps/account/migrations/0010_group_avatar.py +20 -0
- mojo/apps/account/migrations/0011_user_org_registereddevice_pushconfig_and_more.py +118 -0
- mojo/apps/account/migrations/0012_remove_pushconfig_apns_key_file_and_more.py +21 -0
- mojo/apps/account/migrations/0013_pushconfig_test_mode_alter_pushconfig_apns_enabled_and_more.py +28 -0
- mojo/apps/account/migrations/0014_notificationdelivery_data_payload_and_more.py +48 -0
- mojo/apps/account/models/__init__.py +2 -0
- mojo/apps/account/models/device.py +279 -0
- mojo/apps/account/models/group.py +294 -8
- mojo/apps/account/models/member.py +14 -1
- mojo/apps/account/models/push/__init__.py +4 -0
- mojo/apps/account/models/push/config.py +112 -0
- mojo/apps/account/models/push/delivery.py +93 -0
- mojo/apps/account/models/push/device.py +66 -0
- mojo/apps/account/models/push/template.py +99 -0
- mojo/apps/account/models/user.py +190 -17
- mojo/apps/account/rest/__init__.py +2 -0
- mojo/apps/account/rest/device.py +39 -0
- mojo/apps/account/rest/group.py +8 -0
- mojo/apps/account/rest/push.py +187 -0
- mojo/apps/account/rest/user.py +95 -5
- mojo/apps/account/services/__init__.py +1 -0
- mojo/apps/account/services/push.py +363 -0
- mojo/apps/aws/migrations/0001_initial.py +206 -0
- mojo/apps/aws/migrations/0002_emaildomain_can_recv_emaildomain_can_send_and_more.py +28 -0
- mojo/apps/aws/migrations/0003_mailbox_is_domain_default_mailbox_is_system_default_and_more.py +31 -0
- mojo/apps/aws/migrations/0004_s3bucket.py +39 -0
- mojo/apps/aws/migrations/0005_alter_emaildomain_region_delete_s3bucket.py +21 -0
- mojo/apps/aws/models/__init__.py +19 -0
- mojo/apps/aws/models/email_attachment.py +99 -0
- mojo/apps/aws/models/email_domain.py +218 -0
- mojo/apps/aws/models/email_template.py +132 -0
- mojo/apps/aws/models/incoming_email.py +197 -0
- mojo/apps/aws/models/mailbox.py +288 -0
- mojo/apps/aws/models/sent_message.py +175 -0
- mojo/apps/aws/rest/__init__.py +6 -0
- mojo/apps/aws/rest/email.py +33 -0
- mojo/apps/aws/rest/email_ops.py +183 -0
- mojo/apps/aws/rest/messages.py +32 -0
- mojo/apps/aws/rest/send.py +101 -0
- mojo/apps/aws/rest/sns.py +403 -0
- mojo/apps/aws/rest/templates.py +19 -0
- mojo/apps/aws/services/__init__.py +32 -0
- mojo/apps/aws/services/email.py +390 -0
- mojo/apps/aws/services/email_ops.py +548 -0
- mojo/apps/docit/__init__.py +6 -0
- mojo/apps/docit/markdown_plugins/syntax_highlight.py +25 -0
- mojo/apps/docit/markdown_plugins/toc.py +12 -0
- mojo/apps/docit/migrations/0001_initial.py +113 -0
- mojo/apps/docit/migrations/0002_alter_book_modified_by_alter_page_modified_by.py +26 -0
- mojo/apps/docit/migrations/0003_alter_book_group.py +20 -0
- mojo/apps/docit/models/__init__.py +17 -0
- mojo/apps/docit/models/asset.py +231 -0
- mojo/apps/docit/models/book.py +227 -0
- mojo/apps/docit/models/page.py +319 -0
- mojo/apps/docit/models/page_revision.py +203 -0
- mojo/apps/docit/rest/__init__.py +10 -0
- mojo/apps/docit/rest/asset.py +17 -0
- mojo/apps/docit/rest/book.py +22 -0
- mojo/apps/docit/rest/page.py +22 -0
- mojo/apps/docit/rest/page_revision.py +17 -0
- mojo/apps/docit/services/__init__.py +11 -0
- mojo/apps/docit/services/docit.py +315 -0
- mojo/apps/docit/services/markdown.py +44 -0
- mojo/apps/fileman/backends/s3.py +209 -0
- mojo/apps/fileman/models/file.py +45 -9
- mojo/apps/fileman/models/manager.py +269 -3
- mojo/apps/incident/migrations/0007_event_uid.py +18 -0
- mojo/apps/incident/migrations/0008_ticket_ticketnote.py +55 -0
- mojo/apps/incident/migrations/0009_incident_status.py +18 -0
- mojo/apps/incident/migrations/0010_event_country_code.py +18 -0
- mojo/apps/incident/migrations/0011_incident_country_code.py +18 -0
- mojo/apps/incident/migrations/0012_alter_incident_status.py +18 -0
- mojo/apps/incident/models/__init__.py +1 -0
- mojo/apps/incident/models/event.py +35 -0
- mojo/apps/incident/models/incident.py +2 -0
- mojo/apps/incident/models/ticket.py +62 -0
- mojo/apps/incident/reporter.py +21 -3
- mojo/apps/incident/rest/__init__.py +1 -0
- mojo/apps/incident/rest/ticket.py +43 -0
- mojo/apps/jobs/__init__.py +489 -0
- mojo/apps/jobs/adapters.py +24 -0
- mojo/apps/jobs/cli.py +616 -0
- mojo/apps/jobs/daemon.py +370 -0
- mojo/apps/jobs/examples/sample_jobs.py +376 -0
- mojo/apps/jobs/examples/webhook_examples.py +203 -0
- mojo/apps/jobs/handlers/__init__.py +5 -0
- mojo/apps/jobs/handlers/webhook.py +317 -0
- mojo/apps/jobs/job_engine.py +734 -0
- mojo/apps/jobs/keys.py +203 -0
- mojo/apps/jobs/local_queue.py +363 -0
- mojo/apps/jobs/management/__init__.py +3 -0
- mojo/apps/jobs/management/commands/__init__.py +3 -0
- mojo/apps/jobs/manager.py +1327 -0
- mojo/apps/jobs/migrations/0001_initial.py +97 -0
- mojo/apps/jobs/migrations/0002_alter_job_max_retries_joblog.py +39 -0
- mojo/apps/jobs/models/__init__.py +6 -0
- mojo/apps/jobs/models/job.py +441 -0
- mojo/apps/jobs/rest/__init__.py +2 -0
- mojo/apps/jobs/rest/control.py +466 -0
- mojo/apps/jobs/rest/jobs.py +421 -0
- mojo/apps/jobs/scheduler.py +571 -0
- mojo/apps/jobs/services/__init__.py +6 -0
- mojo/apps/jobs/services/job_actions.py +465 -0
- mojo/apps/jobs/settings.py +209 -0
- mojo/apps/logit/models/log.py +3 -0
- mojo/apps/metrics/__init__.py +8 -1
- mojo/apps/metrics/redis_metrics.py +198 -0
- mojo/apps/metrics/rest/__init__.py +3 -0
- mojo/apps/metrics/rest/categories.py +266 -0
- mojo/apps/metrics/rest/helpers.py +48 -0
- mojo/apps/metrics/rest/permissions.py +99 -0
- mojo/apps/metrics/rest/values.py +277 -0
- mojo/apps/metrics/utils.py +17 -0
- mojo/decorators/http.py +40 -1
- mojo/helpers/aws/__init__.py +11 -7
- mojo/helpers/aws/inbound_email.py +309 -0
- mojo/helpers/aws/kms.py +413 -0
- mojo/helpers/aws/ses_domain.py +959 -0
- mojo/helpers/crypto/__init__.py +1 -1
- mojo/helpers/crypto/utils.py +15 -0
- mojo/helpers/location/__init__.py +2 -0
- mojo/helpers/location/countries.py +262 -0
- mojo/helpers/location/geolocation.py +196 -0
- mojo/helpers/logit.py +37 -0
- mojo/helpers/redis/__init__.py +2 -0
- mojo/helpers/redis/adapter.py +606 -0
- mojo/helpers/redis/client.py +48 -0
- mojo/helpers/redis/pool.py +225 -0
- mojo/helpers/request.py +8 -0
- mojo/helpers/response.py +8 -0
- mojo/middleware/auth.py +1 -1
- mojo/middleware/cors.py +40 -0
- mojo/middleware/logging.py +131 -12
- mojo/middleware/mojo.py +5 -0
- mojo/models/rest.py +271 -57
- mojo/models/secrets.py +86 -0
- mojo/serializers/__init__.py +16 -10
- mojo/serializers/core/__init__.py +90 -0
- mojo/serializers/core/cache/__init__.py +121 -0
- mojo/serializers/core/cache/backends.py +518 -0
- mojo/serializers/core/cache/base.py +102 -0
- mojo/serializers/core/cache/disabled.py +181 -0
- mojo/serializers/core/cache/memory.py +287 -0
- mojo/serializers/core/cache/redis.py +533 -0
- mojo/serializers/core/cache/utils.py +454 -0
- mojo/serializers/{manager.py → core/manager.py} +53 -4
- mojo/serializers/core/serializer.py +475 -0
- mojo/serializers/{advanced/formats → formats}/csv.py +116 -139
- mojo/serializers/suggested_improvements.md +388 -0
- testit/client.py +1 -1
- testit/helpers.py +14 -0
- testit/runner.py +23 -6
- django_nativemojo-0.1.15.dist-info/RECORD +0 -234
- mojo/apps/notify/README.md +0 -91
- mojo/apps/notify/README_NOTIFICATIONS.md +0 -566
- mojo/apps/notify/admin.py +0 -52
- mojo/apps/notify/handlers/example_handlers.py +0 -516
- mojo/apps/notify/handlers/ses/__init__.py +0 -25
- mojo/apps/notify/handlers/ses/complaint.py +0 -25
- mojo/apps/notify/handlers/ses/message.py +0 -86
- mojo/apps/notify/management/commands/__init__.py +0 -1
- mojo/apps/notify/management/commands/process_notifications.py +0 -370
- mojo/apps/notify/mod +0 -0
- mojo/apps/notify/models/__init__.py +0 -12
- mojo/apps/notify/models/account.py +0 -128
- mojo/apps/notify/models/attachment.py +0 -24
- mojo/apps/notify/models/bounce.py +0 -68
- mojo/apps/notify/models/complaint.py +0 -40
- mojo/apps/notify/models/inbox.py +0 -113
- mojo/apps/notify/models/inbox_message.py +0 -173
- mojo/apps/notify/models/outbox.py +0 -129
- mojo/apps/notify/models/outbox_message.py +0 -288
- mojo/apps/notify/models/template.py +0 -30
- mojo/apps/notify/providers/aws.py +0 -73
- mojo/apps/notify/rest/ses.py +0 -0
- mojo/apps/notify/utils/__init__.py +0 -2
- mojo/apps/notify/utils/notifications.py +0 -404
- mojo/apps/notify/utils/parsing.py +0 -202
- mojo/apps/notify/utils/render.py +0 -144
- mojo/apps/tasks/README.md +0 -118
- mojo/apps/tasks/__init__.py +0 -44
- mojo/apps/tasks/manager.py +0 -644
- mojo/apps/tasks/rest/__init__.py +0 -2
- mojo/apps/tasks/rest/hooks.py +0 -0
- mojo/apps/tasks/rest/tasks.py +0 -76
- mojo/apps/tasks/runner.py +0 -439
- mojo/apps/tasks/task.py +0 -99
- mojo/apps/tasks/tq_handlers.py +0 -132
- 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/redis.py +0 -10
- mojo/models/meta.py +0 -262
- mojo/serializers/advanced/README.md +0 -363
- mojo/serializers/advanced/__init__.py +0 -247
- mojo/serializers/advanced/formats/__init__.py +0 -28
- mojo/serializers/advanced/formats/excel.py +0 -516
- mojo/serializers/advanced/formats/json.py +0 -239
- mojo/serializers/advanced/formats/response.py +0 -485
- mojo/serializers/advanced/serializer.py +0 -568
- mojo/serializers/optimized.py +0 -618
- {django_nativemojo-0.1.15.dist-info → django_nativemojo-0.1.17.dist-info}/LICENSE +0 -0
- {django_nativemojo-0.1.15.dist-info → django_nativemojo-0.1.17.dist-info}/NOTICE +0 -0
- {django_nativemojo-0.1.15.dist-info → django_nativemojo-0.1.17.dist-info}/WHEEL +0 -0
- /mojo/apps/{notify → aws/migrations}/__init__.py +0 -0
- /mojo/apps/{notify/handlers → docit/markdown_plugins}/__init__.py +0 -0
- /mojo/apps/{notify/management → docit/migrations}/__init__.py +0 -0
- /mojo/apps/{notify/providers → jobs/examples}/__init__.py +0 -0
- /mojo/apps/{notify/rest → jobs/migrations}/__init__.py +0 -0
- /mojo/{serializers → rest}/openapi.py +0 -0
- /mojo/serializers/{settings_example.py → examples/settings.py} +0 -0
- /mojo/{apps/notify/handlers/ses/bounce.py → serializers/formats/__init__.py} +0 -0
- /mojo/serializers/{advanced/formats → formats}/localizers.py +0 -0
mojo/models/rest.py
CHANGED
@@ -1,12 +1,12 @@
|
|
1
1
|
# from django.http import JsonResponse
|
2
2
|
from mojo.helpers.response import JsonResponse
|
3
|
-
from mojo.serializers
|
4
|
-
from mojo.serializers.manager import get_serializer_manager
|
5
|
-
from mojo.helpers import modules
|
3
|
+
from mojo.serializers import get_serializer_manager
|
6
4
|
from mojo.helpers.settings import settings
|
5
|
+
from mojo import errors as me
|
7
6
|
from django.core.exceptions import ObjectDoesNotExist
|
8
7
|
from django.db import transaction, models as dm
|
9
8
|
import objict
|
9
|
+
import datetime
|
10
10
|
from mojo.helpers import dates, logit
|
11
11
|
|
12
12
|
|
@@ -14,6 +14,17 @@ logger = logit.get_logger("debug", "debug.log")
|
|
14
14
|
ACTIVE_REQUEST = None
|
15
15
|
LOGGING_CLASS = None
|
16
16
|
MOJO_APP_STATUS_200_ON_ERROR = settings.MOJO_APP_STATUS_200_ON_ERROR
|
17
|
+
MOJO_REST_LIST_PERM_DENY = settings.get("MOJO_REST_LIST_PERM_DENY", False)
|
18
|
+
|
19
|
+
# use this when there is no ACTIVE_REQUEST
|
20
|
+
SYSTEM_REQUEST = objict.objict()
|
21
|
+
SYSTEM_REQUEST.user = objict.objict()
|
22
|
+
SYSTEM_REQUEST.user.id = 1
|
23
|
+
SYSTEM_REQUEST.user.username = "system"
|
24
|
+
SYSTEM_REQUEST.user.email = ""
|
25
|
+
SYSTEM_REQUEST.user.is_authenticated = True
|
26
|
+
SYSTEM_REQUEST.user.has_permission = lambda perm: True
|
27
|
+
SYSTEM_REQUEST.DATA = objict.objict()
|
17
28
|
|
18
29
|
class MojoModel:
|
19
30
|
"""Base model class for REST operations with GraphSerializer integration."""
|
@@ -52,6 +63,17 @@ class MojoModel:
|
|
52
63
|
return default
|
53
64
|
return getattr(cls.RestMeta, name, default)
|
54
65
|
|
66
|
+
@classmethod
|
67
|
+
def get_rest_meta_graph(cls, graph_name):
|
68
|
+
graphs = cls.get_rest_meta_prop("GRAPHS", {})
|
69
|
+
if isinstance(graph_name, list):
|
70
|
+
for n in graph_name:
|
71
|
+
res = graphs.get(n, None)
|
72
|
+
if res is not None:
|
73
|
+
return res
|
74
|
+
return None
|
75
|
+
return graphs.get(graph_name, None)
|
76
|
+
|
55
77
|
@classmethod
|
56
78
|
def rest_error_response(cls, request, status=500, **kwargs):
|
57
79
|
"""
|
@@ -121,6 +143,16 @@ class MojoModel:
|
|
121
143
|
except ObjectDoesNotExist:
|
122
144
|
return cls.rest_error_response(None, 404, error=f"{cls.__name__} not found")
|
123
145
|
|
146
|
+
@classmethod
|
147
|
+
def get_instance_from_request(cls, request, field_name=None):
|
148
|
+
if field_name is None:
|
149
|
+
field_name = cls.__name__.lower()
|
150
|
+
if field_name not in request.DATA:
|
151
|
+
field_name = f"{field_name}_id"
|
152
|
+
if field_name not in request.DATA:
|
153
|
+
return None
|
154
|
+
return cls.objects.filter(pk=request.DATA.get(field_name)).last()
|
155
|
+
|
124
156
|
@classmethod
|
125
157
|
def rest_check_permission(cls, request, permission_keys, instance=None):
|
126
158
|
"""
|
@@ -153,6 +185,25 @@ class MojoModel:
|
|
153
185
|
return False
|
154
186
|
|
155
187
|
if instance is not None:
|
188
|
+
is_view = isinstance(permission_keys, list) and "VIEW_PERMS" in permission_keys
|
189
|
+
if not is_view and isinstance(permission_keys, str):
|
190
|
+
is_view = permission_keys == "VIEW_PERMS"
|
191
|
+
if is_view:
|
192
|
+
if hasattr(instance, "check_view_permission"):
|
193
|
+
allowed = instance.check_view_permission(perms, request)
|
194
|
+
if not allowed:
|
195
|
+
cls.class_report_incident(
|
196
|
+
details="Permission denied: view_permission_denied",
|
197
|
+
event_type="view_permission_denied",
|
198
|
+
request=request,
|
199
|
+
perms=perms,
|
200
|
+
permission_keys=permission_keys,
|
201
|
+
branch="instance.check_view_permission",
|
202
|
+
instance=repr(instance),
|
203
|
+
request_path=getattr(request, "path", None),
|
204
|
+
)
|
205
|
+
return allowed
|
206
|
+
|
156
207
|
if hasattr(instance, "check_edit_permission"):
|
157
208
|
allowed = instance.check_edit_permission(perms, request)
|
158
209
|
if not allowed:
|
@@ -167,12 +218,17 @@ class MojoModel:
|
|
167
218
|
request_path=getattr(request, "path", None),
|
168
219
|
)
|
169
220
|
return allowed
|
170
|
-
|
171
|
-
|
221
|
+
|
222
|
+
if "owner" in perms:
|
223
|
+
owner_field = instance.get_rest_meta_prop("OWNER_FIELD", "user")
|
224
|
+
owner = getattr(instance, owner_field, None)
|
225
|
+
if owner is not None and owner.id == request.user.id:
|
172
226
|
return True
|
227
|
+
if hasattr(instance, "group"):
|
228
|
+
request.group = getattr(instance, "group", None)
|
173
229
|
|
174
230
|
if request.group and hasattr(cls, "group"):
|
175
|
-
allowed = request.group.
|
231
|
+
allowed = request.group.user_has_permission(request.user, perms)
|
176
232
|
if not allowed:
|
177
233
|
cls.class_report_incident(
|
178
234
|
details="Permission denied: group_member_permission_denied",
|
@@ -181,12 +237,13 @@ class MojoModel:
|
|
181
237
|
perms=perms,
|
182
238
|
permission_keys=permission_keys,
|
183
239
|
group=getattr(request, "group", None),
|
184
|
-
branch="group.
|
240
|
+
branch="group.user_has_permission",
|
185
241
|
instance=repr(instance) if instance else None,
|
186
242
|
request_path=getattr(request, "path", None),
|
187
243
|
)
|
188
244
|
return allowed
|
189
|
-
|
245
|
+
if request.user is None or not request.user.is_authenticated:
|
246
|
+
return False
|
190
247
|
allowed = request.user.has_permission(perms)
|
191
248
|
if not allowed:
|
192
249
|
cls.class_report_incident(
|
@@ -263,10 +320,41 @@ class MojoModel:
|
|
263
320
|
Returns:
|
264
321
|
JsonResponse representing the list of resources.
|
265
322
|
"""
|
266
|
-
cls.debug("on_rest_handle_list")
|
323
|
+
# cls.debug("on_rest_handle_list")
|
267
324
|
if cls.rest_check_permission(request, "VIEW_PERMS"):
|
268
325
|
return cls.on_rest_list(request)
|
269
|
-
|
326
|
+
else:
|
327
|
+
perms = cls.get_rest_meta_prop("VIEW_PERMS", [])
|
328
|
+
if perms and "owner" in perms and request.user.is_authenticated:
|
329
|
+
owner_field = cls.get_rest_meta_prop("OWNER_FIELD", "user")
|
330
|
+
q = {owner_field: request.user}
|
331
|
+
return cls.on_rest_list(request, cls.objects.filter(**q))
|
332
|
+
if MOJO_REST_LIST_PERM_DENY:
|
333
|
+
return cls.rest_error_response(request, 403, error=f"GET permission denied: {cls.__name__}")
|
334
|
+
return cls.on_rest_list_response(request, cls.objects.none())
|
335
|
+
|
336
|
+
def update_from_dict(self, dict_data):
|
337
|
+
request = ACTIVE_REQUEST or SYSTEM_REQUEST
|
338
|
+
return self.on_rest_save(request, dict_data)
|
339
|
+
|
340
|
+
@classmethod
|
341
|
+
def create_from_dict(cls, dict_data, **kwargs):
|
342
|
+
request = kwargs.pop('request', ACTIVE_REQUEST or SYSTEM_REQUEST)
|
343
|
+
instance = cls(**kwargs)
|
344
|
+
instance.on_rest_save(request, dict_data)
|
345
|
+
instance.on_rest_created()
|
346
|
+
if cls.get_rest_meta_prop("LOG_CHANGES", False):
|
347
|
+
instance.log(kind="model:created", log=f"{request.user.username} created {instance.pk}")
|
348
|
+
return instance
|
349
|
+
|
350
|
+
@classmethod
|
351
|
+
def create_from_request(cls, request, **kwargs):
|
352
|
+
instance = cls(**kwargs)
|
353
|
+
instance.on_rest_save(request, request.DATA)
|
354
|
+
instance.on_rest_created()
|
355
|
+
if cls.get_rest_meta_prop("LOG_CHANGES", False):
|
356
|
+
instance.log(kind="model:created", log=f"{ACTIVE_REQUEST.user.username} created {instance.pk}")
|
357
|
+
return instance
|
270
358
|
|
271
359
|
@classmethod
|
272
360
|
def on_rest_handle_create(cls, request):
|
@@ -279,10 +367,8 @@ class MojoModel:
|
|
279
367
|
Returns:
|
280
368
|
JsonResponse representing the result of the create operation.
|
281
369
|
"""
|
282
|
-
if cls.rest_check_permission(request, ["SAVE_PERMS", "VIEW_PERMS"]):
|
283
|
-
instance = cls()
|
284
|
-
instance.on_rest_save(request, request.DATA)
|
285
|
-
instance.on_rest_created()
|
370
|
+
if cls.rest_check_permission(request, ["CREATE_PERMS", "SAVE_PERMS", "VIEW_PERMS"]):
|
371
|
+
instance = cls.create_from_request(request)
|
286
372
|
return instance.on_rest_get(request)
|
287
373
|
return cls.rest_error_response(request, 403, error=f"CREATE permission denied: {cls.__name__}")
|
288
374
|
|
@@ -314,31 +400,62 @@ class MojoModel:
|
|
314
400
|
Returns:
|
315
401
|
JsonResponse representing the paginated and serialized list of objects.
|
316
402
|
"""
|
317
|
-
cls.debug("on_rest_list:start")
|
318
403
|
if queryset is None:
|
319
404
|
queryset = cls.objects.all()
|
320
|
-
|
321
|
-
|
322
|
-
|
323
|
-
|
405
|
+
# for better query perfomance we want raw request GET data
|
406
|
+
request.QUERY_PARAMS = request.GET.copy()
|
407
|
+
if request.group is not None:
|
408
|
+
GROUP_FIELD = cls.get_rest_meta_prop("GROUP_FIELD", None)
|
409
|
+
if GROUP_FIELD or hasattr(cls, "group"):
|
410
|
+
if "group" in request.QUERY_PARAMS:
|
411
|
+
del request.QUERY_PARAMS["group"]
|
412
|
+
if GROUP_FIELD:
|
413
|
+
q = {GROUP_FIELD: request.group}
|
414
|
+
else:
|
415
|
+
q = {"group": request.group}
|
416
|
+
queryset = queryset.filter(**q)
|
324
417
|
queryset = cls.on_rest_list_filter(request, queryset)
|
325
418
|
queryset = cls.on_rest_list_date_range_filter(request, queryset)
|
326
419
|
queryset = cls.on_rest_list_sort(request, queryset)
|
327
|
-
cls.debug("on_rest_list:end")
|
328
420
|
return cls.on_rest_list_response(request, queryset)
|
329
421
|
|
330
422
|
@classmethod
|
331
423
|
def on_rest_list_response(cls, request, queryset):
|
332
424
|
# Implement pagination
|
333
|
-
page_size = request.DATA.get_typed("size", 10, int)
|
334
|
-
page_start = request.DATA.get_typed("start", 0, int)
|
425
|
+
page_size = request.DATA.get_typed(["size", "limit"], 10, int)
|
426
|
+
page_start = request.DATA.get_typed(["start", "offset"], 0, int)
|
335
427
|
page_end = page_start+page_size
|
336
428
|
paged_queryset = queryset[page_start:page_end]
|
337
429
|
graph = request.DATA.get("graph", "list")
|
430
|
+
format = request.DATA.get("download_format", "json")
|
431
|
+
count = queryset.count()
|
338
432
|
# Use serializer manager for optimal performance
|
339
433
|
manager = get_serializer_manager()
|
434
|
+
if format != "json":
|
435
|
+
format_key = format.split("_")[0]
|
436
|
+
serializer = manager.get_format_serializer(format_key)
|
437
|
+
formats = cls.get_rest_meta_prop("FORMATS")
|
438
|
+
if formats is not None and format in formats:
|
439
|
+
fields = formats[format]
|
440
|
+
else:
|
441
|
+
graph = cls.get_rest_meta_graph(["basic", "default"])
|
442
|
+
if not graph or not graph.get("fields"):
|
443
|
+
raise me.ValueException("No valid graph found")
|
444
|
+
fields = graph.get("fields")
|
445
|
+
logger.info(f"Serializing queryset with fields: {fields}")
|
446
|
+
return serializer.serialize_queryset(queryset, fields=fields, filename=request.DATA.get("filename", f"{cls.__name__}.csv"))
|
340
447
|
serializer = manager.get_serializer(paged_queryset, graph=graph, many=True)
|
341
|
-
|
448
|
+
resp = serializer.to_response(request, count=count, start=page_start, size=page_size)
|
449
|
+
resp.log_context = {
|
450
|
+
"endpoint": "list",
|
451
|
+
"model": cls.__name__,
|
452
|
+
"page_size": page_size,
|
453
|
+
"page_start": page_start,
|
454
|
+
"page_end": page_end,
|
455
|
+
"graph": graph,
|
456
|
+
"count": count
|
457
|
+
}
|
458
|
+
return resp
|
342
459
|
|
343
460
|
@classmethod
|
344
461
|
def on_rest_list_date_range_filter(cls, request, queryset):
|
@@ -369,6 +486,22 @@ class MojoModel:
|
|
369
486
|
queryset = queryset.filter(**{f"{dr_field}__lte": dr_end})
|
370
487
|
return queryset
|
371
488
|
|
489
|
+
@classmethod
|
490
|
+
def normalize_rest_value(cls, request, field_name, value):
|
491
|
+
field = cls.get_model_field(field_name)
|
492
|
+
if field.get_internal_type() == "BooleanField":
|
493
|
+
if isinstance(value, str):
|
494
|
+
value = value.lower() in ("true", "1")
|
495
|
+
elif isinstance(value, int):
|
496
|
+
value = bool(value)
|
497
|
+
else:
|
498
|
+
value = bool(value)
|
499
|
+
elif value is not None:
|
500
|
+
if field.get_internal_type() in ["DateTimeField", "DateField"]:
|
501
|
+
if not isinstance(value, (datetime.datetime, datetime.date)):
|
502
|
+
value = dates.parse_datetime(value)
|
503
|
+
return value
|
504
|
+
|
372
505
|
@classmethod
|
373
506
|
def on_rest_list_filter(cls, request, queryset):
|
374
507
|
"""
|
@@ -381,16 +514,22 @@ class MojoModel:
|
|
381
514
|
Returns:
|
382
515
|
The filtered queryset.
|
383
516
|
"""
|
517
|
+
reserved_keys = ["start", "size", "download_format", "dr_start", "dr_end", "limit", "offset"]
|
384
518
|
filters = {}
|
385
|
-
for key, value in request.
|
519
|
+
for key, value in request.QUERY_PARAMS.items():
|
386
520
|
# Split key to check for foreign key relationships
|
521
|
+
if "." in key:
|
522
|
+
key = key.replace(".", "__")
|
387
523
|
key_parts = key.split('__')
|
388
524
|
field_name = key_parts[0]
|
525
|
+
if field_name in reserved_keys:
|
526
|
+
continue
|
389
527
|
if hasattr(cls, field_name):
|
390
|
-
filters[key] = value
|
528
|
+
filters[key] = cls.normalize_rest_value(request, field_name, value)
|
391
529
|
elif field_name in cls.__rest_field_names__ and cls._meta.get_field(field_name).is_relation:
|
530
|
+
# TODO Normalize relation field values
|
392
531
|
filters[key] = value
|
393
|
-
|
532
|
+
logger.info("filters", filters)
|
394
533
|
queryset = cls.on_rest_list_search(request, queryset)
|
395
534
|
return queryset.filter(**filters)
|
396
535
|
|
@@ -406,7 +545,7 @@ class MojoModel:
|
|
406
545
|
Returns:
|
407
546
|
The filtered queryset based on the search criteria.
|
408
547
|
"""
|
409
|
-
search_query = request.
|
548
|
+
search_query = request.DATA.get('search', None)
|
410
549
|
if not search_query:
|
411
550
|
return queryset
|
412
551
|
|
@@ -500,6 +639,19 @@ class MojoModel:
|
|
500
639
|
# Perform any additional actions after object creation
|
501
640
|
pass
|
502
641
|
|
642
|
+
def on_rest_response(self, request, graph="default"):
|
643
|
+
"""
|
644
|
+
Handle the response after a REST request.
|
645
|
+
|
646
|
+
Args:
|
647
|
+
request: Django HTTP request object.
|
648
|
+
graph: String representing the graph to use for serialization.
|
649
|
+
|
650
|
+
Returns:
|
651
|
+
JsonResponse representing the object.
|
652
|
+
"""
|
653
|
+
return self.on_rest_get(request, graph=graph)
|
654
|
+
|
503
655
|
def on_rest_get(self, request, graph="default"):
|
504
656
|
"""
|
505
657
|
Handle the retrieval of a single object.
|
@@ -510,7 +662,7 @@ class MojoModel:
|
|
510
662
|
Returns:
|
511
663
|
JsonResponse representing the object.
|
512
664
|
"""
|
513
|
-
graph = request.
|
665
|
+
graph = request.DATA.get("graph", graph)
|
514
666
|
# Use serializer manager for optimal performance
|
515
667
|
manager = get_serializer_manager()
|
516
668
|
serializer = manager.get_serializer(self, graph=graph)
|
@@ -525,6 +677,15 @@ class MojoModel:
|
|
525
677
|
def has_field_changed(self, key):
|
526
678
|
return key in self.__changed_fields__
|
527
679
|
|
680
|
+
def has_changed(self):
|
681
|
+
return bool(self.__changed_fields__)
|
682
|
+
|
683
|
+
def get_changes(self, data):
|
684
|
+
changes = {}
|
685
|
+
for key, value in self.__changed_fields__.items():
|
686
|
+
changes[key] = f"{value} -> {data.get(key, None)}"
|
687
|
+
return changes
|
688
|
+
|
528
689
|
def on_rest_save(self, request, data_dict):
|
529
690
|
"""
|
530
691
|
Create a model instance from a dictionary.
|
@@ -539,47 +700,78 @@ class MojoModel:
|
|
539
700
|
self.__changed_fields__ = objict.objict()
|
540
701
|
# Get fields that should not be saved
|
541
702
|
no_save_fields = self.get_rest_meta_prop("NO_SAVE_FIELDS", ["id", "pk", "created", "uuid"])
|
542
|
-
|
703
|
+
post_save_actions = self.get_rest_meta_prop("POST_SAVE_ACTIONS", ['action'])
|
704
|
+
post_save_data = {}
|
705
|
+
action_resp = None # an action may have a specific response
|
543
706
|
# Iterate through data_dict keys instead of model fields
|
544
707
|
for key, value in data_dict.items():
|
545
708
|
# Skip fields that shouldn't be saved
|
546
709
|
if key in no_save_fields:
|
547
710
|
continue
|
548
|
-
|
549
|
-
|
550
|
-
set_field_method = getattr(self, f'set_{key}', None)
|
551
|
-
if callable(set_field_method):
|
552
|
-
old_value = getattr(self, key, None)
|
553
|
-
set_field_method(value)
|
554
|
-
new_value = getattr(self, key, None)
|
555
|
-
self._set_field_change(key, old_value, new_value)
|
556
|
-
continue
|
557
|
-
|
558
|
-
# Check if this is a model field
|
559
|
-
field = self.get_model_field(key)
|
560
|
-
if field is None:
|
711
|
+
if key in post_save_actions:
|
712
|
+
post_save_data[key] = value
|
561
713
|
continue
|
562
|
-
|
563
|
-
self.on_rest_save_related_field(field, value, request)
|
564
|
-
elif field.get_internal_type() == "JSONField":
|
565
|
-
self.on_rest_update_jsonfield(key, value)
|
566
|
-
else:
|
567
|
-
self._set_field_change(key, getattr(self, key), value)
|
568
|
-
setattr(self, key, value)
|
714
|
+
self.on_rest_save_field(key, value, request)
|
569
715
|
|
570
716
|
created = self.pk is None
|
571
717
|
if created:
|
572
|
-
|
573
|
-
|
574
|
-
|
718
|
+
owner_field = self.get_rest_meta_prop("CREATED_BY_OWNER_FIELD", "user")
|
719
|
+
if request.user.is_authenticated and self.get_model_field(owner_field):
|
720
|
+
setattr(self, owner_field, request.user)
|
575
721
|
if request.group and self.get_model_field("group"):
|
576
722
|
if getattr(self, "group", None) is None:
|
577
723
|
self.group = request.group
|
724
|
+
else:
|
725
|
+
owner_field = self.get_rest_meta_prop("UPDATED_BY_OWNER_FIELD", "modified_by")
|
726
|
+
if request.user.is_authenticated and self.get_model_field(owner_field):
|
727
|
+
setattr(self, owner_field, request.user)
|
578
728
|
self.on_rest_pre_save(self.__changed_fields__, created)
|
579
729
|
if "files" in data_dict:
|
580
730
|
self.on_rest_save_files(data_dict["files"])
|
581
731
|
self.atomic_save()
|
582
732
|
self.on_rest_saved(self.__changed_fields__, created)
|
733
|
+
for key, value in post_save_data.items():
|
734
|
+
# post save fields can only be called via on_action_
|
735
|
+
handler = getattr(self, f'on_action_{key}', None)
|
736
|
+
if callable(handler):
|
737
|
+
action_resp = handler(value)
|
738
|
+
|
739
|
+
if self.get_rest_meta_prop("LOG_CHANGES", False) and self.has_changed():
|
740
|
+
self.log(kind="model:changed", log=self.get_changes(data_dict))
|
741
|
+
return action_resp
|
742
|
+
|
743
|
+
def on_rest_save_field(self, key, value, request):
|
744
|
+
# First check for custom setter method
|
745
|
+
set_field_method = getattr(self, f'set_{key}', None)
|
746
|
+
if callable(set_field_method):
|
747
|
+
old_value = getattr(self, key, None)
|
748
|
+
set_field_method(value)
|
749
|
+
new_value = getattr(self, key, None)
|
750
|
+
self._set_field_change(key, old_value, new_value)
|
751
|
+
return
|
752
|
+
|
753
|
+
# Check if this is a model field
|
754
|
+
field = self.get_model_field(key)
|
755
|
+
if field is None:
|
756
|
+
return
|
757
|
+
if field.get_internal_type() == "ForeignKey":
|
758
|
+
self.on_rest_save_related_field(field, value, request)
|
759
|
+
elif field.get_internal_type() == "JSONField":
|
760
|
+
self.on_rest_update_jsonfield(key, value)
|
761
|
+
else:
|
762
|
+
if field.get_internal_type() == "BooleanField":
|
763
|
+
if isinstance(value, str):
|
764
|
+
value = value.lower() in ("true", "1")
|
765
|
+
elif isinstance(value, int):
|
766
|
+
value = bool(value)
|
767
|
+
else:
|
768
|
+
value = bool(value)
|
769
|
+
elif value is not None:
|
770
|
+
if field.get_internal_type() in ["DateTimeField", "DateField"]:
|
771
|
+
if not isinstance(value, (datetime.datetime, datetime.date)):
|
772
|
+
value = dates.parse_datetime(value)
|
773
|
+
self._set_field_change(key, getattr(self, key), value)
|
774
|
+
setattr(self, key, value)
|
583
775
|
|
584
776
|
def on_rest_save_files(self, files):
|
585
777
|
for name, file in files.items():
|
@@ -599,8 +791,10 @@ class MojoModel:
|
|
599
791
|
setattr(self, name, instance)
|
600
792
|
|
601
793
|
def on_rest_save_and_respond(self, request):
|
602
|
-
self.on_rest_save(request, request.DATA)
|
603
|
-
|
794
|
+
resp = self.on_rest_save(request, request.DATA)
|
795
|
+
if resp is None:
|
796
|
+
return self.on_rest_get(request)
|
797
|
+
return JsonResponse(resp)
|
604
798
|
|
605
799
|
def on_rest_save_related_field(self, field, field_value, request):
|
606
800
|
if isinstance(field_value, dict):
|
@@ -619,8 +813,10 @@ class MojoModel:
|
|
619
813
|
field.related_model.on_rest_related_save(self, field.name, field_value, related_instance)
|
620
814
|
elif isinstance(field_value, int) or (isinstance(field_value, str)):
|
621
815
|
# self.debug(f"Related Model: {field.related_model.__name__}, Field Value: {field_value}")
|
816
|
+
field_value = int(field_value)
|
622
817
|
if not bool(field_value):
|
623
818
|
# None, "", 0 will set it to None
|
819
|
+
logger.info(f"Setting field {field.name} to None")
|
624
820
|
setattr(self, field.name, None)
|
625
821
|
return
|
626
822
|
field_value = int(field_value)
|
@@ -698,13 +894,27 @@ class MojoModel:
|
|
698
894
|
Instance-level audit/event reporting. Automatically includes model+id.
|
699
895
|
"""
|
700
896
|
context = dict(context)
|
701
|
-
context.setdefault("model_name", self.__class__.
|
702
|
-
if hasattr(self, 'id'):
|
897
|
+
context.setdefault("model_name", self.__class__.get_model_string())
|
898
|
+
if hasattr(self, 'id') and self.id is not None:
|
703
899
|
context.setdefault("model_id", self.id)
|
704
900
|
self.__class__.class_report_incident(
|
705
901
|
details, event_type=event_type, level=level, request=request, **context
|
706
902
|
)
|
707
903
|
|
904
|
+
@classmethod
|
905
|
+
def class_report_incident_for_user(cls, details, event_type="info", level=1, request=None, **context):
|
906
|
+
"""
|
907
|
+
Class-level audit/event reporting for a specific user.
|
908
|
+
details: Human description.
|
909
|
+
event_type: Category/kind (e.g. "permission_denied", "security_alert").
|
910
|
+
level: Numeric severity.
|
911
|
+
request: Optional HTTP request or actor.
|
912
|
+
**context: Any additional context.
|
913
|
+
"""
|
914
|
+
if ACTIVE_REQUEST and ACTIVE_REQUEST.user.is_authenticated:
|
915
|
+
return request.user.report_incident(details, event_type=event_type, level=level, request=request, **context)
|
916
|
+
return cls.class_report_incident(details, event_type=event_type, level=level, request=request, **context)
|
917
|
+
|
708
918
|
@classmethod
|
709
919
|
def class_report_incident(cls, details, event_type="info", level=1, request=None, **context):
|
710
920
|
"""
|
@@ -737,10 +947,14 @@ class MojoModel:
|
|
737
947
|
def debug(cls, log, *args):
|
738
948
|
return logger.info(log, *args)
|
739
949
|
|
950
|
+
@classmethod
|
951
|
+
def get_model_string(cls):
|
952
|
+
return f"{ cls._meta.app_label.lower()}.{cls.__name__}"
|
953
|
+
|
740
954
|
@classmethod
|
741
955
|
def class_logit(cls, request, log, kind="cls_log", model_id=0, level="info", **kwargs):
|
742
956
|
from mojo.apps.logit.models import Log
|
743
|
-
return Log.logit(request, log, kind, cls.
|
957
|
+
return Log.logit(request, log, kind, cls.get_model_string(), model_id, level, **kwargs)
|
744
958
|
|
745
959
|
@classmethod
|
746
960
|
def get_model_field(cls, field_name):
|
mojo/models/secrets.py
CHANGED
@@ -66,3 +66,89 @@ class MojoSecrets(models.Model):
|
|
66
66
|
super().save(*args, **kwargs)
|
67
67
|
self.save_secrets()
|
68
68
|
super().save()
|
69
|
+
|
70
|
+
|
71
|
+
|
72
|
+
class KSMSecrets(models.Model):
|
73
|
+
"""Base model class for adding secrets to a model using AWS KMS (envelope encryption)."""
|
74
|
+
class Meta:
|
75
|
+
abstract = True
|
76
|
+
|
77
|
+
mojo_secrets = models.TextField(blank=True, null=True, default=None)
|
78
|
+
_exposed_secrets = None
|
79
|
+
_secrets_changed = False
|
80
|
+
_kms_cache = {}
|
81
|
+
|
82
|
+
def set_secrets(self, value):
|
83
|
+
self.debug("Setting secrets", repr(value))
|
84
|
+
if isinstance(value, str):
|
85
|
+
value = objict.from_json(value)
|
86
|
+
self._exposed_secrets = merge_dicts(self.secrets, value)
|
87
|
+
self._secrets_changed = True
|
88
|
+
|
89
|
+
def set_secret(self, key, value):
|
90
|
+
self.secrets[key] = value
|
91
|
+
self._secrets_changed = True
|
92
|
+
|
93
|
+
def get_secret(self, key, default=None):
|
94
|
+
return self.secrets.get(key, default)
|
95
|
+
|
96
|
+
def clear_secrets(self):
|
97
|
+
self.mojo_secrets = None
|
98
|
+
self._exposed_secrets = objict()
|
99
|
+
self._secrets_changed = True
|
100
|
+
|
101
|
+
@property
|
102
|
+
def secrets(self):
|
103
|
+
if self._exposed_secrets is not None:
|
104
|
+
return self._exposed_secrets
|
105
|
+
if self.mojo_secrets is None or self.pk is None:
|
106
|
+
self._exposed_secrets = objict()
|
107
|
+
return self._exposed_secrets
|
108
|
+
if self._exposed_secrets is None:
|
109
|
+
try:
|
110
|
+
data = self._get_kms().decrypt_dict_field(self._kms_context(), self.mojo_secrets)
|
111
|
+
# convert to objict mapping
|
112
|
+
self._exposed_secrets = objict.from_dict(data)
|
113
|
+
except Exception:
|
114
|
+
# On failure, expose empty to avoid leaking details
|
115
|
+
self._exposed_secrets = objict()
|
116
|
+
return self._exposed_secrets
|
117
|
+
|
118
|
+
def save_secrets(self):
|
119
|
+
if self._secrets_changed:
|
120
|
+
if self._exposed_secrets:
|
121
|
+
# objict behaves like a dict; KMSHelper accepts dict
|
122
|
+
blob = self._get_kms().encrypt_field(self._kms_context(), self._exposed_secrets)
|
123
|
+
self.mojo_secrets = blob
|
124
|
+
else:
|
125
|
+
self.mojo_secrets = None
|
126
|
+
self._secrets_changed = False
|
127
|
+
|
128
|
+
def save(self, *args, **kwargs):
|
129
|
+
if self.pk is not None:
|
130
|
+
self.save_secrets()
|
131
|
+
super().save(*args, **kwargs)
|
132
|
+
else:
|
133
|
+
super().save(*args, **kwargs)
|
134
|
+
self.save_secrets()
|
135
|
+
super().save()
|
136
|
+
|
137
|
+
@classmethod
|
138
|
+
def _get_kms(cls):
|
139
|
+
kms_key_id = settings.get("KMS_KEY_ID", None)
|
140
|
+
region = settings.get("AWS_REGION", settings.get("AWS_DEFAULT_REGION", "us-east-1"))
|
141
|
+
if not kms_key_id:
|
142
|
+
raise RuntimeError("KMS_KEY_ID must be configured to use KSMSecrets")
|
143
|
+
cache_key = (kms_key_id, region)
|
144
|
+
helper = cls._kms_cache.get(cache_key)
|
145
|
+
if helper is None:
|
146
|
+
from mojo.helpers.aws.kms import KMSHelper
|
147
|
+
helper = KMSHelper(kms_key_id=kms_key_id, region_name=region)
|
148
|
+
cls._kms_cache[cache_key] = helper
|
149
|
+
return helper
|
150
|
+
|
151
|
+
def _kms_context(self):
|
152
|
+
# Bind to app_label.Model.<pk>.mojo_secrets for contextual integrity
|
153
|
+
app_label = getattr(self._meta, "app_label", self.__class__.__module__.split(".")[0])
|
154
|
+
return f"{app_label}.{self.__class__.__name__}.{self.pk}.mojo_secrets"
|
mojo/serializers/__init__.py
CHANGED
@@ -24,16 +24,10 @@ Usage:
|
|
24
24
|
|
25
25
|
# Core serializer classes
|
26
26
|
from .simple import GraphSerializer
|
27
|
-
from .optimized import OptimizedGraphSerializer
|
28
27
|
|
29
|
-
#
|
30
|
-
|
31
|
-
|
32
|
-
except ImportError:
|
33
|
-
AdvancedGraphSerializer = None
|
34
|
-
|
35
|
-
# Manager and convenience functions
|
36
|
-
from .manager import (
|
28
|
+
# New optimized core system (default)
|
29
|
+
from .core import (
|
30
|
+
OptimizedGraphSerializer,
|
37
31
|
SerializerManager,
|
38
32
|
get_serializer_manager,
|
39
33
|
register_serializer,
|
@@ -43,9 +37,17 @@ from .manager import (
|
|
43
37
|
to_response,
|
44
38
|
get_performance_stats,
|
45
39
|
clear_serializer_caches,
|
46
|
-
benchmark_serializers
|
40
|
+
benchmark_serializers,
|
41
|
+
HAS_UJSON,
|
42
|
+
UJSON_VERSION
|
47
43
|
)
|
48
44
|
|
45
|
+
# Advanced serializer (optional - may not be available)
|
46
|
+
try:
|
47
|
+
from .advanced import AdvancedGraphSerializer
|
48
|
+
except ImportError:
|
49
|
+
AdvancedGraphSerializer = None
|
50
|
+
|
49
51
|
# Version and metadata
|
50
52
|
__version__ = "2.0.0"
|
51
53
|
__author__ = "Django-MOJO Team"
|
@@ -74,6 +76,10 @@ __all__ = [
|
|
74
76
|
'get_performance_stats',
|
75
77
|
'clear_serializer_caches',
|
76
78
|
'benchmark_serializers',
|
79
|
+
|
80
|
+
# Performance info
|
81
|
+
'HAS_UJSON',
|
82
|
+
'UJSON_VERSION',
|
77
83
|
]
|
78
84
|
|
79
85
|
# Initialize default manager on import
|