Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions edx_django_utils/storage/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
"""
Storage utilities for edx_django_utils.

This module exposes helper functions for working with Django storage backends.
"""

from .utils import get_named_storage
106 changes: 106 additions & 0 deletions edx_django_utils/storage/test_utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
"""
Unit tests for storage utilities in edx_django_utils.
"""

import ddt
from django.conf import settings
from django.core.files.storage import default_storage
from django.test import TestCase
from django.test.utils import override_settings

from edx_django_utils.storage.utils import get_named_storage


@ddt.ddt
class TestGetNamedStorage(TestCase):
"""
Tests for the get_named_storage utility.
"""

@ddt.data(
(
'avatars',
'AVATAR_BACKEND',
(
{'avatars': {
'BACKEND': 'django.core.files.storage.FileSystemStorage',
'OPTIONS': {'location': '/tmp/avatars'}
}},
None # No legacy config
),
'/tmp/avatars'
),
(
'avatars',
'AVATAR_BACKEND',
(
{}, # Empty STORAGES dict
{
'class': 'django.core.files.storage.FileSystemStorage',
'options': {'location': '/tmp/legacy_avatars'}
}
),
'/tmp/legacy_avatars'
),
(
'certificates',
'CERTIFICATE_BACKEND',
(
{'certificates': {
'BACKEND': 'django.core.files.storage.FileSystemStorage',
'OPTIONS': {'location': '/tmp/certificates'}
}},
None
),
'/tmp/certificates'
),
(
'certificates',
'CERTIFICATE_BACKEND',
(
{}, # Empty STORAGES dict
{
'class': 'django.core.files.storage.FileSystemStorage',
'options': {'location': '/tmp/legacy_certificates'}
}
),
'/tmp/legacy_certificates'
),
)
@ddt.unpack
def test_get_named_storage(self, storage_name, legacy_setting_name, config, expected_location):
"""
Test get_named_storage with both STORAGES dict and legacy config.
"""
storages_config, legacy_config = config

with override_settings(STORAGES=storages_config or {}):
if legacy_config:
setattr(settings, legacy_setting_name, legacy_config)
elif hasattr(settings, legacy_setting_name):
delattr(settings, legacy_setting_name)

Check warning on line 81 in edx_django_utils/storage/test_utils.py

View check run for this annotation

Codecov / codecov/patch

edx_django_utils/storage/test_utils.py#L81

Added line #L81 was not covered by tests

storage = get_named_storage(storage_name, legacy_setting_name=legacy_setting_name)
self.assertEqual(storage.location, expected_location)

def test_fallback_to_default_storage(self):
"""
Test fallback to default_storage when neither STORAGES dict nor legacy config is defined.
"""
with override_settings(STORAGES={
'default': {
'BACKEND': 'django.core.files.storage.FileSystemStorage',
'OPTIONS': {'location': '/tmp/default'}
}
}):
for legacy_setting in ['AVATAR_BACKEND', 'CERTIFICATE_BACKEND', 'PROFILE_IMAGE_BACKEND']:
if hasattr(settings, legacy_setting):
delattr(settings, legacy_setting)

Check warning on line 98 in edx_django_utils/storage/test_utils.py

View check run for this annotation

Codecov / codecov/patch

edx_django_utils/storage/test_utils.py#L98

Added line #L98 was not covered by tests

for storage_name, legacy_setting_name in [
('avatars', 'AVATAR_BACKEND'),
('certificates', 'CERTIFICATE_BACKEND'),
('profile_image', 'PROFILE_IMAGE_BACKEND'),
]:
storage = get_named_storage(storage_name, legacy_setting_name=legacy_setting_name)
self.assertEqual(storage, default_storage)
55 changes: 55 additions & 0 deletions edx_django_utils/storage/utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
"""
Utilities for working with Django storage backends.

This module provides helper functions to retrieve storage instances
based on Django's STORAGES setting, legacy configuration, or
fallback to the default storage.
"""

from django.conf import settings
from django.core.files.storage import default_storage, storages
from django.utils.module_loading import import_string


def get_named_storage(name=None, legacy_setting_name=None):
"""
Returns an instance of the configured storage backend.

This function prioritizes configuration in the following order:

1. Use the named storage from Django's STORAGES if `name` is defined.
2. If not found, check the legacy setting (if `legacy_setting_name` is provided).
3. If still undefined, fall back to Django's default_storage.

Args:
name (str, optional): The name of the storage as defined in Django's STORAGES setting.
legacy_setting_name (str, optional): The legacy setting dict to check
for a storage class path and options.

Returns:
An instance of the configured storage backend.

Raises:
ValueError: If neither `name` nor `legacy_setting_name` are provided.
ImportError: If the specified storage class cannot be imported.
"""
if not name and not legacy_setting_name:
raise ValueError("You must provide at least 'name' or 'legacy_setting_name'.")

Check warning on line 37 in edx_django_utils/storage/utils.py

View check run for this annotation

Codecov / codecov/patch

edx_django_utils/storage/utils.py#L37

Added line #L37 was not covered by tests

# 1. Check Django 4.2+ STORAGES dict if `name` is provided
if name:
storages_config = getattr(settings, 'STORAGES', {})
if name in storages_config:
return storages[name]

# 2. Check legacy config if `legacy_setting_name` is provided
if legacy_setting_name:
legacy_config = getattr(settings, legacy_setting_name, {})
storage_class_path = legacy_config.get('class') or legacy_config.get('STORAGE_CLASS')
options = legacy_config.get('options', {}) or legacy_config.get('STORAGE_KWARGS', {})
if storage_class_path:
storage_class = import_string(storage_class_path)
return storage_class(**options)

# 3. Fallback to Django's default_storage
return default_storage