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

View File

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

View File

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

View File

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

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

View File

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

View File

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