diff --git a/cms/djangoapps/export_course_metadata/test_signals.py b/cms/djangoapps/export_course_metadata/test_signals.py index 5e71887f2b..1fc007dd39 100644 --- a/cms/djangoapps/export_course_metadata/test_signals.py +++ b/cms/djangoapps/export_course_metadata/test_signals.py @@ -3,11 +3,15 @@ Tests for signals.py """ from unittest.mock import patch +from django.test.utils import override_settings +from django.conf import settings from edx_toggles.toggles.testutils import override_waffle_flag from xmodule.modulestore.django import SignalHandler 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 .signals import export_course_metadata from .toggles import EXPORT_COURSE_METADATA_FLAG @@ -53,3 +57,66 @@ class TestExportCourseMetadata(SharedModuleStoreTestCase): patched_storage.save.assert_called_once_with( f'course_metadata_export/{self.course_key}.json', patched_content.return_value ) + + @override_settings( + COURSE_METADATA_EXPORT_STORAGE="cms.djangoapps.export_course_metadata.storage.CourseMetadataExportS3Storage", + DEFAULT_FILE_STORAGE="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("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", + COURSE_METADATA_EXPORT_BUCKET="bucket_name_test" + ) + def test_resolve_happy_path_storage(self): + """ Make sure that the correct course export storage is being used """ + storage = resolve_storage_backend("COURSE_METADATA_EXPORT_STORAGE") + self.assertEqual(storage.__class__.__name__, "CourseMetadataExportS3Storage") + self.assertEqual(storage.bucket_name, "bucket_name_test") + + def test_resolve_storage_with_no_config(self): + """ If no storage setup is defined, we get FileSystemStorage by default """ + del settings.DEFAULT_FILE_STORAGE + del settings.COURSE_METADATA_EXPORT_STORAGE + del settings.COURSE_METADATA_EXPORT_BUCKET + storage = resolve_storage_backend("COURSE_METADATA_EXPORT_STORAGE") + self.assertEqual(storage.__class__.__name__, "FileSystemStorage") + + @override_settings( + COURSE_METADATA_EXPORT_STORAGE=None, + COURSE_METADATA_EXPORT_BUCKET="bucket_name_test", + STORAGES={ + 'COURSE_METADATA_EXPORT_STORAGE': { + 'BACKEND': 'cms.djangoapps.export_course_metadata.storage.CourseMetadataExportS3Storage', + 'OPTIONS': {} + } + } + ) + def test_resolve_storage_using_django5_settings(self): + """ Simulating a Django 4 environment using Django 5 Storages configuration """ + storage = resolve_storage_backend("COURSE_METADATA_EXPORT_STORAGE") + self.assertEqual(storage.__class__.__name__, "CourseMetadataExportS3Storage") + self.assertEqual(storage.bucket_name, "bucket_name_test") + + @override_settings( + STORAGES={ + 'COURSE_METADATA_EXPORT_STORAGE': { + 'BACKEND': 'storages.backends.s3boto3.S3Boto3Storage', + 'OPTIONS': { + 'bucket_name': 'bucket_name_test' + } + } + } + ) + def test_resolve_storage_using_django5_settings_with_options(self): + """ Ensure we call the storage class with the correct parameters and Django 5 setup """ + del settings.DEFAULT_FILE_STORAGE + del settings.COURSE_METADATA_EXPORT_STORAGE + del settings.COURSE_METADATA_EXPORT_BUCKET + storage = resolve_storage_backend("COURSE_METADATA_EXPORT_STORAGE") + self.assertEqual(storage.__class__.__name__, S3Boto3Storage.__name__) + self.assertEqual(storage.bucket_name, "bucket_name_test") diff --git a/common/djangoapps/util/storage.py b/common/djangoapps/util/storage.py index 22824b267e..b7bdbdc291 100644 --- a/common/djangoapps/util/storage.py +++ b/common/djangoapps/util/storage.py @@ -6,14 +6,27 @@ from django.core.files.storage import default_storage from django.utils.module_loading import import_string -if django.VERSION >= (5, 0): - from django.core.files.storage import storages +def resolve_storage_backend(storage_key, options=None): + """ + Configures and returns a Django `Storage` instance, compatible with both Django 4 and Django 5. + Main goal: + Deprecate the use of `django.core.files.storage.get_storage_class`. + How: + Replace `get_storage_class` with direct configuration logic, + ensuring backward compatibility with both Django 4 and Django 5 storage settings. + Returns: + An instance of the configured storage backend. + Raises: + ImportError: If the specified storage class cannot be imported. + """ -def resolve_storage_backend(storage_key, options={}): - storage_path = getattr(settings, storage_key) + storage_path = getattr(settings, storage_key, None) storages_config = getattr(settings, 'STORAGES', {}) + if options is None: + options = {} + if storage_key == "default": # Use case 1: Default storage # Works consistently across Django 4.2 and 5.x @@ -29,6 +42,7 @@ def resolve_storage_backend(storage_key, options={}): # "custom": {"BACKEND": "...", "OPTIONS": {...}}, # } # See: https://docs.djangoproject.com/en/5.2/ref/settings/#std-setting-STORAGES + from django.core.files.storage import storages return storages[storage_key] if not storage_path and storage_key in storages_config: @@ -37,7 +51,7 @@ def resolve_storage_backend(storage_key, options={}): # Manually load the backend and options storage_path = storages_config.get(storage_key, {}).get("BACKEND") options = storages_config.get(storage_key, {}).get("OPTIONS", {}) - + if not storage_path: # if no specific storage was resolved, use the default storage return default_storage