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
@@ -44,7 +44,7 @@ class LoggerMiddleware:
44
44
  if LOGIT_DB_ALL:
45
45
  request.request_log = Log.logit(request, request.DATA.to_json(as_string=True), "request")
46
46
  if LOGIT_FILE_ALL:
47
- LOGGER.info(f"REQUEST - {request.method} - {request.ip} - {request.path}", request.body)
47
+ LOGGER.info(f"REQUEST - {request.method} - {request.ip} - {request.path}", request._raw_body)
48
48
 
49
49
  def log_response(self, request, response):
50
50
  if not self.can_log(request):
mojo/middleware/mojo.py CHANGED
@@ -1,6 +1,7 @@
1
1
  from mojo.helpers import request as rhelper
2
2
  import time
3
3
  from objict import objict
4
+ from mojo.helpers.settings import settings
4
5
 
5
6
  ANONYMOUS_USER = objict(is_authenticated=False)
6
7
 
@@ -17,5 +18,9 @@ class MojoMiddleware:
17
18
  request.ip = rhelper.get_remote_ip(request)
18
19
  request.user_agent = rhelper.get_user_agent(request)
19
20
  request.duid = rhelper.get_device_id(request)
21
+ if settings.LOGIT_REQUEST_BODY:
22
+ request._raw_body = str(request.body)
23
+ else:
24
+ request._raw_body = None
20
25
  request.DATA = rhelper.parse_request_data(request)
21
26
  return self.get_response(request)
mojo/models/rest.py CHANGED
@@ -1,6 +1,7 @@
1
1
  # from django.http import JsonResponse
2
2
  from mojo.helpers.response import JsonResponse
3
- from mojo.serializers.models import GraphSerializer
3
+ from mojo.serializers.simple import GraphSerializer
4
+ from mojo.serializers.manager import get_serializer_manager
4
5
  from mojo.helpers import modules
5
6
  from mojo.helpers.settings import settings
6
7
  from django.core.exceptions import ObjectDoesNotExist
@@ -22,6 +23,13 @@ class MojoModel:
22
23
  """Returns the active request being processed."""
23
24
  return ACTIVE_REQUEST
24
25
 
26
+ @property
27
+ def active_user(self):
28
+ """Returns the active user being processed."""
29
+ if ACTIVE_REQUEST:
30
+ return ACTIVE_REQUEST.user
31
+ return None
32
+
25
33
  @classmethod
26
34
  def get_rest_meta_prop(cls, name, default=None):
27
35
  """
@@ -116,7 +124,7 @@ class MojoModel:
116
124
  @classmethod
117
125
  def rest_check_permission(cls, request, permission_keys, instance=None):
118
126
  """
119
- Check permissions for a given request.
127
+ Check permissions for a given request. Reports granular denied feedback to incident/event system.
120
128
 
121
129
  Args:
122
130
  request: Django HTTP request object.
@@ -129,20 +137,69 @@ class MojoModel:
129
137
  perms = cls.get_rest_meta_prop(permission_keys, [])
130
138
  if perms is None or len(perms) == 0:
131
139
  return True
140
+
132
141
  if "all" not in perms:
133
142
  if request.user is None or not request.user.is_authenticated:
143
+ cls.class_report_incident(
144
+ details="Permission denied: unauthenticated user",
145
+ event_type="unauthenticated",
146
+ request=request,
147
+ perms=perms,
148
+ permission_keys=permission_keys,
149
+ branch="unauthenticated",
150
+ instance=repr(instance) if instance else None,
151
+ request_path=getattr(request, "path", None),
152
+ )
134
153
  return False
154
+
135
155
  if instance is not None:
136
156
  if hasattr(instance, "check_edit_permission"):
137
- return instance.check_edit_permission(perms, request)
157
+ allowed = instance.check_edit_permission(perms, request)
158
+ if not allowed:
159
+ cls.class_report_incident(
160
+ details="Permission denied: edit_permission_denied",
161
+ event_type="edit_permission_denied",
162
+ request=request,
163
+ perms=perms,
164
+ permission_keys=permission_keys,
165
+ branch="instance.check_edit_permission",
166
+ instance=repr(instance),
167
+ request_path=getattr(request, "path", None),
168
+ )
169
+ return allowed
138
170
  if "owner" in perms and getattr(instance, "user", None) is not None:
