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

Merged
merged 30 commits into from
Jun 5, 2025

Conversation

awais786
Copy link
Contributor

@awais786 awais786 commented Apr 29, 2025

#36740

Fixing breaking change for django52.

  • Verify the image functionality on sandbox.

https://app.pr-36628-139931.sandboxes.opencraft.hosting/profile/u/openedx
opened/openedx

Replaced usage of `get_storage_class`, which was deprecated in Django 4.2 and removed in 5.1,
with `import_string` to support dynamic storage class loading.
@awais786 awais786 changed the title feat!: upgrade codebase for compatibility with Django 4.2 and 5.2 feat!: upgrade code and fix get_storage_class ( compatibility django42 and django52) Apr 29, 2025
Comment on lines 39 to 40
storage_class_path = config.get('class')
storage_class = import_string(storage_class_path)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unsure if this case could ever occur but if the contents of storage_class_path are incorrect, there could be an error which we should catch e.g

try: storage_class = import_string(storage_class_path) except ImportError as e: raise ImproperlyConfigured( f"Could not import storage class '{storage_class_path}': {e}" )

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@musanaeem
Copy link
Contributor

LGTM!

Comment on lines 39 to 41
storage_class_path = config.get('class')
storage_class = import_string(storage_class_path)
return storage_class(**config.get('options', {}))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are not we using the below one?

from django.core.files.storage import storages
storage_class = storages(config.get('class'))
return storage_class(**config.get('options', {}))

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@awais786 awais786 force-pushed the fixing-django-storages-dj52 branch from 2ffa43e to 03e8c83 Compare May 6, 2025 11:17
@awais786
Copy link
Contributor Author

@feanil Please review

@awais786 awais786 marked this pull request as ready for review May 13, 2025 16:20
@awais786 awais786 requested a review from a team as a code owner May 13, 2025 16:20
Comment on lines 39 to 44
if not storage_class_path:
storage_class_path = (
getattr(settings, 'DEFAULT_FILE_STORAGE', None) or
getattr(settings, 'STORAGES', {}).get('default', {}).get('BACKEND') or
'django.core.files.storage.FileSystemStorage'
)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Before we fallback to the default storage class, I think we should also have a named storage class in the STORAGES dictionary. Maybe named profile_images?

This way, operators could migrate to defining their various storages config via the new STORAGES config and we can eventually deprecate the old configuration settings.

@awais786
Copy link
Contributor Author

awais786 commented May 15, 2025

@feanil I have verified on django42 this solution will not work. These settings cannot declare together.

STORAGES = {
    "profile_images": {
        "BACKEND": "openedx.core.storage.OverwriteStorage",
        "OPTIONS": {
            "location": "profile-images/",
            "base_url": "profile-images/",
        },
    },
    'default': {
        'BACKEND': 'django.core.files.storage.FileSystemStorage'
    }
}

DEFAULT_FILE_STORAGE = 'django.core.files.storage.FileSystemStorage'

These cannot declared together. It raises error

django.core.exceptions.ImproperlyConfigured: DEFAULT_FILE_STORAGE/STORAGES are mutually exclusive.

Django code reference

Right now DEFAULT_FILE_STORAGE is declared at multiple places and this change will increase the scope of this PR. We can add followup ticket for cleaning purpose. openedx/public-engineering#369

@awais786
Copy link
Contributor Author

@feanil its ready for final review.

@awais786
Copy link
Contributor Author

@feanil its ready.

@feanil feanil requested a review from Copilot June 2, 2025 17:10
Copy link

@Copilot Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

This PR refactors the profile image storage helper to support both the existing PROFILE_IMAGE_BACKEND setting and the new Django 5+ STORAGES configuration, and adds tests in test_views.py to validate each fallback path.

  • Migrate from get_storage_class to import_string and explicit resolution order for storage backends.
  • Introduce support for the new STORAGES setting (with profile_image and default keys) before falling back to DEFAULT_FILE_STORAGE or FileSystemStorage.
  • Add comprehensive tests in test_views.py to verify default, parameterized, STORAGES-based, and hardcoded fallback behaviors.

