Skip to content

feat!: upgrade code and fix get_storage_class ( compatibility django42 and django52) #36628

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 22 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 15 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
37 changes: 29 additions & 8 deletions openedx/core/djangoapps/user_api/accounts/image_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,10 @@
from django.conf import settings
from django.contrib.staticfiles.storage import staticfiles_storage
from django.core.exceptions import ObjectDoesNotExist
from django.core.files.storage import get_storage_class
from django.utils.module_loading import import_string

from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers
from common.djangoapps.student.models import UserProfile
from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers

from ..errors import UserNotFound

Expand All @@ -22,12 +22,33 @@

def get_profile_image_storage():
"""
Configures and returns a django Storage instance that can be used
to physically locate, read and write profile images.
"""
config = settings.PROFILE_IMAGE_BACKEND
storage_class = get_storage_class(config['class'])
return storage_class(**config['options'])
Returns an instance of the configured Django storage class for profile images.

The function looks for the `PROFILE_IMAGE_BACKEND` setting in the Django settings.
If it exists and includes a `'class'` key, that class is used as the storage backend.
If the setting is missing or does not include a `'class'`, the default storage backend
DEFAULT_FILE_STORAGE will return.

Returns:
An instance of the configured storage backend.
"""
config = getattr(settings, 'PROFILE_IMAGE_BACKEND', {})
storage_class_path = config.get('class')
options = config.get('options', {})

if not storage_class_path:
storage_class_path = (
getattr(settings, 'STORAGES', {}).get('profile_images', {}).get('BACKEND') or # named storages
getattr(settings, 'DEFAULT_FILE_STORAGE', None) or
getattr(settings, 'STORAGES', {}).get('default', {}).get('BACKEND') or
'django.core.files.storage.FileSystemStorage'
)

# For Django 5.x, pick options if available
options = getattr(settings, 'STORAGES', {}).get('default', {}).get('OPTIONS', {})

storage_class = import_string(storage_class_path)
return storage_class(**options)


def _make_profile_image_name(username):
Expand Down
34 changes: 34 additions & 0 deletions openedx/core/djangoapps/user_api/accounts/tests/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,13 @@
import ddt
import pytz
from django.conf import settings
from django.core.files.storage import FileSystemStorage
from django.test.testcases import TransactionTestCase
from django.test.utils import override_settings
from django.urls import reverse
from rest_framework import status
from rest_framework.test import APIClient, APITestCase
from storages.backends.s3boto3 import S3Boto3Storage

from common.djangoapps.student.models import PendingEmailChange, UserProfile
from common.djangoapps.student.models_api import do_name_change_request, get_pending_name_change
Expand All @@ -33,6 +35,7 @@
RetirementStateFactory,
UserRetirementStatusFactory
)
from openedx.core.djangoapps.user_api.accounts.image_helpers import get_profile_image_storage
from openedx.core.djangoapps.user_api.models import UserPreference, UserRetirementStatus
from openedx.core.djangoapps.user_api.preferences.api import set_user_preference
from openedx.core.djangoapps.waffle_utils.testutils import WAFFLE_TABLES
Expand Down Expand Up @@ -1156,6 +1159,37 @@ def test_patch_serializer_save_fails(self, serializer_save):
assert "Error thrown when saving account updates: 'bummer'" == error_response.data['developer_message']
assert error_response.data['user_message'] is None

def test_profile_image_backend(self):
# settings file contains the `VIDEO_IMAGE_SETTINGS` but dont'have STORAGE_CLASS
# so it returns the default storage.
storage = get_profile_image_storage()
storage_class = storage.__class__
self.assertEqual(
settings.PROFILE_IMAGE_BACKEND['class'],
f"{storage_class.__module__}.{storage_class.__name__}",
)
self.assertEqual(storage.base_url, settings.PROFILE_IMAGE_BACKEND['options']['base_url'])

@override_settings(PROFILE_IMAGE_BACKEND={
'class': 'storages.backends.s3boto3.S3Boto3Storage',
'options': {
'bucket_name': 'test',
'default_acl': 'public',
'location': 'abc/def'
}
})
def test_profile_backend_with_params(self):
storage = get_profile_image_storage()
self.assertIsInstance(storage, S3Boto3Storage)
self.assertEqual(storage.bucket_name, "test")
self.assertEqual(storage.default_acl, 'public')
self.assertEqual(storage.location, "abc/def")

@override_settings(PROFILE_IMAGE_BACKEND={'class': None, 'options': {}})
def test_profile_backend_without_backend(self):
storage = get_profile_image_storage()
self.assertIsInstance(storage, FileSystemStorage)

@override_settings(PROFILE_IMAGE_BACKEND=TEST_PROFILE_IMAGE_BACKEND)
def test_convert_relative_profile_url(self):
"""
Expand Down
Loading