chore: remove deprecated DEFAULT_FILE_STORAGE and STATICFILES_STORAGE settings (#37002)

This commit is contained in:
Ram Chandra Bhavirisetty
2025-09-05 15:52:33 -06:00
committed by GitHub
parent 06b54e79f2
commit 42afa1bb62
20 changed files with 249 additions and 50 deletions

View File

@@ -13,6 +13,7 @@ import ddt
from django.conf import settings
from django.test.client import Client
from django.test.utils import override_settings
from django.core.files.storage import storages
from xmodule.contentstore.django import contentstore
from xmodule.exceptions import NotFoundError
@@ -281,19 +282,24 @@ class ContentStoreImportTest(ModuleStoreTestCase):
@override_settings(
COURSE_IMPORT_EXPORT_STORAGE="cms.djangoapps.contentstore.storage.ImportExportS3Storage",
DEFAULT_FILE_STORAGE="django.core.files.storage.FileSystemStorage"
STORAGES={
'default': {
'BACKEND': "django.core.files.storage.FileSystemStorage"
}
}
)
def test_resolve_default_storage(self):
def test_default_storage(self):
""" Ensure the default storage is invoked, even if course export storage is configured """
storage = resolve_storage_backend(
storage_key="default",
legacy_setting_key="DEFAULT_FILE_STORAGE"
)
storage = storages["default"]
self.assertEqual(storage.__class__.__name__, "FileSystemStorage")
@override_settings(
COURSE_IMPORT_EXPORT_STORAGE="cms.djangoapps.contentstore.storage.ImportExportS3Storage",
DEFAULT_FILE_STORAGE="django.core.files.storage.FileSystemStorage",
STORAGES={
'default': {
'BACKEND': "django.core.files.storage.FileSystemStorage"
}
},
COURSE_IMPORT_EXPORT_BUCKET="bucket_name_test"
)
def test_resolve_happy_path_storage(self):

View File

@@ -12,6 +12,7 @@ from xmodule.modulestore.tests.django_utils import SharedModuleStoreTestCase
from xmodule.modulestore.tests.factories import CourseFactory, BlockFactory
from common.djangoapps.util.storage import resolve_storage_backend
from storages.backends.s3boto3 import S3Boto3Storage
from django.core.files.storage import storages
from .signals import export_course_metadata
from .toggles import EXPORT_COURSE_METADATA_FLAG
@@ -60,16 +61,24 @@ class TestExportCourseMetadata(SharedModuleStoreTestCase):
@override_settings(
COURSE_METADATA_EXPORT_STORAGE="cms.djangoapps.export_course_metadata.storage.CourseMetadataExportS3Storage",
DEFAULT_FILE_STORAGE="django.core.files.storage.FileSystemStorage"
STORAGES={
'default': {
'BACKEND': "django.core.files.storage.FileSystemStorage"
}
}
)
def test_resolve_default_storage(self):
""" Ensure the default storage is invoked, even if course export storage is configured """
storage = resolve_storage_backend(storage_key="default", legacy_setting_key="default")
storage = storages["default"]
self.assertEqual(storage.__class__.__name__, "FileSystemStorage")
@override_settings(
COURSE_METADATA_EXPORT_STORAGE="cms.djangoapps.export_course_metadata.storage.CourseMetadataExportS3Storage",
DEFAULT_FILE_STORAGE="django.core.files.storage.FileSystemStorage",
STORAGES={
"default": {
"BACKEND": "django.core.files.storage.FileSystemStorage"
}
},
COURSE_METADATA_EXPORT_BUCKET="bucket_name_test"
)
def test_resolve_happy_path_storage(self):

View File

@@ -1157,7 +1157,6 @@ PIPELINE = {
'YUI_BINARY': 'yui-compressor',
}
STATICFILES_STORAGE = 'openedx.core.storage.ProductionStorage'
STATICFILES_STORAGE_KWARGS = {}
# List of finder classes that know how to find static files in various locations.
@@ -2386,7 +2385,15 @@ BULK_EMAIL_DEFAULT_FROM_EMAIL = 'no-reply@example.com'
BULK_EMAIL_LOG_SENT_EMAILS = False
############### Settings for django file storage ##################
DEFAULT_FILE_STORAGE = 'django.core.files.storage.FileSystemStorage'
STORAGES = {
'default': {
'BACKEND': 'django.core.files.storage.FileSystemStorage'
},
'staticfiles': {
'BACKEND': 'openedx.core.storage.ProductionStorage'
}
}
###################### Grade Downloads ######################
# These keys are used for all of our asynchronous downloadable files, including

View File

@@ -9,7 +9,7 @@ from os.path import abspath, dirname, join
from .production import * # pylint: disable=wildcard-import, unused-wildcard-import
# Don't use S3 in devstack, fall back to filesystem
del DEFAULT_FILE_STORAGE
STORAGES['default']['BACKEND'] = 'django.core.files.storage.FileSystemStorage'
COURSE_IMPORT_EXPORT_STORAGE = 'django.core.files.storage.FileSystemStorage'
USER_TASKS_ARTIFACT_STORAGE = COURSE_IMPORT_EXPORT_STORAGE
@@ -56,7 +56,7 @@ FEATURES['ENABLE_VIDEO_UPLOAD_PIPELINE'] = True
# Skip packaging and optimization in development
PIPELINE['PIPELINE_ENABLED'] = False
STATICFILES_STORAGE = 'openedx.core.storage.DevelopmentStorage'
STORAGES['staticfiles']['BACKEND'] = 'openedx.core.storage.DevelopmentStorage'
# Revert to the default set of finders as we don't want the production pipeline
STATICFILES_FINDERS = [

View File

@@ -291,7 +291,11 @@ DATABASES:
USER: user
DATA_DIR: /edx/var/edxapp
DEFAULT_FEEDBACK_EMAIL: feedback@example.com
DEFAULT_FILE_STORAGE: storages.backends.s3boto3.S3Boto3Storage
STORAGES:
default:
BACKEND: storages.backends.s3boto3.S3Boto3Storage
staticfiles:
BACKEND: openedx.core.storage.ProductionStorage
DEFAULT_FROM_EMAIL: no-reply@registration.localhost
DEFAULT_HASHING_ALGORITHM: sha256
DEFAULT_JWT_ISSUER:

View File

@@ -82,10 +82,11 @@ with codecs.open(CONFIG_FILE, encoding='utf-8') as f:
'MKTG_URL_LINK_MAP',
'REST_FRAMEWORK',
'EVENT_BUS_PRODUCER_CONFIG',
'DEFAULT_FILE_STORAGE',
'STATICFILES_STORAGE',
]
})
#######################################################################################################################
#### LOAD THE EDX-PLATFORM GIT REVISION
####
@@ -150,11 +151,6 @@ if 'loc_cache' not in CACHES:
if 'staticfiles' in CACHES:
CACHES['staticfiles']['KEY_PREFIX'] = EDX_PLATFORM_REVISION
# In order to transition from local disk asset storage to S3 backed asset storage,
# we need to run asset collection twice, once for local disk and once for S3.
# Once we have migrated to service assets off S3, then we can convert this back to
# managed by the yaml file contents
STATICFILES_STORAGE = os.environ.get('STATICFILES_STORAGE', STATICFILES_STORAGE)
MKTG_URL_LINK_MAP.update(_YAML_TOKENS.get('MKTG_URL_LINK_MAP', {}))
@@ -194,21 +190,38 @@ AWS_BUCKET_ACL = AWS_DEFAULT_ACL
# The number of seconds that a generated URL is valid for.
AWS_QUERYSTRING_EXPIRE = 7 * 24 * 60 * 60 # 7 days
# Change to S3Boto3 if we haven't specified another default storage AND we have specified AWS creds.
if (not _YAML_TOKENS.get('DEFAULT_FILE_STORAGE')) and AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY:
DEFAULT_FILE_STORAGE = 'storages.backends.s3boto3.S3Boto3Storage'
_yaml_storages = _YAML_TOKENS.get('STORAGES', {})
_storages_default_backend_is_missing = not _yaml_storages.get('default', {}).get('BACKEND')
# For backward compatibility, if YAML provides legacy keys (DEFAULT_FILE_STORAGE, STATICFILES_STORAGE)
# and STORAGES doesnt explicitly define the corresponding backend, migrate the legacy value into STORAGES.
# If YAML doesn't provide lagacy keys, no backend is defined in STORAGES['default'] and AWS creds are present,
# fall back to S3Boto3Storage.
#
# This ensures YAML-provided values take precedence over defaults from common.py,
# without overwriting user-defined STORAGES and AWS creds are treated only as a fallback.
if _storages_default_backend_is_missing:
if 'DEFAULT_FILE_STORAGE' in _YAML_TOKENS:
STORAGES['default']['BACKEND'] = _YAML_TOKENS['DEFAULT_FILE_STORAGE']
elif AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY:
STORAGES['default']['BACKEND'] = 'storages.backends.s3boto3.S3Boto3Storage'
# Apply legacy STATICFILES_STORAGE if no backend is defined for "staticfiles"
if 'STATICFILES_STORAGE' in _YAML_TOKENS and not _yaml_storages.get('staticfiles', {}).get('BACKEND'):
STORAGES['staticfiles']['BACKEND'] = _YAML_TOKENS['STATICFILES_STORAGE']
if COURSE_IMPORT_EXPORT_BUCKET:
COURSE_IMPORT_EXPORT_STORAGE = 'cms.djangoapps.contentstore.storage.ImportExportS3Storage'
else:
COURSE_IMPORT_EXPORT_STORAGE = DEFAULT_FILE_STORAGE
COURSE_IMPORT_EXPORT_STORAGE = STORAGES['default']['BACKEND']
USER_TASKS_ARTIFACT_STORAGE = COURSE_IMPORT_EXPORT_STORAGE
if COURSE_METADATA_EXPORT_BUCKET:
COURSE_METADATA_EXPORT_STORAGE = 'cms.djangoapps.export_course_metadata.storage.CourseMetadataExportS3Storage'
else:
COURSE_METADATA_EXPORT_STORAGE = DEFAULT_FILE_STORAGE
COURSE_METADATA_EXPORT_STORAGE = STORAGES['default']['BACKEND']
# The normal database user does not have enough permissions to run migrations.
# Migrations are run with separate credentials, given as DB_MIGRATION_*

