Fixed #28184 -- Allowed using a callable for FileField and ImageField storage.

This commit is contained in:
miigotu 2020-03-31 12:12:39 +02:00 committed by Carlton Gibson
parent db6933a032
commit 210657b791
7 changed files with 105 additions and 4 deletions

View File

@ -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/>

View File

@ -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)

View File

@ -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`.

View File

@ -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
~~~~~~~~~~~~

View File

@ -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)

View File

@ -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)

View File

@ -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.