django-nativemojo 0.1.10__py3-none-any.whl → 0.1.15__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 (120) hide show
  1. django_nativemojo-0.1.15.dist-info/METADATA +136 -0
  2. {django_nativemojo-0.1.10.dist-info → django_nativemojo-0.1.15.dist-info}/RECORD +105 -65
  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 +531 -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/models/group.py +25 -7
  10. mojo/apps/account/models/member.py +15 -4
  11. mojo/apps/account/models/user.py +197 -20
  12. mojo/apps/account/rest/group.py +1 -0
  13. mojo/apps/account/rest/user.py +6 -2
  14. mojo/apps/aws/rest/__init__.py +1 -0
  15. mojo/apps/aws/rest/s3.py +64 -0
  16. mojo/apps/fileman/README.md +8 -8
  17. mojo/apps/fileman/backends/base.py +76 -70
  18. mojo/apps/fileman/backends/filesystem.py +86 -86
  19. mojo/apps/fileman/backends/s3.py +200 -108
  20. mojo/apps/fileman/migrations/0001_initial.py +106 -0
  21. mojo/apps/fileman/migrations/0002_filemanager_parent_alter_filemanager_max_file_size.py +24 -0
  22. mojo/apps/fileman/migrations/0003_remove_file_fileman_fil_upload__c4bc35_idx_and_more.py +25 -0
  23. mojo/apps/fileman/migrations/0004_remove_file_original_filename_and_more.py +39 -0
  24. mojo/apps/fileman/migrations/0005_alter_file_upload_token.py +18 -0
  25. mojo/apps/fileman/migrations/0006_file_download_url_filemanager_forever_urls.py +23 -0
  26. mojo/apps/fileman/migrations/0007_remove_filemanager_forever_urls_and_more.py +22 -0
  27. mojo/apps/fileman/migrations/0008_file_category.py +18 -0
  28. mojo/apps/fileman/migrations/0009_rename_file_path_file_storage_file_path.py +18 -0
  29. mojo/apps/fileman/migrations/0010_filerendition.py +33 -0
  30. mojo/apps/fileman/migrations/0011_alter_filerendition_original_file.py +19 -0
  31. mojo/apps/fileman/models/__init__.py +1 -5
  32. mojo/apps/fileman/models/file.py +204 -58
  33. mojo/apps/fileman/models/manager.py +161 -31
  34. mojo/apps/fileman/models/rendition.py +118 -0
  35. mojo/apps/fileman/renderer/__init__.py +111 -0
  36. mojo/apps/fileman/renderer/audio.py +403 -0
  37. mojo/apps/fileman/renderer/base.py +205 -0
  38. mojo/apps/fileman/renderer/document.py +404 -0
  39. mojo/apps/fileman/renderer/image.py +222 -0
  40. mojo/apps/fileman/renderer/utils.py +297 -0
  41. mojo/apps/fileman/renderer/video.py +304 -0
  42. mojo/apps/fileman/rest/__init__.py +1 -18
  43. mojo/apps/fileman/rest/upload.py +22 -32
  44. mojo/apps/fileman/signals.py +58 -0
  45. mojo/apps/fileman/tasks.py +254 -0
  46. mojo/apps/fileman/utils/__init__.py +40 -16
  47. mojo/apps/incident/migrations/0005_incidenthistory.py +39 -0
  48. mojo/apps/incident/migrations/0006_alter_incident_state.py +18 -0
  49. mojo/apps/incident/models/__init__.py +1 -0
  50. mojo/apps/incident/models/history.py +36 -0
  51. mojo/apps/incident/models/incident.py +1 -1
  52. mojo/apps/incident/reporter.py +3 -1
  53. mojo/apps/incident/rest/event.py +7 -1
  54. mojo/apps/logit/migrations/0004_alter_log_level.py +18 -0
  55. mojo/apps/logit/models/log.py +4 -1
  56. mojo/apps/metrics/utils.py +2 -2
  57. mojo/apps/notify/handlers/ses/message.py +1 -1
  58. mojo/apps/notify/providers/aws.py +2 -2
  59. mojo/apps/tasks/__init__.py +34 -1
  60. mojo/apps/tasks/manager.py +200 -45
  61. mojo/apps/tasks/rest/tasks.py +24 -10
  62. mojo/apps/tasks/runner.py +283 -18
  63. mojo/apps/tasks/task.py +99 -0
  64. mojo/apps/tasks/tq_handlers.py +118 -0
  65. mojo/decorators/auth.py +6 -1
  66. mojo/decorators/http.py +7 -2
  67. mojo/helpers/aws/__init__.py +41 -0
  68. mojo/helpers/aws/ec2.py +804 -0
  69. mojo/helpers/aws/iam.py +748 -0
  70. mojo/helpers/aws/s3.py +451 -11
  71. mojo/helpers/aws/ses.py +483 -0
  72. mojo/helpers/aws/sns.py +461 -0
  73. mojo/helpers/crypto/__pycache__/hash.cpython-310.pyc +0 -0
  74. mojo/helpers/crypto/__pycache__/sign.cpython-310.pyc +0 -0
  75. mojo/helpers/crypto/__pycache__/utils.cpython-310.pyc +0 -0
  76. mojo/helpers/dates.py +18 -0
  77. mojo/helpers/response.py +6 -2
  78. mojo/helpers/settings/__init__.py +2 -0
  79. mojo/helpers/{settings.py → settings/helper.py} +1 -37
  80. mojo/helpers/settings/parser.py +132 -0
  81. mojo/middleware/logging.py +1 -1
  82. mojo/middleware/mojo.py +5 -0
  83. mojo/models/rest.py +261 -46
  84. mojo/models/secrets.py +13 -4
  85. mojo/serializers/__init__.py +100 -0
  86. mojo/serializers/advanced/README.md +363 -0
  87. mojo/serializers/advanced/__init__.py +247 -0
  88. mojo/serializers/advanced/formats/__init__.py +28 -0
  89. mojo/serializers/advanced/formats/csv.py +416 -0
  90. mojo/serializers/advanced/formats/excel.py +516 -0
  91. mojo/serializers/advanced/formats/json.py +239 -0
  92. mojo/serializers/advanced/formats/localizers.py +509 -0
  93. mojo/serializers/advanced/formats/response.py +485 -0
  94. mojo/serializers/advanced/serializer.py +568 -0
  95. mojo/serializers/manager.py +501 -0
  96. mojo/serializers/optimized.py +618 -0
  97. mojo/serializers/settings_example.py +322 -0
  98. mojo/serializers/{models.py → simple.py} +38 -15
  99. testit/helpers.py +21 -4
  100. django_nativemojo-0.1.10.dist-info/METADATA +0 -96
  101. mojo/apps/metrics/rest/db.py +0 -0
  102. mojo/helpers/aws/setup_email.py +0 -0
  103. mojo/ws4redis/README.md +0 -174
  104. mojo/ws4redis/__init__.py +0 -2
  105. mojo/ws4redis/client.py +0 -283
  106. mojo/ws4redis/connection.py +0 -327
  107. mojo/ws4redis/exceptions.py +0 -32
  108. mojo/ws4redis/redis.py +0 -183
  109. mojo/ws4redis/servers/base.py +0 -86
  110. mojo/ws4redis/servers/django.py +0 -171
  111. mojo/ws4redis/servers/uwsgi.py +0 -63
  112. mojo/ws4redis/settings.py +0 -45
  113. mojo/ws4redis/utf8validator.py +0 -128
  114. mojo/ws4redis/websocket.py +0 -403
  115. {django_nativemojo-0.1.10.dist-info → django_nativemojo-0.1.15.dist-info}/LICENSE +0 -0
  116. {django_nativemojo-0.1.10.dist-info → django_nativemojo-0.1.15.dist-info}/NOTICE +0 -0
  117. {django_nativemojo-0.1.10.dist-info → django_nativemojo-0.1.15.dist-info}/WHEEL +0 -0
  118. /mojo/{ws4redis/servers → apps/aws}/__init__.py +0 -0
  119. /mojo/apps/{fileman/models/render.py → aws/models/__init__.py} +0 -0
  120. /mojo/apps/fileman/{rest/__init__ → migrations/__init__.py} +0 -0
