django-nativemojo 0.1.10__py3-none-any.whl → 0.1.16__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 (276) hide show
  1. django_nativemojo-0.1.16.dist-info/METADATA +138 -0
  2. django_nativemojo-0.1.16.dist-info/RECORD +302 -0
  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 +651 -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/migrations/0006_add_device_tracking_models.py +72 -0
  10. mojo/apps/account/migrations/0007_delete_userdevicelocation.py +16 -0
  11. mojo/apps/account/migrations/0008_userdevicelocation.py +33 -0
  12. mojo/apps/account/migrations/0009_geolocatedip_subnet.py +18 -0
  13. mojo/apps/account/migrations/0010_group_avatar.py +20 -0
  14. mojo/apps/account/migrations/0011_user_org_registereddevice_pushconfig_and_more.py +118 -0
  15. mojo/apps/account/migrations/0012_remove_pushconfig_apns_key_file_and_more.py +21 -0
  16. mojo/apps/account/migrations/0013_pushconfig_test_mode_alter_pushconfig_apns_enabled_and_more.py +28 -0
  17. mojo/apps/account/migrations/0014_notificationdelivery_data_payload_and_more.py +48 -0
  18. mojo/apps/account/models/__init__.py +2 -0
  19. mojo/apps/account/models/device.py +281 -0
  20. mojo/apps/account/models/group.py +319 -15
  21. mojo/apps/account/models/member.py +29 -5
  22. mojo/apps/account/models/push/__init__.py +4 -0
  23. mojo/apps/account/models/push/config.py +112 -0
  24. mojo/apps/account/models/push/delivery.py +93 -0
  25. mojo/apps/account/models/push/device.py +66 -0
  26. mojo/apps/account/models/push/template.py +99 -0
  27. mojo/apps/account/models/user.py +369 -19
  28. mojo/apps/account/rest/__init__.py +2 -0
  29. mojo/apps/account/rest/device.py +39 -0
  30. mojo/apps/account/rest/group.py +9 -0
  31. mojo/apps/account/rest/push.py +187 -0
  32. mojo/apps/account/rest/user.py +100 -6
  33. mojo/apps/account/services/__init__.py +1 -0
  34. mojo/apps/account/services/push.py +363 -0
  35. mojo/apps/aws/migrations/0001_initial.py +206 -0
  36. mojo/apps/aws/migrations/0002_emaildomain_can_recv_emaildomain_can_send_and_more.py +28 -0
  37. mojo/apps/aws/migrations/0003_mailbox_is_domain_default_mailbox_is_system_default_and_more.py +31 -0
  38. mojo/apps/aws/migrations/0004_s3bucket.py +39 -0
  39. mojo/apps/aws/migrations/0005_alter_emaildomain_region_delete_s3bucket.py +21 -0
  40. mojo/apps/aws/models/__init__.py +19 -0
  41. mojo/apps/aws/models/email_attachment.py +99 -0
  42. mojo/apps/aws/models/email_domain.py +218 -0
  43. mojo/apps/aws/models/email_template.py +132 -0
  44. mojo/apps/aws/models/incoming_email.py +197 -0
  45. mojo/apps/aws/models/mailbox.py +288 -0
  46. mojo/apps/aws/models/sent_message.py +175 -0
  47. mojo/apps/aws/rest/__init__.py +7 -0
  48. mojo/apps/aws/rest/email.py +33 -0
  49. mojo/apps/aws/rest/email_ops.py +183 -0
  50. mojo/apps/aws/rest/messages.py +32 -0
  51. mojo/apps/aws/rest/s3.py +64 -0
  52. mojo/apps/aws/rest/send.py +101 -0
  53. mojo/apps/aws/rest/sns.py +403 -0
  54. mojo/apps/aws/rest/templates.py +19 -0
  55. mojo/apps/aws/services/__init__.py +32 -0
  56. mojo/apps/aws/services/email.py +390 -0
  57. mojo/apps/aws/services/email_ops.py +548 -0
  58. mojo/apps/docit/__init__.py +6 -0
  59. mojo/apps/docit/markdown_plugins/syntax_highlight.py +25 -0
  60. mojo/apps/docit/markdown_plugins/toc.py +12 -0
  61. mojo/apps/docit/migrations/0001_initial.py +113 -0
  62. mojo/apps/docit/migrations/0002_alter_book_modified_by_alter_page_modified_by.py +26 -0
  63. mojo/apps/docit/migrations/0003_alter_book_group.py +20 -0
  64. mojo/apps/docit/models/__init__.py +17 -0
  65. mojo/apps/docit/models/asset.py +231 -0
  66. mojo/apps/docit/models/book.py +227 -0
  67. mojo/apps/docit/models/page.py +319 -0
  68. mojo/apps/docit/models/page_revision.py +203 -0
  69. mojo/apps/docit/rest/__init__.py +10 -0
  70. mojo/apps/docit/rest/asset.py +17 -0
  71. mojo/apps/docit/rest/book.py +22 -0
  72. mojo/apps/docit/rest/page.py +22 -0
  73. mojo/apps/docit/rest/page_revision.py +17 -0
  74. mojo/apps/docit/services/__init__.py +11 -0
  75. mojo/apps/docit/services/docit.py +315 -0
  76. mojo/apps/docit/services/markdown.py +44 -0
  77. mojo/apps/fileman/README.md +8 -8
  78. mojo/apps/fileman/backends/base.py +76 -70
  79. mojo/apps/fileman/backends/filesystem.py +86 -86
  80. mojo/apps/fileman/backends/s3.py +409 -108
  81. mojo/apps/fileman/migrations/0001_initial.py +106 -0
  82. mojo/apps/fileman/migrations/0002_filemanager_parent_alter_filemanager_max_file_size.py +24 -0
  83. mojo/apps/fileman/migrations/0003_remove_file_fileman_fil_upload__c4bc35_idx_and_more.py +25 -0
  84. mojo/apps/fileman/migrations/0004_remove_file_original_filename_and_more.py +39 -0
  85. mojo/apps/fileman/migrations/0005_alter_file_upload_token.py +18 -0
  86. mojo/apps/fileman/migrations/0006_file_download_url_filemanager_forever_urls.py +23 -0
  87. mojo/apps/fileman/migrations/0007_remove_filemanager_forever_urls_and_more.py +22 -0
  88. mojo/apps/fileman/migrations/0008_file_category.py +18 -0
  89. mojo/apps/fileman/migrations/0009_rename_file_path_file_storage_file_path.py +18 -0
  90. mojo/apps/fileman/migrations/0010_filerendition.py +33 -0
  91. mojo/apps/fileman/migrations/0011_alter_filerendition_original_file.py +19 -0
  92. mojo/apps/fileman/models/__init__.py +1 -5
  93. mojo/apps/fileman/models/file.py +240 -58
  94. mojo/apps/fileman/models/manager.py +427 -31
  95. mojo/apps/fileman/models/rendition.py +118 -0
  96. mojo/apps/fileman/renderer/__init__.py +111 -0
  97. mojo/apps/fileman/renderer/audio.py +403 -0
  98. mojo/apps/fileman/renderer/base.py +205 -0
  99. mojo/apps/fileman/renderer/document.py +404 -0
  100. mojo/apps/fileman/renderer/image.py +222 -0
  101. mojo/apps/fileman/renderer/utils.py +297 -0
  102. mojo/apps/fileman/renderer/video.py +304 -0
  103. mojo/apps/fileman/rest/__init__.py +1 -18
  104. mojo/apps/fileman/rest/upload.py +22 -32
  105. mojo/apps/fileman/signals.py +58 -0
  106. mojo/apps/fileman/tasks.py +254 -0
  107. mojo/apps/fileman/utils/__init__.py +40 -16
  108. mojo/apps/incident/migrations/0005_incidenthistory.py +39 -0
  109. mojo/apps/incident/migrations/0006_alter_incident_state.py +18 -0
  110. mojo/apps/incident/migrations/0007_event_uid.py +18 -0
  111. mojo/apps/incident/migrations/0008_ticket_ticketnote.py +55 -0
  112. mojo/apps/incident/migrations/0009_incident_status.py +18 -0
  113. mojo/apps/incident/migrations/0010_event_country_code.py +18 -0
  114. mojo/apps/incident/migrations/0011_incident_country_code.py +18 -0
  115. mojo/apps/incident/migrations/0012_alter_incident_status.py +18 -0
  116. mojo/apps/incident/models/__init__.py +2 -0
  117. mojo/apps/incident/models/event.py +35 -0
  118. mojo/apps/incident/models/history.py +36 -0
  119. mojo/apps/incident/models/incident.py +3 -1
  120. mojo/apps/incident/models/ticket.py +62 -0
  121. mojo/apps/incident/reporter.py +21 -1
  122. mojo/apps/incident/rest/__init__.py +1 -0
  123. mojo/apps/incident/rest/event.py +7 -1
  124. mojo/apps/incident/rest/ticket.py +43 -0
  125. mojo/apps/jobs/__init__.py +489 -0
  126. mojo/apps/jobs/adapters.py +24 -0
  127. mojo/apps/jobs/cli.py +616 -0
  128. mojo/apps/jobs/daemon.py +370 -0
  129. mojo/apps/jobs/examples/sample_jobs.py +376 -0
  130. mojo/apps/jobs/examples/webhook_examples.py +203 -0
  131. mojo/apps/jobs/handlers/__init__.py +5 -0
  132. mojo/apps/jobs/handlers/webhook.py +317 -0
  133. mojo/apps/jobs/job_engine.py +734 -0
  134. mojo/apps/jobs/keys.py +203 -0
  135. mojo/apps/jobs/local_queue.py +363 -0
  136. mojo/apps/jobs/management/__init__.py +3 -0
  137. mojo/apps/jobs/management/commands/__init__.py +3 -0
  138. mojo/apps/jobs/manager.py +1327 -0
  139. mojo/apps/jobs/migrations/0001_initial.py +97 -0
  140. mojo/apps/jobs/migrations/0002_alter_job_max_retries_joblog.py +39 -0
  141. mojo/apps/jobs/models/__init__.py +6 -0
  142. mojo/apps/jobs/models/job.py +441 -0
  143. mojo/apps/jobs/rest/__init__.py +2 -0
  144. mojo/apps/jobs/rest/control.py +466 -0
  145. mojo/apps/jobs/rest/jobs.py +421 -0
  146. mojo/apps/jobs/scheduler.py +571 -0
  147. mojo/apps/jobs/services/__init__.py +6 -0
  148. mojo/apps/jobs/services/job_actions.py +465 -0
  149. mojo/apps/jobs/settings.py +209 -0
  150. mojo/apps/logit/migrations/0004_alter_log_level.py +18 -0
  151. mojo/apps/logit/models/log.py +7 -1
  152. mojo/apps/metrics/__init__.py +8 -1
  153. mojo/apps/metrics/redis_metrics.py +198 -0
  154. mojo/apps/metrics/rest/__init__.py +3 -0
  155. mojo/apps/metrics/rest/categories.py +266 -0
  156. mojo/apps/metrics/rest/helpers.py +48 -0
  157. mojo/apps/metrics/rest/permissions.py +99 -0
  158. mojo/apps/metrics/rest/values.py +277 -0
  159. mojo/apps/metrics/utils.py +19 -2
  160. mojo/decorators/auth.py +6 -1
  161. mojo/decorators/http.py +47 -3
  162. mojo/helpers/aws/__init__.py +45 -0
  163. mojo/helpers/aws/ec2.py +804 -0
  164. mojo/helpers/aws/iam.py +748 -0
  165. mojo/helpers/aws/inbound_email.py +309 -0
  166. mojo/helpers/aws/kms.py +413 -0
  167. mojo/helpers/aws/s3.py +451 -11
  168. mojo/helpers/aws/ses.py +483 -0
  169. mojo/helpers/aws/ses_domain.py +959 -0
  170. mojo/helpers/aws/sns.py +461 -0
  171. mojo/helpers/crypto/__init__.py +1 -1
  172. mojo/helpers/crypto/utils.py +15 -0
  173. mojo/helpers/dates.py +18 -0
  174. mojo/helpers/location/__init__.py +2 -0
  175. mojo/helpers/location/countries.py +262 -0
  176. mojo/helpers/location/geolocation.py +196 -0
  177. mojo/helpers/logit.py +37 -0
  178. mojo/helpers/redis/__init__.py +2 -0
  179. mojo/helpers/redis/adapter.py +606 -0
  180. mojo/helpers/redis/client.py +48 -0
  181. mojo/helpers/redis/pool.py +225 -0
  182. mojo/helpers/request.py +8 -0
  183. mojo/helpers/response.py +14 -2
  184. mojo/helpers/settings/__init__.py +2 -0
  185. mojo/helpers/{settings.py → settings/helper.py} +1 -37
  186. mojo/helpers/settings/parser.py +132 -0
  187. mojo/middleware/auth.py +1 -1
  188. mojo/middleware/cors.py +40 -0
  189. mojo/middleware/logging.py +131 -12
  190. mojo/middleware/mojo.py +10 -0
  191. mojo/models/rest.py +494 -65
  192. mojo/models/secrets.py +98 -3
  193. mojo/serializers/__init__.py +106 -0
  194. mojo/serializers/core/__init__.py +90 -0
  195. mojo/serializers/core/cache/__init__.py +121 -0
  196. mojo/serializers/core/cache/backends.py +518 -0
  197. mojo/serializers/core/cache/base.py +102 -0
  198. mojo/serializers/core/cache/disabled.py +181 -0
  199. mojo/serializers/core/cache/memory.py +287 -0
  200. mojo/serializers/core/cache/redis.py +533 -0
  201. mojo/serializers/core/cache/utils.py +454 -0
  202. mojo/serializers/core/manager.py +550 -0
  203. mojo/serializers/core/serializer.py +475 -0
  204. mojo/serializers/examples/settings.py +322 -0
  205. mojo/serializers/formats/csv.py +393 -0
  206. mojo/serializers/formats/localizers.py +509 -0
  207. mojo/serializers/{models.py → simple.py} +38 -15
  208. mojo/serializers/suggested_improvements.md +388 -0
  209. testit/client.py +1 -1
  210. testit/helpers.py +35 -4
  211. testit/runner.py +23 -6
  212. django_nativemojo-0.1.10.dist-info/METADATA +0 -96
  213. django_nativemojo-0.1.10.dist-info/RECORD +0 -194
  214. mojo/apps/metrics/rest/db.py +0 -0
  215. mojo/apps/notify/README.md +0 -91
  216. mojo/apps/notify/README_NOTIFICATIONS.md +0 -566
  217. mojo/apps/notify/admin.py +0 -52
  218. mojo/apps/notify/handlers/example_handlers.py +0 -516
  219. mojo/apps/notify/handlers/ses/__init__.py +0 -25
  220. mojo/apps/notify/handlers/ses/bounce.py +0 -0
  221. mojo/apps/notify/handlers/ses/complaint.py +0 -25
  222. mojo/apps/notify/handlers/ses/message.py +0 -86
  223. mojo/apps/notify/management/commands/__init__.py +0 -1
  224. mojo/apps/notify/management/commands/process_notifications.py +0 -370
  225. mojo/apps/notify/mod +0 -0
  226. mojo/apps/notify/models/__init__.py +0 -12
  227. mojo/apps/notify/models/account.py +0 -128
  228. mojo/apps/notify/models/attachment.py +0 -24
  229. mojo/apps/notify/models/bounce.py +0 -68
  230. mojo/apps/notify/models/complaint.py +0 -40
  231. mojo/apps/notify/models/inbox.py +0 -113
  232. mojo/apps/notify/models/inbox_message.py +0 -173
  233. mojo/apps/notify/models/outbox.py +0 -129
  234. mojo/apps/notify/models/outbox_message.py +0 -288
  235. mojo/apps/notify/models/template.py +0 -30
  236. mojo/apps/notify/providers/aws.py +0 -73
  237. mojo/apps/notify/rest/ses.py +0 -0
  238. mojo/apps/notify/utils/__init__.py +0 -2
  239. mojo/apps/notify/utils/notifications.py +0 -404
  240. mojo/apps/notify/utils/parsing.py +0 -202
  241. mojo/apps/notify/utils/render.py +0 -144
  242. mojo/apps/tasks/README.md +0 -118
  243. mojo/apps/tasks/__init__.py +0 -11
  244. mojo/apps/tasks/manager.py +0 -489
  245. mojo/apps/tasks/rest/__init__.py +0 -2
  246. mojo/apps/tasks/rest/hooks.py +0 -0
  247. mojo/apps/tasks/rest/tasks.py +0 -62
  248. mojo/apps/tasks/runner.py +0 -174
  249. mojo/apps/tasks/tq_handlers.py +0 -14
  250. mojo/helpers/aws/setup_email.py +0 -0
  251. mojo/helpers/redis.py +0 -10
  252. mojo/models/meta.py +0 -262
  253. mojo/ws4redis/README.md +0 -174
  254. mojo/ws4redis/__init__.py +0 -2
  255. mojo/ws4redis/client.py +0 -283
  256. mojo/ws4redis/connection.py +0 -327
  257. mojo/ws4redis/exceptions.py +0 -32
  258. mojo/ws4redis/redis.py +0 -183
  259. mojo/ws4redis/servers/base.py +0 -86
  260. mojo/ws4redis/servers/django.py +0 -171
  261. mojo/ws4redis/servers/uwsgi.py +0 -63
  262. mojo/ws4redis/settings.py +0 -45
  263. mojo/ws4redis/utf8validator.py +0 -128
  264. mojo/ws4redis/websocket.py +0 -403
  265. {django_nativemojo-0.1.10.dist-info → django_nativemojo-0.1.16.dist-info}/LICENSE +0 -0
  266. {django_nativemojo-0.1.10.dist-info → django_nativemojo-0.1.16.dist-info}/NOTICE +0 -0
  267. {django_nativemojo-0.1.10.dist-info → django_nativemojo-0.1.16.dist-info}/WHEEL +0 -0
  268. /mojo/apps/{notify → aws}/__init__.py +0 -0
  269. /mojo/apps/{notify/handlers → aws/migrations}/__init__.py +0 -0
  270. /mojo/apps/{notify/management → docit/markdown_plugins}/__init__.py +0 -0
  271. /mojo/apps/{notify/providers → docit/migrations}/__init__.py +0 -0
  272. /mojo/apps/{notify/rest → fileman/migrations}/__init__.py +0 -0
  273. /mojo/{ws4redis/servers → apps/jobs/examples}/__init__.py +0 -0
  274. /mojo/apps/{fileman/models/render.py → jobs/migrations/__init__.py} +0 -0
  275. /mojo/{serializers → rest}/openapi.py +0 -0
  276. /mojo/{apps/fileman/rest/__init__ → serializers/formats/__init__.py} +0 -0
