Fixed #28184 -- Allowed using a callable for FileField and ImageField storage.
This commit is contained in:
parent
db6933a032
commit
210657b791
1
AUTHORS
1
AUTHORS
|
@ -268,6 +268,7 @@ answer newbie questions, and generally made Django that much better:
|
||||||
Doug Napoleone <doug@dougma.com>
|
Doug Napoleone <doug@dougma.com>
|
||||||
dready <wil@mojipage.com>
|
dready <wil@mojipage.com>
|
||||||
dusk@woofle.net
|
dusk@woofle.net
|
||||||
|
Dustyn Gibson <miigotu@gmail.com>
|
||||||
Ed Morley <https://github.com/edmorley>
|
Ed Morley <https://github.com/edmorley>
|
||||||
eibaan@gmail.com
|
eibaan@gmail.com
|
||||||
elky <http://elky.me/>
|
elky <http://elky.me/>
|
||||||
|
|
|
@ -5,7 +5,7 @@ from django import forms
|
||||||
from django.core import checks
|
from django.core import checks
|
||||||
from django.core.files.base import File
|
from django.core.files.base import File
|
||||||
from django.core.files.images import ImageFile
|
from django.core.files.images import ImageFile
|
||||||
from django.core.files.storage import default_storage
|
from django.core.files.storage import Storage, default_storage
|
||||||
from django.db.models import signals
|
from django.db.models import signals
|
||||||
from django.db.models.fields import Field
|
from django.db.models.fields import Field
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
@ -234,6 +234,13 @@ class FileField(Field):
|
||||||
self._primary_key_set_explicitly = 'primary_key' in kwargs
|
self._primary_key_set_explicitly = 'primary_key' in kwargs
|
||||||
|
|
||||||
self.storage = storage or default_storage
|
self.storage = storage or default_storage
|
||||||
|
if callable(self.storage):
|
||||||
|
self.storage = self.storage()
|
||||||
|
if not isinstance(self.storage, Storage):
|
||||||
|
raise TypeError(
|
||||||
|
"%s.storage must be a subclass/instance of %s.%s"
|
||||||
|
% (self.__class__.__qualname__, Storage.__module__, Storage.__qualname__)
|
||||||
|
)
|
||||||
self.upload_to = upload_to
|
self.upload_to = upload_to
|
||||||
|
|
||||||
kwargs.setdefault('max_length', 100)
|
kwargs.setdefault('max_length', 100)
|
||||||
|
|
|
@ -822,8 +822,13 @@ Has two optional arguments:
|
||||||
|
|
||||||
.. attribute:: FileField.storage
|
.. attribute:: FileField.storage
|
||||||
|
|
||||||
A storage object, which handles the storage and retrieval of your
|
A storage object, or a callable which returns a storage object. This
|
||||||
files. See :doc:`/topics/files` for details on how to provide this object.
|
handles the storage and retrieval of your files. See :doc:`/topics/files`
|
||||||
|
for details on how to provide this object.
|
||||||
|
|
||||||
|
.. versionchanged:: 3.1
|
||||||
|
|
||||||
|
The ability to provide a callable was added.
|
||||||
|
|
||||||
The default form widget for this field is a
|
The default form widget for this field is a
|
||||||
:class:`~django.forms.ClearableFileInput`.
|
:class:`~django.forms.ClearableFileInput`.
|
||||||
|
|
|
@ -248,6 +248,11 @@ File Storage
|
||||||
|
|
||||||
* ``FileSystemStorage.save()`` method now supports :class:`pathlib.Path`.
|
* ``FileSystemStorage.save()`` method now supports :class:`pathlib.Path`.
|
||||||
|
|
||||||
|
* :class:`~django.db.models.FileField` and
|
||||||
|
:class:`~django.db.models.ImageField` now accept a callable for ``storage``.
|
||||||
|
This allows you to modify the used storage at runtime, selecting different
|
||||||
|
storages for different environments, for example.
|
||||||
|
|
||||||
File Uploads
|
File Uploads
|
||||||
~~~~~~~~~~~~
|
~~~~~~~~~~~~
|
||||||
|
|
||||||
|
|
|
@ -202,3 +202,31 @@ For example, the following code will store uploaded files under
|
||||||
:doc:`Custom storage systems </howto/custom-file-storage>` work the same way:
|
:doc:`Custom storage systems </howto/custom-file-storage>` work the same way:
|
||||||
you can pass them in as the ``storage`` argument to a
|
you can pass them in as the ``storage`` argument to a
|
||||||
:class:`~django.db.models.FileField`.
|
:class:`~django.db.models.FileField`.
|
||||||
|
|
||||||
|
Using a callable
|
||||||
|
----------------
|
||||||
|
|
||||||
|
.. versionadded:: 3.1
|
||||||
|
|
||||||
|
You can use a callable as the :attr:`~django.db.models.FileField.storage`
|
||||||
|
parameter for :class:`~django.db.models.FileField` or
|
||||||
|
:class:`~django.db.models.ImageField`. This allows you to modify the used
|
||||||
|
storage at runtime, selecting different storages for different environments,
|
||||||
|
for example.
|
||||||
|
|
||||||
|
Your callable will be evaluated when your models classes are loaded, and must
|
||||||
|
return an instance of :class:`~django.core.files.storage.Storage`.
|
||||||
|
|
||||||
|
For example::
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import models
|
||||||
|
from .storages import MyLocalStorage, MyRemoteStorage
|
||||||
|
|
||||||
|
|
||||||
|
def select_storage():
|
||||||
|
return MyLocalStorage() if settings.DEBUG else MyRemoteStorage()
|
||||||
|
|
||||||
|
|
||||||
|
class MyModel(models.Model):
|
||||||
|
my_file = models.FileField(storage=select_storage)
|
||||||
|
|
|
@ -23,6 +23,16 @@ temp_storage_location = tempfile.mkdtemp()
|
||||||
temp_storage = FileSystemStorage(location=temp_storage_location)
|
temp_storage = FileSystemStorage(location=temp_storage_location)
|
||||||
|
|
||||||
|
|
||||||
|
def callable_storage():
|
||||||
|
return temp_storage
|
||||||
|
|
||||||
|
|
||||||
|
class CallableStorage(FileSystemStorage):
|
||||||
|
def __call__(self):
|
||||||
|
# no-op implementation.
|
||||||
|
return self
|
||||||
|
|
||||||
|
|
||||||
class Storage(models.Model):
|
class Storage(models.Model):
|
||||||
def custom_upload_to(self, filename):
|
def custom_upload_to(self, filename):
|
||||||
return 'foo'
|
return 'foo'
|
||||||
|
@ -44,6 +54,8 @@ class Storage(models.Model):
|
||||||
storage=CustomValidNameStorage(location=temp_storage_location),
|
storage=CustomValidNameStorage(location=temp_storage_location),
|
||||||
upload_to=random_upload_to,
|
upload_to=random_upload_to,
|
||||||
)
|
)
|
||||||
|
storage_callable = models.FileField(storage=callable_storage, upload_to='storage_callable')
|
||||||
|
storage_callable_class = models.FileField(storage=CallableStorage, upload_to='storage_callable_class')
|
||||||
default = models.FileField(storage=temp_storage, upload_to='tests', default='tests/default.txt')
|
default = models.FileField(storage=temp_storage, upload_to='tests', default='tests/default.txt')
|
||||||
empty = models.FileField(storage=temp_storage)
|
empty = models.FileField(storage=temp_storage)
|
||||||
limited_length = models.FileField(storage=temp_storage, upload_to='tests', max_length=20)
|
limited_length = models.FileField(storage=temp_storage, upload_to='tests', max_length=20)
|
||||||
|
|
|
@ -13,10 +13,13 @@ from urllib.request import urlopen
|
||||||
from django.core.cache import cache
|
from django.core.cache import cache
|
||||||
from django.core.exceptions import SuspiciousFileOperation
|
from django.core.exceptions import SuspiciousFileOperation
|
||||||
from django.core.files.base import ContentFile, File
|
from django.core.files.base import ContentFile, File
|
||||||
from django.core.files.storage import FileSystemStorage, get_storage_class
|
from django.core.files.storage import (
|
||||||
|
FileSystemStorage, Storage as BaseStorage, get_storage_class,
|
||||||
|
)
|
||||||
from django.core.files.uploadedfile import (
|
from django.core.files.uploadedfile import (
|
||||||
InMemoryUploadedFile, SimpleUploadedFile, TemporaryUploadedFile,
|
InMemoryUploadedFile, SimpleUploadedFile, TemporaryUploadedFile,
|
||||||
)
|
)
|
||||||
|
from django.db.models import FileField
|
||||||
from django.db.models.fields.files import FileDescriptor
|
from django.db.models.fields.files import FileDescriptor
|
||||||
from django.test import (
|
from django.test import (
|
||||||
LiveServerTestCase, SimpleTestCase, TestCase, override_settings,
|
LiveServerTestCase, SimpleTestCase, TestCase, override_settings,
|
||||||
|
@ -866,6 +869,46 @@ class FileFieldStorageTests(TestCase):
|
||||||
self.assertEqual(f.read(), b'content')
|
self.assertEqual(f.read(), b'content')
|
||||||
|
|
||||||
|
|
||||||
|
class FieldCallableFileStorageTests(SimpleTestCase):
|
||||||
|
def setUp(self):
|
||||||
|
self.temp_storage_location = tempfile.mkdtemp(suffix='filefield_callable_storage')
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
shutil.rmtree(self.temp_storage_location)
|
||||||
|
|
||||||
|
def test_callable_base_class_error_raises(self):
|
||||||
|
class NotStorage:
|
||||||
|
pass
|
||||||
|
msg = 'FileField.storage must be a subclass/instance of django.core.files.storage.Storage'
|
||||||
|
for invalid_type in (NotStorage, str, list, set, tuple):
|
||||||
|
with self.subTest(invalid_type=invalid_type):
|
||||||
|
with self.assertRaisesMessage(TypeError, msg):
|
||||||
|
FileField(storage=invalid_type)
|
||||||
|
|
||||||
|
def test_callable_function_storage_file_field(self):
|
||||||
|
storage = FileSystemStorage(location=self.temp_storage_location)
|
||||||
|
|
||||||
|
def get_storage():
|
||||||
|
return storage
|
||||||
|
|
||||||
|
obj = FileField(storage=get_storage)
|
||||||
|
self.assertEqual(obj.storage, storage)
|
||||||
|
self.assertEqual(obj.storage.location, storage.location)
|
||||||
|
|
||||||
|
def test_callable_class_storage_file_field(self):
|
||||||
|
class GetStorage(FileSystemStorage):
|
||||||
|
pass
|
||||||
|
|
||||||
|
obj = FileField(storage=GetStorage)
|
||||||
|
self.assertIsInstance(obj.storage, BaseStorage)
|
||||||
|
|
||||||
|
def test_callable_storage_file_field_in_model(self):
|
||||||
|
obj = Storage()
|
||||||
|
self.assertEqual(obj.storage_callable.storage, temp_storage)
|
||||||
|
self.assertEqual(obj.storage_callable.storage.location, temp_storage_location)
|
||||||
|
self.assertIsInstance(obj.storage_callable_class.storage, BaseStorage)
|
||||||
|
|
||||||
|
|
||||||
# Tests for a race condition on file saving (#4948).
|
# Tests for a race condition on file saving (#4948).
|
||||||
# This is written in such a way that it'll always pass on platforms
|
# This is written in such a way that it'll always pass on platforms
|
||||||
# without threading.
|
# without threading.
|
||||||
|
|
Loading…
Reference in New Issue