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,9 +1,16 @@
1
1
  from django.db import models
2
2
  from mojo.models import MojoModel
3
+ from objict import objict
4
+ import io
3
5
  import uuid
4
6
  import hashlib
7
+ import base64
8
+ import magic
5
9
  import mimetypes
6
10
  from datetime import datetime
11
+ import os
12
+ from mojo.apps.fileman import utils
13
+ from mojo.apps.fileman.models import FileManager
7
14
 
8
15
 
9
16
  class File(models.Model, MojoModel):
@@ -15,25 +22,39 @@ class File(models.Model, MojoModel):
15
22
  CAN_SAVE = CAN_CREATE = True
16
23
  CAN_DELETE = True
17
24
  DEFAULT_SORT = "-created"
18
- VIEW_PERMS = ["view_fileman"]
19
- SEARCH_FIELDS = ["filename", "original_filename", "content_type"]
25
+ VIEW_PERMS = ["view_fileman", "manage_files"]
26
+ SEARCH_FIELDS = ["filename", "content_type"]
27
+ POST_SAVE_ACTIONS = ["action"]
20
28
  SEARCH_TERMS = [
21
- "filename", "original_filename", "content_type",
29
+ "filename", "content_type",
22
30
  ("group", "group__name"),
23
31
  ("file_manager", "file_manager__name")]
24
32
 
25
33
  GRAPHS = {
26
- "default": {
34
+ "upload": {
35
+ "fields": ["id", "filename", "content_type", "file_size", "upload_url"],
36
+ },
37
+ "detailed": {
38
+ "extra": ["url", "renditions"],
27
39
  "graphs": {
28
40
  "group": "basic",
29
41
  "file_manager": "basic",
30
- "uploaded_by": "basic"
42
+ "user": "basic"
31
43
  }
32
44
  },
45
+ "basic": {
46
+ "fields": ["id", "filename", "content_type", "category"],
47
+ "extra": ["url", "thumbnail"],
48
+ },
49
+ "default": {
50
+ "extra": ["url", "renditions"],
51
+ },
33
52
  "list": {
53
+ "extra": ["url", "renditions"],
34
54
  "graphs": {
35
55
  "group": "basic",
36
- "file_manager": "basic"
56
+ "file_manager": "basic",
57
+ "user": "basic"
37
58
  }
38
59
  }
39
60
  }
@@ -66,9 +87,9 @@ class File(models.Model, MojoModel):
66
87
  help_text="Group that owns this file"
67
88
  )
68
89
 