139
171
  if instance.user.id == request.user.id:
140
172
  return True
173
+
141
174
  if request.group and hasattr(cls, "group"):
142
- # lets check our group member permissions
143
- # this will now force any queries to include the group
144
- return request.group.member_has_permission(request.user, perms)
145
- return request.user.has_permission(perms)
175
+ allowed = request.group.member_has_permission(request.user, perms)
176
+ if not allowed:
177
+ cls.class_report_incident(
178
+ details="Permission denied: group_member_permission_denied",
179
+ event_type="group_member_permission_denied",
180
+ request=request,
181
+ perms=perms,
182
+ permission_keys=permission_keys,
183
+ group=getattr(request, "group", None),
184
+ branch="group.member_has_permission",
185
+ instance=repr(instance) if instance else None,
186
+ request_path=getattr(request, "path", None),
187
+ )
188
+ return allowed
189
+
190
+ allowed = request.user.has_permission(perms)
191
+ if not allowed:
192
+ cls.class_report_incident(
193
+ details="Permission denied: user_permission_denied",
194
+ event_type="user_permission_denied",
195
+ request=request,
196
+ perms=perms,
197
+ permission_keys=permission_keys,
198
+ branch="user.has_permission",
199
+ instance=repr(instance) if instance else None,
200
+ request_path=getattr(request, "path", None),
201
+ )
202
+ return allowed
146
203
 
147
204
  @classmethod
148
205
  def on_rest_handle_get(cls, request, instance):
@@ -206,6 +263,7 @@ class MojoModel:
206
263
  Returns:
207
264
  JsonResponse representing the list of resources.
208
265
  """
266
+ cls.debug("on_rest_handle_list")
209
267
  if cls.rest_check_permission(request, "VIEW_PERMS"):
210
268
  return cls.on_rest_list(request)
211
269
  return cls.rest_error_response(request, 403, error=f"GET permission denied: {cls.__name__}")
@@ -223,7 +281,9 @@ class MojoModel:
223
281
  """
224
282
  if cls.rest_check_permission(request, ["SAVE_PERMS", "VIEW_PERMS"]):
225
283
  instance = cls()
226
- return instance.on_rest_save_and_respond(request)
284
+ instance.on_rest_save(request, request.DATA)
285
+ instance.on_rest_created()
286
+ return instance.on_rest_get(request)
227
287
  return cls.rest_error_response(request, 403, error=f"CREATE permission denied: {cls.__name__}")
228
288
 
229
289
  @classmethod
@@ -254,6 +314,7 @@ class MojoModel:
254
314
  Returns:
255
315
  JsonResponse representing the paginated and serialized list of objects.
256
316
  """
317
+ cls.debug("on_rest_list:start")
257
318
  if queryset is None:
258
319
  queryset = cls.objects.all()
259
320
  if request.group is not None and hasattr(cls, "group"):
@@ -263,6 +324,7 @@ class MojoModel:
263
324
  queryset = cls.on_rest_list_filter(request, queryset)
264
325
  queryset = cls.on_rest_list_date_range_filter(request, queryset)
265
326
  queryset = cls.on_rest_list_sort(request, queryset)
327
+ cls.debug("on_rest_list:end")
266
328
  return cls.on_rest_list_response(request, queryset)
267
329
 
268
330
  @classmethod
@@ -273,7 +335,9 @@ class MojoModel:
273
335
  page_end = page_start+page_size
274
336
  paged_queryset = queryset[page_start:page_end]
275
337
  graph = request.DATA.get("graph", "list")
276
- serializer = GraphSerializer(paged_queryset, graph=graph, many=True)
338
+ # Use serializer manager for optimal performance
339
+ manager = get_serializer_manager()
340
+ serializer = manager.get_serializer(paged_queryset, graph=graph, many=True)
277
341
  return serializer.to_response(request, count=queryset.count(), start=page_start, size=page_size)
278
342
 
279
343
  @classmethod
@@ -333,7 +397,7 @@ class MojoModel:
333
397
  @classmethod
334
398
  def on_rest_list_search(cls, request, queryset):
335
399
  """
336
- Search queryset based on 'q' param in the request for fields defined in 'SEARCH_FIELDS'.
400
+ Search queryset based on 'search' param in the request for fields defined in 'SEARCH_FIELDS'.
337
401
 
338
402
  Args:
