Skip to content

Commit 53ac66a

Browse files
committed
feat: update file storage access to support Django 5.0 storages registry
1 parent 755f757 commit 53ac66a

File tree

3 files changed

+120
-2
lines changed

3 files changed

+120
-2
lines changed

cms/djangoapps/contentstore/storage.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55

66
from django.conf import settings
7-
from django.core.files.storage import get_storage_class
7+
from common.djangoapps.util.storage import resolve_storage_backend
88
from storages.backends.s3boto3 import S3Boto3Storage
99
from storages.utils import setting
1010

@@ -19,4 +19,4 @@ def __init__(self):
1919
super().__init__(bucket_name=bucket, custom_domain=None, querystring_auth=True)
2020

2121
# pylint: disable=invalid-name
22-
course_import_export_storage = get_storage_class(settings.COURSE_IMPORT_EXPORT_STORAGE)()
22+
course_import_export_storage = resolve_storage_backend("COURSE_IMPORT_EXPORT_STORAGE")

cms/djangoapps/contentstore/tests/test_import.py

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,9 @@
2121
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
2222
from xmodule.modulestore.xml_importer import import_course_from_xml
2323

24+
from common.djangoapps.util.storage import resolve_storage_backend
25+
from storages.backends.s3boto3 import S3Boto3Storage
26+
2427
TEST_DATA_CONTENTSTORE = copy.deepcopy(settings.CONTENTSTORE)
2528
TEST_DATA_CONTENTSTORE['DOC_STORE_CONFIG']['db'] = 'test_xcontent_%s' % uuid4().hex
2629

@@ -275,3 +278,66 @@ def test_video_components_present_while_import(self):
275278

276279
video = module_store.get_item(vertical.children[1])
277280
self.assertEqual(video.display_name, 'default')
281+
282+
@override_settings(
283+
COURSE_IMPORT_EXPORT_STORAGE="cms.djangoapps.contentstore.storage.ImportExportS3Storage",
284+
DEFAULT_FILE_STORAGE="django.core.files.storage.FileSystemStorage"
285+
)
286+
def test_resolve_default_storage(self):
287+
""" Ensure the default storage is invoked, even if course export storage is configured """
288+
storage = resolve_storage_backend("default")
289+
self.assertEqual(storage.__class__.__name__, "FileSystemStorage")
290+
291+
@override_settings(
292+
COURSE_IMPORT_EXPORT_STORAGE="cms.djangoapps.contentstore.storage.ImportExportS3Storage",
293+
DEFAULT_FILE_STORAGE="django.core.files.storage.FileSystemStorage",
294+
COURSE_IMPORT_EXPORT_BUCKET="bucket_name_test"
295+
)
296+
def test_resolve_happy_path_storage(self):
297+
""" Make sure that the correct course export storage is being used """
298+
storage = resolve_storage_backend("COURSE_IMPORT_EXPORT_STORAGE")
299+
self.assertEqual(storage.__class__.__name__, "ImportExportS3Storage")
300+
self.assertEqual(storage.bucket_name, "bucket_name_test")
301+
302+
@override_settings()
303+
def test_resolve_storage_with_no_config(self):
304+
""" If no storage setup is defined, we get FileSystemStorage by default """
305+
del settings.DEFAULT_FILE_STORAGE
306+
del settings.COURSE_IMPORT_EXPORT_STORAGE
307+
del settings.COURSE_IMPORT_EXPORT_BUCKET
308+
storage = resolve_storage_backend("COURSE_IMPORT_EXPORT_STORAGE")
309+
self.assertEqual(storage.__class__.__name__, "FileSystemStorage")
310+
311+
@override_settings(
312+
COURSE_IMPORT_EXPORT_STORAGE=None,
313+
COURSE_IMPORT_EXPORT_BUCKET="bucket_name_test",
314+
STORAGES={
315+
'COURSE_IMPORT_EXPORT_STORAGE': {
316+
'BACKEND': 'cms.djangoapps.contentstore.storage.ImportExportS3Storage',
317+
'OPTIONS': {}
318+
}
319+
}
320+
)
321+
def test_resolve_storage_using_django5_settings(self):
322+
""" Simulating a Django 4 environment using Django 5 Storages configuration """
323+
storage = resolve_storage_backend("COURSE_IMPORT_EXPORT_STORAGE")
324+
self.assertEqual(storage.__class__.__name__, "ImportExportS3Storage")
325+
self.assertEqual(storage.bucket_name, "bucket_name_test")
326+
327+
@override_settings(
328+
STORAGES={
329+
'COURSE_IMPORT_EXPORT_STORAGE': {
330+
'BACKEND': 'storages.backends.s3boto3.S3Boto3Storage',
331+
'OPTIONS': {
332+
'bucket_name': 'bucket_name_test'
333+
}
334+
}
335+
}
336+
)
337+
def test_resolve_storage_using_django5_settings_with_options(self):
338+
""" Ensure we call the storage class with the correct parameters and Django 5 setup """
339+
del settings.COURSE_IMPORT_EXPORT_STORAGE
340+
del settings.COURSE_IMPORT_EXPORT_BUCKET
341+
storage = resolve_storage_backend("COURSE_IMPORT_EXPORT_STORAGE")
342+
self.assertEqual(storage.__class__.__name__, S3Boto3Storage.__name__)
343+
self.assertEqual(storage.bucket_name, "bucket_name_test")

common/djangoapps/util/storage.py

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
""" Utility functions related to django storages """
2+
3+
from django.conf import settings
4+
from django.core.files.storage import default_storage, storages
5+
from django.utils.module_loading import import_string
6+
7+
8+
def resolve_storage_backend(storage_key, options=None):
9+
"""
10+
Configures and returns a Django `Storage` instance, compatible with both Django 4 and Django 5.
11+
Main goal:
12+
Deprecate the use of `django.core.files.storage.get_storage_class`.
13+
How:
14+
Replace `get_storage_class` with direct configuration logic,
15+
ensuring backward compatibility with both Django 4 and Django 5 storage settings.
16+
Returns:
17+
An instance of the configured storage backend.
18+
Raises:
19+
ImportError: If the specified storage class cannot be imported.
20+
"""
21+
22+
storage_path = getattr(settings, storage_key, None)
23+
storages_config = getattr(settings, 'STORAGES', {})
24+
25+
if options is None:
26+
options = {}
27+
28+
if storage_key == "default":
29+
# Use case 1: Default storage
30+
# Works consistently across Django 4.2 and 5.x
31+
# In Django 4.2 and above, `default_storage` uses
32+
# either `DEFAULT_FILE_STORAGE` or `STORAGES['default']`.
33+
return default_storage
34+
35+
if storage_key in storages_config:
36+
# Use case 2: STORAGES is defined
37+
# If STORAGES is present, we retrieve it through the storages API
38+
# settings.py must define STORAGES like:
39+
# STORAGES = {
40+
# "default": {"BACKEND": "...", "OPTIONS": {...}},
41+
# "custom": {"BACKEND": "...", "OPTIONS": {...}},
42+
# }
43+
# See: https://docs.djangoproject.com/en/5.2/ref/settings/#std-setting-STORAGES
44+
return storages[storage_key]
45+
46+
if not storage_path:
47+
# If no storage settings are defined anywhere, use the default storage
48+
return default_storage
49+
50+
# Fallback to import the storage_path manually
51+
StorageClass = import_string(storage_path)
52+
return StorageClass(**options)

0 commit comments

Comments
 (0)