mojo/models/rest.py CHANGED
@@ -1,11 +1,12 @@
1
1
  # from django.http import JsonResponse
2
2
  from mojo.helpers.response import JsonResponse
3
- from mojo.serializers.models import GraphSerializer
4
- from mojo.helpers import modules
3
+ from mojo.serializers import get_serializer_manager
5
4
  from mojo.helpers.settings import settings
5
+ from mojo import errors as me
6
6
  from django.core.exceptions import ObjectDoesNotExist
7
7
  from django.db import transaction, models as dm
8
8
  import objict
9
+ import datetime
9
10
  from mojo.helpers import dates, logit
10
11
 
11
12
 
@@ -13,6 +14,17 @@ logger = logit.get_logger("debug", "debug.log")
13
14
  ACTIVE_REQUEST = None
14
15
  LOGGING_CLASS = None
15
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()
16
28
 
17
29
  class MojoModel:
18
30
  """Base model class for REST operations with GraphSerializer integration."""
@@ -22,6 +34,13 @@ class MojoModel:
22
34
  """Returns the active request being processed."""
23
35
  return ACTIVE_REQUEST
24
36
 
37
+ @property
38
+ def active_user(self):
39
+ """Returns the active user being processed."""
40
+ if ACTIVE_REQUEST:
41
+ return ACTIVE_REQUEST.user
42
+ return None
43
+
25
44
  @classmethod
