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
@@ -1,6 +1,10 @@
1
+ import os
2
+ import boto3
3
+ from botocore.exceptions import ClientError
1
4
  from django.db import models
2
5
  from mojo.models import MojoModel, MojoSecrets
3
-
6
+ from urllib.parse import urlparse
7
+ from mojo.helpers.settings import settings
4
8
 
5
9
  class FileManager(MojoSecrets, MojoModel):
6
10
  """
@@ -11,7 +15,8 @@ class FileManager(MojoSecrets, MojoModel):
11
15
  CAN_SAVE = CAN_CREATE = True
12
16
  CAN_DELETE = True
13
17
  DEFAULT_SORT = "-id"
14
- VIEW_PERMS = ["view_fileman"]
18
+ POST_SAVE_ACTIONS = ["test_connection", "fix_cors", "clone", "check_cors"]
19
+ VIEW_PERMS = ["view_fileman", "manage_files"]
15
20
  SEARCH_FIELDS = ["name", "backend_type", "description"]
16
21
  SEARCH_TERMS = [
17
22
  "name", "backend_type", "description",
@@ -19,12 +24,21 @@ class FileManager(MojoSecrets, MojoModel):
19
24
 
20
25
  GRAPHS = {
21
26
  "default": {
27
+ "extra": ["aws_region", "aws_key", "aws_secret_masked", "allowed_origins"],
28
+ "fields": [
29
+ "created", "id", "name", "backend_type", "backend_url",
30
+ "is_active", "is_default"],
22
31
  "graphs": {
32
+ "user": "basic",
23
33
  "group": "basic"
24
34
  }
25
35
  },
26
36
  "list": {
37
+ "extra": ["aws_region", "aws_key", "aws_secret_masked", "allowed_origins"],
38
+ "fields": ["created", "id", "name", "backend_type", "backend_url",
39
+ "is_active", "is_default"],
27
40
  "graphs": {
41
+ "user": "basic",
28
42
  "group": "basic"
29
43
  }
30
44
  }
@@ -58,6 +72,16 @@ class FileManager(MojoSecrets, MojoModel):
58
72
  help_text="Group that owns this file manager configuration"
59
73
  )
60
74
 
75
+ user = models.ForeignKey(
76
+ "account.User",
77
+ related_name="file_managers",
78
+ null=True,
79
+ blank=True,
80
+ default=None,
81
+ on_delete=models.CASCADE,
82
+ help_text="User that owns this file manager configuration"
83
+ )
84
+
61
85
  name = models.CharField(
62
86
  max_length=255,
63
87
  db_index=True,
@@ -88,7 +112,7 @@ class FileManager(MojoSecrets, MojoModel):
88
112
  )
89
113
 
90
114
  max_file_size = models.BigIntegerField(
91
- default=100 * 1024 * 1024, # 100MB default
115
+ default=1000 * 1024 * 1024, # 100MB default
92
116
  help_text="Maximum file size in bytes (0 for unlimited)"
93
117
  )
94
118
 
@@ -114,6 +138,19 @@ class FileManager(MojoSecrets, MojoModel):
114
138
  help_text="Whether this is the default file manager for the group or user"
115
139
  )
116
140
 
141
+ is_public = models.BooleanField(
142
+ default=True,
143
+ help_text="Whether this allows public access to the files"
144
+ )
145
+
146
+ parent = models.ForeignKey(
147
+ 'self',
148
+ on_delete=models.CASCADE,
149
+ null=True,
150
+ blank=True,
151
+ help_text="Used if this file manager is a child of another file manager, and inherits settings from its parent"
152
+ )
153
+
117
154
  class Meta:
118
155
  unique_together = [
119
156
  ['group', 'name'],
@@ -131,16 +168,115 @@ class FileManager(MojoSecrets, MojoModel):
131
168
 
132
169
  def get_setting(self, key, default=None):
133
170
  """Get a specific setting value"""
134
- return self.get_secret(key, default)
171
+ value = self.get_secret(key, default)
172
+ if value is None:
173
+ value = self.primary_parent.get_secret(key, default)
174
+ return value
135
175
 
136
176
  def set_setting(self, key, value):
137
177
  """Set a specific setting value"""
138
178
  self.set_secret(key, value)
139
179
 
180
+ def set_settings(self, value):
181
+ """Set a specific setting value"""
182
+ self.set_secrets(value)
183
+
184
+ def set_backend_url(self, url, *args):
185
+ """Set the backend URL"""
186
+ self.backend_url = os.path.join(url, *args)
187
+ self.backend_type = self.backend_url.split(':')[0]
188
+
189
+ def _update_default(self):
190
+ if self.is_default:
191
+ if self.pk is None:
192
+ FileManager.objects.filter(
193
+ group=self.group,
194
+ user=self.user,
195
+ is_default=True
196
+ ).update(is_default=False)
197
+ else:
198
+ FileManager.objects.filter(
199
+ group=self.group,
200
+ user=self.user,
201
+ is_default=True
202
+ ).exclude(pk=self.pk).update(is_default=False)
203
+
204
+ _backend = None
205
+
206
+ @property
207
+ def aws_key(self):
208
+ return self.get_secret('aws_key')
209
+
210
+ @property
211
+ def aws_secret(self):
212
+ return self.get_secret('aws_secret')
213
+
214
+ @property
215
+ def aws_secret_masked(self):
216
+ secret = self.get_secret('aws_secret', '')
217
+ if len(secret) > 4:
218
+ return '*' * (len(secret) - 4) + secret[-4:]
219
+ return secret
220
+
221
+ @property
222
+ def aws_region(self):
223
+ return self.get_secret('aws_region')
224
+
225
+ @property
226
+ def is_verified(self):
227
+ return self.status in ["verified", "ready"]
228
+
229
+ def set_aws_key(self, key):
230
+ self.set_secret('aws_key', key)
231
+
232
+ def set_aws_secret(self, secret):
233
+ self.set_secret('aws_secret', secret)
234
+
235
+ def set_aws_region(self, secret):
236
+ self.set_secret('aws_region', secret)
237
+
238
+ def set_allowed_origins(self, origins):
239
+ if isinstance(origins, str) and "," in origins:
240
+ origins = [origin.strip() for origin in origins.split(',')]
241
+ self.set_secret('allowed_origins', origins)
242
+
243
+ @property
244
+ def allowed_origins(self):
245
+ return self.get_secret('allowed_origins')
246
+
247
+ @property
248
+ def backend(self):
249
+ """Get the backend instance"""
250
+ from mojo.apps.fileman import backends
251
+ if not self._backend:
252
+ self._backend = backends.get_backend(self)
253
+ return self._backend
254
+
140
255
  @property
141
256
  def settings(self):
142
257
  return self.secrets
143
258
 
259
+ @property
260
+ def primary_settings(self):
261
+ return self.primary_parent.secrets
262
+
263
+ @property
264
+ def primary_parent(self):
265
+ parent = self
266
+ while parent.parent:
267
+ parent = parent.parent
268
+ return parent
269
+
270
+ @property
271
+ def root_path(self):
272
+ purl = urlparse(self.backend_url)
273
+ return purl.path.lstrip('/')
274
+
275
+ @property
276
+ def root_location(self):
277
+ purl = urlparse(self.backend_url)
278
+ return purl.netloc
279
+
144
280
  @property
145
281
  def is_file_system(self):
146
282
  return self.backend_type == self.FILE_SYSTEM
@@ -185,43 +321,303 @@ class FileManager(MojoSecrets, MojoModel):
185
321
  return True
186
322
  return mime_type.lower() in [mt.lower() for mt in self.allowed_mime_types]
187
323
 
188
- def save(self, *args, **kwargs):
189
- """Custom save to enforce only one default per group"""
190
- if self.is_default:
191
- # Set all other file managers in the same group to not default
192
- FileManager.objects.filter(
193
- group=self.group,
194
- is_default=True
195
- ).exclude(pk=self.pk).update(is_default=False)
324
+ def on_rest_created(self):
325
+ self._update_default()
326
+
327
+ def on_rest_pre_save(self, changed_fields, created):
328
+ self._update_default()
329
+ if not self.name:
330
+ self.name = self.generate_name()
331
+ if created:
332
+ if not self.aws_region:
333
+ self.set_aws_region(settings.get("AWS_REGION", "us-east-1"))
334
+ if not self.aws_key:
335
+ self.set_aws_key(settings.get("AWS_KEY", None))
336
+ if not self.aws_secret:
337
+ self.set_aws_secret(settings.get("AWS_SECRET", None))
338
+ if created or "is_default" in changed_fields:
339
+ self._update_default()
340
+
341
+ def on_rest_saved(self, changed_fields, created):
342
+ self._update_default()
343
+ if not self.name:
344
+ self.name = self.generate_name()
345
+ if "is_public" in changed_fields or created:
346
+ if self.is_public:
347
+ self.backend.make_path_public()
348
+ else:
349
+ self.backend.make_path_private()
350
+
351
+ def generate_name(self):
352
+ if self.user and self.group:
353
+ return f"{self.user.username}@{self.group.name}'s {self.backend_type} FileManager"
354
+ elif self.user:
355
+ return f"{self.user.username}'s {self.backend_type} FileManager"
356
+ elif self.group:
357
+ return f"{self.group.name}'s {self.backend_type} FileManager"
358
+ return f"{self.backend_type} FileManager"
359
+
360
+ def on_action_test_connection(self, value):
361
+ try:
362
+ self.backend.test_connection()
363
+ return dict(status=True)
364
+ except Exception as e:
365
+ return dict(status=False, error=str(e))
366
+
367
+ def on_action_fix_cors(self, value):
368
+ try:
369
+ if not self.is_s3:
370
+ return dict(status=False, error="CORS management is only supported for S3 backends.")
371
+ # Validate connectivity first
372
+ self.backend.test_connection()
373
+ allowed_origins = self._resolve_allowed_origins_from_value_or_settings(value or {})
374
+ result = self.update_cors(allowed_origins)
375
+ return dict(status=True, result=result)
376
+ except Exception as e:
377
+ return dict(status=False, error=str(e))
378
+
379
+ def on_action_check_cors(self, value):
380
+ try:
381
+ if not self.is_s3:
382
+ return dict(status=False, error="CORS management is only supported for S3 backends.")
383
+ self.backend.test_connection()
384
+ # allowed_origins = self._resolve_allowed_origins_from_value_or_settings(value or {})
385
+ result = self.check_cors_config(allowed_origins=self.allowed_origins)
386
+ return dict(status=True, result=result)
387
+ except Exception as e:
388
+ return dict(status=False, error=str(e))
389
+
390
+ def on_action_clone(self, value):
391
+ secrets = self.secrets
392
+ new_manager = FileManager(user=self.user, group=self.group)
393
+ new_manager.name = f"Clone of {self.name}"
394
+ new_manager.backend_url = self.backend_url
395
+ new_manager.backend_type = self.backend_type
396
+ new_manager.set_secrets(secrets)
397
+ new_manager.save()
398
+ return dict(status=True, id=new_manager.id)
399
+
400
+ def fix_cors(self):
401
+ """
402
+ Ensure bucket CORS allows direct uploads from configured origins.
403
+ This uses manager settings and does not require manual AWS console changes.
404
+ """
405
+ if not self.is_s3:
406
+ return
407
+ allowed_origins = self._resolve_allowed_origins_from_value_or_settings({})
408
+ self.update_cors(allowed_origins)
409
+
410
+ # --- CORS helpers for S3 direct upload ---
411
+ def _s3_client(self):
412
+ if not self.is_s3:
413
+ raise ValueError("CORS management is only supported for S3 backends.")
414
+ session = boto3.Session(
415
+ aws_access_key_id=self.aws_key,
416
+ aws_secret_access_key=self.aws_secret,
417
+ region_name=self.aws_region or "us-east-1",
418
+ )
419
+ endpoint_url = self.get_setting("endpoint_url", None)
420
+ return session.client("s3", endpoint_url=endpoint_url)
421
+
422
+ def _resolve_allowed_origins_from_value_or_settings(self, value):
423
+ """
424
+ Resolve a list of allowed origins from action value or global settings.
425
+ Accepts 'origins', 'allowed_origins', 'domains', or 'list_of_domains' keys.
426
+ Falls back to settings such as CORS_ALLOWED_ORIGINS, ALLOWED_ORIGINS, FRONTEND_ORIGIN/URL.
427
+ """
428
+ origins = []
429
+
430
+ if isinstance(value, dict):
431
+ for key in ("origins", "allowed_origins", "domains", "list_of_domains"):
432
+ v = value.get(key)
433
+ if v:
434
+ if isinstance(v, str):
435
+ origins.extend([s.strip() for s in v.split(",") if s.strip()])
436
+ elif isinstance(v, (list, tuple)):
437
+ origins.extend([str(s).strip() for s in v if str(s).strip()])
438
+ break
439
+
440
+ for key in ("CORS_ALLOWED_ORIGINS", "ALLOWED_ORIGINS"):
441
+ v = settings.get(key)
442
+ if v:
443
+ if isinstance(v, str):
444
+ origins.extend([s.strip() for s in v.split(",") if s.strip()])
445
+ elif isinstance(v, (list, tuple)):
446
+ origins.extend([str(s).strip() for s in v if str(s).strip()])
447
+
448
+ for key in ("FRONTEND_ORIGIN", "FRONTEND_URL", "SITE_URL", "BASE_URL"):
449
+ v = settings.get(key)
450
+ if v:
451
+ origins.append(str(v).strip())
452
+
453
+ # Normalize: dedupe, drop trailing slash
454
+ cleaned = []
455
+ seen = set()
456
+ for o in origins:
457
+ if not o:
458
+ continue
459
+ if o.endswith("/"):
460
+ o = o[:-1]
461
+ if o not in seen:
462
+ seen.add(o)
463
+ cleaned.append(o)
464
+
465
+ if not cleaned:
466
+ raise ValueError("No allowed origins provided. Please pass at least one origin.")
467
+ return cleaned
468
+
469
+ def check_cors_config(self, allowed_origins=None, required_methods=None, required_headers=None):
470
+ """
471
+ Check the current CORS configuration to ensure it supports direct uploads.
472
+ Note: S3 CORS is bucket-wide. Prefix-level restriction must be enforced by IAM/policy and presigned URLs.
473
+ """
474
+ if not self.is_s3:
475
+ raise ValueError("CORS management is only supported for S3 backends.")
476
+
477
+ s3 = self._s3_client()
478
+ bucket = self.root_location
479
+
480
+ try:
481
+ resp = s3.get_bucket_cors(Bucket=bucket)
482
+ config = resp
483
+ except ClientError as e:
484
+ if e.response.get("Error", {}).get("Code") == "NoSuchCORSConfiguration":
485
+ return {"ok": False, "issues": ["No CORS configuration set on this bucket."], "config": None}
486
+ return {"ok": False, "issues": [str(e)], "config": None}
487
+
488
+ if allowed_origins is None:
489
+ allowed_origins = self._resolve_allowed_origins_from_value_or_settings({})
490
+ if not allowed_origins:
491
+ raise ValueError("No allowed origins provided. Please pass at least one origin.")
492
+
493
+ required_methods = [m.upper() for m in (required_methods or ["GET", "PUT", "POST", "HEAD"])]
494
+ required_headers = [h.lower() for h in (required_headers or ["content-type"])]
495
+
496
+ rules = config.get("CORSRules", [])
497
+ issues = []
498
+
499
+ def origin_covered(origin: str) -> bool:
500
+ for r in rules:
501
+ origins = r.get("AllowedOrigins", [])
502
+ if "*" in origins or origin in origins:
503
+ methods = [m.upper() for m in r.get("AllowedMethods", [])]
504
+ if not all(m in methods for m in required_methods):
505
+ continue
506
+ headers = [h.lower() for h in r.get("AllowedHeaders", [])]
507
+ if "*" in headers or all(h in headers for h in required_headers):
508
+ return True
509
+ return False
510
+
511
+ for origin in allowed_origins:
512
+ if not origin_covered(origin):
513
+ issues.append(f"Origin not covered for direct upload: {origin}")
514
+
515
+ return {"ok": len(issues) == 0, "issues": issues, "config": config}
516
+
517
+ def update_cors(self, allowed_origins, merge=True, allowed_methods=None, allowed_headers=None, expose_headers=None, max_age_seconds=3000):
518
+ """
519
+ Update bucket CORS to support direct uploads from allowed_origins.
520
+ If merge=True, append our rule to any existing rules; otherwise replace entirely.
521
+ """
522
+ if not self.is_s3:
523
+ raise ValueError("CORS management is only supported for S3 backends.")
524
+
525
+ s3 = self._s3_client()
526
+ bucket = self.root_location
527
+ if not allowed_origins:
528
+ raise ValueError("No allowed origins provided. Please pass at least one origin.")
529
+
530
+ if allowed_methods is None:
531
+ allowed_methods = ["POST", "HEAD"] if getattr(self.backend, "server_side_encryption", None) else ["PUT", "HEAD"]
532
+ allowed_methods = [m.upper() for m in allowed_methods]
533
+ allowed_headers = [h for h in (allowed_headers or ["*"])]
534
+ expose_headers = expose_headers or ["ETag", "x-amz-request-id", "x-amz-id-2", "x-amz-version-id"]
535
+
536
+ desired = {
537
+ "CORSRules": [
538
+ {
539
+ "AllowedOrigins": allowed_origins,
540
+ "AllowedMethods": allowed_methods,
541
+ "AllowedHeaders": allowed_headers,
542
+ "ExposeHeaders": expose_headers,
543
+ "MaxAgeSeconds": max_age_seconds,
544
+ }
545
+ ]
546
+ }
196
547
 
197
- super().save(*args, **kwargs)
548
+ # If already compliant, no change
549
+ verify_required_headers = [] if getattr(self.backend, "server_side_encryption", None) else ["content-type"]
550
+ check = self.check_cors_config(allowed_origins, required_methods=allowed_methods, required_headers=verify_required_headers)
551
+ if check["ok"]:
552
+ return {"changed": False, "message": "Existing CORS already supports direct uploads.", "current": check["config"]}
553
+
554
+ current = None
555
+ try:
556
+ current = s3.get_bucket_cors(Bucket=bucket)
557
+ except ClientError as e:
558
+ if e.response.get("Error", {}).get("Code") != "NoSuchCORSConfiguration":
559
+ raise
560
+
561
+ if merge and current:
562
+ merged = {"CORSRules": current.get("CORSRules", []) + desired["CORSRules"]}
563
+ s3.put_bucket_cors(Bucket=bucket, CORSConfiguration=merged)
564
+ applied = merged
565
+ else:
566
+ s3.put_bucket_cors(Bucket=bucket, CORSConfiguration=desired)
567
+ applied = desired
568
+
569
+ verify_required_headers = [] if getattr(self.backend, "server_side_encryption", None) else ["content-type"]
570
+ verify = self.check_cors_config(allowed_origins, required_methods=allowed_methods, required_headers=verify_required_headers)
571
+ return {"changed": True, "applied": applied, "verified": verify["ok"], "post_update_issues": verify["issues"]}
198
572
 
199
573
  @classmethod
200
574
  def get_from_request(cls, request):
201
575
  """Get the file manager from the request"""
202
- if request.DATA.fileman:
203
- return cls.objects.get(pk=request.DATA.fileman)
204
- if request.DATA.use_groups_fileman:
576
+ if request.DATA.get(["fileman", "filemanager"]):
577
+ return cls.objects.get(pk=request.DATA.get(["fileman", "filemanager"]))
578
+ if request.DATA.use_groups_fileman and request.group:
205
579
  return cls.get_for_user_group(group=request.group)
206
580
  return cls.get_for_user_group(user=request.user, group=request.group)
207
581
 
208
582
  @classmethod
209
- def get_for_user_group(cls, user=None, group=None):
210
- """Get the file manager from the user and/or group"""
211
- file_manager = None
212
- if not file_manager and user:
213
- file_manager = cls.objects.filter(
214
- user=user, group=group, is_default=True
215
- ).first()
216
-
217
- if not file_manager and group:
218
- file_manager = cls.objects.filter(
219
- group=group, is_default=True
220
- ).first()
583
+ def get_for_user(cls, user, group=None):
584
+ file_manager = cls.objects.filter(
585
+ user=user, group=group, is_default=True, is_active=True
586
+ ).first()
587
+ if file_manager is None:
588
+ if group:
589
+ sys_manager = cls.get_for_group(group=group)
590
+ else:
591
+ sys_manager = cls.objects.filter(user=None, group=None, is_default=True, is_active=True).first()
592
+ if sys_manager is not None:
593
+ file_manager = cls(user=user, is_default=True, group=group, parent=sys_manager)
594
+ file_manager.set_backend_url(sys_manager.backend_url, user.uuid.hex)
595
+ file_manager.save()
596
+ return file_manager
221
597
 
222
- if not file_manager:
223
- file_manager = cls.objects.filter(
224
- group=None, user=None, is_default=True
598
+ @classmethod
599
+ def get_for_group(cls, group=None):
600
+ file_manager = cls.objects.filter(
601
+ user=None, group=group, is_default=True, is_active=True
602
+ ).first()
603
+ if file_manager is None:
604
+ sys_manager = cls.objects.filter(
605
+ user=None, group=None, is_default=True, is_active=True
225
606
  ).first()
607
+ if sys_manager is not None:
608
+ file_manager = cls(group=group, is_default=True, user=None, parent=sys_manager)
609
+ file_manager.set_backend_url(sys_manager.backend_url, group.uuid.hex)
610
+ file_manager.save()
611
+ return file_manager
226
612
 
613
+ @classmethod
614
+ def get_for_user_group(cls, user=None, group=None):
615
+ """Get the file manager from the user and/or group"""
616
+ file_manager = None
617
+ if user and group is None:
618
+ file_manager = cls.get_for_user(user=user)
619
+ if not file_manager and group and user is None:
620
+ file_manager = cls.get_for_user_group(group=group)
621
+ if not file_manager and group and user:
622
+ file_manager = cls.get_for_user(user=user, group=group)
227
623
  return file_manager
@@ -0,0 +1,118 @@
1
+ from django.db import models
2
+ from mojo.models import MojoModel
3
+ import uuid
4
+ import hashlib
5
+ import mimetypes
6
+ from datetime import datetime
7
+ import os
8
+ from mojo.apps.fileman import utils
9
+ from mojo.apps.fileman.models import FileManager
10
+ from typing import Text
11
+
12
+
13
+ class FileRendition(models.Model, MojoModel):
14
+ """
15
+ File model representing uploaded files with metadata and storage information
16
+ """
17
+
18
+ class RestMeta:
19
+ CAN_SAVE = CAN_CREATE = True
20
+ CAN_DELETE = True
21
+ DEFAULT_SORT = "-created"
22
+ VIEW_PERMS = ["view_fileman", "manage_files"]
23
+ SEARCH_FIELDS = ["filename", "content_type"]
24
+ SEARCH_TERMS = [
25
+ "filename", "content_type",
26
+ ("group", "group__name"),
27
+ ("file_manager", "file_manager__name")]
28
+
29
+ GRAPHS = {
30
+ "upload": {
31
+ "fields": ["id", "filename", "content_type", "file_size"],
32
+ },
33
+ "default": {
34
+ "extra": ["url"],
35
+ },
36
+ "list": {
37
+ "extra": ["url"],
38
+ }
39
+ }
40
+
41
+ # Upload status choices
42
+ PENDING = 'pending'
43
+ RENDERING = 'rendering'
44
+ COMPLETED = 'completed'
45
+ FAILED = 'failed'
46
+ EXPIRED = 'expired'
47
+
48
+ created = models.DateTimeField(auto_now_add=True, editable=False, db_index=True)
49
+ modified = models.DateTimeField(auto_now=True)
50
+
51
+ original_file = models.ForeignKey(
52
+ "fileman.File",
53
+ related_name="file_renditions",
54
+ on_delete=models.CASCADE,
55
+ help_text="The parent file"
56
+ )
57
+
58
+ filename = models.CharField(
59
+ max_length=255,
60
+ db_index=True,
61
+ help_text="rendition filename"
62
+ )
63
+
64
+ storage_path = models.TextField(
65
+ help_text="Storage path and filename",
66
+ )
67
+
68
+ download_url = models.TextField(
69
+ blank=True,
70
+ null=True,
71
+ default=None,
72
+ help_text="Persistent URL for downloading the file, (if allowed)"
73
+ )
74
+
75
+ file_size = models.BigIntegerField(
76
+ null=True,
77
+ blank=True,
78
+ help_text="File size in bytes"
79
+ )
80
+
81
+ content_type = models.CharField(
82
+ max_length=255,
83
+ help_text="MIME type of the file"
84
+ )
85
+
86
+ category = models.CharField(
87
+ max_length=255,
88
+ help_text="A category for the file, like 'image', 'document', 'video', etc."
89
+ )
90
+
91
+ role = models.CharField(
92
+ max_length=255,
93
+ db_index=True,
94
+ help_text="The role of the file, like 'thumbnail', 'preview', 'full', etc."
95
+ )
96
+
97
+ upload_status = models.CharField(
98
+ max_length=32,
99
+ default=PENDING,
100
+ db_index=True,
101
+ help_text="Current status of rendering"
102
+ )
103
+
104
+ @property
105
+ def file_manager(self):
106
+ return self.original_file.file_manager
107
+
108
+ @property
109
+ def url(self):
110
+ return self.generate_download_url()
111
+
112
+ def generate_download_url(self):
113
+ if self.download_url:
114
+ return self.download_url
115
+ if self.file_manager.is_public:
116
+ self.download_url = self.file_manager.backend.get_url(self.storage_path)
117
+ return self.download_url
118
+ return self.file_manager.backend.get_url(self.storage_path, self.get_setting("urls_expire_in", 3600))