django-nativemojo 0.1.10__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 (194) hide show
  1. django_nativemojo-0.1.10.dist-info/LICENSE +19 -0
  2. django_nativemojo-0.1.10.dist-info/METADATA +96 -0
  3. django_nativemojo-0.1.10.dist-info/NOTICE +8 -0
  4. django_nativemojo-0.1.10.dist-info/RECORD +194 -0
  5. django_nativemojo-0.1.10.dist-info/WHEEL +4 -0
  6. mojo/__init__.py +3 -0
  7. mojo/apps/account/__init__.py +1 -0
  8. mojo/apps/account/admin.py +91 -0
  9. mojo/apps/account/apps.py +16 -0
  10. mojo/apps/account/migrations/0001_initial.py +77 -0
  11. mojo/apps/account/migrations/0002_user_is_email_verified_user_is_phone_verified.py +23 -0
  12. mojo/apps/account/migrations/0003_group_mojo_secrets_user_mojo_secrets.py +23 -0
  13. mojo/apps/account/migrations/__init__.py +0 -0
  14. mojo/apps/account/models/__init__.py +3 -0
  15. mojo/apps/account/models/group.py +98 -0
  16. mojo/apps/account/models/member.py +95 -0
  17. mojo/apps/account/models/pkey.py +18 -0
  18. mojo/apps/account/models/user.py +211 -0
  19. mojo/apps/account/rest/__init__.py +3 -0
  20. mojo/apps/account/rest/group.py +25 -0
  21. mojo/apps/account/rest/user.py +47 -0
  22. mojo/apps/account/utils/__init__.py +0 -0
  23. mojo/apps/account/utils/jwtoken.py +72 -0
  24. mojo/apps/account/utils/passkeys.py +54 -0
  25. mojo/apps/fileman/README.md +549 -0
  26. mojo/apps/fileman/__init__.py +0 -0
  27. mojo/apps/fileman/apps.py +15 -0
  28. mojo/apps/fileman/backends/__init__.py +117 -0
  29. mojo/apps/fileman/backends/base.py +319 -0
  30. mojo/apps/fileman/backends/filesystem.py +397 -0
  31. mojo/apps/fileman/backends/s3.py +398 -0
  32. mojo/apps/fileman/examples/configurations.py +378 -0
  33. mojo/apps/fileman/examples/usage_example.py +665 -0
  34. mojo/apps/fileman/management/__init__.py +1 -0
  35. mojo/apps/fileman/management/commands/__init__.py +1 -0
  36. mojo/apps/fileman/management/commands/cleanup_expired_uploads.py +222 -0
  37. mojo/apps/fileman/models/__init__.py +7 -0
  38. mojo/apps/fileman/models/file.py +292 -0
  39. mojo/apps/fileman/models/manager.py +227 -0
  40. mojo/apps/fileman/models/render.py +0 -0
  41. mojo/apps/fileman/rest/__init__ +0 -0
  42. mojo/apps/fileman/rest/__init__.py +23 -0
  43. mojo/apps/fileman/rest/fileman.py +13 -0
  44. mojo/apps/fileman/rest/upload.py +92 -0
  45. mojo/apps/fileman/utils/__init__.py +19 -0
  46. mojo/apps/fileman/utils/upload.py +616 -0
  47. mojo/apps/incident/__init__.py +1 -0
  48. mojo/apps/incident/handlers/__init__.py +3 -0
  49. mojo/apps/incident/handlers/event_handlers.py +142 -0
  50. mojo/apps/incident/migrations/0001_initial.py +83 -0
  51. mojo/apps/incident/migrations/0002_rename_bundle_ruleset_bundle_minutes_event_hostname_and_more.py +44 -0
  52. mojo/apps/incident/migrations/0003_alter_event_model_id.py +18 -0
  53. mojo/apps/incident/migrations/0004_alter_incident_model_id.py +18 -0
  54. mojo/apps/incident/migrations/__init__.py +0 -0
  55. mojo/apps/incident/models/__init__.py +3 -0
  56. mojo/apps/incident/models/event.py +135 -0
  57. mojo/apps/incident/models/incident.py +33 -0
  58. mojo/apps/incident/models/rule.py +247 -0
  59. mojo/apps/incident/parsers/__init__.py +0 -0
  60. mojo/apps/incident/parsers/ossec/__init__.py +1 -0
  61. mojo/apps/incident/parsers/ossec/core.py +82 -0
  62. mojo/apps/incident/parsers/ossec/parsed.py +23 -0
  63. mojo/apps/incident/parsers/ossec/rules.py +124 -0
  64. mojo/apps/incident/parsers/ossec/utils.py +169 -0
  65. mojo/apps/incident/reporter.py +42 -0
  66. mojo/apps/incident/rest/__init__.py +2 -0
  67. mojo/apps/incident/rest/event.py +23 -0
  68. mojo/apps/incident/rest/ossec.py +22 -0
  69. mojo/apps/logit/__init__.py +0 -0
  70. mojo/apps/logit/admin.py +37 -0
  71. mojo/apps/logit/migrations/0001_initial.py +32 -0
  72. mojo/apps/logit/migrations/0002_log_duid_log_payload_log_username.py +28 -0
  73. mojo/apps/logit/migrations/0003_log_level.py +18 -0
  74. mojo/apps/logit/migrations/__init__.py +0 -0
  75. mojo/apps/logit/models/__init__.py +1 -0
  76. mojo/apps/logit/models/log.py +57 -0
  77. mojo/apps/logit/rest.py +9 -0
  78. mojo/apps/metrics/README.md +79 -0
  79. mojo/apps/metrics/__init__.py +12 -0
  80. mojo/apps/metrics/redis_metrics.py +331 -0
  81. mojo/apps/metrics/rest/__init__.py +1 -0
  82. mojo/apps/metrics/rest/base.py +152 -0
  83. mojo/apps/metrics/rest/db.py +0 -0
  84. mojo/apps/metrics/utils.py +227 -0
  85. mojo/apps/notify/README.md +91 -0
  86. mojo/apps/notify/README_NOTIFICATIONS.md +566 -0
  87. mojo/apps/notify/__init__.py +0 -0
  88. mojo/apps/notify/admin.py +52 -0
  89. mojo/apps/notify/handlers/__init__.py +0 -0
  90. mojo/apps/notify/handlers/example_handlers.py +516 -0
  91. mojo/apps/notify/handlers/ses/__init__.py +25 -0
  92. mojo/apps/notify/handlers/ses/bounce.py +0 -0
  93. mojo/apps/notify/handlers/ses/complaint.py +25 -0
  94. mojo/apps/notify/handlers/ses/message.py +86 -0
  95. mojo/apps/notify/management/__init__.py +0 -0
  96. mojo/apps/notify/management/commands/__init__.py +1 -0
  97. mojo/apps/notify/management/commands/process_notifications.py +370 -0
  98. mojo/apps/notify/mod +0 -0
  99. mojo/apps/notify/models/__init__.py +12 -0
  100. mojo/apps/notify/models/account.py +128 -0
  101. mojo/apps/notify/models/attachment.py +24 -0
  102. mojo/apps/notify/models/bounce.py +68 -0
  103. mojo/apps/notify/models/complaint.py +40 -0
  104. mojo/apps/notify/models/inbox.py +113 -0
  105. mojo/apps/notify/models/inbox_message.py +173 -0
  106. mojo/apps/notify/models/outbox.py +129 -0
  107. mojo/apps/notify/models/outbox_message.py +288 -0
  108. mojo/apps/notify/models/template.py +30 -0
  109. mojo/apps/notify/providers/__init__.py +0 -0
  110. mojo/apps/notify/providers/aws.py +73 -0
  111. mojo/apps/notify/rest/__init__.py +0 -0
  112. mojo/apps/notify/rest/ses.py +0 -0
  113. mojo/apps/notify/utils/__init__.py +2 -0
  114. mojo/apps/notify/utils/notifications.py +404 -0
  115. mojo/apps/notify/utils/parsing.py +202 -0
  116. mojo/apps/notify/utils/render.py +144 -0
  117. mojo/apps/tasks/README.md +118 -0
  118. mojo/apps/tasks/__init__.py +11 -0
  119. mojo/apps/tasks/manager.py +489 -0
  120. mojo/apps/tasks/rest/__init__.py +2 -0
  121. mojo/apps/tasks/rest/hooks.py +0 -0
  122. mojo/apps/tasks/rest/tasks.py +62 -0
  123. mojo/apps/tasks/runner.py +174 -0
  124. mojo/apps/tasks/tq_handlers.py +14 -0
  125. mojo/decorators/__init__.py +3 -0
  126. mojo/decorators/auth.py +25 -0
  127. mojo/decorators/cron.py +31 -0
  128. mojo/decorators/http.py +132 -0
  129. mojo/decorators/validate.py +14 -0
  130. mojo/errors.py +88 -0
  131. mojo/helpers/__init__.py +0 -0
  132. mojo/helpers/aws/__init__.py +0 -0
  133. mojo/helpers/aws/client.py +8 -0
  134. mojo/helpers/aws/s3.py +268 -0
  135. mojo/helpers/aws/setup_email.py +0 -0
  136. mojo/helpers/cron.py +79 -0
  137. mojo/helpers/crypto/__init__.py +4 -0
  138. mojo/helpers/crypto/aes.py +60 -0
  139. mojo/helpers/crypto/hash.py +59 -0
  140. mojo/helpers/crypto/privpub/__init__.py +1 -0
  141. mojo/helpers/crypto/privpub/hybrid.py +97 -0
  142. mojo/helpers/crypto/privpub/rsa.py +104 -0
  143. mojo/helpers/crypto/sign.py +36 -0
  144. mojo/helpers/crypto/too.l.py +25 -0
  145. mojo/helpers/crypto/utils.py +26 -0
  146. mojo/helpers/daemon.py +94 -0
  147. mojo/helpers/dates.py +69 -0
  148. mojo/helpers/dns/__init__.py +0 -0
  149. mojo/helpers/dns/godaddy.py +62 -0
  150. mojo/helpers/filetypes.py +128 -0
  151. mojo/helpers/logit.py +310 -0
  152. mojo/helpers/modules.py +95 -0
  153. mojo/helpers/paths.py +63 -0
  154. mojo/helpers/redis.py +10 -0
  155. mojo/helpers/request.py +89 -0
  156. mojo/helpers/request_parser.py +269 -0
  157. mojo/helpers/response.py +14 -0
  158. mojo/helpers/settings.py +146 -0
  159. mojo/helpers/sysinfo.py +140 -0
  160. mojo/helpers/ua.py +0 -0
  161. mojo/middleware/__init__.py +0 -0
  162. mojo/middleware/auth.py +26 -0
  163. mojo/middleware/logging.py +55 -0
  164. mojo/middleware/mojo.py +21 -0
  165. mojo/migrations/0001_initial.py +32 -0
  166. mojo/migrations/__init__.py +0 -0
  167. mojo/models/__init__.py +2 -0
  168. mojo/models/meta.py +262 -0
  169. mojo/models/rest.py +538 -0
  170. mojo/models/secrets.py +59 -0
  171. mojo/rest/__init__.py +1 -0
  172. mojo/rest/info.py +26 -0
  173. mojo/serializers/__init__.py +0 -0
  174. mojo/serializers/models.py +165 -0
  175. mojo/serializers/openapi.py +188 -0
  176. mojo/urls.py +38 -0
  177. mojo/ws4redis/README.md +174 -0
  178. mojo/ws4redis/__init__.py +2 -0
  179. mojo/ws4redis/client.py +283 -0
  180. mojo/ws4redis/connection.py +327 -0
  181. mojo/ws4redis/exceptions.py +32 -0
  182. mojo/ws4redis/redis.py +183 -0
  183. mojo/ws4redis/servers/__init__.py +0 -0
  184. mojo/ws4redis/servers/base.py +86 -0
  185. mojo/ws4redis/servers/django.py +171 -0
  186. mojo/ws4redis/servers/uwsgi.py +63 -0
  187. mojo/ws4redis/settings.py +45 -0
  188. mojo/ws4redis/utf8validator.py +128 -0
  189. mojo/ws4redis/websocket.py +403 -0
  190. testit/__init__.py +0 -0
  191. testit/client.py +147 -0
  192. testit/faker.py +20 -0
  193. testit/helpers.py +198 -0
  194. testit/runner.py +262 -0