26
45
  def get_rest_meta_prop(cls, name, default=None):
27
46
  """
@@ -44,6 +63,17 @@ class MojoModel:
44
63
  return default
45
64
  return getattr(cls.RestMeta, name, default)
46
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
+
47
77
  @classmethod
48
78
  def rest_error_response(cls, request, status=500, **kwargs):
49
79
  """
@@ -113,10 +143,20 @@ class MojoModel:
113
143
  except ObjectDoesNotExist:
114
144
  return cls.rest_error_response(None, 404, error=f"{cls.__name__} not found")
115
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
+
116
156
  @classmethod
117
157
  def rest_check_permission(cls, request, permission_keys, instance=None):
118
158
  """
119
- Check permissions for a given request.
159
+ Check permissions for a given request. Reports granular denied feedback to incident/event system.
120
160
 
121
161
  Args:
122
162
  request: Django HTTP request object.
@@ -129,20 +169,94 @@ class MojoModel:
129
169
  perms = cls.get_rest_meta_prop(permission_keys, [])
130
170
  if perms is None or len(perms) == 0:
131
171
  return True
172
+
132
173
  if "all" not in perms:
133
174
  if request.user is None or not request.user.is_authenticated:
175
+ cls.class_report_incident(
176
+ details="Permission denied: unauthenticated user",
177
+ event_type="unauthenticated",
178
+ request=request,
179
+ perms=perms,
180
+ permission_keys=permission_keys,
181
+ branch="unauthenticated",
182
+ instance=repr(instance) if instance else None,
183
+ request_path=getattr(request, "path", None),
184
+ )
134
185
  return False
186
+
135
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
+
136
207
  if hasattr(instance, "check_edit_permission"):
137
- return instance.check_edit_permission(perms, request)
138
- if "owner" in perms and getattr(instance, "user", None) is not None:
139
- if instance.user.id == request.user.id:
208
+ allowed = instance.check_edit_permission(perms, request)
209
+ if not allowed:
210
+ cls.class_report_incident(
211
+ details="Permission denied: edit_permission_denied",
212
+ event_type="edit_permission_denied",
213
+ request=request,
214
+ perms=perms,
215
+ permission_keys=permission_keys,
216
+ branch="instance.check_edit_permission",
217
+ instance=repr(instance),
218
+ request_path=getattr(request, "path", None),
219
+ )
220
+ return allowed
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:
140
226
  return True
227
+ if hasattr(instance, "group"):
228
+ request.group = getattr(instance, "group", None)
229
+
141
230
  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)
231
+ allowed = request.group.user_has_permission(request.user, perms)
232
+ if not allowed:
233
+ cls.class_report_incident(
234
+ details="Permission denied: group_member_permission_denied",
235
+ event_type="group_member_permission_denied",
236
+ request=request,
237
+ perms=perms,
238
+ permission_keys=permission_keys,
239
+ group=getattr(request, "group", None),
240
+ branch="group.user_has_permission",
241
+ instance=repr(instance) if instance else None,
242
+ request_path=getattr(request, "path", None),
243
+ )
244
+ return allowed
245
+ if request.user is None or not request.user.is_authenticated:
246
+ return False
247
+ allowed = request.user.has_permission(perms)
248
+ if not allowed:
249
+ cls.class_report_incident(
250
+ details="Permission denied: user_permission_denied",
251
+ event_type="user_permission_denied",
252
+ request=request,
253
+ perms=perms,
254
+ permission_keys=permission_keys,
255
+ branch="user.has_permission",
256
+ instance=repr(instance) if instance else None,
257
+ request_path=getattr(request, "path", None),
258
+ )
259
+ return allowed
146
260
 
147
261
  @classmethod
148
262
  def on_rest_handle_get(cls, request, instance):
@@ -206,9 +320,41 @@ class MojoModel:
206
320
  Returns:
207
321
  JsonResponse representing the list of resources.
208
322
  """
