Skip to content

Commit e72808c

Browse files
Atiya IshaqAtiya Ishaq
authored andcommitted
feat: Add utility method for getting storages.
1 parent ffed9e0 commit e72808c

3 files changed

Lines changed: 168 additions & 0 deletions

File tree

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
"""
2+
Storage utilities for edx_django_utils.
3+
4+
This module exposes helper functions for working with Django storage backends.
5+
"""
6+
7+
from .utils import get_named_storage
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
"""
2+
Unit tests for storage utilities in edx_django_utils.
3+
"""
4+
5+
import ddt
6+
from django.conf import settings
7+
from django.core.files.storage import default_storage
8+
from django.test import TestCase
9+
from django.test.utils import override_settings
10+
11+
from edx_django_utils.storage.utils import get_named_storage
12+
13+
14+
@ddt.ddt
15+
class TestGetNamedStorage(TestCase):
16+
"""
17+
Tests for the get_named_storage utility.
18+
"""
19+
20+
@ddt.data(
21+
(
22+
'avatars',
23+
'AVATAR_BACKEND',
24+
(
25+
{'avatars': {
26+
'BACKEND': 'django.core.files.storage.FileSystemStorage',
27+
'OPTIONS': {'location': '/tmp/avatars'}
28+
}},
29+
None # No legacy config
30+
),
31+
'/tmp/avatars'
32+
),
33+
(
34+
'avatars',
35+
'AVATAR_BACKEND',
36+
(
37+
{}, # Empty STORAGES dict
38+
{
39+
'class': 'django.core.files.storage.FileSystemStorage',
40+
'options': {'location': '/tmp/legacy_avatars'}
41+
}
42+
),
43+
'/tmp/legacy_avatars'
44+
),
45+
(
46+
'certificates',
47+
'CERTIFICATE_BACKEND',
48+
(
49+
{'certificates': {
50+
'BACKEND': 'django.core.files.storage.FileSystemStorage',
51+
'OPTIONS': {'location': '/tmp/certificates'}
52+
}},
53+
None
54+
),
55+
'/tmp/certificates'
56+
),
57+
(
58+
'certificates',
59+
'CERTIFICATE_BACKEND',
60+
(
61+
{}, # Empty STORAGES dict
62+
{
63+
'class': 'django.core.files.storage.FileSystemStorage',
64+
'options': {'location': '/tmp/legacy_certificates'}
65+
}
66+
),
67+
'/tmp/legacy_certificates'
68+
),
69+
)
70+
@ddt.unpack
71+
def test_get_named_storage(self, storage_name, legacy_setting_name, config, expected_location):
72+
"""
73+
Test get_named_storage with both STORAGES dict and legacy config.
74+
"""
75+
storages_config, legacy_config = config
76+
77+
with override_settings(STORAGES=storages_config or {}):
78+
if legacy_config:
79+
setattr(settings, legacy_setting_name, legacy_config)
80+
elif hasattr(settings, legacy_setting_name):
81+
delattr(settings, legacy_setting_name)
82+
83+
storage = get_named_storage(storage_name, legacy_setting_name=legacy_setting_name)
84+
self.assertEqual(storage.location, expected_location)
85+
86+
def test_fallback_to_default_storage(self):
87+
"""
88+
Test fallback to default_storage when neither STORAGES dict nor legacy config is defined.
89+
"""
90+
with override_settings(STORAGES={
91+
'default': {
92+
'BACKEND': 'django.core.files.storage.FileSystemStorage',
93+
'OPTIONS': {'location': '/tmp/default'}
94+
}
95+
}):
96+
for legacy_setting in ['AVATAR_BACKEND', 'CERTIFICATE_BACKEND', 'PROFILE_IMAGE_BACKEND']:
97+
if hasattr(settings, legacy_setting):
98+
delattr(settings, legacy_setting)
99+
100+
for storage_name, legacy_setting_name in [
101+
('avatars', 'AVATAR_BACKEND'),
102+
('certificates', 'CERTIFICATE_BACKEND'),
103+
('profile_image', 'PROFILE_IMAGE_BACKEND'),
104+
]:
105+
storage = get_named_storage(storage_name, legacy_setting_name=legacy_setting_name)
106+
self.assertEqual(storage, default_storage)

edx_django_utils/storage/utils.py

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
"""
2+
Utilities for working with Django storage backends.
3+
4+
This module provides helper functions to retrieve storage instances
5+
based on Django's STORAGES setting, legacy configuration, or
6+
fallback to the default storage.
7+
"""
8+
9+
from django.conf import settings
10+
from django.core.files.storage import default_storage, storages
11+
from django.utils.module_loading import import_string
12+
13+
14+
def get_named_storage(name=None, legacy_setting_name=None):
15+
"""
16+
Returns an instance of the configured storage backend.
17+
18+
This function prioritizes configuration in the following order:
19+
20+
1. Use the named storage from Django's STORAGES if `name` is defined.
21+
2. If not found, check the legacy setting (if `legacy_setting_name` is provided).
22+
3. If still undefined, fall back to Django's default_storage.
23+
24+
Args:
25+
name (str, optional): The name of the storage as defined in Django's STORAGES setting.
26+
legacy_setting_name (str, optional): The legacy setting dict to check
27+
for a storage class path and options.
28+
29+
Returns:
30+
An instance of the configured storage backend.
31+
32+
Raises:
33+
ValueError: If neither `name` nor `legacy_setting_name` are provided.
34+
ImportError: If the specified storage class cannot be imported.
35+
"""
36+
if not name and not legacy_setting_name:
37+
raise ValueError("You must provide at least 'name' or 'legacy_setting_name'.")
38+
39+
# 1. Check Django 4.2+ STORAGES dict if `name` is provided
40+
if name:
41+
storages_config = getattr(settings, 'STORAGES', {})
42+
if name in storages_config:
43+
return storages[name]
44+
45+
# 2. Check legacy config if `legacy_setting_name` is provided
46+
if legacy_setting_name:
47+
legacy_config = getattr(settings, legacy_setting_name, {})
48+
storage_class_path = legacy_config.get('class') or legacy_config.get('STORAGE_CLASS')
49+
options = legacy_config.get('options', {}) or legacy_config.get('STORAGE_KWARGS', {})
50+
if storage_class_path:
51+
storage_class = import_string(storage_class_path)
52+
return storage_class(**options)
53+
54+
# 3. Fallback to Django's default_storage
55+
return default_storage

0 commit comments

Comments
 (0)