@@ -0,0 +1,24 @@
1
+ # Generated by Django 4.2.21 on 2025-06-07 20:02
2
+
3
+ from django.db import migrations, models
4
+ import django.db.models.deletion
5
+
6
+
7
+ class Migration(migrations.Migration):
8
+
9
+ dependencies = [
10
+ ('fileman', '0001_initial'),
11
+ ]
12
+
13
+ operations = [
14
+ migrations.AddField(
15
+ model_name='filemanager',
16
+ name='parent',
17
+ field=models.ForeignKey(blank=True, help_text='Used if this file manager is a child of another file manager, and inherits settings from its parent', null=True, on_delete=django.db.models.deletion.CASCADE, to='fileman.filemanager'),
18
+ ),
19
+ migrations.AlterField(
20
+ model_name='filemanager',
21
+ name='max_file_size',
22
+ field=models.BigIntegerField(default=1048576000, help_text='Maximum file size in bytes (0 for unlimited)'),
23
+ ),
24
+ ]
@@ -0,0 +1,25 @@
1
+ # Generated by Django 4.2.21 on 2025-06-08 04:49
2
+
3
+ from django.db import migrations
4
+
5
+
6
+ class Migration(migrations.Migration):
7
+
8
+ dependencies = [
9
+ ('fileman', '0002_filemanager_parent_alter_filemanager_max_file_size'),
10
+ ]
11
+
12
+ operations = [
13
+ migrations.RemoveIndex(
14
+ model_name='file',
15
+ name='fileman_fil_upload__c4bc35_idx',
16
+ ),
17
+ migrations.RemoveField(
18
+ model_name='file',
19
+ name='upload_expires_at',
20
+ ),
21
+ migrations.RemoveField(
22
+ model_name='file',
23
+ name='upload_url',
24
+ ),
25
+ ]
@@ -0,0 +1,39 @@
1
+ # Generated by Django 4.2.21 on 2025-06-08 05:14
2
+
3
+ from django.conf import settings
4
+ from django.db import migrations, models
5
+ import django.db.models.deletion
6
+
7
+
8
+ class Migration(migrations.Migration):
9
+
10
+ dependencies = [
11
+ migrations.swappable_dependency(settings.AUTH_USER_MODEL),
12
+ ('fileman', '0003_remove_file_fileman_fil_upload__c4bc35_idx_and_more'),
13
+ ]
14
+
15
+ operations = [
16
+ migrations.RemoveField(
17
+ model_name='file',
18
+ name='original_filename',
19
+ ),
20
+ migrations.RemoveField(
21
+ model_name='file',
22
+ name='uploaded_by',
23
+ ),
24
+ migrations.AddField(
25
+ model_name='file',
26
+ name='storage_filename',
27
+ field=models.CharField(blank=True, default=None, help_text='Storage filename', max_length=255, null=True),
28
+ ),
29
+ migrations.AddField(
30
+ model_name='file',
31
+ name='user',
32
+ field=models.ForeignKey(blank=True, default=None, help_text='User who uploaded this file', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='files', to=settings.AUTH_USER_MODEL),
33
+ ),
34
+ migrations.AlterField(
35
+ model_name='file',
36
+ name='filename',
37
+ field=models.CharField(db_index=True, help_text='User-provided filename', max_length=255),
38
+ ),
39
+ ]
@@ -0,0 +1,18 @@
1
+ # Generated by Django 4.2.21 on 2025-06-08 12:55
2
+
3
+ from django.db import migrations, models
4
+
5
+
6
+ class Migration(migrations.Migration):
7
+
8
+ dependencies = [
9
+ ('fileman', '0004_remove_file_original_filename_and_more'),
10
+ ]
11
+
12
+ operations = [
13
+ migrations.AlterField(
14
+ model_name='file',
15
+ name='upload_token',
16
+ field=models.CharField(db_index=True, help_text='Unique token for tracking direct uploads', max_length=64),
17
+ ),
18
+ ]
@@ -0,0 +1,23 @@
1
+ # Generated by Django 4.2.21 on 2025-06-08 13:25
2
+
3
+ from django.db import migrations, models
4
+
5
+
6
+ class Migration(migrations.Migration):
7
+
8
+ dependencies = [
9
+ ('fileman', '0005_alter_file_upload_token'),
10
+ ]
11
+
12
+ operations = [
13
+ migrations.AddField(
14
+ model_name='file',
15
+ name='download_url',
16
+ field=models.TextField(blank=True, default=None, help_text='Persistent URL for downloading the file, (if allowed)', null=True),
17
+ ),
18
+ migrations.AddField(
19
+ model_name='filemanager',
20
+ name='forever_urls',
21
+ field=models.BooleanField(default=False, help_text='Allow URLs to be permanent and not expire'),
22
+ ),
23
+ ]
@@ -0,0 +1,22 @@
1
+ # Generated by Django 4.2.21 on 2025-06-08 16:40
2
+
3
+ from django.db import migrations, models
4
+
5
+
6
+ class Migration(migrations.Migration):
7
+
8
+ dependencies = [
9
+ ('fileman', '0006_file_download_url_filemanager_forever_urls'),
10
+ ]
11
+
12
+ operations = [
13
+ migrations.RemoveField(
14
+ model_name='filemanager',
15
+ name='forever_urls',
16
+ ),
17
+ migrations.AddField(
18
+ model_name='filemanager',
19
+ name='is_public',
20
+ field=models.BooleanField(default=True, help_text='Whether this allows public access to the files'),
21
+ ),
22
+ ]
@@ -0,0 +1,18 @@
1
+ # Generated by Django 4.2.21 on 2025-06-08 16:59
2
+
3
+ from django.db import migrations, models
4
+
5
+
6
+ class Migration(migrations.Migration):
7
+
8
+ dependencies = [
9
+ ('fileman', '0007_remove_filemanager_forever_urls_and_more'),
10
+ ]
11
+
12
+ operations = [
13
+ migrations.AddField(
14
+ model_name='file',
15
+ name='category',
16
+ field=models.CharField(blank=True, db_index=True, default=None, help_text="A category for the file, like 'image', 'document', 'video', etc.", max_length=255, null=True),
17
+ ),
18
+ ]
@@ -0,0 +1,18 @@
1
+ # Generated by Django 4.2.21 on 2025-06-08 17:32
2
+
3
+ from django.db import migrations
4
+
5
+
6
+ class Migration(migrations.Migration):
7
+
8
+ dependencies = [
9
+ ('fileman', '0008_file_category'),
10
+ ]
11
+
12
+ operations = [
13
+ migrations.RenameField(
14
+ model_name='file',
15
+ old_name='file_path',
16
+ new_name='storage_file_path',
17
+ ),
18
+ ]
@@ -0,0 +1,33 @@
1
+ # Generated by Django 4.2.21 on 2025-06-09 03:49
2
+
3
+ from django.db import migrations, models
4
+ import django.db.models.deletion
5
+ import mojo.models.rest
6
+
7
+
8
+ class Migration(migrations.Migration):
9
+
10
+ dependencies = [
11
+ ('fileman', '0009_rename_file_path_file_storage_file_path'),
12
+ ]
13
+
14
+ operations = [
15
+ migrations.CreateModel(
16
+ name='FileRendition',
17
+ fields=[
18
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
19
+ ('created', models.DateTimeField(auto_now_add=True, db_index=True)),
20
+ ('modified', models.DateTimeField(auto_now=True)),
21
+ ('filename', models.CharField(db_index=True, help_text='rendition filename', max_length=255)),
22
+ ('storage_path', models.TextField(help_text='Storage path and filename')),
23
+ ('download_url', models.TextField(blank=True, default=None, help_text='Persistent URL for downloading the file, (if allowed)', null=True)),
24
+ ('file_size', models.BigIntegerField(blank=True, help_text='File size in bytes', null=True)),
25
+ ('content_type', models.CharField(help_text='MIME type of the file', max_length=255)),
26
+ ('category', models.CharField(help_text="A category for the file, like 'image', 'document', 'video', etc.", max_length=255)),
27
+ ('role', models.CharField(db_index=True, help_text="The role of the file, like 'thumbnail', 'preview', 'full', etc.", max_length=255)),
28
+ ('upload_status', models.CharField(db_index=True, default='pending', help_text='Current status of rendering', max_length=32)),
29
+ ('original_file', models.ForeignKey(help_text='The parent file', on_delete=django.db.models.deletion.CASCADE, related_name='renditions', to='fileman.file')),
30
+ ],
31
+ bases=(models.Model, mojo.models.rest.MojoModel),
32
+ ),
33
+ ]
@@ -0,0 +1,19 @@
1
+ # Generated by Django 4.2.21 on 2025-06-09 05:37
2
+
3
+ from django.db import migrations, models
4
+ import django.db.models.deletion
5
+
6
+
7
+ class Migration(migrations.Migration):
8
+
9
+ dependencies = [
10
+ ('fileman', '0010_filerendition'),
11
+ ]
12
+
13
+ operations = [
14
+ migrations.AlterField(
15
+ model_name='filerendition',
16
+ name='original_file',
17
+ field=models.ForeignKey(help_text='The parent file', on_delete=django.db.models.deletion.CASCADE, related_name='file_renditions', to='fileman.file'),
18
+ ),
19
+ ]
@@ -1,7 +1,3 @@
1
1
  from .manager import FileManager
