From 71ea2a0b124d910a9fc35d2d49e726db153e15f7 Mon Sep 17 00:00:00 2001 From: Feanil Patel Date: Fri, 17 Jul 2026 21:37:44 -0400 Subject: [PATCH 01/10] refactor: migrate EDITABLE_SHORT_DESCRIPTION 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..765a1e3163f7 100644 --- a/cms/djangoapps/contentstore/utils.py +++ b/cms/djangoapps/contentstore/utils.py @@ -1403,7 +1403,7 @@ def get_course_settings(request, course_key, course_block): short_description_editable = configuration_helpers.get_value_for_org( course_block.location.org, 'EDITABLE_SHORT_DESCRIPTION', - settings.FEATURES.get('EDITABLE_SHORT_DESCRIPTION', True) + settings.EDITABLE_SHORT_DESCRIPTION ) sidebar_html_enabled = ENABLE_COURSE_ABOUT_SIDEBAR_HTML.is_enabled() From d4d2a3b7caf3588cc803ff92e64eb3cb30b3df6d Mon Sep 17 00:00:00 2001 From: Feanil Patel Date: Fri, 17 Jul 2026 21:38:30 -0400 Subject: [PATCH 02/10] refactor: migrate ENABLE_COSMETIC_DISPLAY_PRICE off FEATURES-as-dict --- lms/djangoapps/courseware/views/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lms/djangoapps/courseware/views/views.py b/lms/djangoapps/courseware/views/views.py index 8ae58850920d..15c3cc7d2220 100644 --- a/lms/djangoapps/courseware/views/views.py +++ b/lms/djangoapps/courseware/views/views.py @@ -892,7 +892,7 @@ def course_about(request, course_id): # pylint: disable=too-many-statements 'studio_url': studio_url, 'registered': registered, 'course_target': course_target, - 'is_cosmetic_price_enabled': settings.FEATURES.get('ENABLE_COSMETIC_DISPLAY_PRICE'), + 'is_cosmetic_price_enabled': settings.ENABLE_COSMETIC_DISPLAY_PRICE, 'course_price': course_price, 'ecommerce_checkout': ecommerce_checkout, 'ecommerce_checkout_link': ecommerce_checkout_link, From b5a9a9b7b61a5555f750259f16dcc3b80374eae3 Mon Sep 17 00:00:00 2001 From: Feanil Patel Date: Fri, 17 Jul 2026 21:41:55 -0400 Subject: [PATCH 03/10] refactor: migrate ENABLE_EXTENDED_COURSE_DETAILS off FEATURES-as-dict The flag was only ever read via settings.FEATURES.get with an inline default and was never defined in any envs file. Add an annotated flat DjangoSetting definition (default False, matching the old inline default) to cms/envs/common.py so the CMS-only reader can use settings.X directly. --- cms/djangoapps/contentstore/utils.py | 2 +- cms/envs/common.py | 11 +++++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/cms/djangoapps/contentstore/utils.py b/cms/djangoapps/contentstore/utils.py index 765a1e3163f7..e5467b49ccbf 100644 --- a/cms/djangoapps/contentstore/utils.py +++ b/cms/djangoapps/contentstore/utils.py @@ -1395,7 +1395,7 @@ def get_course_settings(request, course_key, course_block): enable_extended_course_details = configuration_helpers.get_value_for_org( course_block.location.org, 'ENABLE_EXTENDED_COURSE_DETAILS', - settings.FEATURES.get('ENABLE_EXTENDED_COURSE_DETAILS', False) + settings.ENABLE_EXTENDED_COURSE_DETAILS ) about_page_editable = not publisher_enabled diff --git a/cms/envs/common.py b/cms/envs/common.py index 949f01f8cf3b..e089d5854e26 100644 --- a/cms/envs/common.py +++ b/cms/envs/common.py @@ -101,6 +101,17 @@ # .. toggle_tickets: https://github.com/openedx/edx-platform/pull/2334 EDITABLE_SHORT_DESCRIPTION = True +# .. toggle_name: settings.ENABLE_EXTENDED_COURSE_DETAILS +# .. toggle_implementation: DjangoSetting +# .. toggle_default: False +# .. toggle_description: When enabled, exposes the extended course detail fields (course background image, +# video thumbnail image, and related text fields) on the Schedule & Details page in Open edX Studio. +# Can also be overridden per-org via site configuration. +# .. toggle_use_cases: open_edx +# .. toggle_creation_date: 2016-04-30 +# .. toggle_tickets: WL-398 +ENABLE_EXTENDED_COURSE_DETAILS = False + # Hide any Personally Identifiable Information from application logs SQUELCH_PII_IN_LOGS = False From 2521faf0599a5cbf18e1303963b7a7776d13b245 Mon Sep 17 00:00:00 2001 From: Feanil Patel Date: Fri, 17 Jul 2026 21:47:57 -0400 Subject: [PATCH 04/10] refactor: migrate ENABLE_FINANCIAL_ASSISTANCE_FORM off FEATURES-as-dict The flag had no envs definition and was only read via settings.FEATURES.get (with lms/envs/test.py already setting the flat value, routed through the FeaturesProxy bridge). Add an annotated flat DjangoSetting definition (default False) to lms/envs/common.py and migrate the two LMS-only readers. In course_tools.py this also drops the now-unnecessary 'FEATURES is None' (Edge) guard, since the flat setting always resolves to its default. --- lms/djangoapps/courseware/course_tools.py | 7 ++----- lms/envs/common.py | 10 ++++++++++ lms/urls.py | 2 +- 3 files changed, 13 insertions(+), 6 deletions(-) diff --git a/lms/djangoapps/courseware/course_tools.py b/lms/djangoapps/courseware/course_tools.py index c0e37c76e068..2a7b6a3be8b1 100644 --- a/lms/djangoapps/courseware/course_tools.py +++ b/lms/djangoapps/courseware/course_tools.py @@ -34,16 +34,13 @@ def is_enabled(cls, request, course_key): Show this link for active courses where financial assistance is available, unless upgrade deadline has passed """ now = datetime.datetime.now(pytz.UTC) - feature_flags = None try: course_overview = CourseOverview.objects.get(id=course_key) except CourseOverview.DoesNotExist: course_overview = None - # hide link if there's no ENABLE_FINANCIAL_ASSISTANCE_FORM setting (ex: Edge) or if it's False - subset_name = 'FEATURES' - feature_flags = getattr(settings, subset_name) - if feature_flags is None or not feature_flags.get('ENABLE_FINANCIAL_ASSISTANCE_FORM'): + # hide link if financial assistance is not enabled + if not settings.ENABLE_FINANCIAL_ASSISTANCE_FORM: return False # hide link for archived courses diff --git a/lms/envs/common.py b/lms/envs/common.py index e3b79d5bbca2..b5b1ae37593c 100644 --- a/lms/envs/common.py +++ b/lms/envs/common.py @@ -267,6 +267,16 @@ # .. toggle_warning: The use case of this feature toggle is uncertain. ENABLE_COSMETIC_DISPLAY_PRICE = False +# .. toggle_name: settings.ENABLE_FINANCIAL_ASSISTANCE_FORM +# .. toggle_implementation: DjangoSetting +# .. toggle_default: False +# .. toggle_description: When enabled, exposes the financial assistance application form and its URL route, +# and shows the "financial assistance" course tool link for eligible learners. +# .. toggle_use_cases: open_edx +# .. toggle_creation_date: 2015-12-04 +# .. toggle_tickets: ECOM-2824 +ENABLE_FINANCIAL_ASSISTANCE_FORM = False + # Automatically approve student identity verification attempts # .. toggle_name: settings.AUTOMATIC_VERIFY_STUDENT_IDENTITY_FOR_TESTING # .. toggle_implementation: DjangoSetting diff --git a/lms/urls.py b/lms/urls.py index f8885ca94fa5..f4226e9615fa 100644 --- a/lms/urls.py +++ b/lms/urls.py @@ -947,7 +947,7 @@ path('__debug__/', include(debug_toolbar.urls)), ] -if settings.FEATURES.get('ENABLE_FINANCIAL_ASSISTANCE_FORM'): +if settings.ENABLE_FINANCIAL_ASSISTANCE_FORM: urlpatterns += [ path( 'financial-assistance/', From 44619a1c5a66d96a64e77c71ba0f4c1449da4e78 Mon Sep 17 00:00:00 2001 From: Feanil Patel Date: Fri, 17 Jul 2026 22:00:29 -0400 Subject: [PATCH 05/10] refactor: migrate ENABLE_FOOTER_MOBILE_APP_LINKS off FEATURES-as-dict --- lms/djangoapps/branding/api.py | 2 +- lms/djangoapps/branding/tests/test_views.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lms/djangoapps/branding/api.py b/lms/djangoapps/branding/api.py index f14868341e60..f9f30943bfa8 100644 --- a/lms/djangoapps/branding/api.py +++ b/lms/djangoapps/branding/api.py @@ -431,7 +431,7 @@ def _footer_mobile_links(is_secure): platform_name = configuration_helpers.get_value('platform_name', settings.PLATFORM_NAME) mobile_links = [] - if settings.FEATURES.get('ENABLE_FOOTER_MOBILE_APP_LINKS'): + if settings.ENABLE_FOOTER_MOBILE_APP_LINKS: mobile_links = [ { "name": "apple", diff --git a/lms/djangoapps/branding/tests/test_views.py b/lms/djangoapps/branding/tests/test_views.py index 672a61aea6c4..aae84a942597 100644 --- a/lms/djangoapps/branding/tests/test_views.py +++ b/lms/djangoapps/branding/tests/test_views.py @@ -45,7 +45,7 @@ def test_footer_content_types(self, theme, accepts, content_type, content): assert resp['Content-Type'] == content_type self.assertContains(resp, content) - @mock.patch.dict(settings.FEATURES, {'ENABLE_FOOTER_MOBILE_APP_LINKS': True}) + @override_settings(ENABLE_FOOTER_MOBILE_APP_LINKS=True) def test_footer_json(self): self._set_feature_flag(True) with with_comprehensive_theme_context(None): From 0a5d5f33d10aa02e5907ac31f4b6af10e10e7fa7 Mon Sep 17 00:00:00 2001 From: Feanil Patel Date: Fri, 17 Jul 2026 22:01:09 -0400 Subject: [PATCH 06/10] refactor: migrate ENABLE_HIDE_FROM_TOC_UI off FEATURES-as-dict --- .../contentstore/xblock_storage_handlers/view_handlers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cms/djangoapps/contentstore/xblock_storage_handlers/view_handlers.py b/cms/djangoapps/contentstore/xblock_storage_handlers/view_handlers.py index 7e41876aafc3..4b23da215a2f 100644 --- a/cms/djangoapps/contentstore/xblock_storage_handlers/view_handlers.py +++ b/cms/djangoapps/contentstore/xblock_storage_handlers/view_handlers.py @@ -1347,7 +1347,7 @@ def create_xblock_info( # pylint: disable=too-many-statements "user_partitions": user_partitions, "show_correctness": xblock.show_correctness, "hide_from_toc": xblock.hide_from_toc, - "enable_hide_from_toc_ui": settings.FEATURES.get("ENABLE_HIDE_FROM_TOC_UI", False), + "enable_hide_from_toc_ui": settings.ENABLE_HIDE_FROM_TOC_UI, "xblock_type": get_icon(xblock), } ) From 4b404b511587dc12f9569a0e4938fb19d4f21045 Mon Sep 17 00:00:00 2001 From: Feanil Patel Date: Fri, 17 Jul 2026 22:02:02 -0400 Subject: [PATCH 07/10] refactor: migrate ENABLE_NEW_BULK_EMAIL_EXPERIENCE off FEATURES-as-dict --- lms/djangoapps/instructor/views/instructor_dashboard.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lms/djangoapps/instructor/views/instructor_dashboard.py b/lms/djangoapps/instructor/views/instructor_dashboard.py index 82de958b49a8..4dd3ac0ae6c4 100644 --- a/lms/djangoapps/instructor/views/instructor_dashboard.py +++ b/lms/djangoapps/instructor/views/instructor_dashboard.py @@ -759,7 +759,7 @@ def _section_send_email(course, access): 'list_email_content', kwargs={'course_id': str(course_key)} ), } - if settings.FEATURES.get("ENABLE_NEW_BULK_EMAIL_EXPERIENCE", False) is not False: + if settings.ENABLE_NEW_BULK_EMAIL_EXPERIENCE is not False: section_data[ "communications_mfe_url" ] = f"{settings.COMMUNICATIONS_MICROFRONTEND_URL}/courses/{str(course_key)}/bulk_email" From 672e743521784bd3904d1bfd9276bde95266adfc Mon Sep 17 00:00:00 2001 From: Feanil Patel Date: Fri, 17 Jul 2026 22:02:45 -0400 Subject: [PATCH 08/10] refactor: migrate ENABLE_PUBLISHER 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 e5467b49ccbf..87bf375cbda3 100644 --- a/cms/djangoapps/contentstore/utils.py +++ b/cms/djangoapps/contentstore/utils.py @@ -1385,7 +1385,7 @@ def get_course_settings(request, course_key, course_block): publisher_enabled = configuration_helpers.get_value_for_org( course_block.location.org, 'ENABLE_PUBLISHER', - settings.FEATURES.get('ENABLE_PUBLISHER', False) + settings.ENABLE_PUBLISHER ) marketing_enabled = configuration_helpers.get_value_for_org( course_block.location.org, From 63d0fdd77d30b3244bc17340f4535d1a139b2821 Mon Sep 17 00:00:00 2001 From: Feanil Patel Date: Fri, 17 Jul 2026 22:04:03 -0400 Subject: [PATCH 09/10] refactor: migrate ENABLE_SOFTWARE_SECURE_FAKE off FEATURES-as-dict --- .../verify_student/tests/test_fake_software_secure.py | 6 ++---- lms/djangoapps/verify_student/urls.py | 2 +- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/lms/djangoapps/verify_student/tests/test_fake_software_secure.py b/lms/djangoapps/verify_student/tests/test_fake_software_secure.py index 5b0f750e477f..85fa72db858e 100644 --- a/lms/djangoapps/verify_student/tests/test_fake_software_secure.py +++ b/lms/djangoapps/verify_student/tests/test_fake_software_secure.py @@ -3,9 +3,7 @@ """ -from unittest.mock import patch - -from django.test import TestCase +from django.test import TestCase, override_settings from common.djangoapps.student.tests.factories import UserFactory from common.djangoapps.util.testing import UrlResetMixin @@ -21,7 +19,7 @@ class SoftwareSecureFakeViewTest(UrlResetMixin, TestCase): def setUp(self, **kwargs): enable_software_secure_fake = kwargs.get('enable_software_secure_fake', False) - with patch.dict('django.conf.settings.FEATURES', {'ENABLE_SOFTWARE_SECURE_FAKE': enable_software_secure_fake}): + with override_settings(ENABLE_SOFTWARE_SECURE_FAKE=enable_software_secure_fake): super().setUp() self.user = UserFactory.create(username="test", password="test") diff --git a/lms/djangoapps/verify_student/urls.py b/lms/djangoapps/verify_student/urls.py index 6438eea73ea0..748781867fef 100644 --- a/lms/djangoapps/verify_student/urls.py +++ b/lms/djangoapps/verify_student/urls.py @@ -121,7 +121,7 @@ ] # Fake response page for incourse reverification ( software secure ) -if settings.FEATURES.get('ENABLE_SOFTWARE_SECURE_FAKE'): +if settings.ENABLE_SOFTWARE_SECURE_FAKE: from lms.djangoapps.verify_student.tests.fake_software_secure import SoftwareSecureFakeView urlpatterns += [ path('software-secure-fake-response', SoftwareSecureFakeView.as_view()), From c8362b543b72d4b6ddb5f3d6fb7daa2f198eb47c Mon Sep 17 00:00:00 2001 From: Feanil Patel Date: Fri, 17 Jul 2026 22:07:04 -0400 Subject: [PATCH 10/10] refactor: migrate FALLBACK_TO_ENGLISH_TRANSCRIPTS off FEATURES-as-dict The two test classes are plain unittest.TestCase, so @override_settings can't decorate them; patch.object the flat setting directly instead, mirroring the prior patch.object(settings, 'FEATURES', ...) approach. --- openedx/core/djangoapps/video_config/services.py | 2 +- xmodule/tests/test_video.py | 10 +++------- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/openedx/core/djangoapps/video_config/services.py b/openedx/core/djangoapps/video_config/services.py index cf79cec5e1f0..519b9054074b 100644 --- a/openedx/core/djangoapps/video_config/services.py +++ b/openedx/core/djangoapps/video_config/services.py @@ -175,7 +175,7 @@ def available_translations( """ translations = [] if verify_assets is None: - verify_assets = not settings.FEATURES.get('FALLBACK_TO_ENGLISH_TRANSCRIPTS') + verify_assets = not settings.FALLBACK_TO_ENGLISH_TRANSCRIPTS sub, other_langs = transcripts["sub"], transcripts["transcripts"] diff --git a/xmodule/tests/test_video.py b/xmodule/tests/test_video.py index a0c470df5634..7829fc24ca28 100644 --- a/xmodule/tests/test_video.py +++ b/xmodule/tests/test_video.py @@ -899,9 +899,7 @@ def test_export_to_xml_unicode_characters(self): @ddt.ddt -@patch.object(settings, 'FEATURES', create=True, new={ - 'FALLBACK_TO_ENGLISH_TRANSCRIPTS': False, -}) +@patch.object(settings, 'FALLBACK_TO_ENGLISH_TRANSCRIPTS', False) class VideoBlockStudentViewDataTestCase(unittest.TestCase): """ Make sure that VideoBlock returns the expected student_view_data. @@ -1023,10 +1021,8 @@ def test_student_view_data_with_hls_flag(self, mock_get_video_info, mock_get_vid } } }) -@patch.object(settings, 'FEATURES', create=True, new={ - # The default value in {lms,cms}/envs/common.py and xmodule/tests/test_video.py should be consistent. - 'FALLBACK_TO_ENGLISH_TRANSCRIPTS': True, -}) +# The default value in openedx/envs/common.py and xmodule/tests/test_video.py should be consistent. +@patch.object(settings, 'FALLBACK_TO_ENGLISH_TRANSCRIPTS', True) class VideoBlockIndexingTestCase(unittest.TestCase): """ Make sure that VideoBlock can format data for indexing as expected.