summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authormiigotu <miigotu@gmail.com>2020-03-31 12:12:39 +0200
committerCarlton Gibson <carlton@noumenal.es>2020-04-08 11:26:17 +0200
commit210657b791fec359a9109b69e566018253edfad0 (patch)
treed9bb41fcd6aae39772ac373697834b24f5193524
parentdb6933a032c850153a688b6c977691b37ca02745 (diff)
Fixed #28184 -- Allowed using a callable for FileField and ImageField storage.
-rw-r--r--AUTHORS1
-rw-r--r--django/db/models/fields/files.py9
-rw-r--r--docs/ref/models/fields.txt9
-rw-r--r--docs/releases/3.1.txt5
-rw-r--r--docs/topics/files.txt28
-rw-r--r--tests/file_storage/models.py12
-rw-r--r--tests/file_storage/tests.py45
7 files changed, 105 insertions, 4 deletions
diff --git a/AUTHORS b/AUTHORS
index d0b01a5eb2..41758bb38d 100644
--- a/AUTHORS
+++ b/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/>
diff --git a/django/db/models/fields/files.py b/django/db/models/fields/files.py
index 7ee38d937f..b682db414c 100644
--- a/django/db/models/fields/files.py
+++ b/django/db/models/fields/files.py
@@ -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)
diff --git a/docs/ref/models/fields.txt b/docs/ref/models/fields.txt
index 7c5401046b..7ca0b96d85 100644
--- a/docs/ref/models/fields.txt
+++ b/docs/ref/models/fields.txt
@@ -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`.
diff --git a/docs/releases/3.1.txt b/docs/releases/3.1.txt
index fd1bd4f40c..7df81329b4 100644
--- a/docs/releases/3.1.txt
+++ b/docs/releases/3.1.txt
@@ -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
~~~~~~~~~~~~
diff --git a/docs/topics/files.txt b/docs/topics/files.txt
index dbe9549818..73d0a11fff 100644
--- a/docs/topics/files.txt
+++ b/docs/topics/files.txt
@@ -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)
diff --git a/tests/file_storage/models.py b/tests/file_storage/models.py
index 8085a8bb1a..9097f465dd 100644
--- a/tests/file_storage/models.py
+++ b/tests/file_storage/models.py
@@ -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)
diff --git a/tests/file_storage/tests.py b/tests/file_storage/tests.py
index 9b41f5250d..a1d2bd372c 100644
--- a/tests/file_storage/tests.py
+++ b/tests/file_storage/tests.py
@@ -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.