323
+ # cls.debug("on_rest_handle_list")
209
324
  if cls.rest_check_permission(request, "VIEW_PERMS"):
210
325
  return cls.on_rest_list(request)
211
- return cls.rest_error_response(request, 403, error=f"GET permission denied: {cls.__name__}")
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
212
358
 
213
359
  @classmethod
214
360
  def on_rest_handle_create(cls, request):
@@ -221,9 +367,9 @@ class MojoModel:
221
367
  Returns:
222
368
  JsonResponse representing the result of the create operation.
223
369
  """
224
- if cls.rest_check_permission(request, ["SAVE_PERMS", "VIEW_PERMS"]):
225
- instance = cls()
226
- return instance.on_rest_save_and_respond(request)
370
+ if cls.rest_check_permission(request, ["CREATE_PERMS", "SAVE_PERMS", "VIEW_PERMS"]):
371
+ instance = cls.create_from_request(request)
372
+ return instance.on_rest_get(request)
227
373
  return cls.rest_error_response(request, 403, error=f"CREATE permission denied: {cls.__name__}")
228
374
 
229
375
  @classmethod
@@ -256,10 +402,18 @@ class MojoModel:
256
402
  """
257
403
  if queryset is None:
258
404
  queryset = cls.objects.all()
259
- if request.group is not None and hasattr(cls, "group"):
260
- if "group" in request.DATA:
261
- del request.DATA["group"]
262
- queryset = queryset.filter(group=request.group)
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)
263
417
  queryset = cls.on_rest_list_filter(request, queryset)
