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>
|
||||
dready <wil@mojipage.com>
|
||||
dusk@woofle.net
|
||||
Dustyn Gibson <miigotu@gmail.com>
|
||||
Ed Morley <https://github.com/edmorley>
|
||||
eibaan@gmail.com
|
||||
elky <http://elky.me/>
|
||||
|
|
|
@ -5,7 +5,7 @@ from django import forms
|
|||
from django.core import checks
|
||||
from django.core.files.base import File
|
||||
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.fields import Field
|
||||
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.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
|
||||
|
||||
kwargs.setdefault('max_length', 100)
|
||||
|
|
|
@ -822,8 +822,13 @@ Has two optional arguments:
|
|||
|
||||
.. attribute:: FileField.storage
|
||||
|
||||
A storage object, which handles the storage and retrieval of your
|
||||
files. See :doc:`/topics/files` for details on how to provide this object.
|
||||
A storage object, or a callable which returns a storage object. This
|
||||
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
|
||||
:class:`~django.forms.ClearableFileInput`.
|
||||
|
|
|
@ -248,6 +248,11 @@ File Storage
|
|||
|
||||
* ``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
|
||||
~~~~~~~~~~~~
|
||||
|
||||
|
|
|
@ -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:
|
||||
you can pass them in as the ``storage`` argument to a
|
||||
: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)
|
||||
|
||||
|
||||
def callable_storage():
|
||||
return temp_storage
|
||||
|
||||
|
||||
class CallableStorage(FileSystemStorage):
|
||||
def __call__(self):
|
||||
# no-op implementation.
|
||||
return self
|
||||
|
||||
|
||||
class Storage(models.Model):
|
||||
def custom_upload_to(self, filename):
|
||||
return 'foo'
|
||||
|
@ -44,6 +54,8 @@ class Storage(models.Model):
|
|||
storage=CustomValidNameStorage(location=temp_storage_location),
|
||||
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')
|
||||
empty = models.FileField(storage=temp_storage)
|
||||
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.exceptions import SuspiciousFileOperation
|
||||
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 (
|
||||
InMemoryUploadedFile, SimpleUploadedFile, TemporaryUploadedFile,
|
||||
)
|
||||
from django.db.models import FileField
|
||||
from django.db.models.fields.files import FileDescriptor
|
||||
from django.test import (
|
||||
LiveServerTestCase, SimpleTestCase, TestCase, override_settings,
|
||||
|
@ -866,6 +869,46 @@ class FileFieldStorageTests(TestCase):
|
|||
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).
|
||||
# This is written in such a way that it'll always pass on platforms
|
||||
# without threading.
|
||||
|
|
Loading…
Reference in New Issue