mojo/models/rest.py ADDED
@@ -0,0 +1,538 @@
1
+ # from django.http import JsonResponse
2
+ from mojo.helpers.response import JsonResponse
3
+ from mojo.serializers.models import GraphSerializer
4
+ from mojo.helpers import modules
5
+ from mojo.helpers.settings import settings
6
+ from django.core.exceptions import ObjectDoesNotExist
7
+ from django.db import transaction, models as dm
8
+ import objict
9
+ from mojo.helpers import dates, logit
10
+
11
+
12
+ logger = logit.get_logger("debug", "debug.log")
13
+ ACTIVE_REQUEST = None
14
+ LOGGING_CLASS = None
15
+ MOJO_APP_STATUS_200_ON_ERROR = settings.MOJO_APP_STATUS_200_ON_ERROR
16
+
17
+ class MojoModel:
18
+ """Base model class for REST operations with GraphSerializer integration."""
19
+
20
+ @property
21
+ def active_request(self):
22
+ """Returns the active request being processed."""
23
+ return ACTIVE_REQUEST
24
+
25
+ @classmethod
26
+ def get_rest_meta_prop(cls, name, default=None):
27
+ """
28
+ Retrieve a property from the RestMeta class if it exists.
29
+
30
+ Args:
31
+ name (str or list): Name of the property to retrieve.
32
+ default: Default value to return if the property does not exist.
33
+
34
+ Returns:
35
+ The value of the requested property or the default value.
36
+ """
37
+ if getattr(cls, "RestMeta", None) is None:
38
+ return default
39
+ if isinstance(name, list):
40
+ for n in name:
41
+ res = getattr(cls.RestMeta, n, None)
42
+ if res is not None:
43
+ return res
44
+ return default
45
+ return getattr(cls.RestMeta, name, default)
46
+
47
+ @classmethod
48
+ def rest_error_response(cls, request, status=500, **kwargs):
49
+ """
50
+ Create a JsonResponse for an error.
51
+
52
+ Args:
53
+ request: Django HTTP request object.
54
+ status (int): HTTP status code for the response.
55
+ kwargs: Additional data to include in the response.
56
+
57
+ Returns:
58
+ JsonResponse representing the error.
59
+ """
60
+ payload = dict(kwargs)
61
+ payload["is_authenticated"] = request.user.is_authenticated
62
+ payload["status"] = False
63
+ if "code" not in payload:
64
+ payload["code"] = status
65
+ if MOJO_APP_STATUS_200_ON_ERROR:
66
+ status = 200
67
+ return JsonResponse(payload, status=status)
68
+
69
+ @classmethod
70
+ def on_rest_request(cls, request, pk=None):
71
+ """
72
+ Handle REST requests dynamically based on HTTP method.
73
+
74
+ Args:
75
+ request: Django HTTP request object.
76
+ pk: Primary key of the object, if available.
77
+
78
+ Returns:
79
+ JsonResponse representing the result of the request.
80
+ """
81
+ cls.__rest_field_names__ = [f.name for f in cls._meta.get_fields()]
82
+ if pk:
83
+ instance = cls.get_instance_or_404(pk)
84
+ if isinstance(instance, dict): # If it's a response, return early
85
+ return instance
86
+
87
+ if request.method == 'GET':
88
+ return cls.on_rest_handle_get(request, instance)
89
+
90
+ elif request.method in ['POST', 'PUT']:
91
+ return cls.on_rest_handle_save(request, instance)
92
+
93
+ elif request.method == 'DELETE':
94
+ return cls.on_rest_handle_delete(request, instance)
95
+ else:
96
+ return cls.on_handle_list_or_create(request)
97
+
98
+ return cls.rest_error_response(request, 500, error=f"{cls.__name__} not found")
99
+
100
+ @classmethod
101
+ def get_instance_or_404(cls, pk):
102
+ """
103
+ Helper method to get an instance or return a 404 response.
104
+
105
+ Args:
106
+ pk: Primary key of the instance to retrieve.
107
+
108
+ Returns:
109
+ The requested instance or a JsonResponse for a 404 error.
110
+ """
111
+ try:
112
+ return cls.objects.get(pk=pk)
113
+ except ObjectDoesNotExist:
114
+ return cls.rest_error_response(None, 404, error=f"{cls.__name__} not found")
115
+
116
+ @classmethod
117
+ def rest_check_permission(cls, request, permission_keys, instance=None):
118
+ """
119
+ Check permissions for a given request.
120
+
121
+ Args:
122
+ request: Django HTTP request object.
123
+ permission_keys: Keys to check for permissions.
124
+ instance: Optional instance to check instance-specific permissions.
125
+
126
+ Returns:
127
+ True if the request has the necessary permissions, otherwise False.
128
+ """
129
+ perms = cls.get_rest_meta_prop(permission_keys, [])
130
+ if perms is None or len(perms) == 0:
131
+ return True
132
+ if "all" not in perms:
133
+ if request.user is None or not request.user.is_authenticated:
134
+ return False
135
+ if instance is not None:
136
+ 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:
140
+ return True
141
+ 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)
146
+
147
+ @classmethod
148
+ def on_rest_handle_get(cls, request, instance):
149
+ """
150
+ Handle GET requests with permission checks.
151
+
152
+ Args:
153
+ request: Django HTTP request object.
154
+ instance: The instance to retrieve.
155
+
156
+ Returns:
157
+ JsonResponse representing the result of the GET request.
158
+ """
159
+ if cls.rest_check_permission(request, "VIEW_PERMS", instance):
160
+ return instance.on_rest_get(request)
161
+ return cls.rest_error_response(request, 403, error=f"GET permission denied: {cls.__name__}")
162
+
163
+ @classmethod
164
+ def on_rest_handle_save(cls, request, instance):
165
+ """
166
+ Handle POST and PUT requests with permission checks.
167
+
168
+ Args:
169
+ request: Django HTTP request object.
170
+ instance: The instance to save or update.
171
+
172
+ Returns:
173
+ JsonResponse representing the result of the save operation.
174
+ """
175
+ if cls.rest_check_permission(request, ["SAVE_PERMS", "VIEW_PERMS"], instance):
176
+ return instance.on_rest_save_and_respond(request)
177
+ return cls.rest_error_response(request, 403, error=f"{request.method} permission denied: {cls.__name__}")
178
+
179
+ @classmethod
180
+ def on_rest_handle_delete(cls, request, instance):
181
+ """
182
+ Handle DELETE requests with permission checks.
183
+
184
+ Args:
185
+ request: Django HTTP request object.
186
+ instance: The instance to delete.
187
+
188
+ Returns:
189
+ JsonResponse representing the result of the delete operation.
190
+ """
191
+ if not cls.get_rest_meta_prop("CAN_DELETE", False):
192
+ return cls.rest_error_response(request, 403, error=f"DELETE not allowed: {cls.__name__}")
193
+
194
+ if cls.rest_check_permission(request, ["DELETE_PERMS", "SAVE_PERMS", "VIEW_PERMS"], instance):
195
+ return instance.on_rest_delete(request)
196
+ return cls.rest_error_response(request, 403, error=f"DELETE permission denied: {cls.__name__}")
197
+
198
+ @classmethod
199
+ def on_rest_handle_list(cls, request):
200
+ """
201
+ Handle GET requests for listing resources with permission checks.
202
+
203
+ Args:
204
+ request: Django HTTP request object.
205
+
206
+ Returns:
207
+ JsonResponse representing the list of resources.
208
+ """
209
+ if cls.rest_check_permission(request, "VIEW_PERMS"):
210
+ return cls.on_rest_list(request)
211
+ return cls.rest_error_response(request, 403, error=f"GET permission denied: {cls.__name__}")
212
+
213
+ @classmethod
214
+ def on_rest_handle_create(cls, request):
215
+ """
216
+ Handle POST and PUT requests for creating resources with permission checks.
217
+
218
+ Args:
219
+ request: Django HTTP request object.
220
+
221
+ Returns:
222
+ JsonResponse representing the result of the create operation.
223
+ """
224
+ if cls.rest_check_permission(request, ["SAVE_PERMS", "VIEW_PERMS"]):
225
+ instance = cls()
226
+ return instance.on_rest_save_and_respond(request)
227
+ return cls.rest_error_response(request, 403, error=f"CREATE permission denied: {cls.__name__}")
228
+
229
+ @classmethod
230
+ def on_handle_list_or_create(cls, request):
231
+ """
232
+ Handle listing (GET without pk) and creating (POST/PUT without pk) operations.
233
+
234
+ Args:
235
+ request: Django HTTP request object.
236
+
237
+ Returns:
238
+ JsonResponse representing the result of the operation.
239
+ """
240
+ if request.method == 'GET':
241
+ return cls.on_rest_handle_list(request)
242
+ elif request.method in ['POST', 'PUT']:
243
+ return cls.on_rest_handle_create(request)
244
+
245
+ @classmethod
246
+ def on_rest_list(cls, request, queryset=None):
247
+ """
248
+ List objects with filtering, sorting, and pagination.
249
+
250
+ Args:
251
+ request: Django HTTP request object.
252
+ queryset: Optional initial queryset to use.
253
+
254
+ Returns:
255
+ JsonResponse representing the paginated and serialized list of objects.
256
+ """
257
+ if queryset is None:
258
+ 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)
263
+ queryset = cls.on_rest_list_filter(request, queryset)
264
+ queryset = cls.on_rest_list_date_range_filter(request, queryset)
265
+ queryset = cls.on_rest_list_sort(request, queryset)
266
+ return cls.on_rest_list_response(request, queryset)
267
+
268
+ @classmethod
269
+ def on_rest_list_response(cls, request, queryset):
270
+ # Implement pagination
271
+ page_size = request.DATA.get_typed("size", 10, int)
272
+ page_start = request.DATA.get_typed("start", 0, int)
273
+ page_end = page_start+page_size
274
+ paged_queryset = queryset[page_start:page_end]
275
+ 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)
278
+
279
+ @classmethod
280
+ def on_rest_list_date_range_filter(cls, request, queryset):
281
+ """
282
+ Filter queryset based on a date range provided in the request.
283
+
284
+ Args:
285
+ request: Django HTTP request object.
286
+ queryset: The queryset to filter.
287
+
288
+ Returns:
289
+ The filtered queryset.
290
+ """
291
+ dr_field = request.DATA.get("dr_field", "created")
292
+ dr_start = request.DATA.get("dr_start")
293
+ dr_end = request.DATA.get("dr_end")
294
+
295
+ if dr_start:
296
+ dr_start = dates.parse_datetime(dr_start)
297
+ if request.group:
298
+ dr_start = request.group.get_local_time(dr_start)
299
+ queryset = queryset.filter(**{f"{dr_field}__gte": dr_start})
300
+
301
+ if dr_end:
302
+ dr_end = dates.parse_datetime(dr_end)
303
+ if request.group:
304
+ dr_end = request.group.get_local_time(dr_end)
305
+ queryset = queryset.filter(**{f"{dr_field}__lte": dr_end})
306
+ return queryset
307
+
308
+ @classmethod
309
+ def on_rest_list_filter(cls, request, queryset):
310
+ """
311
+ Apply filtering logic based on request parameters, including foreign key fields.
312
+
313
+ Args:
314
+ request: Django HTTP request object.
315
+ queryset: The queryset to filter.
316
+
317
+ Returns:
318
+ The filtered queryset.
319
+ """
320
+ filters = {}
321
+ for key, value in request.GET.items():
322
+ # Split key to check for foreign key relationships
323
+ key_parts = key.split('__')
324
+ field_name = key_parts[0]
325
+ if hasattr(cls, field_name):
326
+ filters[key] = value
327
+ elif field_name in cls.__rest_field_names__ and cls._meta.get_field(field_name).is_relation:
328
+ filters[key] = value
329
+ # logger.info("filters", filters)
330
+ queryset = cls.on_rest_list_search(request, queryset)
331
+ return queryset.filter(**filters)
332
+
333
+ @classmethod
334
+ def on_rest_list_search(cls, request, queryset):
335
+ """
336
+ Search queryset based on 'q' param in the request for fields defined in 'SEARCH_FIELDS'.
337
+
338
+ Args:
339
+ request: Django HTTP request object.
340
+ queryset: The queryset to search.
341
+
342
+ Returns:
343
+ The filtered queryset based on the search criteria.
344
+ """
345
+ search_query = request.GET.get('q', None)
346
+ if not search_query:
347
+ return queryset
348
+
349
+ search_fields = getattr(cls.RestMeta, 'SEARCH_FIELDS', None)
350
+ if search_fields is None:
351
+ search_fields = [
352
+ field.name for field in cls._meta.get_fields()
353
+ if field.get_internal_type() in ["CharField", "TextField"]
354
+ ]
355
+
356
+ query_filters = dm.Q()
357
+ for field in search_fields:
358
+ query_filters |= dm.Q(**{f"{field}__icontains": search_query})
359
+
360
+ logger.info("search_filters", query_filters)
361
+ return queryset.filter(query_filters)
362
+
363
+ @classmethod
364
+ def on_rest_list_sort(cls, request, queryset):
365
+ """
366
+ Apply sorting to the queryset.
367
+
368
+ Args:
369
+ request: Django HTTP request object.
370
+ queryset: The queryset to sort.
371
+
372
+ Returns:
373
+ The sorted queryset.
374
+ """
375
+ sort_field = request.DATA.pop("sort", "-id")
376
+ if sort_field.lstrip('-') in cls.__rest_field_names__:
377
+ return queryset.order_by(sort_field)
378
+ return queryset
379
+
380
+ @classmethod
381
+ def return_rest_response(cls, data, flat=False):
382
+ """
383
+ Return the passed in data as a JSONResponse with root values of status=True and data=.
384
+
385
+ Args:
386
+ data: Data to include in the response.
387
+
388
+ Returns:
389
+ JsonResponse representing the data.
390
+ """
391
+ if flat:
392
+ response_payload = data
393
+ else:
394
+ response_payload = {
395
+ "status": True,
396
+ "data": data
397
+ }
398
+ return JsonResponse(response_payload)
399
+
400
+ @classmethod
401
+ def on_rest_create(cls, request):
402
+ """
403
+ Handle the creation of an object.
404
+
405
+ Args:
406
+ request: Django HTTP request object.
407
+
408
+ Returns:
409
+ JsonResponse representing the newly created object.
410
+ """
411
+ instance = cls()
412
+ return instance.on_rest_save_and_respond(request)
413
+
414
+ def on_rest_get(self, request):
415
+ """
416
+ Handle the retrieval of a single object.
417
+
418
+ Args:
419
+ request: Django HTTP request object.
420
+
421
+ Returns:
422
+ JsonResponse representing the object.
423
+ """
424
+ graph = request.GET.get("graph", "default")
425
+ serializer = GraphSerializer(self, graph=graph)
426
+ return serializer.to_response(request)
427
+
428
+ def on_rest_save(self, request, data_dict):
429
+ """
430
+ Create a model instance from a dictionary.
431
+
432
+ Args:
433
+ request: Django HTTP request object.
434
+
435
+ Returns:
436
+ None
437
+ """
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)
451
+ self.atomic_save()
452
+
453
+ def on_rest_save_and_respond(self, request):
454
+ self.on_rest_save(request, request.DATA)
455
+ return self.on_rest_get(request)
456
+
457
+ def on_rest_save_related_field(self, field, field_value, request):
458
+ if isinstance(field_value, dict):
459
+ # we want to check if we have an existing field and if so we will update it after security
460
+ related_instance = getattr(self, field.name)
461
+ if related_instance is None:
462
+ # skip None fields for now
463
+ # FUTURE look at creating a new instance
464
+ return
465
+ if hasattr(field.related_model, "rest_check_permission"):
466
+ if field.related_model.rest_check_permission(request, ["SAVE_PERMS", "VIEW_PERMS"], related_instance):
467
+ related_instance.on_rest_save(request, field_value)
468
+ return
469
+ try:
470
+ related_instance = field.related_model.objects.get(pk=field_value)
471
+ setattr(self, field.name, related_instance)
472
+ except field.related_model.DoesNotExist:
473
+ pass # Skip invalid related instances
474
+
475
+ def on_rest_update_jsonfield(self, field_name, field_value):
476
+ """helper to update jsonfield by merge in changes"""
477
+ existing_value = getattr(self, field_name, {})
478
+ # logger.info("JSONField", existing_value, "New Value", field_value)
479
+ if isinstance(field_value, dict) and isinstance(existing_value, dict):
480
+ merged_value = objict.merge_dicts(existing_value, field_value)
481
+ setattr(self, field_name, merged_value)
482
+
483
+ def jsonfield_as_objict(self, field_name):
484
+ existing_value = getattr(self, field_name, {})
485
+ if not isinstance(existing_value, objict.objict):
486
+ existing_value = objict.objict.fromdict(existing_value)
487
+ setattr(self, field_name, existing_value)
488
+ return existing_value
489
+
490
+ def on_rest_delete(self, request):
491
+ """
492
+ Handle the deletion of an object.
493
+
494
+ Args:
495
+ request: Django HTTP request object.
496
+
497
+ Returns:
498
+ JsonResponse representing the result of the delete operation.
499
+ """
500
+ try:
501
+ with transaction.atomic():
502
+ self.delete()
503
+ return JsonResponse({"status": "deleted"}, status=204)
504
+ except Exception as e:
505
+ return JsonResponse({"error": str(e)}, status=400)
506
+
507
+ def to_dict(self, graph="default"):
508
+ serializer = GraphSerializer(self, graph=graph)
509
+ return serializer.serialize()
510
+
511
+ @classmethod
512
+ def queryset_to_dict(cls, qset, graph="default"):
513
+ serializer = GraphSerializer(qset, graph=graph)
514
+ return serializer.serialize()
515
+
516
+ def atomic_save(self):
517
+ """
518
+ Save the object atomically to the database.
519
+ """
520
+ with transaction.atomic():
521
+ self.save()
522
+
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)
528
+
529
+ def log(self, log, kind="model_log", level="info", **kwargs):
530
+ return self.class_logit(ACTIVE_REQUEST, log, kind, self.id, level, **kwargs)
531
+
532
+ def model_logit(self, request, log, kind="model_log", level="info", **kwargs):
533
+ return self.class_logit(request, log, kind, self.id, level, **kwargs)
534
+
535
+ @classmethod
536
+ def class_logit(cls, request, log, kind="cls_log", model_id=0, level="info", **kwargs):
537
+ from mojo.apps.logit.models import Log
538
+ return Log.logit(request, log, kind, cls.__name__, model_id, level, **kwargs)
mojo/models/secrets.py ADDED
@@ -0,0 +1,59 @@
1
+ from django.db import models
2
+ from mojo.helpers import crypto
3
+ from mojo.helpers.settings import settings
4
+ from objict import objict, merge_dicts
5
+
6
+
7
+ class MojoSecrets(models.Model):
8
+ """Base model class for adding secrets to a model"""
9
+ class Meta:
10
+ abstract = True
11
+
12
+ mojo_secrets = models.TextField(blank=True, null=True, default=None)
13
+ _exposed_secrets = None
14
+
15
+ def set_secrets(self, value):
16
+ self._exposed_secrets = merge_dicts(self.secrets, value)
17
+
18
+ def set_secret(self, key, value):
19
+ self.secrets[key] = value
20
+
21
+ def get_secret(self, key, default=None):
22
+ return self.secrets.get(key, default)
23
+
24
+ def clear_secrets(self):
25
+ self.mojo_secrets = None
26
+ self._exposed_secrets = objict()
27
+
28
+ @property
29
+ def secrets(self):
30
+ if self._exposed_secrets is not None:
31
+ return self._exposed_secrets
32
+ if self.mojo_secrets is None:
33
+ self._exposed_secrets = objict()
34
+ return self._exposed_secrets
35
+ if self._exposed_secrets is None:
36
+ self._exposed_secrets = crypto.decrypt(self.mojo_secrets, self._get_secrets_password(), False)
37
+ return self._exposed_secrets
38
+
39
+ def _get_secrets_password(self):
40
+ # override this to create your own secrets password
41
+ salt = f"{self.pk}{self.__class__.__name__}"
42
+ if hasattr(self, 'created'):
43
+ return f"{self.created}{salt}"
44
+ return salt
45
+
46
+ 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
51
+
52
+ def save(self, *args, **kwargs):
53
+ if self.pk is not None:
54
+ self.save_secrets()
55
+ super().save(*args, **kwargs)
56
+ else:
57
+ super().save(*args, **kwargs)
58
+ self.save_secrets()
59
+ super().save()
mojo/rest/__init__.py ADDED
@@ -0,0 +1 @@
1
+ from .info import *
mojo/rest/info.py ADDED
@@ -0,0 +1,26 @@
1
+ from mojo import decorators as md
2
+ # from django.http import JsonResponse
3
+ from mojo.helpers.response import JsonResponse
4
+ from mojo.helpers.settings import settings
5
+ import mojo
6
+ import django
7
+
8
+ @md.GET('version')
9
+ def rest_version(request):
10
+ return JsonResponse(dict(status=True, version=settings.VERSION, ip=request.ip))
11
+
12
+
13
+ @md.GET('versions')
14
+ def rest_versions(request):
15
+ import sys
16
+ return JsonResponse(dict(status=True, version={
17
+ "mojo": mojo.__version__,
18
+ "project": settings.VERSION,
19
+ "django": django.__version__,
20
+ "python": sys.version.split(' ')[0]
21
+ }))
22
+
23
+
24
+ @md.GET('myip')
25
+ def rest_my_ip(request):
26
+ return JsonResponse(dict(status=True, ip=request.ip))
File without changes