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.
- django_nativemojo-0.1.16.dist-info/METADATA +138 -0
- django_nativemojo-0.1.16.dist-info/RECORD +302 -0
- mojo/__init__.py +1 -1
- mojo/apps/account/management/__init__.py +5 -0
- mojo/apps/account/management/commands/__init__.py +6 -0
- mojo/apps/account/management/commands/serializer_admin.py +651 -0
- mojo/apps/account/migrations/0004_user_avatar.py +20 -0
- mojo/apps/account/migrations/0005_group_last_activity.py +18 -0
- mojo/apps/account/migrations/0006_add_device_tracking_models.py +72 -0
- mojo/apps/account/migrations/0007_delete_userdevicelocation.py +16 -0
- mojo/apps/account/migrations/0008_userdevicelocation.py +33 -0
- mojo/apps/account/migrations/0009_geolocatedip_subnet.py +18 -0
- mojo/apps/account/migrations/0010_group_avatar.py +20 -0
- mojo/apps/account/migrations/0011_user_org_registereddevice_pushconfig_and_more.py +118 -0
- mojo/apps/account/migrations/0012_remove_pushconfig_apns_key_file_and_more.py +21 -0
- mojo/apps/account/migrations/0013_pushconfig_test_mode_alter_pushconfig_apns_enabled_and_more.py +28 -0
- mojo/apps/account/migrations/0014_notificationdelivery_data_payload_and_more.py +48 -0
- mojo/apps/account/models/__init__.py +2 -0
- mojo/apps/account/models/device.py +281 -0
- mojo/apps/account/models/group.py +319 -15
- mojo/apps/account/models/member.py +29 -5
- mojo/apps/account/models/push/__init__.py +4 -0
- mojo/apps/account/models/push/config.py +112 -0
- mojo/apps/account/models/push/delivery.py +93 -0
- mojo/apps/account/models/push/device.py +66 -0
- mojo/apps/account/models/push/template.py +99 -0
- mojo/apps/account/models/user.py +369 -19
- mojo/apps/account/rest/__init__.py +2 -0
- mojo/apps/account/rest/device.py +39 -0
- mojo/apps/account/rest/group.py +9 -0
- mojo/apps/account/rest/push.py +187 -0
- mojo/apps/account/rest/user.py +100 -6
- mojo/apps/account/services/__init__.py +1 -0
- mojo/apps/account/services/push.py +363 -0
- mojo/apps/aws/migrations/0001_initial.py +206 -0
- mojo/apps/aws/migrations/0002_emaildomain_can_recv_emaildomain_can_send_and_more.py +28 -0
- mojo/apps/aws/migrations/0003_mailbox_is_domain_default_mailbox_is_system_default_and_more.py +31 -0
- mojo/apps/aws/migrations/0004_s3bucket.py +39 -0
- mojo/apps/aws/migrations/0005_alter_emaildomain_region_delete_s3bucket.py +21 -0
- mojo/apps/aws/models/__init__.py +19 -0
- mojo/apps/aws/models/email_attachment.py +99 -0
- mojo/apps/aws/models/email_domain.py +218 -0
- mojo/apps/aws/models/email_template.py +132 -0
- mojo/apps/aws/models/incoming_email.py +197 -0
- mojo/apps/aws/models/mailbox.py +288 -0
- mojo/apps/aws/models/sent_message.py +175 -0
- mojo/apps/aws/rest/__init__.py +7 -0
- mojo/apps/aws/rest/email.py +33 -0
- mojo/apps/aws/rest/email_ops.py +183 -0
- mojo/apps/aws/rest/messages.py +32 -0
- mojo/apps/aws/rest/s3.py +64 -0
- mojo/apps/aws/rest/send.py +101 -0
- mojo/apps/aws/rest/sns.py +403 -0
- mojo/apps/aws/rest/templates.py +19 -0
- mojo/apps/aws/services/__init__.py +32 -0
- mojo/apps/aws/services/email.py +390 -0
- mojo/apps/aws/services/email_ops.py +548 -0
- mojo/apps/docit/__init__.py +6 -0
- mojo/apps/docit/markdown_plugins/syntax_highlight.py +25 -0
- mojo/apps/docit/markdown_plugins/toc.py +12 -0
- mojo/apps/docit/migrations/0001_initial.py +113 -0
- mojo/apps/docit/migrations/0002_alter_book_modified_by_alter_page_modified_by.py +26 -0
- mojo/apps/docit/migrations/0003_alter_book_group.py +20 -0
- mojo/apps/docit/models/__init__.py +17 -0
- mojo/apps/docit/models/asset.py +231 -0
- mojo/apps/docit/models/book.py +227 -0
- mojo/apps/docit/models/page.py +319 -0
- mojo/apps/docit/models/page_revision.py +203 -0
- mojo/apps/docit/rest/__init__.py +10 -0
- mojo/apps/docit/rest/asset.py +17 -0
- mojo/apps/docit/rest/book.py +22 -0
- mojo/apps/docit/rest/page.py +22 -0
- mojo/apps/docit/rest/page_revision.py +17 -0
- mojo/apps/docit/services/__init__.py +11 -0
- mojo/apps/docit/services/docit.py +315 -0
- mojo/apps/docit/services/markdown.py +44 -0
- mojo/apps/fileman/README.md +8 -8
- mojo/apps/fileman/backends/base.py +76 -70
- mojo/apps/fileman/backends/filesystem.py +86 -86
- mojo/apps/fileman/backends/s3.py +409 -108
- mojo/apps/fileman/migrations/0001_initial.py +106 -0
- mojo/apps/fileman/migrations/0002_filemanager_parent_alter_filemanager_max_file_size.py +24 -0
- mojo/apps/fileman/migrations/0003_remove_file_fileman_fil_upload__c4bc35_idx_and_more.py +25 -0
- mojo/apps/fileman/migrations/0004_remove_file_original_filename_and_more.py +39 -0
- mojo/apps/fileman/migrations/0005_alter_file_upload_token.py +18 -0
- mojo/apps/fileman/migrations/0006_file_download_url_filemanager_forever_urls.py +23 -0
- mojo/apps/fileman/migrations/0007_remove_filemanager_forever_urls_and_more.py +22 -0
- mojo/apps/fileman/migrations/0008_file_category.py +18 -0
- mojo/apps/fileman/migrations/0009_rename_file_path_file_storage_file_path.py +18 -0
- mojo/apps/fileman/migrations/0010_filerendition.py +33 -0
- mojo/apps/fileman/migrations/0011_alter_filerendition_original_file.py +19 -0
- mojo/apps/fileman/models/__init__.py +1 -5
- mojo/apps/fileman/models/file.py +240 -58
- mojo/apps/fileman/models/manager.py +427 -31
- mojo/apps/fileman/models/rendition.py +118 -0
- mojo/apps/fileman/renderer/__init__.py +111 -0
- mojo/apps/fileman/renderer/audio.py +403 -0
- mojo/apps/fileman/renderer/base.py +205 -0
- mojo/apps/fileman/renderer/document.py +404 -0
- mojo/apps/fileman/renderer/image.py +222 -0
- mojo/apps/fileman/renderer/utils.py +297 -0
- mojo/apps/fileman/renderer/video.py +304 -0
- mojo/apps/fileman/rest/__init__.py +1 -18
- mojo/apps/fileman/rest/upload.py +22 -32
- mojo/apps/fileman/signals.py +58 -0
- mojo/apps/fileman/tasks.py +254 -0
- mojo/apps/fileman/utils/__init__.py +40 -16
- mojo/apps/incident/migrations/0005_incidenthistory.py +39 -0
- mojo/apps/incident/migrations/0006_alter_incident_state.py +18 -0
- mojo/apps/incident/migrations/0007_event_uid.py +18 -0
- mojo/apps/incident/migrations/0008_ticket_ticketnote.py +55 -0
- mojo/apps/incident/migrations/0009_incident_status.py +18 -0
- mojo/apps/incident/migrations/0010_event_country_code.py +18 -0
- mojo/apps/incident/migrations/0011_incident_country_code.py +18 -0
- mojo/apps/incident/migrations/0012_alter_incident_status.py +18 -0
- mojo/apps/incident/models/__init__.py +2 -0
- mojo/apps/incident/models/event.py +35 -0
- mojo/apps/incident/models/history.py +36 -0
- mojo/apps/incident/models/incident.py +3 -1
- mojo/apps/incident/models/ticket.py +62 -0
- mojo/apps/incident/reporter.py +21 -1
- mojo/apps/incident/rest/__init__.py +1 -0
- mojo/apps/incident/rest/event.py +7 -1
- mojo/apps/incident/rest/ticket.py +43 -0
- mojo/apps/jobs/__init__.py +489 -0
- mojo/apps/jobs/adapters.py +24 -0
- mojo/apps/jobs/cli.py +616 -0
- mojo/apps/jobs/daemon.py +370 -0
- mojo/apps/jobs/examples/sample_jobs.py +376 -0
- mojo/apps/jobs/examples/webhook_examples.py +203 -0
- mojo/apps/jobs/handlers/__init__.py +5 -0
- mojo/apps/jobs/handlers/webhook.py +317 -0
- mojo/apps/jobs/job_engine.py +734 -0
- mojo/apps/jobs/keys.py +203 -0
- mojo/apps/jobs/local_queue.py +363 -0
- mojo/apps/jobs/management/__init__.py +3 -0
- mojo/apps/jobs/management/commands/__init__.py +3 -0
- mojo/apps/jobs/manager.py +1327 -0
- mojo/apps/jobs/migrations/0001_initial.py +97 -0
- mojo/apps/jobs/migrations/0002_alter_job_max_retries_joblog.py +39 -0
- mojo/apps/jobs/models/__init__.py +6 -0
- mojo/apps/jobs/models/job.py +441 -0
- mojo/apps/jobs/rest/__init__.py +2 -0
- mojo/apps/jobs/rest/control.py +466 -0
- mojo/apps/jobs/rest/jobs.py +421 -0
- mojo/apps/jobs/scheduler.py +571 -0
- mojo/apps/jobs/services/__init__.py +6 -0
- mojo/apps/jobs/services/job_actions.py +465 -0
- mojo/apps/jobs/settings.py +209 -0
- mojo/apps/logit/migrations/0004_alter_log_level.py +18 -0
- mojo/apps/logit/models/log.py +7 -1
- mojo/apps/metrics/__init__.py +8 -1
- mojo/apps/metrics/redis_metrics.py +198 -0
- mojo/apps/metrics/rest/__init__.py +3 -0
- mojo/apps/metrics/rest/categories.py +266 -0
- mojo/apps/metrics/rest/helpers.py +48 -0
- mojo/apps/metrics/rest/permissions.py +99 -0
- mojo/apps/metrics/rest/values.py +277 -0
- mojo/apps/metrics/utils.py +19 -2
- mojo/decorators/auth.py +6 -1
- mojo/decorators/http.py +47 -3
- mojo/helpers/aws/__init__.py +45 -0
- mojo/helpers/aws/ec2.py +804 -0
- mojo/helpers/aws/iam.py +748 -0
- mojo/helpers/aws/inbound_email.py +309 -0
- mojo/helpers/aws/kms.py +413 -0
- mojo/helpers/aws/s3.py +451 -11
- mojo/helpers/aws/ses.py +483 -0
- mojo/helpers/aws/ses_domain.py +959 -0
- mojo/helpers/aws/sns.py +461 -0
- mojo/helpers/crypto/__init__.py +1 -1
- mojo/helpers/crypto/utils.py +15 -0
- mojo/helpers/dates.py +18 -0
- mojo/helpers/location/__init__.py +2 -0
- mojo/helpers/location/countries.py +262 -0
- mojo/helpers/location/geolocation.py +196 -0
- mojo/helpers/logit.py +37 -0
- mojo/helpers/redis/__init__.py +2 -0
- mojo/helpers/redis/adapter.py +606 -0
- mojo/helpers/redis/client.py +48 -0
- mojo/helpers/redis/pool.py +225 -0
- mojo/helpers/request.py +8 -0
- mojo/helpers/response.py +14 -2
- mojo/helpers/settings/__init__.py +2 -0
- mojo/helpers/{settings.py → settings/helper.py} +1 -37
- mojo/helpers/settings/parser.py +132 -0
- mojo/middleware/auth.py +1 -1
- mojo/middleware/cors.py +40 -0
- mojo/middleware/logging.py +131 -12
- mojo/middleware/mojo.py +10 -0
- mojo/models/rest.py +494 -65
- mojo/models/secrets.py +98 -3
- mojo/serializers/__init__.py +106 -0
- mojo/serializers/core/__init__.py +90 -0
- mojo/serializers/core/cache/__init__.py +121 -0
- mojo/serializers/core/cache/backends.py +518 -0
- mojo/serializers/core/cache/base.py +102 -0
- mojo/serializers/core/cache/disabled.py +181 -0
- mojo/serializers/core/cache/memory.py +287 -0
- mojo/serializers/core/cache/redis.py +533 -0
- mojo/serializers/core/cache/utils.py +454 -0
- mojo/serializers/core/manager.py +550 -0
- mojo/serializers/core/serializer.py +475 -0
- mojo/serializers/examples/settings.py +322 -0
- mojo/serializers/formats/csv.py +393 -0
- mojo/serializers/formats/localizers.py +509 -0
- mojo/serializers/{models.py → simple.py} +38 -15
- mojo/serializers/suggested_improvements.md +388 -0
- testit/client.py +1 -1
- testit/helpers.py +35 -4
- testit/runner.py +23 -6
- django_nativemojo-0.1.10.dist-info/METADATA +0 -96
- django_nativemojo-0.1.10.dist-info/RECORD +0 -194
- mojo/apps/metrics/rest/db.py +0 -0
- mojo/apps/notify/README.md +0 -91
- mojo/apps/notify/README_NOTIFICATIONS.md +0 -566
- mojo/apps/notify/admin.py +0 -52
- mojo/apps/notify/handlers/example_handlers.py +0 -516
- mojo/apps/notify/handlers/ses/__init__.py +0 -25
- mojo/apps/notify/handlers/ses/bounce.py +0 -0
- mojo/apps/notify/handlers/ses/complaint.py +0 -25
- mojo/apps/notify/handlers/ses/message.py +0 -86
- mojo/apps/notify/management/commands/__init__.py +0 -1
- mojo/apps/notify/management/commands/process_notifications.py +0 -370
- mojo/apps/notify/mod +0 -0
- mojo/apps/notify/models/__init__.py +0 -12
- mojo/apps/notify/models/account.py +0 -128
- mojo/apps/notify/models/attachment.py +0 -24
- mojo/apps/notify/models/bounce.py +0 -68
- mojo/apps/notify/models/complaint.py +0 -40
- mojo/apps/notify/models/inbox.py +0 -113
- mojo/apps/notify/models/inbox_message.py +0 -173
- mojo/apps/notify/models/outbox.py +0 -129
- mojo/apps/notify/models/outbox_message.py +0 -288
- mojo/apps/notify/models/template.py +0 -30
- mojo/apps/notify/providers/aws.py +0 -73
- mojo/apps/notify/rest/ses.py +0 -0
- mojo/apps/notify/utils/__init__.py +0 -2
- mojo/apps/notify/utils/notifications.py +0 -404
- mojo/apps/notify/utils/parsing.py +0 -202
- mojo/apps/notify/utils/render.py +0 -144
- mojo/apps/tasks/README.md +0 -118
- mojo/apps/tasks/__init__.py +0 -11
- mojo/apps/tasks/manager.py +0 -489
- mojo/apps/tasks/rest/__init__.py +0 -2
- mojo/apps/tasks/rest/hooks.py +0 -0
- mojo/apps/tasks/rest/tasks.py +0 -62
- mojo/apps/tasks/runner.py +0 -174
- mojo/apps/tasks/tq_handlers.py +0 -14
- mojo/helpers/aws/setup_email.py +0 -0
- mojo/helpers/redis.py +0 -10
- mojo/models/meta.py +0 -262
- mojo/ws4redis/README.md +0 -174
- mojo/ws4redis/__init__.py +0 -2
- mojo/ws4redis/client.py +0 -283
- mojo/ws4redis/connection.py +0 -327
- mojo/ws4redis/exceptions.py +0 -32
- mojo/ws4redis/redis.py +0 -183
- mojo/ws4redis/servers/base.py +0 -86
- mojo/ws4redis/servers/django.py +0 -171
- mojo/ws4redis/servers/uwsgi.py +0 -63
- mojo/ws4redis/settings.py +0 -45
- mojo/ws4redis/utf8validator.py +0 -128
- mojo/ws4redis/websocket.py +0 -403
- {django_nativemojo-0.1.10.dist-info → django_nativemojo-0.1.16.dist-info}/LICENSE +0 -0
- {django_nativemojo-0.1.10.dist-info → django_nativemojo-0.1.16.dist-info}/NOTICE +0 -0
- {django_nativemojo-0.1.10.dist-info → django_nativemojo-0.1.16.dist-info}/WHEEL +0 -0
- /mojo/apps/{notify → aws}/__init__.py +0 -0
- /mojo/apps/{notify/handlers → aws/migrations}/__init__.py +0 -0
- /mojo/apps/{notify/management → docit/markdown_plugins}/__init__.py +0 -0
- /mojo/apps/{notify/providers → docit/migrations}/__init__.py +0 -0
- /mojo/apps/{notify/rest → fileman/migrations}/__init__.py +0 -0
- /mojo/{ws4redis/servers → apps/jobs/examples}/__init__.py +0 -0
- /mojo/apps/{fileman/models/render.py → jobs/migrations/__init__.py} +0 -0
- /mojo/{serializers → rest}/openapi.py +0 -0
- /mojo/{apps/fileman/rest/__init__ → serializers/formats/__init__.py} +0 -0
mojo/apps/fileman/models/file.py
CHANGED
@@ -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", "
|
25
|
+
VIEW_PERMS = ["view_fileman", "manage_files"]
|
26
|
+
SEARCH_FIELDS = ["filename", "content_type"]
|
27
|
+
POST_SAVE_ACTIONS = ["action"]
|
20
28
|
SEARCH_TERMS = [
|
21
|
-
"filename",
|
29
|
+
"filename", "content_type",
|
22
30
|
("group", "group__name"),
|
23
31
|
("file_manager", "file_manager__name")]
|
24
32
|
|
25
33
|
GRAPHS = {
|
26
|
-
"
|
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
|
-
"
|
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
|
-
|
90
|
+
user = models.ForeignKey(
|
70
91
|
"account.User",
|
71
|
-
related_name="
|
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="
|
110
|
+
help_text="User-provided filename"
|
90
111
|
)
|
91
112
|
|
92
|
-
|
113
|
+
storage_filename = models.CharField(
|
93
114
|
max_length=255,
|
94
|
-
help_text="
|
115
|
+
help_text="Storage filename",
|
116
|
+
default=None,
|
117
|
+
blank=True,
|
118
|
+
null=True,
|
95
119
|
)
|
96
120
|
|
97
|
-
|
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.
|
174
|
-
|
175
|
-
def
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
self.
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
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
|
-
|
223
|
+
self.upload_token = hashlib.sha256(f"{uuid.uuid4()}{datetime.now()}".encode()).hexdigest()[:32]
|
224
|
+
if commit:
|
225
|
+
self.save()
|
192
226
|
|
193
|
-
def
|
227
|
+
def generate_storage_filename(self):
|
194
228
|
"""Generate a unique filename for storage"""
|
195
|
-
|
196
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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.
|
249
|
-
|
250
|
-
|
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
|
-
|
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.
|
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)
|