69
- uploaded_by = models.ForeignKey(
90
+ user = models.ForeignKey(
70
91
  "account.User",
71
- related_name="uploaded_files",
92
+ related_name="files",
72
93
  null=True,
73
94
  blank=True,
74
95
  default=None,
@@ -86,18 +107,28 @@ class File(models.Model, MojoModel):
86
107
  filename = models.CharField(
87
108
  max_length=255,
88
109
  db_index=True,
89
- help_text="Final filename used for storage"
110
+ help_text="User-provided filename"
90
111
  )
91
112
 
92
- original_filename = models.CharField(
113
+ storage_filename = models.CharField(
93
114
  max_length=255,
94
- help_text="Original filename as uploaded by user"
115
+ help_text="Storage filename",
116
+ default=None,
117
+ blank=True,
118
+ null=True,
95
119
  )
96
120
 
97
- file_path = models.TextField(
121
+ storage_file_path = models.TextField(
98
122
  help_text="Full path to file in storage backend"
99
123
  )
100
124
 
125
+ download_url = models.TextField(
126
+ blank=True,
127
+ null=True,
128
+ default=None,
129
+ help_text="Persistent URL for downloading the file, (if allowed)"
130
+ )
131
+
101
132
  file_size = models.BigIntegerField(
102
133
  null=True,
103
134
  blank=True,
@@ -110,6 +141,15 @@ class File(models.Model, MojoModel):
110
141
  help_text="MIME type of the file"
111
142
  )
112
143
 
144
+ category = models.CharField(
145
+ max_length=255,
146
+ db_index=True,
147
+ default=None,
148
+ blank=True,
149
+ null=True,
150
+ help_text="A category for the file, like 'image', 'document', 'video', etc."
151
+ )
152
+
113
153
  checksum = models.CharField(
114
154
  max_length=128,
115
155
  blank=True,
@@ -119,7 +159,6 @@ class File(models.Model, MojoModel):
119
159
 
120
160
  upload_token = models.CharField(
121
161
  max_length=64,
122
- unique=True,
123
162
  db_index=True,
124
163
  help_text="Unique token for tracking direct uploads"
125
164
  )
@@ -132,18 +171,6 @@ class File(models.Model, MojoModel):
132
171
  help_text="Current status of the file upload"
133
172
  )
134
173
 
135
- upload_url = models.TextField(
136
- blank=True,
137
- default="",
138
- help_text="Pre-signed URL for direct upload (temporary)"
139
- )
140
-
141
- upload_expires_at = models.DateTimeField(
142
- null=True,
143
- blank=True,
144
- help_text="When the upload URL expires"
145
- )
146
-
147
174
  metadata = models.JSONField(
148
175
  default=dict,
149
176
  blank=True,
@@ -160,43 +187,64 @@ class File(models.Model, MojoModel):
160
187
  help_text="Whether this file can be accessed without authentication"
161
188
  )
162
189
 
190
+ upload_url = None
191
+
163
192
  class Meta:
164
193
  indexes = [
165
194
  models.Index(fields=['upload_status', 'created']),
166
195
  models.Index(fields=['file_manager', 'upload_status']),
167
196
  models.Index(fields=['group', 'is_active']),
168
197
  models.Index(fields=['content_type', 'is_active']),
169
- models.Index(fields=['upload_expires_at']),
170
198
  ]
171
199
 
172
200
  def __str__(self):
173
- return f"{self.original_filename} ({self.get_upload_status_display()})"
174
-
175
- def save(self, *args, **kwargs):
176
- """Custom save to generate upload token and set defaults"""
177
- if not self.upload_token:
178
- self.upload_token = self.generate_upload_token()
179
-
180
- if not self.filename:
181
- self.filename = self.generate_unique_filename()
182
-
183
- if not self.content_type:
184
- self.content_type = mimetypes.guess_type(self.filename)[0] or 'application/octet-stream'
185
-
186
- super().save(*args, **kwargs)
187
-
188
- @classmethod
189
- def generate_upload_token(cls):
201
+ return f"{self.filename} ({self.get_upload_status_display()})"
202
+
203
+ def on_rest_pre_save(self, changed_fields, created):
204
+ if created:
205
+ if not hasattr(self, "file_manager") or self.file_manager is None:
206
+ self.file_manager = FileManager.get_from_request(self.active_request)
207
+ if not self.content_type:
208
+ self.content_type = mimetypes.guess_type(self.filename)[0] or 'application/octet-stream'
209
+ self.category = utils.get_file_category(self.content_type)
210
+ if not self.storage_filename:
211
+ self.generate_storage_filename()
212
+
213
+ def on_rest_pre_delete(self):
214
+ # we need to handle the deletion of the file from storage
215
+ if self.storage_file_path:
216
+ name, ext = os.path.splitext(self.filename)
217
+ renditions_path = os.path.join(self.file_manager.root_path, name)
218
+ self.file_manager.backend.delete_folder(renditions_path)
219
+ self.file_manager.backend.delete(self.storage_file_path)
220
+
221
+ def generate_upload_token(self, commit=False):
190
222
  """Generate a unique upload token"""
191
- return hashlib.sha256(f"{uuid.uuid4()}{datetime.now()}".encode()).hexdigest()[:32]
223
+ self.upload_token = hashlib.sha256(f"{uuid.uuid4()}{datetime.now()}".encode()).hexdigest()[:32]
224
+ if commit:
225
+ self.save()
192
226
 
193
- def generate_unique_filename(self):
227
+ def generate_storage_filename(self):
194
228
  """Generate a unique filename for storage"""
195
- import os
196
- name, ext = os.path.splitext(self.original_filename)
197
- timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
229
+ name, ext = os.path.splitext(self.filename)
230
+ # timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
198
231
  unique_id = str(uuid.uuid4())[:8]
199
- return f"{name}_{timestamp}_{unique_id}{ext}"
232
+ self.storage_filename = f"{name}_{unique_id}{ext}"
233
+ self.storage_file_path = os.path.join(self.file_manager.root_path, self.storage_filename)
234
+
235
+ def request_upload_url(self):
236
+ """Request a pre-signed URL for direct upload"""
237
+ if not self.file_manager.backend.supports_direct_upload:
238
+ self.generate_upload_token(True)
239
+ self.upload_url = f"/api/fileman/upload/{self.upload_token}"
240
+ else:
241
+ data = self.file_manager.backend.generate_upload_url(self.storage_file_path, self.content_type, self.file_size)
242
+ self.debug("request_upload_url", data)
243
+ if "url" in data:
244
+ self.upload_url = data['url']
245
+ else:
246
+ self.upload_url = data
247
+ return self.upload_url
200
248
 
201
249
  def get_metadata(self, key, default=None):
202
250
  """Get a specific metadata value"""
@@ -206,6 +254,13 @@ class File(models.Model, MojoModel):
206
254
  """Set a specific metadata value"""
207
255
  self.metadata[key] = value
208
256
 
257
+ _renditions = None
258
+ @property
259
+ def renditions(self):
260
+ if self._renditions is None:
261
+ self._renditions = objict.from_dict({r.role: r.to_dict() for r in self.file_renditions.all()})
262
+ return self._renditions
263
+
209
264
  @property
210
265
  def is_pending(self):
211
266
  return self.upload_status == self.PENDING
@@ -233,26 +288,75 @@ class File(models.Model, MojoModel):
233
288
  return False
234
289
  return datetime.now() > self.upload_expires_at
235
290
 
236
- def mark_as_uploading(self):
291
+ @property
292
+ def url(self):
293
+ return self.generate_download_url()
294
+
295
+ @property
296
+ def thumbnail(self):
297
+ r = self.get_rendition_by_role('thumbnail')
298
+ if r:
299
+ return r.url
300
+ return None
301
+
302
+ def get_rendition_by_role(self, role):
303
+ return self.file_renditions.filter(role=role).first()
304
+
305
+ def generate_download_url(self):
306
+ if self.download_url:
307
+ return self.download_url
308
+ if self.file_manager.is_public:
309
+ self.download_url = self.file_manager.backend.get_url(self.storage_file_path)
310
+ return self.download_url
311
+ return self.file_manager.backend.get_url(self.storage_file_path, self.get_setting("urls_expire_in", 3600))
312
+
313
+ def on_action_action(self, action):
314
+ if action == "mark_as_completed":
315
+ self.mark_as_completed(commit=True)
316
+ elif action == "mark_as_failed":
317
+ self.mark_as_failed(commit=True)
318
+ elif action == "mark_as_uploading":
319
+ self.mark_as_uploading(commit=True)
320
+
321
+ def set_filename(self, filename):
322
+ self.filename = filename
323
+ if not self.content_type:
324
+ self.content_type = mimetypes.guess_type(filename)[0]
325
+ self.category = utils.get_file_category(self.content_type)
326
+
327
+
328
+ def create_renditions(self):
329
+ """Create renditions for the file"""
330
+ from mojo.apps.fileman import renderer
331
+ renderer.create_all_renditions(self)
332
+
333
+ def mark_as_uploading(self, commit=False):
237
334
  """Mark file as currently being uploaded"""
238
335
  self.upload_status = self.UPLOADING
239
- self.save(update_fields=['upload_status', 'modified'])
336
+ if commit:
337
+ self.atomic_save()
240
338
 
241
- def mark_as_completed(self, file_size=None, checksum=None):
339
+ def mark_as_completed(self, file_size=None, checksum=None, commit=False):
242
340
  """Mark file upload as completed"""
243
- self.upload_status = self.COMPLETED
244
341
  if file_size:
245
342
  self.file_size = file_size
246
343
  if checksum:
247
344
  self.checksum = checksum
248
- self.save(update_fields=['upload_status', 'file_size', 'checksum', 'modified'])
249
-
250
- def mark_as_failed(self, error_message=None):
345
+ if self.file_manager.backend.exists(self.storage_file_path):
346
+ self.upload_status = self.COMPLETED
347
+ self.create_renditions()
348
+ else:
349
+ self.upload_status = self.FAILED
350
+ if commit:
351
+ self.atomic_save()
352
+
353
+ def mark_as_failed(self, error_message=None, commit=False):
251
354
  """Mark file upload as failed"""
252
355
  self.upload_status = self.FAILED
253
356
  if error_message:
254
357
  self.set_metadata('error_message', error_message)
255
- self.save(update_fields=['upload_status', 'metadata', 'modified'])
358
+ if commit:
359
+ self.atomic_save()
256
360
 
257
361
  def mark_as_expired(self):
258
362
  """Mark file upload as expired"""
@@ -262,7 +366,7 @@ class File(models.Model, MojoModel):
262
366
  def get_file_extension(self):
263
367
  """Get the file extension"""
264
368
  import os
265
- return os.path.splitext(self.original_filename)[1].lower()
369
+ return os.path.splitext(self.filename)[1].lower()
266
370
 
267
371
  def get_human_readable_size(self):
268
372
  """Get human readable file size"""
@@ -290,3 +394,81 @@ class File(models.Model, MojoModel):
290
394
  return True
291
395
 
292
396
  return False
397
+
398
+ def on_rest_save_file(self, name, file):
399
+ self.content_type = file.content_type
400
+ self.category = utils.get_file_category(self.content_type)
401
+ self.set_filename(file.name)
402
+ self.file_manager = FileManager.get_from_request(self.active_request)
403
+ self.generate_storage_filename()
404
+ self.mark_as_uploading(True)
405
+ self.file_manager.backend.save(file, self.storage_file_path, self.content_type)
406
+ self.mark_as_completed(commit=True)
407
+
408
+ @classmethod
409
+ def create_from_file(cls, file, name, request=None, user=None, group=None):
410
+ """Create a new file instance from a file"""
411
+ if request:
412
+ file_manager = FileManager.get_from_request(request)
413
+ else:
414
+ file_manager = FileManager.get_for_user_group(user, group)
415
+ instance = cls()
416
+ instance.filename = file.name
417
+ instance.file_size = file.size
418
+ instance.file_manager = file_manager
419
+ instance.user = user
420
+ instance.group = group
421
+ instance.set_filename(file.name)
422
+ instance.category = utils.get_file_category(instance.content_type)
423
+ instance.on_rest_pre_save({}, True)
424
+ instance.save()
425
+
426
+ # now we need to upload the file
427
+ instance.on_rest_save_file(name, file)
428
+
429
+ return instance
430
+
431
+ @classmethod
432
+ def on_rest_related_save(cls, related_instance, related_field_name, field_value, current_instance=None):
433
+ # this allows us to handle json posts with inline base64 file data
434
+ if isinstance(field_value, str):
435
+ mime_type = None
436
+ b64_data = field_value
437
+
438
+ # Check for and parse Data URL scheme (e.g., "data:image/png;base64,iVBOR...")
439
+ if field_value.startswith('data:') and ',' in field_value:
440
+ header, b64_data = field_value.split(',', 1)
441
+ mime_type = header.split(';')[0].split(':')[1]
442
+
443
+ # Fix incorrect padding, which can occur with base64 strings from web clients
444
+ missing_padding = len(b64_data) % 4
445
+ if missing_padding:
446
+ b64_data += '=' * (4 - missing_padding)
447
+
448
+ try:
449
+ file_bytes = base64.b64decode(b64_data)
450
+ except (TypeError, base64.binascii.Error):
451
+ # If decoding fails, it's not a valid base64 string.
452
+ # In a real app, you might want to raise a validation error here.
453
+ return
454
+
455
+ # If mime_type wasn't in the data URL, detect it with python-magic
456
+ if not mime_type:
457
+ mime_type = magic.from_buffer(file_bytes, mime=True)
458
+
459
+ # Safely guess the extension, defaulting to an empty string if unknown
460
+ ext = mimetypes.guess_extension(mime_type) or ''
461
+
462
+ file_obj = io.BytesIO(file_bytes)
463
+ file_obj.name = f"{related_field_name}{ext}"
464
+ file_obj.content_type = mime_type
465
+ file_obj.size = len(file_bytes)
466
+
467
+ # now we need to upload the file
468
+ instance = cls.create_from_file(file_obj, file_obj.name)
469
+ setattr(related_instance, related_field_name, instance)
470
+
471
+ elif isinstance(field_value, int):
472
+ # assume file id
473
+ instance = File.objects.get(id=field_value)
474
+ setattr(related_instance, related_field_name, instance)