264
418
  queryset = cls.on_rest_list_date_range_filter(request, queryset)
265
419
  queryset = cls.on_rest_list_sort(request, queryset)
@@ -268,13 +422,40 @@ class MojoModel:
268
422
  @classmethod
269
423
  def on_rest_list_response(cls, request, queryset):
270
424
  # Implement pagination
271
- page_size = request.DATA.get_typed("size", 10, int)
272
- 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)
273
427
  page_end = page_start+page_size
274
428
  paged_queryset = queryset[page_start:page_end]
275
429
  graph = request.DATA.get("graph", "list")
276
- serializer = GraphSerializer(paged_queryset, graph=graph, many=True)
277
- return serializer.to_response(request, count=queryset.count(), start=page_start, size=page_size)
430
+ format = request.DATA.get("download_format", "json")
431
+ count = queryset.count()
432
+ # Use serializer manager for optimal performance
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"))
447
+ serializer = manager.get_serializer(paged_queryset, graph=graph, many=True)
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
278
459
 
279
460
  @classmethod
280
461
  def on_rest_list_date_range_filter(cls, request, queryset):
@@ -305,6 +486,22 @@ class MojoModel:
305
486
  queryset = queryset.filter(**{f"{dr_field}__lte": dr_end})
306
487
  return queryset