339
403
  request: Django HTTP request object.
@@ -342,7 +406,7 @@ class MojoModel:
342
406
  Returns:
343
407
  The filtered queryset based on the search criteria.
344
408
  """
345
- search_query = request.GET.get('q', None)
409
+ search_query = request.GET.get('search', None)
346
410
  if not search_query:
347
411
  return queryset
348
412
 
@@ -397,8 +461,7 @@ class MojoModel:
397
461
  }
398
462
  return JsonResponse(response_payload)
399
463
 
400
- @classmethod
401
- def on_rest_create(cls, request):
464
+ def on_rest_created(self):
402
465
  """
403
466
  Handle the creation of an object.
404
467
 
@@ -406,12 +469,38 @@ class MojoModel:
406
469
  request: Django HTTP request object.
407
470
 
408
471
  Returns:
409
- JsonResponse representing the newly created object.
472
+ None
410
473
  """
411
- instance = cls()
412
- return instance.on_rest_save_and_respond(request)
474
+ # Perform any additional actions after object creation
475
+ pass
413
476
 
414
- def on_rest_get(self, request):
477
+ def on_rest_pre_save(self, changed_fields, created):
478
+ """
479
+ Handle the pre-save of an object.
480
+
481
+ Args:
482
+ created: Boolean indicating whether the object is being created.
483
+ changed_fields: Dictionary of fields that have changed.
484
+ Returns:
485
+ None
486
+ """
487
+ # Perform any additional actions before object save
488
+ pass
489
+
490
+ def on_rest_saved(self, changed_fields, created):
491
+ """
492
+ Handle the saving of an object.
493
+
494
+ Args:
495
+ created: Boolean indicating whether the object is being created.
496
+ changed_fields: Dictionary of fields that have changed.
497
+ Returns:
498
+ None
499
+ """
500
+ # Perform any additional actions after object creation
501
+ pass
502
+
503
+ def on_rest_get(self, request, graph="default"):
415
504
  """
416
505
  Handle the retrieval of a single object.
417
506
 
@@ -421,34 +510,93 @@ class MojoModel:
421
510
  Returns:
422
511
  JsonResponse representing the object.
423
512
  """
424
- graph = request.GET.get("graph", "default")
425
- serializer = GraphSerializer(self, graph=graph)
513
+ graph = request.GET.get("graph", graph)
514
+ # Use serializer manager for optimal performance
515
+ manager = get_serializer_manager()
516
+ serializer = manager.get_serializer(self, graph=graph)
426
517
  return serializer.to_response(request)
427
518
 
519
+ def _set_field_change(self, key, old_value=None, new_value=None):
520
+ if not hasattr(self, "__changed_fields__"):
521
+ self.__changed_fields__ = objict.objict()
522
+ if old_value != new_value:
523
+ self.__changed_fields__[key] = old_value
524
+
525
+ def has_field_changed(self, key):
526
+ return key in self.__changed_fields__
527
+
428
528
  def on_rest_save(self, request, data_dict):
429
529
  """
430
530
  Create a model instance from a dictionary.
431
531
 
432
532
  Args:
433
533
  request: Django HTTP request object.
534
+ data_dict: Dictionary containing the data to save.
434
535
 
435
536
  Returns:
436
537
  None