2
2
  from .file import File
3
-
4
- __all__ = [
5
- 'FileManager',
6
- 'File',
7
- ]
3
+ from .rendition import FileRendition
@@ -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,38 @@ 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"]
20
27
  SEARCH_TERMS = [
21
- "filename", "original_filename", "content_type",
28
+ "filename", "content_type",
22
29
  ("group", "group__name"),
23
30
  ("file_manager", "file_manager__name")]
24
31
 
25
32
  GRAPHS = {
26
- "default": {
33
+ "upload": {
34
+ "fields": ["id", "filename", "content_type", "file_size", "upload_url"],
35
+ },
36
+ "detailed": {
37
+ "extra": ["url", "renditions"],
27
38
  "graphs": {
28
39
  "group": "basic",
29
40
  "file_manager": "basic",
30
- "uploaded_by": "basic"
41
+ "user": "basic"
31
42
  }
32
43
  },
44
+ "basic": {
45
+ "fields": ["id", "filename", "content_type", "category"],
46
+ "extra": ["url", "renditions"],
47
+ },
48
+ "default": {
49
+ "extra": ["url", "renditions"],
50
+ },
33
51
  "list": {
52
+ "extra": ["url", "renditions"],
34
53
  "graphs": {
35
54
  "group": "basic",
36
- "file_manager": "basic"
55
+ "file_manager": "basic",
56
+ "user": "basic"
37
57
  }
38
58
  }
39
59
  }
@@ -66,9 +86,9 @@ class File(models.Model, MojoModel):
66
86
  help_text="Group that owns this file"
67
87
  )