307
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
+
308
505
  @classmethod
309
506
  def on_rest_list_filter(cls, request, queryset):
310
507
  """
@@ -317,23 +514,29 @@ class MojoModel:
317
514
  Returns:
318
515
  The filtered queryset.
319
516
  """
517
+ reserved_keys = ["start", "size", "download_format", "dr_start", "dr_end", "limit", "offset"]
320
518
  filters = {}
321
- for key, value in request.GET.items():
519
+ for key, value in request.QUERY_PARAMS.items():
322
520
  # Split key to check for foreign key relationships
521
+ if "." in key:
522
+ key = key.replace(".", "__")
323
523
  key_parts = key.split('__')
324
524
  field_name = key_parts[0]
525
+ if field_name in reserved_keys:
526
+ continue
325
527
  if hasattr(cls, field_name):
326
- filters[key] = value
528
+ filters[key] = cls.normalize_rest_value(request, field_name, value)
327
529
  elif field_name in cls.__rest_field_names__ and cls._meta.get_field(field_name).is_relation:
530
+ # TODO Normalize relation field values
328
531
  filters[key] = value
329
- # logger.info("filters", filters)
532
+ logger.info("filters", filters)
330
533
  queryset = cls.on_rest_list_search(request, queryset)
331
534
  return queryset.filter(**filters)
332
535
 
333
536
  @classmethod
334
537
  def on_rest_list_search(cls, request, queryset):
335
538
  """
336
- Search queryset based on 'q' param in the request for fields defined in 'SEARCH_FIELDS'.
539
+ Search queryset based on 'search' param in the request for fields defined in 'SEARCH_FIELDS'.
337
540
 
338
541
  Args:
339
542
  request: Django HTTP request object.
@@ -342,7 +545,7 @@ class MojoModel:
342
545
  Returns:
343
546
  The filtered queryset based on the search criteria.
344
547
  """
345
- search_query = request.GET.get('q', None)
548
+ search_query = request.DATA.get('search', None)
346
549
  if not search_query:
347
550
  return queryset
348
551
 
@@ -397,8 +600,7 @@ class MojoModel:
397
600
  }
398
601
  return JsonResponse(response_payload)
399
602
 
400
- @classmethod
401
- def on_rest_create(cls, request):
603
+ def on_rest_created(self):
402
604
  """
403
605
  Handle the creation of an object.
404
606
 
@@ -406,12 +608,51 @@ class MojoModel:
406
608
  request: Django HTTP request object.
407
609
 
408
610
  Returns:
409
- JsonResponse representing the newly created object.
611
+ None
612
+ """
613
+ # Perform any additional actions after object creation
614
+ pass
615
+
616
+ def on_rest_pre_save(self, changed_fields, created):
617
+ """
618
+ Handle the pre-save of an object.
619
+
620
+ Args:
621
+ created: Boolean indicating whether the object is being created.
622
+ changed_fields: Dictionary of fields that have changed.
623
+ Returns:
624
+ None
625
+ """
626
+ # Perform any additional actions before object save
627
+ pass
628
+
629
+ def on_rest_saved(self, changed_fields, created):
630
+ """
631
+ Handle the saving of an object.
632
+
633
+ Args:
634
+ created: Boolean indicating whether the object is being created.
635
+ changed_fields: Dictionary of fields that have changed.
636
+ Returns:
637
+ None
410
638
  """