437
538
  """
438
- for field in self._meta.get_fields():
439
- field_name = field.name
440
- if field_name in data_dict:
441
- field_value = data_dict[field_name]
442
- set_field_method = getattr(self, f'set_{field_name}', None)
443
- if callable(set_field_method):
444
- set_field_method(field_value, request)
445
- elif field.is_relation and hasattr(field, 'related_model'):
446
- self.on_rest_save_related_field(field, field_value, request)
447
- elif field.get_internal_type() == "JSONField":
448
- self.on_rest_update_jsonfield(field_name, field_value)
449
- else:
450
- setattr(self, field_name, field_value)
539
+ self.__changed_fields__ = objict.objict()
540
+ # Get fields that should not be saved
541
+ no_save_fields = self.get_rest_meta_prop("NO_SAVE_FIELDS", ["id", "pk", "created", "uuid"])
542
+
543
+ # Iterate through data_dict keys instead of model fields
544
+ for key, value in data_dict.items():
545
+ # Skip fields that shouldn't be saved
546
+ if key in no_save_fields:
547
+ continue
548
+
549
+ # First check for custom setter method
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:
561
+ continue
562
+ if field.get_internal_type() == "ForeignKey":
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)
569
+
570
+ created = self.pk is None
571
+ if created:
572
+ if request.user.is_authenticated and self.get_model_field("user"):
573
+ if getattr(self, "user", None) is None:
574
+ self.user = request.user
575
+ if request.group and self.get_model_field("group"):
576
+ if getattr(self, "group", None) is None:
577
+ self.group = request.group
578
+ self.on_rest_pre_save(self.__changed_fields__, created)
579
+ if "files" in data_dict:
580
+ self.on_rest_save_files(data_dict["files"])
451
581
  self.atomic_save()
582
+ self.on_rest_saved(self.__changed_fields__, created)
583
+
584
+ def on_rest_save_files(self, files):
585
+ for name, file in files.items():
586
+ self.on_rest_save_file(name, file)
587
+
588
+ def on_rest_save_file(self, name, file):
589
+ # Implement file saving logic here
590
+ self.debug("Finding file for field: %s", name)
591
+ field = self.get_model_field(name)
592
+ if field is None:
593
+ return
594
+ self.debug("Saving file for field: %s", name)
595
+ if field.related_model and hasattr(field.related_model, "create_from_file"):
596
+ self.debug("Found file for field: %s", name)
597
+ related_model = field.related_model
598
+ instance = related_model.create_from_file(file, name)
599
+ setattr(self, name, instance)
452
600
 
453
601
  def on_rest_save_and_respond(self, request):
454
602
  self.on_rest_save(request, request.DATA)
@@ -466,11 +614,21 @@ class MojoModel:
466
614
  if field.related_model.rest_check_permission(request, ["SAVE_PERMS", "VIEW_PERMS"], related_instance):
467
615
  related_instance.on_rest_save(request, field_value)
468
616
  return
469
- try:
617
+ if hasattr(field.related_model, "on_rest_related_save"):
618
+ related_instance = getattr(self, field.name)
619
+ field.related_model.on_rest_related_save(self, field.name, field_value, related_instance)
620
+ elif isinstance(field_value, int) or (isinstance(field_value, str)):
621
+ # self.debug(f"Related Model: {field.related_model.__name__}, Field Value: {field_value}")
622
+ if not bool(field_value):
623
+ # None, "", 0 will set it to None
624
+ setattr(self, field.name, None)
625
+ return
626
+ field_value = int(field_value)
627
+ if (self.pk == field_value):
628
+ self.debug("Skipping self-reference")
629
+ return
470
630
  related_instance = field.related_model.objects.get(pk=field_value)
471
631
  setattr(self, field.name, related_instance)
472
- except field.related_model.DoesNotExist:
473
- pass # Skip invalid related instances
474
632
 
475
633
  def on_rest_update_jsonfield(self, field_name, field_value):
476
634
  """helper to update jsonfield by merge in changes"""
@@ -487,6 +645,18 @@ class MojoModel:
487
645
  setattr(self, field_name, existing_value)
488
646
  return existing_value
489
647
 
648
+ def on_rest_pre_delete(self):
649
+ """
650
+ Handle the pre-deletion of an object.
651
+
652
+ Args:
653
+ request: Django HTTP request object.
654
+
655
+ Returns:
656
+ JsonResponse representing the result of the pre-delete operation.
657
+ """
658
+ pass
659
+
490
660
  def on_rest_delete(self, request):
491
661
  """
492
662
  Handle the deletion of an object.
@@ -498,20 +668,23 @@ class MojoModel:
498
668
  JsonResponse representing the result of the delete operation.
499
669
  """
500
670
  try:
671
+ self.on_rest_pre_delete()
501
672
  with transaction.atomic():
502
673
  self.delete()
503
- return JsonResponse({"status": "deleted"}, status=204)
674
+ return JsonResponse({"status": "deleted"}, status=200)
504
675
  except Exception as e:
505
676
  return JsonResponse({"error": str(e)}, status=400)
506
677
 
507
678
  def to_dict(self, graph="default"):
508
- serializer = GraphSerializer(self, graph=graph)
509
- return serializer.serialize()
679
+ # Use serializer manager for optimal performance
680
+ manager = get_serializer_manager()
681
+ return manager.serialize(self, graph=graph)
510
682
 
511
683
  @classmethod
512
684
  def queryset_to_dict(cls, qset, graph="default"):
513
- serializer = GraphSerializer(qset, graph=graph)
514
- return serializer.serialize()
685
+ # Use serializer manager for optimal performance
686
+ manager = get_serializer_manager()
687
+ return manager.serialize(qset, graph=graph, many=True)
515
688
 
516
689
  def atomic_save(self):
517
690
  """