View File

@@ -29,7 +29,6 @@ from .common import *
from lms.envs.test import ( # pylint: disable=wrong-import-order, disable=unused-import
ACCOUNT_MICROFRONTEND_URL,
COMPREHENSIVE_THEME_DIRS, # unimport:skip
DEFAULT_FILE_STORAGE,
ECOMMERCE_API_URL,
ENABLE_COMPREHENSIVE_THEMING,
JWT_AUTH,
@@ -91,7 +90,7 @@ STATICFILES_DIRS += [
# If we don't add these settings, then Django templates that can't
# find pipelined assets will raise a ValueError.
# http://stackoverflow.com/questions/12816941/unit-testing-with-django-pipeline
STATICFILES_STORAGE = "pipeline.storage.NonPackagingPipelineStorage"
STORAGES['staticfiles']['BACKEND'] = "pipeline.storage.NonPackagingPipelineStorage"
STATIC_URL = "/static/"
# Update module store settings per defaults for tests

View File

@@ -78,7 +78,7 @@ def store_uploaded_file(
file_storage = DefaultStorage()
# If a file already exists with the supplied name, file_storage will make the filename unique.
stored_file_name = file_storage.save(stored_file_name, uploaded_file)
if is_private and settings.DEFAULT_FILE_STORAGE == 'storages.backends.s3boto3.S3Boto3Storage':
if is_private and settings.STORAGES['default']['BACKEND'] == 'storages.backends.s3boto3.S3Boto3Storage':
S3Boto3Storage().connection.meta.client.put_object_acl(
ACL='private',
Bucket=settings.AWS_STORAGE_BUCKET_NAME,

View File

@@ -38,9 +38,7 @@ def resolve_storage_backend(
storage_path = getattr(settings, legacy_setting_key, None)
storages_config = getattr(settings, 'STORAGES', {})
if options is None:
options = {}
options = options or {}
if storage_key in storages_config:
# Use case 1: STORAGES is defined
@@ -70,5 +68,5 @@ def resolve_storage_backend(
break
storage_path = storage_path.get(deep_setting_key)
StorageClass = import_string(storage_path or settings.DEFAULT_FILE_STORAGE)
StorageClass = import_string(storage_path or storages_config["default"]["BACKEND"])
return StorageClass(**options)

View File

@@ -4,6 +4,7 @@ Tests for the resolve_storage_backend function in common.djangoapps.util.storage
from django.test import TestCase
from django.test.utils import override_settings
from unittest.mock import patch, MagicMock
from common.djangoapps.util.storage import resolve_storage_backend
@@ -20,6 +21,7 @@ class ResolveStorageTest(TestCase):
BLOCK_STRUCTURES_SETTINGS="cms.djangoapps.contentstore.storage.ImportExportS3Storage"
)
def test_legacy_settings(self):
"""Test legacy string-based storage settings."""
storage = resolve_storage_backend(
storage_key="block_structures_settings",
legacy_setting_key="BLOCK_STRUCTURES_SETTINGS",
@@ -33,6 +35,7 @@ class ResolveStorageTest(TestCase):
}
)
def test_nested_legacy_settings(self):
"""Test legacy nested dictionary."""
storage = resolve_storage_backend(
storage_key="block_structures_settings",
legacy_setting_key="BLOCK_STRUCTURES_SETTINGS",
@@ -47,6 +50,7 @@ class ResolveStorageTest(TestCase):
}
)
def test_nested_legacy_settings_failed(self):
"""Test legacy nested dictionary settings with missing key falls back to default."""
storage = resolve_storage_backend(
storage_key="block_structures_settings",
legacy_setting_key="BLOCK_STRUCTURES_SETTINGS",
@@ -54,3 +58,75 @@ class ResolveStorageTest(TestCase):
options={}
)
assert storage.__class__.__name__ == DEFAULT_STORAGE_CLASS_NAME
@override_settings(
STORAGES={
"default": {
"BACKEND": "django.core.files.storage.FileSystemStorage",
"OPTIONS": {}
}
},
LEGACY_SETTING="cms.djangoapps.contentstore.storage.ImportExportS3Storage"
)
def test_missing_storage_key_fallback_to_legacy(self):
"""Test fallback to legacy settings when storage key not found in STORAGES."""
storage = resolve_storage_backend(
storage_key="nonexistent_storage",
legacy_setting_key="LEGACY_SETTING",
options={}
)
assert storage.__class__.__name__ == "ImportExportS3Storage"
def test_no_storages_no_legacy_setting(self):
"""Test fallback to default storage when neither STORAGES nor legacy setting exists."""
storage = resolve_storage_backend(
storage_key="nonexistent_storage",
legacy_setting_key="NONEXISTENT_LEGACY_SETTING",
options={}
)
assert storage.__class__.__name__ == DEFAULT_STORAGE_CLASS_NAME
@override_settings(
STORAGES={
"default": {
"BACKEND": "cms.djangoapps.contentstore.storage.ImportExportS3Storage",
"OPTIONS": {}
}
}
)
def test_fallback_to_custom_default_backend(self):
"""Test fallback uses custom default backend from STORAGES config."""
storage = resolve_storage_backend(
storage_key="nonexistent_storage",
legacy_setting_key="NONEXISTENT_LEGACY_SETTING",
options={}
)
assert storage.__class__.__name__ == "ImportExportS3Storage"
@override_settings(
STORAGES={
"default": {
"BACKEND": "django.core.files.storage.FileSystemStorage",
"OPTIONS": {}
},
"custom_storage_key": {
"BACKEND": "cms.djangoapps.contentstore.storage.ImportExportS3Storage",
"OPTIONS": {}
}
}
)
@patch('common.djangoapps.util.storage.storages')
def test_modern_storages_config(self, mock_storages):
"""Test modern Django STORAGES configuration that takes precedence."""
mock_storage_instance = MagicMock()
mock_storage_instance.__class__.__name__ = "ImportExportS3Storage"
mock_storages.__getitem__.return_value = mock_storage_instance
storage = resolve_storage_backend(
storage_key="custom_storage_key",
legacy_setting_key="SOME_LEGACY_SETTING",
options={}
)
mock_storages.__getitem__.assert_called_once_with("custom_storage_key")
assert storage.__class__.__name__ == "ImportExportS3Storage"

View File

@@ -272,12 +272,12 @@ class DjangoStorageReportStore(ReportStore):
@classmethod
def from_config(cls, config_name):
"""
By default, the default file storage specified by the `DEFAULT_FILE_STORAGE`
By default, the default file storage specified by the `STORAGES['default']`
setting will be used. To configure the storage used, add a dict in
settings with the following fields::
STORAGE_CLASS : The import path of the storage class to use. If
not set, the DEFAULT_FILE_STORAGE setting will be used.
not set, the STORAGES['default']['BACKEND'] setting will be used.
STORAGE_KWARGS : An optional dict of kwargs to pass to the storage
constructor. This can be used to specify a
different S3 bucket or root path, for example.

View File

@@ -2182,7 +2182,6 @@ PIPELINE = {
'UGLIFYJS_BINARY': 'node_modules/.bin/uglifyjs',
}
STATICFILES_STORAGE = 'openedx.core.storage.ProductionStorage'
STATICFILES_STORAGE_KWARGS = {}
# List of finder classes that know how to find static files in various locations.
@@ -4609,7 +4608,14 @@ VIDEO_UPLOAD_PIPELINE = {
}
############### Settings for django file storage ##################
DEFAULT_FILE_STORAGE = 'django.core.files.storage.FileSystemStorage'
STORAGES = {
'default': {
'BACKEND': 'django.core.files.storage.FileSystemStorage'
},
'staticfiles': {
'BACKEND': 'openedx.core.storage.ProductionStorage'
}
}
### Proctoring configuration (redirct URLs and keys shared between systems) ####
PROCTORING_BACKENDS = {

View File

@@ -17,7 +17,7 @@ from openedx.core.djangoapps.plugins.constants import ProjectType, SettingsType
from .production import * # pylint: disable=wildcard-import, unused-wildcard-import
# Don't use S3 in devstack, fall back to filesystem
del DEFAULT_FILE_STORAGE
STORAGES['default']['BACKEND'] = 'django.core.files.storage.FileSystemStorage'
ORA2_FILEUPLOAD_BACKEND = 'django'
@@ -123,7 +123,7 @@ def should_show_debug_toolbar(request): # lint-amnesty, pylint: disable=missing
########################### PIPELINE #################################
PIPELINE['PIPELINE_ENABLED'] = False
STATICFILES_STORAGE = 'openedx.core.storage.DevelopmentStorage'
STORAGES['staticfiles']['BACKEND'] = 'openedx.core.storage.DevelopmentStorage'
# Revert to the default set of finders as we don't want the production pipeline
STATICFILES_FINDERS = [

View File

@@ -376,7 +376,11 @@ DATABASES:
DATA_DIR: /edx/var/edxapp
DEFAULT_COURSE_VISIBILITY_IN_CATALOG: both
DEFAULT_FEEDBACK_EMAIL: feedback@example.com
DEFAULT_FILE_STORAGE: storages.backends.s3boto3.S3Boto3Storage
STORAGES:
default:
BACKEND: storages.backends.s3boto3.S3Boto3Storage
staticfiles:
BACKEND: openedx.core.storage.ProductionStorage
DEFAULT_FROM_EMAIL: sandbox-notifications@example.com
DEFAULT_HASHING_ALGORITHM: sha256
DEFAULT_JWT_ISSUER:

View File

@@ -77,10 +77,11 @@ with codecs.open(CONFIG_FILE, encoding='utf-8') as f:
'MKTG_URL_LINK_MAP',
'REST_FRAMEWORK',
'EVENT_BUS_PRODUCER_CONFIG',
'DEFAULT_FILE_STORAGE',
'STATICFILES_STORAGE',
]
})
#######################################################################################################################
#### LOAD THE EDX-PLATFORM GIT REVISION
####
@@ -218,9 +219,26 @@ if AWS_SECRET_ACCESS_KEY == "":
AWS_DEFAULT_ACL = 'public-read'
AWS_BUCKET_ACL = AWS_DEFAULT_ACL
# Change to S3Boto3 if we haven't specified another default storage AND we have specified AWS creds.
if (not _YAML_TOKENS.get('DEFAULT_FILE_STORAGE')) and AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY:
DEFAULT_FILE_STORAGE = 'storages.backends.s3boto3.S3Boto3Storage'
_yaml_storages = _YAML_TOKENS.get('STORAGES', {})
_storages_default_backend_is_missing = not _yaml_storages.get('default', {}).get('BACKEND')
# For backward compatibility, if YAML provides legacy keys (DEFAULT_FILE_STORAGE, STATICFILES_STORAGE)
# and STORAGES doesnt explicitly define the corresponding backend, migrate the legacy value into STORAGES.
# If YAML doesn't provide lagacy keys, no backend is defined in STORAGES['default'] and AWS creds are present,
# fall back to S3Boto3Storage.
#
# This ensures YAML-provided values take precedence over defaults from common.py,
# without overwriting user-defined STORAGES and AWS creds are treated only as a fallback.
if _storages_default_backend_is_missing:
if 'DEFAULT_FILE_STORAGE' in _YAML_TOKENS:
STORAGES['default']['BACKEND'] = _YAML_TOKENS['DEFAULT_FILE_STORAGE']
elif AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY:
STORAGES['default']['BACKEND'] = 'storages.backends.s3boto3.S3Boto3Storage'
# Apply legacy STATICFILES_STORAGE if no backend is defined for "staticfiles"
if 'STATICFILES_STORAGE' in _YAML_TOKENS and not _yaml_storages.get('staticfiles', {}).get('BACKEND'):
STORAGES['staticfiles']['BACKEND'] = _YAML_TOKENS['STATICFILES_STORAGE']
# The normal database user does not have enough permissions to run migrations.
# Migrations are run with separate credentials, given as DB_MIGRATION_*

View File

@@ -151,7 +151,7 @@ STATICFILES_DIRS += [
# If we don't add these settings, then Django templates that can't
# find pipelined assets will raise a ValueError.
# http://stackoverflow.com/questions/12816941/unit-testing-with-django-pipeline
STATICFILES_STORAGE = 'pipeline.storage.NonPackagingPipelineStorage'
STORAGES['staticfiles']['BACKEND'] = 'pipeline.storage.NonPackagingPipelineStorage'
# Don't use compression during tests
PIPELINE['JS_COMPRESSOR'] = None
@@ -298,7 +298,7 @@ ENTERPRISE_MARKETING_FOOTER_QUERY_PARAMS = OrderedDict([
])
############################ STATIC FILES #############################
DEFAULT_FILE_STORAGE = 'django.core.files.storage.FileSystemStorage'
STORAGES['default']['BACKEND'] = 'django.core.files.storage.FileSystemStorage'
MEDIA_ROOT = TEST_ROOT / "uploads"
MEDIA_URL = "/uploads/"
STATICFILES_DIRS.append(("uploads", MEDIA_ROOT))

View File

@@ -100,7 +100,13 @@ class TestThemingViews(TestCase):
assert response.status_code == 302
assert response.url == "/static/images/logo.png"
@override_settings(STATICFILES_STORAGE="openedx.core.storage.DevelopmentStorage")
@override_settings(
STORAGES={
'staticfiles': {
'BACKEND': 'openedx.core.storage.DevelopmentStorage'
}
}
)
def test_asset_with_theme(self):
"""
Fetch theme asset when a theme is set.

View File

@@ -1231,7 +1231,6 @@ class TestAccountsAPI(FilteredQueryCountMixin, CacheIsolationTestCase, UserAPITe
)
def test_profile_backend_with_default_hardcoded_backend(self):
""" In case of empty storages scenario uses the hardcoded backend."""
del settings.DEFAULT_FILE_STORAGE
del settings.STORAGES
storage = get_profile_image_storage()
self.assertIsInstance(storage, FileSystemStorage)

View File

@@ -54,7 +54,7 @@ class ProductionMixin(
We use this version on production.
"""
def __init__(self, *args, **kwargs):
kwargs.update(settings.STATICFILES_STORAGE_KWARGS.get(settings.STATICFILES_STORAGE, {}))
kwargs.update(settings.STATICFILES_STORAGE_KWARGS.get(settings.STORAGES['staticfiles']['BACKEND'], {}))
super().__init__(*args, **kwargs) # lint-amnesty, pylint: disable=super-with-arguments
@@ -112,5 +112,5 @@ def get_storage(storage_class=None, **kwargs):
the storage implementation makes http requests when instantiated, for
example.
"""
storage_cls = import_string(storage_class or settings.DEFAULT_FILE_STORAGE)
storage_cls = import_string(storage_class or settings.STORAGES["default"]["BACKEND"])
return storage_cls(**kwargs)

View File

@@ -0,0 +1,54 @@
"""
Tests for the get_storage utility function.
"""
from django.test import TestCase, override_settings
from django.core.files.storage import FileSystemStorage
from openedx.core.storage import get_storage
class TestGetStorage(TestCase):
"""
Tests of the get_storage function
"""
def setUp(self):
super().setUp()
get_storage.cache_clear()
def tearDown(self):
get_storage.cache_clear()
@override_settings(
STORAGES={
'default': {
'BACKEND': 'django.core.files.storage.FileSystemStorage'
}
}
)
def test_get_storage_returns_default_storage_when_no_class_specified(self):
"""Test that get_storage returns the default storage when no storage_class is provided."""
storage = get_storage()
self.assertIsInstance(storage, FileSystemStorage)
def test_get_storage_returns_custom_storage_when_class_specified(self):
"""Test that get_storage returns the specified storage class."""
storage_class = 'django.core.files.storage.FileSystemStorage'
storage = get_storage(storage_class=storage_class)
self.assertIsInstance(storage, FileSystemStorage)
def test_get_storage_caching_behavior(self):
"""Test that get_storage caches instances with identical arguments."""
storage_class = 'django.core.files.storage.FileSystemStorage'
kwargs = {'location': '/test/path'}
# First Call
storage1 = get_storage(storage_class=storage_class, **kwargs)
# Second Call
storage2 = get_storage(storage_class=storage_class, **kwargs)
self.assertIs(storage1, storage2)
def test_get_storage_handles_invalid_storage_class(self):
"""Test that get_storage raises appropriate error for invalid storage class."""
with self.assertRaises(ImportError):
get_storage(storage_class='nonexistent.storage.InvalidStorage')