alliance-platform-storage 0.0.2__tar.gz
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.
- alliance_platform_storage-0.0.2/PKG-INFO +16 -0
- alliance_platform_storage-0.0.2/README.md +0 -0
- alliance_platform_storage-0.0.2/alliance_platform/py.typed +0 -0
- alliance_platform_storage-0.0.2/alliance_platform/storage/__init__.py +0 -0
- alliance_platform_storage-0.0.2/alliance_platform/storage/apps.py +11 -0
- alliance_platform_storage-0.0.2/alliance_platform/storage/async_uploads/forms.py +203 -0
- alliance_platform_storage-0.0.2/alliance_platform/storage/async_uploads/models.py +730 -0
- alliance_platform_storage-0.0.2/alliance_platform/storage/async_uploads/registry.py +110 -0
- alliance_platform_storage-0.0.2/alliance_platform/storage/async_uploads/rest_framework/__init__.py +2 -0
- alliance_platform_storage-0.0.2/alliance_platform/storage/async_uploads/rest_framework/serializer.py +125 -0
- alliance_platform_storage-0.0.2/alliance_platform/storage/async_uploads/storage/azure.py +82 -0
- alliance_platform_storage-0.0.2/alliance_platform/storage/async_uploads/storage/base.py +122 -0
- alliance_platform_storage-0.0.2/alliance_platform/storage/async_uploads/storage/filesystem.py +105 -0
- alliance_platform_storage-0.0.2/alliance_platform/storage/async_uploads/storage/s3.py +76 -0
- alliance_platform_storage-0.0.2/alliance_platform/storage/async_uploads/views/__init__.py +2 -0
- alliance_platform_storage-0.0.2/alliance_platform/storage/async_uploads/views/api.py +204 -0
- alliance_platform_storage-0.0.2/alliance_platform/storage/async_uploads/views/filesystem.py +139 -0
- alliance_platform_storage-0.0.2/alliance_platform/storage/management/commands/cleanup_async_temp_files.py +66 -0
- alliance_platform_storage-0.0.2/alliance_platform/storage/migrations/0001_initial.py +40 -0
- alliance_platform_storage-0.0.2/alliance_platform/storage/migrations/0002_migrate_common_storage.py +37 -0
- alliance_platform_storage-0.0.2/alliance_platform/storage/migrations/__init__.py +0 -0
- alliance_platform_storage-0.0.2/alliance_platform/storage/models.py +2 -0
- alliance_platform_storage-0.0.2/alliance_platform/storage/settings.py +42 -0
- alliance_platform_storage-0.0.2/alliance_platform/storage/staticfiles/__init__.py +0 -0
- alliance_platform_storage-0.0.2/alliance_platform/storage/staticfiles/storage.py +73 -0
- alliance_platform_storage-0.0.2/pyproject.toml +67 -0
- alliance_platform_storage-0.0.2/tests/__init__.py +0 -0
- alliance_platform_storage-0.0.2/tests/test_async_field.py +758 -0
- alliance_platform_storage-0.0.2/tests/test_async_rest_framework.py +175 -0
- alliance_platform_storage-0.0.2/tests/test_excluding_manifest_static_files_storage.py +121 -0
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: alliance-platform-storage
|
|
3
|
+
Version: 0.0.2
|
|
4
|
+
Summary: Alliance Platform Storage
|
|
5
|
+
Author-Email: Alliance Software <support@alliancesoftware.com.au>
|
|
6
|
+
License: BSD-2-Clause
|
|
7
|
+
Requires-Python: >=3.11
|
|
8
|
+
Requires-Dist: alliance-platform-core
|
|
9
|
+
Requires-Dist: Django>=4.2.11
|
|
10
|
+
Requires-Dist: django-allianceutils>=4.0
|
|
11
|
+
Provides-Extra: s3
|
|
12
|
+
Requires-Dist: django-storages[s3]; extra == "s3"
|
|
13
|
+
Provides-Extra: azure
|
|
14
|
+
Requires-Dist: django-storages[azure]; extra == "azure"
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
from alliance_platform.storage.settings import ap_storage_settings
|
|
2
|
+
from django.apps.config import AppConfig
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class AlliancePlatformStorageConfig(AppConfig):
|
|
6
|
+
name = "alliance_platform.storage"
|
|
7
|
+
verbose_name = "Alliance Platform Storage"
|
|
8
|
+
label = "alliance_platform_storage"
|
|
9
|
+
|
|
10
|
+
def ready(self):
|
|
11
|
+
ap_storage_settings.check_settings()
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from json import JSONDecodeError
|
|
3
|
+
|
|
4
|
+
from alliance_platform.storage.async_uploads.models import AsyncFileInputData
|
|
5
|
+
from alliance_platform.storage.async_uploads.models import AsyncTempFile
|
|
6
|
+
from alliance_platform.storage.async_uploads.registry import AsyncFieldRegistry
|
|
7
|
+
from alliance_platform.storage.async_uploads.storage.base import AsyncUploadStorage
|
|
8
|
+
from django import forms
|
|
9
|
+
from django.core import validators
|
|
10
|
+
from django.core.exceptions import ValidationError
|
|
11
|
+
from django.forms.widgets import Input
|
|
12
|
+
from django.urls import reverse
|
|
13
|
+
from django.utils.deconstruct import deconstructible
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
# This does not extend FileInput as we don't actually receive a file
|
|
17
|
+
# from the frontend. Instead we get a string which represents the key
|
|
18
|
+
# for the storage backend.
|
|
19
|
+
class AsyncFileInput(Input):
|
|
20
|
+
"""Input for handling async uploads
|
|
21
|
+
|
|
22
|
+
This handles the submission value from UploadWidget and converts it to an instance
|
|
23
|
+
of :class:`~alliance_platform.storage.async_uploads.models.AsyncFileInputData`. This is then handled
|
|
24
|
+
on the descriptor classes for :class:`~alliance_platform.storage.async_uploads.models.AsyncFileField`
|
|
25
|
+
and :class:`~alliance_platform.storage.async_uploads.models.AsyncImageField`.
|
|
26
|
+
|
|
27
|
+
To customise the widget rendered on the frontend you can override the template
|
|
28
|
+
``alliance_platform/storage/widgets/async_file_input.html``.
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
input_type = "async-file"
|
|
32
|
+
template_name = "alliance_platform/storage/widgets/async_file_input.html"
|
|
33
|
+
|
|
34
|
+
# This is needed to generate URL to file in case where we only
|
|
35
|
+
# have the AsyncTempFile (see `format_value`)
|
|
36
|
+
storage: AsyncUploadStorage
|
|
37
|
+
|
|
38
|
+
def get_context(self, name, value, attrs):
|
|
39
|
+
context = super().get_context(name, value, attrs)
|
|
40
|
+
|
|
41
|
+
# Resolving the upload url needs to be delayed until render so that the
|
|
42
|
+
# GenerateUploadUrl view has a chance to attach to the registry. Resolve
|
|
43
|
+
# that here.
|
|
44
|
+
async_field_registry = context["widget"]["attrs"].pop("async_field_registry", None)
|
|
45
|
+
if not async_field_registry:
|
|
46
|
+
raise ValueError("AsyncFileInput expects a 'async_field_registry' to be passed in widget attrs")
|
|
47
|
+
|
|
48
|
+
context["widget"]["attrs"]["generate_upload_url"] = reverse(async_field_registry.attached_view)
|
|
49
|
+
|
|
50
|
+
return context
|
|
51
|
+
|
|
52
|
+
def format_value(self, value):
|
|
53
|
+
"""Given a value return it in serialized format expected by frontend
|
|
54
|
+
|
|
55
|
+
Frontend expects a json string like:
|
|
56
|
+
|
|
57
|
+
{"key": "/some/storage/location/test.png", "name": "test.png" }
|
|
58
|
+
"""
|
|
59
|
+
if not value:
|
|
60
|
+
return None
|
|
61
|
+
if isinstance(value, AsyncFileInputData):
|
|
62
|
+
# If a submission occurs but isn't successful (eg. validation fails on another
|
|
63
|
+
# field) we end up with a string which should match the key on AsyncTempFile
|
|
64
|
+
try:
|
|
65
|
+
temp_file = AsyncTempFile.objects.get(key=value.key)
|
|
66
|
+
return json.dumps(
|
|
67
|
+
{
|
|
68
|
+
"key": temp_file.key,
|
|
69
|
+
"name": temp_file.original_filename,
|
|
70
|
+
"url": self.storage.url(temp_file.key),
|
|
71
|
+
}
|
|
72
|
+
)
|
|
73
|
+
except AsyncTempFile.DoesNotExist:
|
|
74
|
+
return json.dumps(
|
|
75
|
+
{
|
|
76
|
+
"key": value.key,
|
|
77
|
+
"name": value.name,
|
|
78
|
+
}
|
|
79
|
+
)
|
|
80
|
+
try:
|
|
81
|
+
url = value.url
|
|
82
|
+
except Exception:
|
|
83
|
+
# url isn't critical; if it fails ignore it and frontend may just not be able
|
|
84
|
+
# to show a thumbnail
|
|
85
|
+
url = None
|
|
86
|
+
return json.dumps({"key": value.name, "name": value.name.split("/")[-1], "url": url})
|
|
87
|
+
|
|
88
|
+
def value_from_datadict(self, data, files, name):
|
|
89
|
+
"""This expects to receive a valid json string that can be used to instantiate AsyncFileInputData
|
|
90
|
+
|
|
91
|
+
If invalid JSON is received this is handled by the ``AsyncFileInputDataValidator`` which will
|
|
92
|
+
raise a ValidationError. We load the json here rather than in the field ``clean`` so that we have
|
|
93
|
+
the ``AsyncFileInputData`` in ``format`` for the case where there's a ValidationError and form
|
|
94
|
+
re-renders.
|
|
95
|
+
|
|
96
|
+
AsyncFileDescriptor & AsyncImageDescriptor handle extracting the ``key`` from this which is
|
|
97
|
+
what is set against the field on the model.
|
|
98
|
+
|
|
99
|
+
We need the AsyncFileInputData to pass across extra details that we may need - eg. width & height
|
|
100
|
+
for image fields
|
|
101
|
+
"""
|
|
102
|
+
value = data.get(name)
|
|
103
|
+
try:
|
|
104
|
+
value = json.loads(value)
|
|
105
|
+
if value:
|
|
106
|
+
return AsyncFileInputData.create_from_user_input(value)
|
|
107
|
+
return value
|
|
108
|
+
except JSONDecodeError:
|
|
109
|
+
return value
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
class AsyncFileInputDataLengthValidator(validators.MaxLengthValidator):
|
|
113
|
+
"""Validate length of ``key`` on an AsyncFileInputData
|
|
114
|
+
|
|
115
|
+
FileField passes ``max_length`` to the form field and the field is, at the database level,
|
|
116
|
+
a char field. The normal FileField handles this internally but we can't extend forms.FileField
|
|
117
|
+
as it expects to be handling File submissions which we aren't doing here. This validator
|
|
118
|
+
just extracts the ``key`` and validates the length of that which is what gets written to the
|
|
119
|
+
database.
|
|
120
|
+
"""
|
|
121
|
+
|
|
122
|
+
def clean(self, x):
|
|
123
|
+
if isinstance(x, AsyncFileInputData):
|
|
124
|
+
# We want to check the length of the key which is what gets stored in db
|
|
125
|
+
return len(x.name)
|
|
126
|
+
return len(x)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
@deconstructible
|
|
130
|
+
class AsyncFileInputDataValidator:
|
|
131
|
+
"""AsyncFileInputData has an error key that can be sent from the frontend - this validator just checks that"""
|
|
132
|
+
|
|
133
|
+
def __call__(self, value):
|
|
134
|
+
if isinstance(value, AsyncFileInputData):
|
|
135
|
+
if value.error:
|
|
136
|
+
raise ValidationError(f"There was an unexpected error: {value.error}")
|
|
137
|
+
else:
|
|
138
|
+
raise ValidationError("Bad input for file field")
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
class AsyncFileField(forms.Field):
|
|
142
|
+
"""Form field that renders a :class:`~alliance_platform.storage.async_uploads.models.AsyncFileInput`
|
|
143
|
+
|
|
144
|
+
This is the default form field for :class:`alliance_platform.storage.async_uploads.models.AsyncFileField`
|
|
145
|
+
"""
|
|
146
|
+
|
|
147
|
+
widget = AsyncFileInput
|
|
148
|
+
|
|
149
|
+
async_field_registry: AsyncFieldRegistry
|
|
150
|
+
async_field_id: str
|
|
151
|
+
|
|
152
|
+
def __init__(
|
|
153
|
+
self,
|
|
154
|
+
*args,
|
|
155
|
+
async_field_registry: AsyncFieldRegistry,
|
|
156
|
+
async_field_id: str,
|
|
157
|
+
storage: AsyncUploadStorage,
|
|
158
|
+
max_length=None,
|
|
159
|
+
**kwargs,
|
|
160
|
+
):
|
|
161
|
+
"""
|
|
162
|
+
|
|
163
|
+
Args:
|
|
164
|
+
*args: Any additional arguments to pass through to :class:`django.forms.Field`
|
|
165
|
+
async_field_registry: The async field registry that should used on the frontend to create
|
|
166
|
+
unique upload urls for files. This typically comes from :code:`field.async_field_registry` where :code:`field`
|
|
167
|
+
is an :class:`~alliance_platform.storage.async_uploads.models.AsyncFileField`.
|
|
168
|
+
async_field_id: The string ID of the field used in the registry. Generated with ``field.async_field_registry.generate_id(field)``.
|
|
169
|
+
storage: The storage class. This comes from the field ``field.storage``.
|
|
170
|
+
max_length: Max length for the filename
|
|
171
|
+
**kwargs:ny additional keyword arguments to pass through to :class:`django.forms.Field`
|
|
172
|
+
"""
|
|
173
|
+
self.async_field_registry = async_field_registry
|
|
174
|
+
self.async_field_id = async_field_id
|
|
175
|
+
# This comes from FileField
|
|
176
|
+
self.max_length = max_length
|
|
177
|
+
super().__init__(*args, **kwargs)
|
|
178
|
+
|
|
179
|
+
# No way to pass args to a widget with how forms.Field work.. just
|
|
180
|
+
# set it after creation
|
|
181
|
+
self.widget.storage = storage
|
|
182
|
+
|
|
183
|
+
if max_length is not None:
|
|
184
|
+
self.validators.append(AsyncFileInputDataLengthValidator(int(max_length)))
|
|
185
|
+
self.validators.append(AsyncFileInputDataValidator())
|
|
186
|
+
|
|
187
|
+
def widget_attrs(self, widget):
|
|
188
|
+
attrs = super().widget_attrs(widget)
|
|
189
|
+
attrs["async_field_registry"] = self.async_field_registry
|
|
190
|
+
attrs["async_field_id"] = self.async_field_id
|
|
191
|
+
return attrs
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
class AsyncImageField(AsyncFileField):
|
|
195
|
+
widget = AsyncFileInput
|
|
196
|
+
|
|
197
|
+
def widget_attrs(self, widget):
|
|
198
|
+
attrs = super().widget_attrs(widget)
|
|
199
|
+
if "accept" not in attrs:
|
|
200
|
+
attrs.setdefault("accept", "image/*")
|
|
201
|
+
if "list_type" not in attrs:
|
|
202
|
+
attrs.setdefault("list_type", "picture")
|
|
203
|
+
return attrs
|