@@ -520,19 +693,61 @@ class MojoModel:
520
693
  with transaction.atomic():
521
694
  self.save()
522
695
 
523
- def report_incident(self, description, kind="error", level="critical", **kwargs):
524
- self.model_logit(ACTIVE_REQUEST, description, kind=kind, level=level)
525
- Event = modules.get_model("incidents", "Event")
526
- if Event is None:
527
- Event.report(description, kind, **kwargs)
696
+ def report_incident(self, details, event_type="info", level=1, request=None, **context):
697
+ """
698
+ Instance-level audit/event reporting. Automatically includes model+id.
699
+ """
700
+ context = dict(context)
701
+ context.setdefault("model_name", self.__class__.__name__)
702
+ if hasattr(self, 'id'):
703
+ context.setdefault("model_id", self.id)
704
+ self.__class__.class_report_incident(
705
+ details, event_type=event_type, level=level, request=request, **context
706
+ )
528
707
 
529
- def log(self, log, kind="model_log", level="info", **kwargs):
708
+ @classmethod
709
+ def class_report_incident(cls, details, event_type="info", level=1, request=None, **context):
710
+ """
711
+ Class-level audit/event reporting.
712
+ details: Human description.
713
+ event_type: Category/kind (e.g. "permission_denied", "security_alert").
714
+ level: Numeric severity.
715
+ request: Optional HTTP request or actor.
716
+ **context: Any additional context.
717
+ """
718
+ from mojo.apps import incident
719
+ context = dict(context)
720
+ context.setdefault("model_name", cls.__name__)
721
+ incident.report_event(
722
+ details,
723
+ title=details[:80],
724
+ category=event_type,
725
+ level=level,
726
+ request=request,
727
+ **context
728
+ )
729
+
730
+ def log(self, log="", kind="model_log", level="info", **kwargs):
530
731
  return self.class_logit(ACTIVE_REQUEST, log, kind, self.id, level, **kwargs)
531
732
 
532
733
  def model_logit(self, request, log, kind="model_log", level="info", **kwargs):
533
734
  return self.class_logit(request, log, kind, self.id, level, **kwargs)
534
735
 
736
+ @classmethod
737
+ def debug(cls, log, *args):
738
+ return logger.info(log, *args)
739
+
535
740
  @classmethod
536
741
  def class_logit(cls, request, log, kind="cls_log", model_id=0, level="info", **kwargs):
537
742
  from mojo.apps.logit.models import Log
538
743
  return Log.logit(request, log, kind, cls.__name__, model_id, level, **kwargs)
744
+
745
+ @classmethod
746
+ def get_model_field(cls, field_name):
747
+ """
748
+ Get a model field by name.
749
+ """
750
+ try:
751
+ return cls._meta.get_field(field_name)
752
+ except Exception:
753
+ return None
mojo/models/secrets.py CHANGED
@@ -11,12 +11,18 @@ class MojoSecrets(models.Model):
11
11
 
12
12
  mojo_secrets = models.TextField(blank=True, null=True, default=None)
13
13
  _exposed_secrets = None
14
+ _secrets_changed = False
14
15
 
15
16
  def set_secrets(self, value):
17
+ self.debug("Setting secrets", repr(value))
18
+ if isinstance(value, str):
19
+ value = objict.from_json(value)
16
20
  self._exposed_secrets = merge_dicts(self.secrets, value)
21
+ self._secrets_changed = True
17
22
 
18
23
  def set_secret(self, key, value):
19
24
  self.secrets[key] = value
25
+ self._secrets_changed = True
20
26
 
21
27
  def get_secret(self, key, default=None):
22
28
  return self.secrets.get(key, default)
@@ -24,6 +30,7 @@ class MojoSecrets(models.Model):
24
30
  def clear_secrets(self):
25
31
  self.mojo_secrets = None
26
32
  self._exposed_secrets = objict()
33
+ self._secrets_changed = True
27
34
 
