From d1295284e7c43ef7450030521a69848cc973c5a1 Mon Sep 17 00:00:00 2001 From: Feanil Patel Date: Fri, 17 Jul 2026 22:12:43 -0400 Subject: [PATCH 01/10] refactor: migrate ENABLE_BULK_USER_RETIREMENT 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 f8885ca94fa5..8b498dc7f390 100644 --- a/lms/urls.py +++ b/lms/urls.py @@ -1028,7 +1028,7 @@ ] # Bulk User Retirement API urls -if settings.FEATURES.get('ENABLE_BULK_USER_RETIREMENT'): +if settings.ENABLE_BULK_USER_RETIREMENT: urlpatterns += [ path('', include('lms.djangoapps.bulk_user_retirement.urls')), ] From 0216a3187dc94b584bcc75394f93b31b94384288 Mon Sep 17 00:00:00 2001 From: Feanil Patel Date: Fri, 17 Jul 2026 22:15:19 -0400 Subject: [PATCH 02/10] refactor: migrate ENABLE_CERTIFICATES_IDV_REQUIREMENT off FEATURES-as-dict --- lms/djangoapps/certificates/generation_handler.py | 3 +-- .../commands/tests/test_regenerate_noidv_cert.py | 4 ++-- lms/djangoapps/certificates/tests/test_api.py | 3 +-- .../certificates/tests/test_generation_handler.py | 13 ++++++------- .../certificates/tests/test_webview_views.py | 2 +- lms/djangoapps/certificates/views/webview.py | 4 ++-- lms/djangoapps/courseware/tests/test_views.py | 4 ++-- lms/djangoapps/courseware/views/views.py | 2 +- 8 files changed, 16 insertions(+), 19 deletions(-) diff --git a/lms/djangoapps/certificates/generation_handler.py b/lms/djangoapps/certificates/generation_handler.py index a1e843bead4d..92684f1e87a5 100644 --- a/lms/djangoapps/certificates/generation_handler.py +++ b/lms/djangoapps/certificates/generation_handler.py @@ -451,5 +451,4 @@ def _id_verification_enforced_and_missing(user): """ Return true if IDV is required for this course and the user does not have it """ - return settings.FEATURES.get( - 'ENABLE_CERTIFICATES_IDV_REQUIREMENT') and not IDVerificationService.user_is_verified(user) + return settings.ENABLE_CERTIFICATES_IDV_REQUIREMENT and not IDVerificationService.user_is_verified(user) diff --git a/lms/djangoapps/certificates/management/commands/tests/test_regenerate_noidv_cert.py b/lms/djangoapps/certificates/management/commands/tests/test_regenerate_noidv_cert.py index 47c7e4f19371..2394451a13db 100644 --- a/lms/djangoapps/certificates/management/commands/tests/test_regenerate_noidv_cert.py +++ b/lms/djangoapps/certificates/management/commands/tests/test_regenerate_noidv_cert.py @@ -5,8 +5,8 @@ from unittest import mock import pytest -from django.conf import settings from django.core.management import CommandError, call_command +from django.test import override_settings from common.djangoapps.course_modes.models import CourseMode from common.djangoapps.student.tests.factories import CourseEnrollmentFactory, UserFactory @@ -25,7 +25,7 @@ # base setup is unverified users, Enable certificates IDV requirements turned off, # and normal passing grade certificates for convenience -@mock.patch.dict(settings.FEATURES, ENABLE_CERTIFICATES_IDV_REQUIREMENT=False) +@override_settings(ENABLE_CERTIFICATES_IDV_REQUIREMENT=False) @mock.patch(ID_VERIFIED_METHOD, mock.Mock(return_value=False)) @mock.patch(PASSING_GRADE_METHOD, mock.Mock(return_value=True)) @mock.patch(WEB_CERTS_METHOD, mock.Mock(return_value=True)) diff --git a/lms/djangoapps/certificates/tests/test_api.py b/lms/djangoapps/certificates/tests/test_api.py index f53011d3c31a..e3ad5b356e57 100644 --- a/lms/djangoapps/certificates/tests/test_api.py +++ b/lms/djangoapps/certificates/tests/test_api.py @@ -9,7 +9,6 @@ import ddt import pytz from config_models.models import cache -from django.conf import settings from django.test import RequestFactory, TestCase from django.test.utils import override_settings from django.urls import reverse @@ -553,7 +552,7 @@ def test_generation_unverified(self, enable_idv_requirement): with mock.patch(PASSING_GRADE_METHOD, return_value=True): with mock.patch(ID_VERIFIED_METHOD, return_value=False): - with mock.patch.dict(settings.FEATURES, ENABLE_CERTIFICATES_IDV_REQUIREMENT=enable_idv_requirement): + with override_settings(ENABLE_CERTIFICATES_IDV_REQUIREMENT=enable_idv_requirement): generate_certificate_task(self.user, self.course_run_key) cert = get_certificate_for_user_id(self.user.id, self.course_run_key) diff --git a/lms/djangoapps/certificates/tests/test_generation_handler.py b/lms/djangoapps/certificates/tests/test_generation_handler.py index c340a6fb1c06..249c5911639d 100644 --- a/lms/djangoapps/certificates/tests/test_generation_handler.py +++ b/lms/djangoapps/certificates/tests/test_generation_handler.py @@ -5,7 +5,6 @@ from unittest import mock import ddt -from django.conf import settings from django.test import override_settings from common.djangoapps.course_modes.models import CourseMode @@ -209,7 +208,7 @@ def test_can_generate_not_verified(self, enable_idv_requirement): Test handling when the user's id is not verified """ with mock.patch(ID_VERIFIED_METHOD, return_value=False), \ - mock.patch.dict(settings.FEATURES, ENABLE_CERTIFICATES_IDV_REQUIREMENT=enable_idv_requirement): + override_settings(ENABLE_CERTIFICATES_IDV_REQUIREMENT=enable_idv_requirement): self.assertNotEqual( # noqa: PT009 enable_idv_requirement, _can_generate_allowlist_certificate(self.user, self.course_run_key, self.enrollment_mode)) @@ -360,7 +359,7 @@ def test_generate_allowlist_honor_cert(self): assert not _can_generate_allowlist_certificate(self.user, course_run_key, enrollment_mode) -@mock.patch.dict(settings.FEATURES, ENABLE_CERTIFICATES_IDV_REQUIREMENT=False) +@override_settings(ENABLE_CERTIFICATES_IDV_REQUIREMENT=False) @mock.patch(ID_VERIFIED_METHOD, mock.Mock(return_value=True)) @mock.patch(CCX_COURSE_METHOD, mock.Mock(return_value=False)) @mock.patch(PASSING_GRADE_METHOD, mock.Mock(return_value=True)) @@ -537,7 +536,7 @@ def test_can_generate_not_verified_cert(self, enable_idv_requirement): ) with mock.patch(ID_VERIFIED_METHOD, return_value=False), \ - mock.patch.dict(settings.FEATURES, ENABLE_CERTIFICATES_IDV_REQUIREMENT=enable_idv_requirement): + override_settings(ENABLE_CERTIFICATES_IDV_REQUIREMENT=enable_idv_requirement): self.assertNotEqual( # noqa: PT009 enable_idv_requirement, _can_generate_regular_certificate(u, self.course_run_key, self.enrollment_mode, self.grade) @@ -559,7 +558,7 @@ def test_can_generate_not_verified_no_cert(self, enable_idv_requirement): ) with mock.patch(ID_VERIFIED_METHOD, return_value=False), \ - mock.patch.dict(settings.FEATURES, ENABLE_CERTIFICATES_IDV_REQUIREMENT=enable_idv_requirement): + override_settings(ENABLE_CERTIFICATES_IDV_REQUIREMENT=enable_idv_requirement): self.assertNotEqual( # noqa: PT009 enable_idv_requirement, _can_generate_regular_certificate(u, self.course_run_key, self.enrollment_mode, self.grade) @@ -587,7 +586,7 @@ def test_can_generate_not_verified_not_passing(self, enable_idv_requirement): ) with mock.patch(ID_VERIFIED_METHOD, return_value=False), \ - mock.patch.dict(settings.FEATURES, ENABLE_CERTIFICATES_IDV_REQUIREMENT=enable_idv_requirement), \ + override_settings(ENABLE_CERTIFICATES_IDV_REQUIREMENT=enable_idv_requirement), \ mock.patch(PASSING_GRADE_METHOD, return_value=False): assert not _can_generate_regular_certificate(u, self.course_run_key, self.enrollment_mode, self.grade) if enable_idv_requirement: @@ -617,7 +616,7 @@ def test_can_generate_not_verified_not_passing_allowlist(self, enable_idv_requir CertificateAllowlistFactory(course_id=self.course_run_key, user=u) with mock.patch(ID_VERIFIED_METHOD, return_value=False), \ - mock.patch.dict(settings.FEATURES, ENABLE_CERTIFICATES_IDV_REQUIREMENT=enable_idv_requirement), \ + override_settings(ENABLE_CERTIFICATES_IDV_REQUIREMENT=enable_idv_requirement), \ mock.patch(PASSING_GRADE_METHOD, return_value=False): assert not _can_generate_regular_certificate(u, self.course_run_key, self.enrollment_mode, self.grade) if enable_idv_requirement: diff --git a/lms/djangoapps/certificates/tests/test_webview_views.py b/lms/djangoapps/certificates/tests/test_webview_views.py index 5feb4bc2b3d2..9817ecd290a3 100644 --- a/lms/djangoapps/certificates/tests/test_webview_views.py +++ b/lms/djangoapps/certificates/tests/test_webview_views.py @@ -1676,7 +1676,7 @@ def test_verified_certificate_description(self, enable_cert_idv_requirement): """ Test that for a verified cert, the correct language is used when the integrity signature feature is enabled. """ - with patch.dict(settings.FEATURES, ENABLE_CERTIFICATES_IDV_REQUIREMENT=enable_cert_idv_requirement): + with override_settings(ENABLE_CERTIFICATES_IDV_REQUIREMENT=enable_cert_idv_requirement): self._add_course_certificates(count=1, signatory_count=2, is_active=True) self._create_custom_template_with_verified_description() self.cert.mode = 'verified' diff --git a/lms/djangoapps/certificates/views/webview.py b/lms/djangoapps/certificates/views/webview.py index bfb4840edb5d..5d578a52e7d1 100644 --- a/lms/djangoapps/certificates/views/webview.py +++ b/lms/djangoapps/certificates/views/webview.py @@ -81,7 +81,7 @@ def get_certificate_description(mode, certificate_type, platform_name, course_ke "{platform_name} and has completed all of the required tasks for this course " "under its guidelines. ").format(cert_type=certificate_type, platform_name=platform_name) - if settings.FEATURES.get('ENABLE_CERTIFICATES_IDV_REQUIREMENT'): + if settings.ENABLE_CERTIFICATES_IDV_REQUIREMENT: certificate_type_description += _("A {cert_type} certificate also indicates that the " "identity of the learner has been checked and " "is valid.").format(cert_type=certificate_type) @@ -249,7 +249,7 @@ def _update_course_context(request, context, course, platform_name): context['accomplishment_copy_course_name'] = accomplishment_copy_course_name course_number = course.display_coursenumber if course.display_coursenumber else course.number context['course_number'] = course_number - context['idv_enabled_for_certificates'] = settings.FEATURES.get('ENABLE_CERTIFICATES_IDV_REQUIREMENT') + context['idv_enabled_for_certificates'] = settings.ENABLE_CERTIFICATES_IDV_REQUIREMENT if context['organization_long_name']: # Translators: This text represents the description of course context['accomplishment_copy_course_description'] = _('a course of study offered by {partner_short_name}, ' diff --git a/lms/djangoapps/courseware/tests/test_views.py b/lms/djangoapps/courseware/tests/test_views.py index 85a6d21d3cbd..bd11bc9e564e 100644 --- a/lms/djangoapps/courseware/tests/test_views.py +++ b/lms/djangoapps/courseware/tests/test_views.py @@ -1296,7 +1296,7 @@ def test_progress_queries(self): ), check_mongo_calls(2): self._get_progress_page() - @patch.dict(settings.FEATURES, {'ENABLE_CERTIFICATES_IDV_REQUIREMENT': True}) + @override_settings(ENABLE_CERTIFICATES_IDV_REQUIREMENT=True) @ddt.data( *itertools.product( ( @@ -1581,7 +1581,7 @@ def test_no_certs_generated_and_not_verified(self, enable_cert_idv_requirement): """ certs_api.set_certificate_generation_config(enabled=True) certs_api.set_cert_generation_enabled(self.course.id, True) - with patch.dict(settings.FEATURES, ENABLE_CERTIFICATES_IDV_REQUIREMENT=enable_cert_idv_requirement): + with override_settings(ENABLE_CERTIFICATES_IDV_REQUIREMENT=enable_cert_idv_requirement): with patch( 'lms.djangoapps.certificates.api.certificate_downloadable_status', return_value=self.mock_certificate_downloadable_status() diff --git a/lms/djangoapps/courseware/views/views.py b/lms/djangoapps/courseware/views/views.py index 8ae58850920d..7d3829f121b7 100644 --- a/lms/djangoapps/courseware/views/views.py +++ b/lms/djangoapps/courseware/views/views.py @@ -1091,7 +1091,7 @@ def _downloadable_certificate_message(course, cert_downloadable_status): # pyli def _missing_required_verification(student, enrollment_mode): - return settings.FEATURES.get('ENABLE_CERTIFICATES_IDV_REQUIREMENT') and ( + return settings.ENABLE_CERTIFICATES_IDV_REQUIREMENT and ( enrollment_mode in CourseMode.VERIFIED_MODES and not IDVerificationService.user_is_verified(student) ) From ce3f85b08260ec4a01fee82a2c5bf7a7e23395bd Mon Sep 17 00:00:00 2001 From: Feanil Patel Date: Fri, 17 Jul 2026 22:16:41 -0400 Subject: [PATCH 03/10] refactor: migrate ENABLE_COURSE_ASSESSMENT_GRADE_CHANGE_SIGNAL off FEATURES-as-dict --- lms/djangoapps/grades/subsection_grade_factory.py | 2 +- lms/djangoapps/grades/tests/test_subsection_grade_factory.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lms/djangoapps/grades/subsection_grade_factory.py b/lms/djangoapps/grades/subsection_grade_factory.py index 6b9c75a7606f..e5ec0546c4fa 100644 --- a/lms/djangoapps/grades/subsection_grade_factory.py +++ b/lms/djangoapps/grades/subsection_grade_factory.py @@ -104,7 +104,7 @@ def update(self, subsection, only_if_higher=None, score_deleted=False, force_upd ) self._update_saved_subsection_grade(subsection.location, grade_model) - if settings.FEATURES.get('ENABLE_COURSE_ASSESSMENT_GRADE_CHANGE_SIGNAL'): + if settings.ENABLE_COURSE_ASSESSMENT_GRADE_CHANGE_SIGNAL: COURSE_ASSESSMENT_GRADE_CHANGED.send( sender=self, course_id=self.course_data.course_key, diff --git a/lms/djangoapps/grades/tests/test_subsection_grade_factory.py b/lms/djangoapps/grades/tests/test_subsection_grade_factory.py index 95e0e62361eb..747e69105b5a 100644 --- a/lms/djangoapps/grades/tests/test_subsection_grade_factory.py +++ b/lms/djangoapps/grades/tests/test_subsection_grade_factory.py @@ -6,7 +6,7 @@ from unittest.mock import patch import ddt -from django.conf import settings +from django.test import override_settings from common.djangoapps.student.tests.factories import UserFactory from lms.djangoapps.courseware.tests.test_submitting_problems import ProblemSubmissionTestMixin @@ -43,7 +43,7 @@ def test_create_zero(self): assert isinstance(grade, ZeroSubsectionGrade) self.assert_grade(grade, 0.0, 1.0) - @patch.dict(settings.FEATURES, {'ENABLE_COURSE_ASSESSMENT_GRADE_CHANGE_SIGNAL': True}) + @override_settings(ENABLE_COURSE_ASSESSMENT_GRADE_CHANGE_SIGNAL=True) def test_update(self): """ Assuming the underlying score reporting methods work, From 3c046dde19fe6112c6df0851e6e975fd8a056fc8 Mon Sep 17 00:00:00 2001 From: Feanil Patel Date: Fri, 17 Jul 2026 22:18:31 -0400 Subject: [PATCH 04/10] refactor: migrate ENABLE_READING_FROM_MULTIPLE_HISTORY_TABLES off FEATURES-as-dict --- lms/djangoapps/courseware/models.py | 2 +- lms/djangoapps/coursewarehistoryextended/tests.py | 13 ++++--------- 2 files changed, 5 insertions(+), 10 deletions(-) diff --git a/lms/djangoapps/courseware/models.py b/lms/djangoapps/courseware/models.py index fb4ccd1b0cec..bd82bed13342 100644 --- a/lms/djangoapps/courseware/models.py +++ b/lms/djangoapps/courseware/models.py @@ -225,7 +225,7 @@ def get_history(student_modules): # If we turn off reading from multiple history tables, then we don't want to read from # StudentModuleHistory anymore, we believe that all history is in the Extended table. - if settings.FEATURES.get('ENABLE_READING_FROM_MULTIPLE_HISTORY_TABLES'): + if settings.ENABLE_READING_FROM_MULTIPLE_HISTORY_TABLES: # we want to save later SQL queries on the model which allows us to prefetch history_entries += StudentModuleHistory.objects.prefetch_related('student_module').filter( student_module__in=student_modules diff --git a/lms/djangoapps/coursewarehistoryextended/tests.py b/lms/djangoapps/coursewarehistoryextended/tests.py index a7a45a22d8c8..ae6569d2ce5f 100644 --- a/lms/djangoapps/coursewarehistoryextended/tests.py +++ b/lms/djangoapps/coursewarehistoryextended/tests.py @@ -8,7 +8,6 @@ import json from unittest import skipUnless -from unittest.mock import patch from django.conf import settings from django.db import connections @@ -43,8 +42,7 @@ def setUp(self): max_grade=csm.max_grade) csmh.save() - @override_settings(ENABLE_CSMH_EXTENDED=True) - @patch.dict("django.conf.settings.FEATURES", {"ENABLE_READING_FROM_MULTIPLE_HISTORY_TABLES": True}) + @override_settings(ENABLE_CSMH_EXTENDED=True, ENABLE_READING_FROM_MULTIPLE_HISTORY_TABLES=True) def test_get_history_true_true(self): student_module = StudentModule.objects.all() history = BaseStudentModuleHistory.get_history(student_module) @@ -56,8 +54,7 @@ def test_get_history_true_true(self): assert {'type': 'csmh', 'order': 2} == json.loads(history[4].state) assert {'type': 'csmh', 'order': 1} == json.loads(history[5].state) - @override_settings(ENABLE_CSMH_EXTENDED=True) - @patch.dict("django.conf.settings.FEATURES", {"ENABLE_READING_FROM_MULTIPLE_HISTORY_TABLES": False}) + @override_settings(ENABLE_CSMH_EXTENDED=True, ENABLE_READING_FROM_MULTIPLE_HISTORY_TABLES=False) def test_get_history_true_false(self): student_module = StudentModule.objects.all() history = BaseStudentModuleHistory.get_history(student_module) @@ -66,8 +63,7 @@ def test_get_history_true_false(self): assert {'type': 'csmhe', 'order': 2} == json.loads(history[1].state) assert {'type': 'csmhe', 'order': 1} == json.loads(history[2].state) - @override_settings(ENABLE_CSMH_EXTENDED=False) - @patch.dict("django.conf.settings.FEATURES", {"ENABLE_READING_FROM_MULTIPLE_HISTORY_TABLES": True}) + @override_settings(ENABLE_CSMH_EXTENDED=False, ENABLE_READING_FROM_MULTIPLE_HISTORY_TABLES=True) def test_get_history_false_true(self): student_module = StudentModule.objects.all() history = BaseStudentModuleHistory.get_history(student_module) @@ -76,8 +72,7 @@ def test_get_history_false_true(self): assert {'type': 'csmh', 'order': 2} == json.loads(history[1].state) assert {'type': 'csmh', 'order': 1} == json.loads(history[2].state) - @override_settings(ENABLE_CSMH_EXTENDED=False) - @patch.dict("django.conf.settings.FEATURES", {"ENABLE_READING_FROM_MULTIPLE_HISTORY_TABLES": False}) + @override_settings(ENABLE_CSMH_EXTENDED=False, ENABLE_READING_FROM_MULTIPLE_HISTORY_TABLES=False) def test_get_history_false_false(self): student_module = StudentModule.objects.all() history = BaseStudentModuleHistory.get_history(student_module) From 8e9ddff1effee71807f534f350b5db56f33ffcc7 Mon Sep 17 00:00:00 2001 From: Feanil Patel Date: Mon, 20 Jul 2026 09:38:21 -0400 Subject: [PATCH 05/10] refactor: migrate FRONTEND_APP_PUBLISHER_URL off FEATURES-as-dict The URL was only read via settings.FEATURES.get with an inline False default and was never defined in a common envs file (only in cms/envs/devstack.py). Add an annotated flat setting to cms/envs/common.py, defaulting to None (the idiomatic 'no URL' value, consistent with the sibling keys in the course settings context) and matching the prior falsy default, then read it directly at the CMS-only caller. --- cms/djangoapps/contentstore/utils.py | 2 +- cms/envs/common.py | 7 +++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/cms/djangoapps/contentstore/utils.py b/cms/djangoapps/contentstore/utils.py index 14aa7a6858a3..05d51bb068fa 100644 --- a/cms/djangoapps/contentstore/utils.py +++ b/cms/djangoapps/contentstore/utils.py @@ -1890,7 +1890,7 @@ def _get_course_index_context(request, course_key, course_block): frontend_app_publisher_url = configuration_helpers.get_value_for_org( course_block.location.org, 'FRONTEND_APP_PUBLISHER_URL', - settings.FEATURES.get('FRONTEND_APP_PUBLISHER_URL', False) + settings.FRONTEND_APP_PUBLISHER_URL ) # gather any errors in the currently stored proctoring settings. advanced_dict = CourseMetadata.fetch(course_block) diff --git a/cms/envs/common.py b/cms/envs/common.py index 949f01f8cf3b..a310f583ca88 100644 --- a/cms/envs/common.py +++ b/cms/envs/common.py @@ -320,6 +320,13 @@ def make_lms_template_path(settings): FRONTEND_LOGOUT_URL = '/logout/' FRONTEND_REGISTER_URL = Derived(lambda settings: settings.LMS_ROOT_URL + '/register') +# .. setting_name: FRONTEND_APP_PUBLISHER_URL +# .. setting_default: None +# .. setting_description: Base URL of the publisher frontend app. When set (globally here or per-org via +# site configuration), the course "settings" page in Studio links out to the publisher app. Left unset +# (None) by default, in which case no publisher link is shown. +FRONTEND_APP_PUBLISHER_URL = None + ENTERPRISE_API_URL = Derived(lambda settings: settings.LMS_INTERNAL_ROOT_URL + '/enterprise/api/v1/') ENTERPRISE_CONSENT_API_URL = Derived(lambda settings: settings.LMS_INTERNAL_ROOT_URL + '/consent/api/v1/') From 92f76149b4834dcb2369b1a6427f5c7d421da3c0 Mon Sep 17 00:00:00 2001 From: Feanil Patel Date: Mon, 20 Jul 2026 09:39:31 -0400 Subject: [PATCH 06/10] refactor: migrate IN_CONTEXT_DISCUSSION_ENABLED_DEFAULT off FEATURES-as-dict The setting is only defined in cms/envs/common.py but the reader lives in shared xmodule code reachable from LMS, so use getattr(settings, ..., True) (matching the prior inline default) rather than a bare settings lookup that would AttributeError under LMS. --- xmodule/vertical_block.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/xmodule/vertical_block.py b/xmodule/vertical_block.py index 8377fd19efc3..5ca26088f3a6 100644 --- a/xmodule/vertical_block.py +++ b/xmodule/vertical_block.py @@ -44,7 +44,7 @@ class VerticalFields: discussion_enabled = Boolean( display_name=_("Enable in-context discussions for the Unit"), help=_("Add discussion for the Unit."), - default=settings.FEATURES.get('IN_CONTEXT_DISCUSSION_ENABLED_DEFAULT', True), + default=getattr(settings, 'IN_CONTEXT_DISCUSSION_ENABLED_DEFAULT', True), scope=Scope.settings, ) From 7e71591d36c61326147d521d9a414cbf29b63b2c Mon Sep 17 00:00:00 2001 From: Feanil Patel Date: Mon, 20 Jul 2026 09:41:53 -0400 Subject: [PATCH 07/10] refactor: migrate MARK_LIBRARY_CONTENT_BLOCK_COMPLETE_ON_VIEW off FEATURES-as-dict --- lms/djangoapps/course_home_api/outline/tests/test_view.py | 4 +--- openedx/tests/completion_integration/test_services.py | 3 +-- xmodule/item_bank_block.py | 2 +- 3 files changed, 3 insertions(+), 6 deletions(-) diff --git a/lms/djangoapps/course_home_api/outline/tests/test_view.py b/lms/djangoapps/course_home_api/outline/tests/test_view.py index 73a2a9e875da..b802a806eeca 100644 --- a/lms/djangoapps/course_home_api/outline/tests/test_view.py +++ b/lms/djangoapps/course_home_api/outline/tests/test_view.py @@ -810,9 +810,7 @@ def test_blocks_complete_with_library_content_block( self.create_completion(problem, int(problem_complete)) self.create_completion(library, int(library_complete)) - with override_settings( - FEATURES={**settings.FEATURES, 'MARK_LIBRARY_CONTENT_BLOCK_COMPLETE_ON_VIEW': library_complete_on_view} - ): + with override_settings(MARK_LIBRARY_CONTENT_BLOCK_COMPLETE_ON_VIEW=library_complete_on_view): response = self.client.get(reverse('course-home:course-navigation', args=[self.course.id])) sequence_data = response.data['blocks'][str(self.sequential.location)] diff --git a/openedx/tests/completion_integration/test_services.py b/openedx/tests/completion_integration/test_services.py index ced8bd4c4a9f..95c77d806a6e 100644 --- a/openedx/tests/completion_integration/test_services.py +++ b/openedx/tests/completion_integration/test_services.py @@ -7,7 +7,6 @@ from completion.models import BlockCompletion from completion.services import CompletionService from completion.test_utils import CompletionWaffleTestMixin -from django.conf import settings from django.test import override_settings from opaque_keys.edx.keys import CourseKey @@ -180,7 +179,7 @@ def test_can_mark_block_complete_on_view(self): assert self.completion_service.can_mark_block_complete_on_view(self.html) is True assert self.completion_service.can_mark_block_complete_on_view(self.problem) is False - @override_settings(FEATURES={**settings.FEATURES, 'MARK_LIBRARY_CONTENT_BLOCK_COMPLETE_ON_VIEW': True}) + @override_settings(MARK_LIBRARY_CONTENT_BLOCK_COMPLETE_ON_VIEW=True) def test_can_mark_library_content_complete_on_view(self): library = LibraryFactory.create(modulestore=self.store) lib_vertical = BlockFactory.create(parent=self.sequence, category='vertical', publish_item=False) diff --git a/xmodule/item_bank_block.py b/xmodule/item_bank_block.py index 993323070192..dcf5847a569e 100644 --- a/xmodule/item_bank_block.py +++ b/xmodule/item_bank_block.py @@ -87,7 +87,7 @@ def completion_mode(cls): # pylint: disable=no-self-argument This is a property, so it can be dynamically overridden in tests, as it is not evaluated at runtime. """ - if settings.FEATURES.get('MARK_LIBRARY_CONTENT_BLOCK_COMPLETE_ON_VIEW', False): + if settings.MARK_LIBRARY_CONTENT_BLOCK_COMPLETE_ON_VIEW: return XBlockCompletionMode.COMPLETABLE return XBlockCompletionMode.AGGREGATOR From 3f30a77d81157d63430d70ef06421a32f0704957 Mon Sep 17 00:00:00 2001 From: Feanil Patel Date: Mon, 20 Jul 2026 09:43:38 -0400 Subject: [PATCH 08/10] refactor: migrate MAX_PROBLEM_RESPONSES_COUNT off FEATURES-as-dict Already a flat setting in lms/envs/common.py (5000); just switch the lingering FEATURES.get reader and its test override to the flat setting. --- lms/djangoapps/instructor_task/tasks_helper/grades.py | 2 +- lms/djangoapps/instructor_task/tests/test_tasks_helper.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lms/djangoapps/instructor_task/tasks_helper/grades.py b/lms/djangoapps/instructor_task/tasks_helper/grades.py index 9b6a764137db..4ac75d2fd185 100644 --- a/lms/djangoapps/instructor_task/tasks_helper/grades.py +++ b/lms/djangoapps/instructor_task/tasks_helper/grades.py @@ -909,7 +909,7 @@ def _build_student_data( ]) student_data = [] - max_count = settings.FEATURES.get('MAX_PROBLEM_RESPONSES_COUNT') + max_count = settings.MAX_PROBLEM_RESPONSES_COUNT store = modulestore() user_state_client = DjangoXBlockUserStateClient() diff --git a/lms/djangoapps/instructor_task/tests/test_tasks_helper.py b/lms/djangoapps/instructor_task/tests/test_tasks_helper.py index 6a20da1ee434..e1a1667ed5e4 100644 --- a/lms/djangoapps/instructor_task/tests/test_tasks_helper.py +++ b/lms/djangoapps/instructor_task/tests/test_tasks_helper.py @@ -518,7 +518,7 @@ def _remove_capa_report_generator(self): finally: ProblemBlock.generate_report_data = generate_report_data - @patch.dict('django.conf.settings.FEATURES', {'MAX_PROBLEM_RESPONSES_COUNT': 4}) + @override_settings(MAX_PROBLEM_RESPONSES_COUNT=4) def test_build_student_data_limit(self): """ Ensure that the _build_student_data method respects the global setting for From 744a006539d6cf9fa00729bd9be715a366cb65be Mon Sep 17 00:00:00 2001 From: Feanil Patel Date: Mon, 20 Jul 2026 09:56:58 -0400 Subject: [PATCH 09/10] refactor: migrate MODE_CREATION_FOR_TESTING off FEATURES-as-dict Setting is LMS-only (lms/envs/common.py) but the reader is in shared common/djangoapps/course_modes/urls.py, so use getattr with the prior False default. The test's setUp-level patch.dict becomes a class-level @override_settings (trap: override_settings on setUp conflicts with CacheIsolationMixin, and the class needs the flag set during URL reset). --- common/djangoapps/course_modes/tests/test_views.py | 2 +- common/djangoapps/course_modes/urls.py | 2 +- common/djangoapps/course_modes/views.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/common/djangoapps/course_modes/tests/test_views.py b/common/djangoapps/course_modes/tests/test_views.py index 3e59130eae07..84b52d9d3032 100644 --- a/common/djangoapps/course_modes/tests/test_views.py +++ b/common/djangoapps/course_modes/tests/test_views.py @@ -43,13 +43,13 @@ @ddt.ddt @skip_unless_lms +@override_settings(MODE_CREATION_FOR_TESTING=True) class CourseModeViewTest(CatalogIntegrationMixin, UrlResetMixin, ModuleStoreTestCase, CourseCatalogServiceMockMixin): """ Course Mode View tests """ URLCONF_MODULES = ['common.djangoapps.course_modes.urls'] - @patch.dict(settings.FEATURES, {'MODE_CREATION_FOR_TESTING': True}) def setUp(self): super().setUp() now = datetime.now(ZoneInfo("UTC")) diff --git a/common/djangoapps/course_modes/urls.py b/common/djangoapps/course_modes/urls.py index c02486789abe..f9dbf4684070 100644 --- a/common/djangoapps/course_modes/urls.py +++ b/common/djangoapps/course_modes/urls.py @@ -11,7 +11,7 @@ ] # Enable verified mode creation -if settings.FEATURES.get('MODE_CREATION_FOR_TESTING'): +if getattr(settings, 'MODE_CREATION_FOR_TESTING', False): urlpatterns.append( re_path(fr'^create_mode/{settings.COURSE_ID_PATTERN}/$', views.create_mode, diff --git a/common/djangoapps/course_modes/views.py b/common/djangoapps/course_modes/views.py index a5b40346b2d8..586e39564185 100644 --- a/common/djangoapps/course_modes/views.py +++ b/common/djangoapps/course_modes/views.py @@ -381,7 +381,7 @@ def _redirect_to_course_or_dashboard(self, course, course_key, user): def create_mode(request, course_id): """Add a mode to the course corresponding to the given course ID. - Only available when settings.FEATURES['MODE_CREATION_FOR_TESTING'] is True. + Only available when settings.MODE_CREATION_FOR_TESTING is True. Attempts to use the following querystring parameters from the request: `mode_slug` (str): The mode to add, either 'honor', 'verified', or 'professional' From 4b16493fb953a44f6d97db375a24797531f56cf2 Mon Sep 17 00:00:00 2001 From: Feanil Patel Date: Mon, 20 Jul 2026 10:01:42 -0400 Subject: [PATCH 10/10] refactor: migrate THIRD_PARTY_AUTH_HINT off FEATURES-as-dict The tpa_hint was only read via settings.FEATURES.get with an inline '' default and was never defined in a common envs file. Add an annotated flat setting (default '') to openedx/envs/common.py so the shared reader in common/djangoapps/student/helpers.py can read it directly from both LMS and CMS, and convert the test overrides. --- common/djangoapps/student/helpers.py | 2 +- common/djangoapps/student/tests/test_helpers.py | 2 +- .../djangoapps/user_authn/views/tests/test_logistration.py | 6 +++--- openedx/envs/common.py | 6 ++++++ 4 files changed, 11 insertions(+), 5 deletions(-) diff --git a/common/djangoapps/student/helpers.py b/common/djangoapps/student/helpers.py index 74cd737a3860..e63ae4502d56 100644 --- a/common/djangoapps/student/helpers.py +++ b/common/djangoapps/student/helpers.py @@ -316,7 +316,7 @@ def get_next_url_for_login_page(request, include_host=False): # Append a tpa_hint query parameter, if one is configured tpa_hint = configuration_helpers.get_value( "THIRD_PARTY_AUTH_HINT", - settings.FEATURES.get("THIRD_PARTY_AUTH_HINT", '') + settings.THIRD_PARTY_AUTH_HINT ) if tpa_hint: # Don't add tpa_hint if we're already in the TPA pipeline (prevent infinite loop), diff --git a/common/djangoapps/student/tests/test_helpers.py b/common/djangoapps/student/tests/test_helpers.py index 450f0ed695f9..30fbeb35b47e 100644 --- a/common/djangoapps/student/tests/test_helpers.py +++ b/common/djangoapps/student/tests/test_helpers.py @@ -126,7 +126,7 @@ def validate_login(): next_page = get_next_url_for_login_page(req) assert next_page == expected_url - with override_settings(FEATURES=dict(settings.FEATURES, THIRD_PARTY_AUTH_HINT=tpa_hint)): + with override_settings(THIRD_PARTY_AUTH_HINT=tpa_hint): validate_login() with with_site_configuration_context(configuration=dict(THIRD_PARTY_AUTH_HINT=tpa_hint)): diff --git a/openedx/core/djangoapps/user_authn/views/tests/test_logistration.py b/openedx/core/djangoapps/user_authn/views/tests/test_logistration.py index 711a71a1298a..f28fbe1e726b 100644 --- a/openedx/core/djangoapps/user_authn/views/tests/test_logistration.py +++ b/openedx/core/djangoapps/user_authn/views/tests/test_logistration.py @@ -417,14 +417,14 @@ def test_hinted_login_dialog_disabled(self, url_name, auth_entry): target_status_code=302 ) - @override_settings(FEATURES=dict(settings.FEATURES, THIRD_PARTY_AUTH_HINT='oa2-google-oauth2')) + @override_settings(THIRD_PARTY_AUTH_HINT='oa2-google-oauth2') @ddt.data( 'signin_user', 'register_user', ) def test_settings_tpa_hinted_login(self, url_name): """ - Ensure that settings.FEATURES['THIRD_PARTY_AUTH_HINT'] can set third_party_auth_hint. + Ensure that settings.THIRD_PARTY_AUTH_HINT can set third_party_auth_hint. """ params = [("next", "/courses/something/")] response = self.client.get(reverse(url_name), params, HTTP_ACCEPT="text/html") @@ -442,7 +442,7 @@ def test_settings_tpa_hinted_login(self, url_name): response = self.client.get(reverse(url_name), params, HTTP_ACCEPT="text/html") assert response.content.decode('utf-8') not in tpa_hint - @override_settings(FEATURES=dict(settings.FEATURES, THIRD_PARTY_AUTH_HINT='oa2-google-oauth2')) + @override_settings(THIRD_PARTY_AUTH_HINT='oa2-google-oauth2') @ddt.data( ('signin_user', 'login'), ('register_user', 'register'), diff --git a/openedx/envs/common.py b/openedx/envs/common.py index f0b53db8a5a6..2939e1c1f4ef 100644 --- a/openedx/envs/common.py +++ b/openedx/envs/common.py @@ -1167,6 +1167,12 @@ def add_optional_apps(optional_apps, installed_apps): # .. toggle_tickets: https://github.com/openedx/edx-platform/pull/7315 LICENSING = False +# .. setting_name: THIRD_PARTY_AUTH_HINT +# .. setting_default: '' +# .. setting_description: Default third-party auth provider hint (tpa_hint) to append to login/registration +# redirect URLs site-wide. Usually left empty and overridden per-site via site configuration. +THIRD_PARTY_AUTH_HINT = '' + # .. toggle_name: CERTIFICATES_HTML_VIEW # .. toggle_implementation: DjangoSetting # .. toggle_default: False