Reviewed Changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.

File Description
openedx/core/djangoapps/user_api/accounts/tests/test_views.py New tests covering get_profile_image_storage under various PROFILE_IMAGE_BACKEND and STORAGES scenarios
openedx/core/djangoapps/user_api/accounts/image_helpers.py Refactored get_profile_image_storage to use import_string, support Django 5+ STORAGES, and drop get_storage_class
Comments suppressed due to low confidence (1)

openedx/core/djangoapps/user_api/accounts/tests/test_views.py:1173

  • [nitpick] Test method names are inconsistent: the first test is test_profile_image_backend, but subsequent tests use test_profile_backend_*. For clarity, rename them to test_profile_image_backend_with_params, test_profile_image_backend_without_backend, etc.
@override_settings(PROFILE_IMAGE_BACKEND={

@@ -8,10 +8,11 @@
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.core.files.storage import storages
Copy link
Preview

Copilot AI Jun 2, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Importing storages directly will break on Django versions before 5.0, where this attribute doesn't exist. Wrap this import in a try/except ImportError and fall back to a compatible approach (e.g., using import_string or the legacy get_storage_class) for Django<5.0.

Copilot uses AI. Check for mistakes.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This comment is wrong, django had the storages import available in 4.2, it just drops the get_storage_class function in 5.0 which is what we're trying to fix here.

Copy link
Contributor

@feanil feanil left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks good, just one more change and then I think it's good to merge.

FYI, I've added a note to the Ulmo release page for operators so they know about the new storage class name and can switch to it after Ulmo lands so we can remove support for the conversion from the old way to the new way.

@awais786 once you've made the final change, I don't need to re-review it if it looks good to you and we have test coverage testing the priority order is working correctly.


1. If the `PROFILE_IMAGE_BACKEND` setting is defined and includes a `'class'` key,
it uses the specified storage backend and options.
2. If not, it checks the Django 5+ `STORAGES` setting for a named storage called
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be the highest priority option, we should also make the newest way to do things the highest priority, falling back to the older ways for compatibility. This makes it easier to read and also easier to cleanup in the future when we want to drop all the old ways of doing this.

@awais786 awais786 added the create-sandbox open-craft-grove should create a sandbox environment from this PR label Jun 3, 2025
@open-craft-grove
Copy link

Sandbox deployment successful 🚀
🎓 LMS
📝 Studio
ℹ️ Grove Config, Tutor Config, Tutor Requirements

@open-craft-grove
Copy link

Sandbox deployment failed 💥
Please check the settings and requirements.
Retry deployment by pushing a new commit or updating the requirements/settings in the pull request's description.
📜 Failure Logs
ℹ️ Grove Config, Tutor Config, Tutor Requirements

@open-craft-grove
Copy link

Sandbox deployment failed 💥
Please check the settings and requirements.
Retry deployment by pushing a new commit or updating the requirements/settings in the pull request's description.
📜 Failure Logs
ℹ️ Grove Config, Tutor Config, Tutor Requirements

@awais786 awais786 merged commit 291c5c3 into master Jun 5, 2025
49 checks passed
@awais786 awais786 deleted the fixing-django-storages-dj52 branch June 5, 2025 14:11
@edx-pipeline-bot
Copy link
Contributor

2U Release Notice: This PR has been deployed to the edX staging environment in preparation for a release to production.

@edx-pipeline-bot
Copy link
Contributor

2U Release Notice: This PR has been deployed to the edX production environment.

1 similar comment
@edx-pipeline-bot
Copy link
Contributor

2U Release Notice: This PR has been deployed to the edX production environment.

@edx-pipeline-bot
Copy link
Contributor

2U Release Notice: This PR has been deployed to the edX staging environment in preparation for a release to production.

@edx-pipeline-bot
Copy link
Contributor

2U Release Notice: This PR has been deployed to the edX production environment.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
create-sandbox open-craft-grove should create a sandbox environment from this PR
Projects
None yet
Development

Successfully merging this pull request may close these issues.

7 participants