28
35
  @property
29
36
  def secrets(self):
@@ -44,10 +51,12 @@ class MojoSecrets(models.Model):
44
51
  return salt
45
52
 
46
53
  def save_secrets(self):
47
- if self._exposed_secrets:
48
- self.mojo_secrets = crypto.encrypt( self._exposed_secrets, self._get_secrets_password())
49
- else:
50
- self.mojo_secrets = None
54
+ if self._secrets_changed:
55
+ if self._exposed_secrets:
56
+ self.mojo_secrets = crypto.encrypt( self._exposed_secrets, self._get_secrets_password())
57
+ else:
58
+ self.mojo_secrets = None
59
+ self._secrets_changed = False
51
60
 
52
61
  def save(self, *args, **kwargs):
53
62
  if self.pk is not None:
@@ -0,0 +1,100 @@
1
+ """
2
+ Django-MOJO Serializers Package
3
+
4
+ Provides high-performance serialization for Django models with RestMeta.GRAPHS support.
5
+ Includes multiple serializer backends optimized for different use cases:
6
+
7
+ - OptimizedGraphSerializer: Ultra-fast with intelligent caching (default)
8
+ - GraphSerializer: Simple and reliable fallback
9
+ - AdvancedGraphSerializer: Feature-rich with multiple format support
10
+
11
+ Usage:
12
+ from mojo.serializers import serialize, to_json, to_response
13
+
14
+ # Quick serialization
15
+ data = serialize(instance, graph="detail")
16
+ json_str = to_json(queryset, graph="list")
17
+ response = to_response(instance, request, graph="default")
18
+
19
+ # Direct serializer access
20
+ from mojo.serializers import OptimizedGraphSerializer
21
+ serializer = OptimizedGraphSerializer(instance, graph="detail")
22
+ data = serializer.serialize()
23
+ """
24
+
25
+ # Core serializer classes
26
+ from .simple import GraphSerializer
27
+ from .optimized import OptimizedGraphSerializer
28
+
29
+ # Advanced serializer (optional - may not be available)
30
+ try:
31
+ from .advanced import AdvancedGraphSerializer
32
+ except ImportError:
33
+ AdvancedGraphSerializer = None
34
+
35
+ # Manager and convenience functions
36
+ from .manager import (
37
+ SerializerManager,
38
+ get_serializer_manager,
39
+ register_serializer,
40
+ set_default_serializer,
41
+ serialize,
42
+ to_json,
43
+ to_response,
44
+ get_performance_stats,
45
+ clear_serializer_caches,
46
+ benchmark_serializers
47
+ )
48
+
49
+ # Version and metadata
50
+ __version__ = "2.0.0"
51
+ __author__ = "Django-MOJO Team"
52
+
53
+ # Default exports
54
+ __all__ = [
55
+ # Core serializer classes
56
+ 'GraphSerializer',
57
+ 'OptimizedGraphSerializer',
58
+ 'AdvancedGraphSerializer',
59
+
60
+ # Manager
61
+ 'SerializerManager',
62
+ 'get_serializer_manager',
63
+
64
+ # Registration functions
65
+ 'register_serializer',
66
+ 'set_default_serializer',
67
+
68
+ # Convenience functions
69
+ 'serialize',
70
+ 'to_json',
71
+ 'to_response',
72
+
73
+ # Performance monitoring
74
+ 'get_performance_stats',
75
+ 'clear_serializer_caches',
76
+ 'benchmark_serializers',
77
+ ]
78
+
79
+ # Initialize default manager on import
80
+ _manager = get_serializer_manager()
81
+
82
+ # Convenience shortcuts at package level
83
+ def get_serializer(instance, graph="default", many=None, serializer_type=None, **kwargs):
84
+ """Get configured serializer instance."""
85
+ return _manager.get_serializer(instance, graph, many, serializer_type, **kwargs)
86
+
87
+ def list_serializers():
88
+ """List all registered serializers."""
89
+ return _manager.registry.list_serializers()
90
+
91
+ def get_default_serializer():
92
+ """Get the current default serializer name."""
93
+ return _manager.registry.get_default()
94
+
95
+ # Add shortcuts to exports
96
+ __all__.extend([
97
+ 'get_serializer',
98
+ 'list_serializers',
99
+ 'get_default_serializer'
100
+ ])