411
- instance = cls()
412
- return instance.on_rest_save_and_respond(request)
639
+ # Perform any additional actions after object creation
640
+ pass
413
641
 
414
- def on_rest_get(self, request):
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
+
655
+ def on_rest_get(self, request, graph="default"):
415
656
  """
416
657
  Handle the retrieval of a single object.
417
658
 
@@ -421,38 +662,139 @@ class MojoModel:
421
662
  Returns:
422
663
  JsonResponse representing the object.
423
664
  """
424
- graph = request.GET.get("graph", "default")
425
- serializer = GraphSerializer(self, graph=graph)
665
+ graph = request.DATA.get("graph", graph)
666
+ # Use serializer manager for optimal performance
667
+ manager = get_serializer_manager()
668
+ serializer = manager.get_serializer(self, graph=graph)
426
669
  return serializer.to_response(request)
427
670
 
671
+ def _set_field_change(self, key, old_value=None, new_value=None):
672
+ if not hasattr(self, "__changed_fields__"):
673
+ self.__changed_fields__ = objict.objict()
674
+ if old_value != new_value:
675
+ self.__changed_fields__[key] = old_value
676
+
677
+ def has_field_changed(self, key):
678
+ return key in self.__changed_fields__
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
+
428
689
  def on_rest_save(self, request, data_dict):
429
690
  """
430
691
  Create a model instance from a dictionary.
431
692
 
432
693
  Args:
433
694
  request: Django HTTP request object.
695
+ data_dict: Dictionary containing the data to save.
434
696
 
435
697
  Returns:
436
698
  None
437
699
  """
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)
700
+ self.__changed_fields__ = objict.objict()
701
+ # Get fields that should not be saved
702
+ no_save_fields = self.get_rest_meta_prop("NO_SAVE_FIELDS", ["id", "pk", "created", "uuid"])
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
706
+ # Iterate through data_dict keys instead of model fields
707
+ for key, value in data_dict.items():
708
+ # Skip fields that shouldn't be saved
709
+ if key in no_save_fields:
710
+ continue
711
+ if key in post_save_actions:
712
+ post_save_data[key] = value
713
+ continue
714
+ self.on_rest_save_field(key, value, request)
715
+
716
+ created = self.pk is None
717
+ if created:
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)
721
+ if request.group and self.get_model_field("group"):
722
+ if getattr(self, "group", None) is None:
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)
728
+ self.on_rest_pre_save(self.__changed_fields__, created)
729
+ if "files" in data_dict:
730
+ self.on_rest_save_files(data_dict["files"])
451
731
  self.atomic_save()
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)
775
+
776
+ def on_rest_save_files(self, files):
777
+ for name, file in files.items():
778
+ self.on_rest_save_file(name, file)
779
+
780
+ def on_rest_save_file(self, name, file):
781
+ # Implement file saving logic here
782
+ self.debug("Finding file for field: %s", name)
783
+ field = self.get_model_field(name)
784
+ if field is None:
785
+ return
786
+ self.debug("Saving file for field: %s", name)
787
+ if field.related_model and hasattr(field.related_model, "create_from_file"):
788
+ self.debug("Found file for field: %s", name)
789
+ related_model = field.related_model
790
+ instance = related_model.create_from_file(file, name)
791
+ setattr(self, name, instance)
452
792
 
453
793
  def on_rest_save_and_respond(self, request):
454
- self.on_rest_save(request, request.DATA)
455
- return self.on_rest_get(request)
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)
456
798
 
457
799
  def on_rest_save_related_field(self, field, field_value, request):
458
800
  if isinstance(field_value, dict):
@@ -466,11 +808,23 @@ class MojoModel:
466
808
  if field.related_model.rest_check_permission(request, ["SAVE_PERMS", "VIEW_PERMS"], related_instance):
467
809
  related_instance.on_rest_save(request, field_value)
468
810
  return
469
- try:
811
+ if hasattr(field.related_model, "on_rest_related_save"):
812
+ related_instance = getattr(self, field.name)
813
+ field.related_model.on_rest_related_save(self, field.name, field_value, related_instance)
814
+ elif isinstance(field_value, int) or (isinstance(field_value, str)):
815
+ # self.debug(f"Related Model: {field.related_model.__name__}, Field Value: {field_value}")
816
+ field_value = int(field_value)
817
+ if not bool(field_value):
818
+ # None, "", 0 will set it to None
819
+ logger.info(f"Setting field {field.name} to None")
820
+ setattr(self, field.name, None)
821
+ return
822
+ field_value = int(field_value)
823
+ if (self.pk == field_value):
824
+ self.debug("Skipping self-reference")
825
+ return
470
826
  related_instance = field.related_model.objects.get(pk=field_value)