68
88
 
69
- uploaded_by = models.ForeignKey(
89
+ user = models.ForeignKey(
70
90
  "account.User",
71
- related_name="uploaded_files",
91
+ related_name="files",
72
92
  null=True,
73
93
  blank=True,
74
94
  default=None,
@@ -86,18 +106,28 @@ class File(models.Model, MojoModel):
86
106
  filename = models.CharField(
87
107
  max_length=255,
88
108
  db_index=True,
89
- help_text="Final filename used for storage"
109
+ help_text="User-provided filename"
90
110
  )
91
111
 
92
- original_filename = models.CharField(
112
+ storage_filename = models.CharField(
93
113
  max_length=255,
94
- help_text="Original filename as uploaded by user"
114
+ help_text="Storage filename",
115
+ default=None,
116
+ blank=True,
117
+ null=True,
95
118
  )
96
119
 
97
- file_path = models.TextField(
120
+ storage_file_path = models.TextField(
98
121
  help_text="Full path to file in storage backend"
99
122
  )
100
123
 
124
+ download_url = models.TextField(
125
+ blank=True,
126
+ null=True,
127
+ default=None,
128
+ help_text="Persistent URL for downloading the file, (if allowed)"
129
+ )
130
+
101
131
  file_size = models.BigIntegerField(
102
132
  null=True,
103
133
  blank=True,
@@ -110,6 +140,15 @@ class File(models.Model, MojoModel):
110
140
  help_text="MIME type of the file"
111
141
  )
112
142
 
143
+ category = models.CharField(
144
+ max_length=255,
145
+ db_index=True,
146
+ default=None,
147
+ blank=True,
148
+ null=True,
149
+ help_text="A category for the file, like 'image', 'document', 'video', etc."
150
+ )
151
+
113
152
  checksum = models.CharField(
114
153
  max_length=128,
115
154
  blank=True,
@@ -119,7 +158,6 @@ class File(models.Model, MojoModel):
119
158
 
120
159
  upload_token = models.CharField(
121
160
  max_length=64,
122
- unique=True,
123
161
  db_index=True,
124
162
  help_text="Unique token for tracking direct uploads"
125
163
  )
@@ -132,18 +170,6 @@ class File(models.Model, MojoModel):
132
170
  help_text="Current status of the file upload"
133
171
  )
134
172
 
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
173
  metadata = models.JSONField(
148
174
  default=dict,
149
175
  blank=True,
@@ -160,43 +186,64 @@ class File(models.Model, MojoModel):
160
186
  help_text="Whether this file can be accessed without authentication"
161
187
  )
162
188
 
189
+ upload_url = None
190
+
163
191
  class Meta:
164
192
  indexes = [
165
193
  models.Index(fields=['upload_status', 'created']),
166
194
  models.Index(fields=['file_manager', 'upload_status']),
167
195
  models.Index(fields=['group', 'is_active']),
168
196
  models.Index(fields=['content_type', 'is_active']),
169
- models.Index(fields=['upload_expires_at']),
170
197
  ]
171
198
 
172
199
  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):
200
+ return f"{self.filename} ({self.get_upload_status_display()})"
201
+
202
+ def on_rest_pre_save(self, changed_fields, created):
203
+ if created:
204
+ if not hasattr(self, "file_manager") or self.file_manager is None:
205
+ self.file_manager = FileManager.get_from_request(self.active_request)
206
+ if not self.content_type:
207
+ self.content_type = mimetypes.guess_type(self.filename)[0] or 'application/octet-stream'
208
+ self.category = utils.get_file_category(self.content_type)
209
+ if not self.storage_filename:
210
+ self.generate_storage_filename()
211
+
212
+ def on_rest_pre_delete(self):
213
+ # we need to handle the deletion of the file from storage
214
+ if self.storage_file_path:
215
+ name, ext = os.path.splitext(self.filename)
216
+ renditions_path = os.path.join(self.file_manager.root_path, name)
217
+ self.file_manager.backend.delete_folder(renditions_path)
218
+ self.file_manager.backend.delete(self.storage_file_path)
219
+
220
+ def generate_upload_token(self, commit=False):
190
221
  """Generate a unique upload token"""
191
- return hashlib.sha256(f"{uuid.uuid4()}{datetime.now()}".encode()).hexdigest()[:32]
222
+ self.upload_token = hashlib.sha256(f"{uuid.uuid4()}{datetime.now()}".encode()).hexdigest()[:32]
223
+ if commit:
224
+ self.save()
192
225
 
193
- def generate_unique_filename(self):
226
+ def generate_storage_filename(self):
194
227
  """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")
228
+ name, ext = os.path.splitext(self.filename)
229
+ # timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
198
230
  unique_id = str(uuid.uuid4())[:8]
199
- return f"{name}_{timestamp}_{unique_id}{ext}"
231
+ self.storage_filename = f"{name}_{unique_id}{ext}"
232
+ self.storage_file_path = os.path.join(self.file_manager.root_path, self.storage_filename)
233
+
234
+ def request_upload_url(self):
235
+ """Request a pre-signed URL for direct upload"""
236
+ if not self.file_manager.backend.supports_direct_upload:
237
+ self.generate_upload_token(True)
238
+ self.upload_url = f"/api/fileman/upload/{self.upload_token}"
239
+ else:
240
+ data = self.file_manager.backend.generate_upload_url(self.storage_file_path, self.content_type, self.file_size)
241
+ self.debug("request_upload_url", data)
242
+ if "url" in data:
243
+ self.upload_url = data['url']
244
+ else:
245
+ self.upload_url = data
246
+ return self.upload_url
200
247
 
201
248
  def get_metadata(self, key, default=None):
202
249
  """Get a specific metadata value"""
@@ -206,6 +253,13 @@ class File(models.Model, MojoModel):
206
253
  """Set a specific metadata value"""
207
254
  self.metadata[key] = value
208
255
 
256
+ _renditions = None
257
+ @property
258
+ def renditions(self):
259
+ if self._renditions is None:
260
+ self._renditions = objict.from_dict({r.role: r.to_dict() for r in self.file_renditions.all()})
261
+ return self._renditions
262
+
209
263
  @property
210
264
  def is_pending(self):
211
265
  return self.upload_status == self.PENDING
@@ -233,26 +287,65 @@ class File(models.Model, MojoModel):
233
287
  return False
234
288
  return datetime.now() > self.upload_expires_at
235
289
 
236
- def mark_as_uploading(self):
290
+ @property
291
+ def url(self):
292
+ return self.generate_download_url()
293
+
294
+ def generate_download_url(self):
295
+ if self.download_url:
296
+ return self.download_url
297
+ if self.file_manager.is_public:
298
+ self.download_url = self.file_manager.backend.get_url(self.storage_file_path)
299
+ return self.download_url
300
+ return self.file_manager.backend.get_url(self.storage_file_path, self.get_setting("urls_expire_in", 3600))
301
+
302
+ def set_action(self, action):
303
+ if action == "mark_as_completed":
304
+ self.mark_as_completed()
305
+ elif action == "mark_as_failed":
306
+ self.mark_as_failed()
307
+ elif action == "mark_as_uploading":
308
+ self.mark_as_uploading()
309
+
310
+ def set_filename(self, filename):
311
+ self.filename = filename
312
+ if not self.content_type:
313
+ self.content_type = mimetypes.guess_type(filename)[0]
314
+ self.category = utils.get_file_category(self.content_type)
315
+
316
+
317
+ def create_renditions(self):
318
+ """Create renditions for the file"""
319
+ from mojo.apps.fileman import renderer
320
+ renderer.create_all_renditions(self)
321
+
322
+ def mark_as_uploading(self, commit=False):
237
323
  """Mark file as currently being uploaded"""
238
324
  self.upload_status = self.UPLOADING
239
- self.save(update_fields=['upload_status', 'modified'])
325
+ if commit:
326
+ self.atomic_save()
240
327
 
241
- def mark_as_completed(self, file_size=None, checksum=None):
328
+ def mark_as_completed(self, file_size=None, checksum=None, commit=False):
242
329
  """Mark file upload as completed"""
243
- self.upload_status = self.COMPLETED
244
330
  if file_size:
245
331
  self.file_size = file_size
246
332
  if checksum:
247
333
  self.checksum = checksum
248
- self.save(update_fields=['upload_status', 'file_size', 'checksum', 'modified'])
249
-
250
- def mark_as_failed(self, error_message=None):
334
+ if self.file_manager.backend.exists(self.storage_file_path):
335
+ self.upload_status = self.COMPLETED
336
+ self.create_renditions()
337
+ else:
338
+ self.upload_status = self.FAILED
339
+ if commit:
340
+ self.atomic_save()
341
+
342
+ def mark_as_failed(self, error_message=None, commit=False):
251
343
  """Mark file upload as failed"""
252
344
  self.upload_status = self.FAILED
253
345
  if error_message:
254
346
  self.set_metadata('error_message', error_message)
255
- self.save(update_fields=['upload_status', 'metadata', 'modified'])
347
+ if commit:
348
+ self.atomic_save()
256
349
 
257
350
  def mark_as_expired(self):
258
351
  """Mark file upload as expired"""
@@ -262,7 +355,7 @@ class File(models.Model, MojoModel):
262
355
  def get_file_extension(self):
263
356
  """Get the file extension"""
264
357
  import os
265
- return os.path.splitext(self.original_filename)[1].lower()
358
+ return os.path.splitext(self.filename)[1].lower()
266
359
 
267
360
  def get_human_readable_size(self):
268
361
  """Get human readable file size"""
@@ -290,3 +383,56 @@ class File(models.Model, MojoModel):
290
383
  return True
291
384
 
292
385
  return False
386
+
387
+ def on_rest_save_file(self, name, file):
388
+ self.content_type = file.content_type
389
+ self.category = utils.get_file_category(self.content_type)
390
+ self.set_filename(file.name)
391
+ self.file_manager = FileManager.get_from_request(self.active_request)
392
+ self.generate_storage_filename()
393
+ self.mark_as_uploading(True)
394
+ self.file_manager.backend.save(file, self.storage_file_path, self.content_type)
395
+ self.mark_as_completed(commit=True)
396
+
397
+ @classmethod
398
+ def create_from_file(cls, file, name, request=None, user=None, group=None):
399
+ """Create a new file instance from a file"""
400
+ if request:
401
+ file_manager = FileManager.get_from_request(request)
402
+ else:
403
+ file_manager = FileManager.get_for_user_group(user, group)
404
+ instance = cls()
405
+ instance.filename = file.name
406
+ instance.file_size = file.size
407
+ instance.file_manager = file_manager
408
+ instance.user = user
409
+ instance.group = group
410
+ instance.set_filename(file.name)
411
+ instance.category = utils.get_file_category(instance.content_type)
412
+ instance.on_rest_pre_save({}, True)
413
+ instance.save()
414
+
415
+ # now we need to upload the file
416
+ instance.on_rest_save_file(name, file)
417
+
418
+ return instance
419
+
420
+ @classmethod
421
+ def on_rest_related_save(cls, related_instance, related_field_name, field_value, current_instance=None):
422
+ # this allows us to handle json posts with inline base64 file data
423
+ if isinstance(field_value, str):
424
+ # assume base64 encoded data
425
+ file_bytes = base64.b64decode(field_value)
426
+ mime_type = magic.from_buffer(file_bytes, mime=True)
427
+ ext = mimetypes.guess_extension(mime_type)
428
+ file_obj = io.BytesIO(file_bytes)
429
+ file_obj.name = f"{related_field_name}{ext}"
430
+ file_obj.content_type = mime_type
431
+ file_obj.size = len(file_bytes)
432
+ # now we need to upload the file
433
+ instance = cls.create_from_file(file_obj, file_obj.name)
434
+ setattr(related_instance, related_field_name, instance)
435
+ elif isinstance(field_value, int):
436
+ # assume file id
437
+ instance = File.objects.get(id=field_value)
438
+ setattr(related_instance, related_field_name, instance)