From 0d05d564bdafd4640999d8f687834d3826c9c6e0 Mon Sep 17 00:00:00 2001 From: Feanil Patel Date: Fri, 17 Jul 2026 14:31:37 -0400 Subject: [PATCH 01/10] refactor: migrate ENABLE_DJANGO_ADMIN_SITE off FEATURES-as-dict --- lms/urls.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lms/urls.py b/lms/urls.py index f8885ca94fa5..3a3da9ed29e2 100644 --- a/lms/urls.py +++ b/lms/urls.py @@ -58,7 +58,7 @@ RENDER_VIDEO_XBLOCK_EMBED_NAME = 'render_public_video_xblock_embed' COURSE_PROGRESS_NAME = 'progress' -if settings.DEBUG or settings.FEATURES.get('ENABLE_DJANGO_ADMIN_SITE'): +if settings.DEBUG or settings.ENABLE_DJANGO_ADMIN_SITE: django_autodiscover() admin.site.site_header = _('LMS Administration') admin.site.site_title = admin.site.site_header @@ -800,7 +800,7 @@ ), ] -if settings.DEBUG or settings.FEATURES.get('ENABLE_DJANGO_ADMIN_SITE'): +if settings.DEBUG or settings.ENABLE_DJANGO_ADMIN_SITE: # Jasmine and admin # The password pages in the admin tool are disabled so that all password From 1700af88db719bed782fa9d913a763b8a6b6af3f Mon Sep 17 00:00:00 2001 From: Feanil Patel Date: Fri, 17 Jul 2026 14:32:24 -0400 Subject: [PATCH 02/10] refactor: migrate ENABLE_SERVICE_STATUS off FEATURES-as-dict --- cms/urls.py | 2 +- lms/urls.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/cms/urls.py b/cms/urls.py index 6861b45c3f9b..1fc6cc186172 100644 --- a/cms/urls.py +++ b/cms/urls.py @@ -246,7 +246,7 @@ name='export_git') ] -if settings.FEATURES.get('ENABLE_SERVICE_STATUS'): +if settings.ENABLE_SERVICE_STATUS: urlpatterns.append(path('status/', include('openedx.core.djangoapps.service_status.urls'))) # The password pages in the admin tool are disabled so that all password diff --git a/lms/urls.py b/lms/urls.py index 3a3da9ed29e2..28412b0d95ae 100644 --- a/lms/urls.py +++ b/lms/urls.py @@ -848,7 +848,7 @@ path('_o/', include('oauth2_provider.urls', namespace='oauth2_provider')), ] -if settings.FEATURES.get('ENABLE_SERVICE_STATUS'): +if settings.ENABLE_SERVICE_STATUS: urlpatterns += [ path('status/', include('openedx.core.djangoapps.service_status.urls')), ] From bac172e70b7fcb279a0a964e1380a7a61ccd3916 Mon Sep 17 00:00:00 2001 From: Feanil Patel Date: Fri, 17 Jul 2026 14:32:59 -0400 Subject: [PATCH 03/10] refactor: migrate ENABLE_SEPARATE_ARCHIVED_COURSES off FEATURES-as-dict --- cms/djangoapps/contentstore/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cms/djangoapps/contentstore/utils.py b/cms/djangoapps/contentstore/utils.py index 14aa7a6858a3..6000e24630bc 100644 --- a/cms/djangoapps/contentstore/utils.py +++ b/cms/djangoapps/contentstore/utils.py @@ -1631,7 +1631,7 @@ def format_in_process_course_view(uca): } courses_iter, in_process_course_actions = get_courses_accessible_to_user(request) - split_archived = settings.FEATURES.get('ENABLE_SEPARATE_ARCHIVED_COURSES', False) + split_archived = settings.ENABLE_SEPARATE_ARCHIVED_COURSES active_courses, archived_courses = _process_courses_list(courses_iter, in_process_course_actions, split_archived) in_process_course_actions = [format_in_process_course_view(uca) for uca in in_process_course_actions] return active_courses, archived_courses, in_process_course_actions From 245c596c8847bfb04c8e0b4efa4dd33647aa9737 Mon Sep 17 00:00:00 2001 From: Feanil Patel Date: Fri, 17 Jul 2026 14:33:42 -0400 Subject: [PATCH 04/10] refactor: migrate ENABLE_MKTG_EMAIL_OPT_IN off FEATURES-as-dict --- common/djangoapps/student/tests/test_enrollment.py | 3 +-- common/djangoapps/student/views/management.py | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/common/djangoapps/student/tests/test_enrollment.py b/common/djangoapps/student/tests/test_enrollment.py index f7f929b0725e..dcfb98f2c722 100644 --- a/common/djangoapps/student/tests/test_enrollment.py +++ b/common/djangoapps/student/tests/test_enrollment.py @@ -6,7 +6,6 @@ import ddt import pytest -from django.conf import settings from django.test import override_settings from django.urls import reverse from openedx_events.testing import OpenEdxEventsTestMixin @@ -178,7 +177,7 @@ def test_enrollment_properties_in_segment_traits(self): assert traits['mode'] == 'verified' assert traits['email'] == self.EMAIL - @patch.dict(settings.FEATURES, {'ENABLE_MKTG_EMAIL_OPT_IN': True}) + @override_settings(ENABLE_MKTG_EMAIL_OPT_IN=True) @patch('openedx.core.djangoapps.user_api.preferences.api.update_email_opt_in') @ddt.data( ([], 'true'), diff --git a/common/djangoapps/student/views/management.py b/common/djangoapps/student/views/management.py index 77483dd99c4e..5c2a09584926 100644 --- a/common/djangoapps/student/views/management.py +++ b/common/djangoapps/student/views/management.py @@ -422,7 +422,7 @@ def change_enrollment(request, check_access=True): return HttpResponseBadRequest(_("Course id is invalid")) # Record the user's email opt-in preference - if settings.FEATURES.get('ENABLE_MKTG_EMAIL_OPT_IN'): + if getattr(settings, 'ENABLE_MKTG_EMAIL_OPT_IN', False): _update_email_opt_in(request, course_id.org) available_modes = CourseMode.modes_for_course_dict(course_id) From 842372a1a300ef6ba3a3ad9152e7b3818a29b04d Mon Sep 17 00:00:00 2001 From: Feanil Patel Date: Fri, 17 Jul 2026 14:33:56 -0400 Subject: [PATCH 05/10] refactor: migrate ENABLE_STUDENT_HISTORY_VIEW off FEATURES-as-dict --- lms/urls.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lms/urls.py b/lms/urls.py index 28412b0d95ae..b80534d99682 100644 --- a/lms/urls.py +++ b/lms/urls.py @@ -789,7 +789,7 @@ ), ] -if settings.FEATURES.get('ENABLE_STUDENT_HISTORY_VIEW'): +if settings.ENABLE_STUDENT_HISTORY_VIEW: urlpatterns += [ re_path( r'^courses/{}/submission_history/(?P[^/]*)/(?P.*?)$'.format( # noqa: UP032 From bfa82ce6371be6bd29a263b496f46996bc56f551 Mon Sep 17 00:00:00 2001 From: Feanil Patel Date: Fri, 17 Jul 2026 14:34:31 -0400 Subject: [PATCH 06/10] refactor: migrate ENABLE_CORS_HEADERS off FEATURES-as-dict --- openedx/core/djangoapps/cors_csrf/middleware.py | 2 +- openedx/core/djangoapps/cors_csrf/tests/test_middleware.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/openedx/core/djangoapps/cors_csrf/middleware.py b/openedx/core/djangoapps/cors_csrf/middleware.py index 1bd8accf2709..d3b5349764e1 100644 --- a/openedx/core/djangoapps/cors_csrf/middleware.py +++ b/openedx/core/djangoapps/cors_csrf/middleware.py @@ -62,7 +62,7 @@ class CorsCSRFMiddleware(CsrfViewMiddleware, MiddlewareMixin): def __init__(self, *args, **kwargs): """Disable the middleware if the feature flag is disabled. """ - if not settings.FEATURES.get('ENABLE_CORS_HEADERS'): + if not settings.ENABLE_CORS_HEADERS: raise MiddlewareNotUsed() super().__init__(*args, **kwargs) diff --git a/openedx/core/djangoapps/cors_csrf/tests/test_middleware.py b/openedx/core/djangoapps/cors_csrf/tests/test_middleware.py index 9262dcec7f53..2d33e0c11b5a 100644 --- a/openedx/core/djangoapps/cors_csrf/tests/test_middleware.py +++ b/openedx/core/djangoapps/cors_csrf/tests/test_middleware.py @@ -32,7 +32,7 @@ def get_request(self, is_secure, http_referer): request.is_secure = lambda: is_secure return request - @override_settings(FEATURES={'ENABLE_CORS_HEADERS': True}) + @override_settings(ENABLE_CORS_HEADERS=True) def setUp(self): super().setUp() self.middleware = CorsCSRFMiddleware(get_response=lambda request: None) @@ -78,8 +78,8 @@ def test_enabled(self, http_referer): self.check_enabled(request) @override_settings( - FEATURES={'ENABLE_CORS_HEADERS': False}, - CORS_ORIGIN_WHITELIST=['https://foo.com'] + ENABLE_CORS_HEADERS=False, + CORS_ORIGIN_WHITELIST=['https://foo.com'], ) def test_disabled_no_cors_headers(self): with pytest.raises(MiddlewareNotUsed): From 0bf94003b4b929b6aaa1fb86f4485db11604d955 Mon Sep 17 00:00:00 2001 From: Feanil Patel Date: Fri, 17 Jul 2026 14:35:25 -0400 Subject: [PATCH 07/10] refactor: migrate ENABLE_CROSS_DOMAIN_CSRF_COOKIE off FEATURES-as-dict --- openedx/core/djangoapps/cors_csrf/middleware.py | 8 ++++---- .../djangoapps/cors_csrf/tests/test_middleware.py | 8 ++++---- .../djangoapps/enrollments/tests/test_views.py | 15 ++++----------- 3 files changed, 12 insertions(+), 19 deletions(-) diff --git a/openedx/core/djangoapps/cors_csrf/middleware.py b/openedx/core/djangoapps/cors_csrf/middleware.py index d3b5349764e1..1ed102936d39 100644 --- a/openedx/core/djangoapps/cors_csrf/middleware.py +++ b/openedx/core/djangoapps/cors_csrf/middleware.py @@ -85,7 +85,7 @@ class CsrfCrossDomainCookieMiddleware(MiddlewareMixin): 2) Set `CROSS_DOMAIN_CSRF_COOKIE_NAME` and `CROSS_DOMAIN_CSRF_COOKIE_DOMAIN` in settings. 3) Add the domain to `CORS_ORIGIN_WHITELIST` - 4) Enable `FEATURES['ENABLE_CROSS_DOMAIN_CSRF_COOKIE']` + 4) Enable `ENABLE_CROSS_DOMAIN_CSRF_COOKIE` For testing, it is often easier to relax the security checks by setting: * `CORS_ALLOW_INSECURE = True` @@ -95,19 +95,19 @@ class CsrfCrossDomainCookieMiddleware(MiddlewareMixin): def __init__(self, *args, **kwargs): """Disable the middleware if the feature is not enabled. """ - if not settings.FEATURES.get('ENABLE_CROSS_DOMAIN_CSRF_COOKIE'): + if not settings.ENABLE_CROSS_DOMAIN_CSRF_COOKIE: raise MiddlewareNotUsed() if not getattr(settings, 'CROSS_DOMAIN_CSRF_COOKIE_NAME', ''): raise ImproperlyConfigured( "You must set `CROSS_DOMAIN_CSRF_COOKIE_NAME` when " - "`FEATURES['ENABLE_CROSS_DOMAIN_CSRF_COOKIE']` is True." + "`ENABLE_CROSS_DOMAIN_CSRF_COOKIE` is True." ) if not getattr(settings, 'CROSS_DOMAIN_CSRF_COOKIE_DOMAIN', ''): raise ImproperlyConfigured( "You must set `CROSS_DOMAIN_CSRF_COOKIE_DOMAIN` when " - "`FEATURES['ENABLE_CROSS_DOMAIN_CSRF_COOKIE']` is True." + "`ENABLE_CROSS_DOMAIN_CSRF_COOKIE` is True." ) super().__init__(*args, **kwargs) diff --git a/openedx/core/djangoapps/cors_csrf/tests/test_middleware.py b/openedx/core/djangoapps/cors_csrf/tests/test_middleware.py index 2d33e0c11b5a..34af81daa7db 100644 --- a/openedx/core/djangoapps/cors_csrf/tests/test_middleware.py +++ b/openedx/core/djangoapps/cors_csrf/tests/test_middleware.py @@ -116,15 +116,15 @@ class TestCsrfCrossDomainCookieMiddleware(TestCase): COOKIE_DOMAIN = '.edx.org' @override_settings( - FEATURES={'ENABLE_CROSS_DOMAIN_CSRF_COOKIE': True}, + ENABLE_CROSS_DOMAIN_CSRF_COOKIE=True, CROSS_DOMAIN_CSRF_COOKIE_NAME=COOKIE_NAME, - CROSS_DOMAIN_CSRF_COOKIE_DOMAIN=COOKIE_DOMAIN + CROSS_DOMAIN_CSRF_COOKIE_DOMAIN=COOKIE_DOMAIN, ) def setUp(self): super().setUp() self.middleware = CsrfCrossDomainCookieMiddleware(get_response=lambda request: None) - @override_settings(FEATURES={'ENABLE_CROSS_DOMAIN_CSRF_COOKIE': False}) + @override_settings(ENABLE_CROSS_DOMAIN_CSRF_COOKIE=False) def test_disabled_by_feature_flag(self): with pytest.raises(MiddlewareNotUsed): CsrfCrossDomainCookieMiddleware(get_response=lambda request: None) @@ -132,7 +132,7 @@ def test_disabled_by_feature_flag(self): @ddt.data('CROSS_DOMAIN_CSRF_COOKIE_NAME', 'CROSS_DOMAIN_CSRF_COOKIE_DOMAIN') def test_improperly_configured(self, missing_setting): settings = { - 'FEATURES': {'ENABLE_CROSS_DOMAIN_CSRF_COOKIE': True}, + 'ENABLE_CROSS_DOMAIN_CSRF_COOKIE': True, 'CROSS_DOMAIN_CSRF_COOKIE_NAME': self.COOKIE_NAME, 'CROSS_DOMAIN_CSRF_COOKIE_DOMAIN': self.COOKIE_DOMAIN } diff --git a/openedx/core/djangoapps/enrollments/tests/test_views.py b/openedx/core/djangoapps/enrollments/tests/test_views.py index 5ca7a0f7a7c2..9d38bee3843a 100644 --- a/openedx/core/djangoapps/enrollments/tests/test_views.py +++ b/openedx/core/djangoapps/enrollments/tests/test_views.py @@ -13,7 +13,6 @@ import ddt import httpretty import pytest -from django.conf import settings from django.core.cache import cache from django.core.exceptions import ImproperlyConfigured from django.core.handlers.wsgi import WSGIRequest @@ -1433,22 +1432,16 @@ def test_embargo_change_enrollment_allow(self): def cross_domain_config(func): """Decorator for configuring a cross-domain request. """ - feature_flag_decorator = patch.dict(settings.FEATURES, { - 'ENABLE_CORS_HEADERS': True, - 'ENABLE_CROSS_DOMAIN_CSRF_COOKIE': True - }) settings_decorator = override_settings( + ENABLE_CORS_HEADERS=True, + ENABLE_CROSS_DOMAIN_CSRF_COOKIE=True, CORS_ORIGIN_WHITELIST=["https://www.edx.org"], CROSS_DOMAIN_CSRF_COOKIE_NAME="prod-edx-csrftoken", - CROSS_DOMAIN_CSRF_COOKIE_DOMAIN=".edx.org" + CROSS_DOMAIN_CSRF_COOKIE_DOMAIN=".edx.org", ) is_secure_decorator = patch.object(WSGIRequest, 'is_secure', return_value=True) - return feature_flag_decorator( - settings_decorator( - is_secure_decorator(func) - ) - ) + return settings_decorator(is_secure_decorator(func)) @skip_unless_lms From e304fd22f566d5c18a7ed4aa753b52f8725bfbe2 Mon Sep 17 00:00:00 2001 From: Feanil Patel Date: Fri, 17 Jul 2026 14:35:57 -0400 Subject: [PATCH 08/10] refactor: migrate ENABLE_COURSEWARE_INDEX off FEATURES-as-dict --- cms/djangoapps/contentstore/courseware_index.py | 2 +- cms/djangoapps/contentstore/utils.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/cms/djangoapps/contentstore/courseware_index.py b/cms/djangoapps/contentstore/courseware_index.py index e5b8c5ce0df9..06a204742ff1 100644 --- a/cms/djangoapps/contentstore/courseware_index.py +++ b/cms/djangoapps/contentstore/courseware_index.py @@ -49,7 +49,7 @@ def indexing_is_enabled(): """ Checks to see if the indexing feature is enabled """ - return settings.FEATURES.get('ENABLE_COURSEWARE_INDEX', False) + return settings.ENABLE_COURSEWARE_INDEX class SearchIndexingError(Exception): diff --git a/cms/djangoapps/contentstore/utils.py b/cms/djangoapps/contentstore/utils.py index 6000e24630bc..f57886599f4a 100644 --- a/cms/djangoapps/contentstore/utils.py +++ b/cms/djangoapps/contentstore/utils.py @@ -1864,7 +1864,7 @@ def _get_course_index_context(request, course_key, course_block): lms_link = get_lms_link_for_item(course_block.location) reindex_link = None - if settings.FEATURES.get('ENABLE_COURSEWARE_INDEX', False): + if settings.ENABLE_COURSEWARE_INDEX: if GlobalStaff().has_user(request.user): reindex_link = f"/course/{str(course_key)}/search_reindex" sections = course_block.get_children() From 4a34844057be8e5126e41bcae126888566df7323 Mon Sep 17 00:00:00 2001 From: Feanil Patel Date: Fri, 17 Jul 2026 14:36:18 -0400 Subject: [PATCH 09/10] refactor: migrate ENABLE_COURSEWARE_SEARCH_VERIFIED_ENROLLMENT_REQUIRED off FEATURES-as-dict --- lms/djangoapps/courseware/tests/test_views.py | 4 ++-- lms/djangoapps/courseware/views/views.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lms/djangoapps/courseware/tests/test_views.py b/lms/djangoapps/courseware/tests/test_views.py index 85a6d21d3cbd..d93da9d5cc92 100644 --- a/lms/djangoapps/courseware/tests/test_views.py +++ b/lms/djangoapps/courseware/tests/test_views.py @@ -3180,7 +3180,7 @@ def setUp(self): (CourseMode.MASTERS, True), ) @ddt.unpack - @patch.dict('django.conf.settings.FEATURES', {'ENABLE_COURSEWARE_SEARCH_VERIFIED_ENROLLMENT_REQUIRED': True}) + @override_settings(ENABLE_COURSEWARE_SEARCH_VERIFIED_ENROLLMENT_REQUIRED=True) def test_courseware_mfe_search_verified_only(self, mode, expected_enabled): """ Only verified enrollees may use Courseware Search if ENABLE_COURSEWARE_SEARCH_VERIFIED_ENROLLMENT_REQUIRED @@ -3196,7 +3196,7 @@ def test_courseware_mfe_search_verified_only(self, mode, expected_enabled): self.assertEqual(response.status_code, 200) # noqa: PT009 self.assertEqual(body, {'enabled': expected_enabled}) # noqa: PT009 - @patch.dict('django.conf.settings.FEATURES', {'ENABLE_COURSEWARE_SEARCH_VERIFIED_ENROLLMENT_REQUIRED': True}) + @override_settings(ENABLE_COURSEWARE_SEARCH_VERIFIED_ENROLLMENT_REQUIRED=True) def test_courseware_mfe_search_staff_access(self): """ Staff users may use Courseware Search regardless of their enrollment status. diff --git a/lms/djangoapps/courseware/views/views.py b/lms/djangoapps/courseware/views/views.py index 8ae58850920d..3fac902f2bed 100644 --- a/lms/djangoapps/courseware/views/views.py +++ b/lms/djangoapps/courseware/views/views.py @@ -2353,7 +2353,7 @@ def courseware_mfe_search_enabled(request, course_id=None): user = request.user has_required_enrollment = False - if settings.FEATURES.get('ENABLE_COURSEWARE_SEARCH_VERIFIED_ENROLLMENT_REQUIRED'): + if settings.ENABLE_COURSEWARE_SEARCH_VERIFIED_ENROLLMENT_REQUIRED: enrollment_mode, _ = CourseEnrollment.enrollment_mode_for_user(user, course_key) if ( auth.user_has_role(user, CourseStaffRole(CourseKey.from_string(course_id))) From 76a57760ea8445d3e513f00ab9952a0811c2fa94 Mon Sep 17 00:00:00 2001 From: Feanil Patel Date: Fri, 17 Jul 2026 14:36:36 -0400 Subject: [PATCH 10/10] refactor: migrate ENABLE_CHANGE_USER_PASSWORD_ADMIN off FEATURES-as-dict --- cms/urls.py | 2 +- common/djangoapps/student/admin.py | 2 +- lms/urls.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/cms/urls.py b/cms/urls.py index 1fc6cc186172..c0f96f489bb8 100644 --- a/cms/urls.py +++ b/cms/urls.py @@ -251,7 +251,7 @@ # The password pages in the admin tool are disabled so that all password # changes go through our user portal and follow complexity requirements. -if not settings.FEATURES.get('ENABLE_CHANGE_USER_PASSWORD_ADMIN'): +if not settings.ENABLE_CHANGE_USER_PASSWORD_ADMIN: urlpatterns.append(re_path(r'^admin/auth/user/\d+/password/$', handler404)) urlpatterns.append(path('admin/password_change/', handler404)) urlpatterns.append( diff --git a/common/djangoapps/student/admin.py b/common/djangoapps/student/admin.py index a68d10427401..d6f6b3eb1762 100644 --- a/common/djangoapps/student/admin.py +++ b/common/djangoapps/student/admin.py @@ -538,7 +538,7 @@ class UserChangeForm(BaseUserChangeForm): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - if not settings.FEATURES.get('ENABLE_CHANGE_USER_PASSWORD_ADMIN'): + if not settings.ENABLE_CHANGE_USER_PASSWORD_ADMIN: self.fields["password"] = ReadOnlyPasswordHashField( label=_("Password"), help_text=_( diff --git a/lms/urls.py b/lms/urls.py index b80534d99682..f1f426bea8ce 100644 --- a/lms/urls.py +++ b/lms/urls.py @@ -807,7 +807,7 @@ # changes go through our user portal and follow complexity requirements. # The form to change another user's password is conditionally enabled # for backwards compatibility. - if not settings.FEATURES.get('ENABLE_CHANGE_USER_PASSWORD_ADMIN'): + if not settings.ENABLE_CHANGE_USER_PASSWORD_ADMIN: urlpatterns += [ re_path(r'^admin/auth/user/\d+/password/$', handler404), ]