471
827
  setattr(self, field.name, related_instance)
472
- except field.related_model.DoesNotExist:
473
- pass # Skip invalid related instances
474
828
 
475
829
  def on_rest_update_jsonfield(self, field_name, field_value):
476
830
  """helper to update jsonfield by merge in changes"""
@@ -487,6 +841,18 @@ class MojoModel:
487
841
  setattr(self, field_name, existing_value)
488
842
  return existing_value
489
843
 
844
+ def on_rest_pre_delete(self):
845
+ """
846
+ Handle the pre-deletion of an object.
847
+
848
+ Args:
849
+ request: Django HTTP request object.
850
+
851
+ Returns:
852
+ JsonResponse representing the result of the pre-delete operation.
853
+ """
854
+ pass
855
+
490
856
  def on_rest_delete(self, request):
491
857
  """
492
858
  Handle the deletion of an object.
@@ -498,20 +864,23 @@ class MojoModel:
498
864
  JsonResponse representing the result of the delete operation.
499
865
  """
500
866
  try:
867
+ self.on_rest_pre_delete()
501
868
  with transaction.atomic():
502
869
  self.delete()
503
- return JsonResponse({"status": "deleted"}, status=204)
870
+ return JsonResponse({"status": "deleted"}, status=200)
504
871
  except Exception as e:
505
872
  return JsonResponse({"error": str(e)}, status=400)
506
873
 
507
874
  def to_dict(self, graph="default"):
508
- serializer = GraphSerializer(self, graph=graph)
509
- return serializer.serialize()
875
+ # Use serializer manager for optimal performance
876
+ manager = get_serializer_manager()
877
+ return manager.serialize(self, graph=graph)
510
878
 
511
879
  @classmethod
512
880
  def queryset_to_dict(cls, qset, graph="default"):
513
- serializer = GraphSerializer(qset, graph=graph)
514
- return serializer.serialize()
881
+ # Use serializer manager for optimal performance
882
+ manager = get_serializer_manager()
883
+ return manager.serialize(qset, graph=graph, many=True)
515
884
 
516
885
  def atomic_save(self):
517
886
  """
@@ -520,19 +889,79 @@ class MojoModel:
520
889
  with transaction.atomic():
521
890
  self.save()
522
891
 
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)
892
+ def report_incident(self, details, event_type="info", level=1, request=None, **context):
893
+ """
894
+ Instance-level audit/event reporting. Automatically includes model+id.
895
+ """
896
+ context = dict(context)
897
+ context.setdefault("model_name", self.__class__.get_model_string())
898
+ if hasattr(self, 'id') and self.id is not None:
899
+ context.setdefault("model_id", self.id)
900
+ self.__class__.class_report_incident(
901
+ details, event_type=event_type, level=level, request=request, **context
902
+ )
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)
528
917
 
529
- def log(self, log, kind="model_log", level="info", **kwargs):
918
+ @classmethod
919
+ def class_report_incident(cls, details, event_type="info", level=1, request=None, **context):
920
+ """
921
+ Class-level audit/event reporting.
922
+ details: Human description.
923
+ event_type: Category/kind (e.g. "permission_denied", "security_alert").
924
+ level: Numeric severity.
925
+ request: Optional HTTP request or actor.
926
+ **context: Any additional context.
927
+ """
928
+ from mojo.apps import incident
929
+ context = dict(context)
930
+ context.setdefault("model_name", cls.__name__)
931
+ incident.report_event(
932
+ details,
933
+ title=details[:80],
934
+ category=event_type,
935
+ level=level,
936
+ request=request,
937
+ **context
938
+ )
939
+
940
+ def log(self, log="", kind="model_log", level="info", **kwargs):
530
941
  return self.class_logit(ACTIVE_REQUEST, log, kind, self.id, level, **kwargs)
531
942
 
532
943
  def model_logit(self, request, log, kind="model_log", level="info", **kwargs):
533
944
  return self.class_logit(request, log, kind, self.id, level, **kwargs)
534
945
 
946
+ @classmethod
947
+ def debug(cls, log, *args):
948
+ return logger.info(log, *args)
949
+
950
+ @classmethod
951
+ def get_model_string(cls):
952
+ return f"{ cls._meta.app_label.lower()}.{cls.__name__}"
953
+
535
954
  @classmethod
536
955
  def class_logit(cls, request, log, kind="cls_log", model_id=0, level="info", **kwargs):
537
956
  from mojo.apps.logit.models import Log
538
- return Log.logit(request, log, kind, cls.__name__, model_id, level, **kwargs)
957
+ return Log.logit(request, log, kind, cls.get_model_string(), model_id, level, **kwargs)
958
+
959
+ @classmethod
960
+ def get_model_field(cls, field_name):
961
+ """
962
+ Get a model field by name.
963
+ """
964
+ try:
965
+ return cls._meta.get_field(field_name)
966
+ except Exception:
967
+ return None