From c898dd1384266a56c40401d4ce78d2b600bb82f4 Mon Sep 17 00:00:00 2001 From: Feanil Patel Date: Mon, 24 Aug 2026 11:13:29 -0400 Subject: [PATCH 1/3] refactor: migrate ENABLE_EXAM_SETTINGS_HTML_VIEW off FEATURES-as-dict xmodule/course_block.py read this flag via getattr(settings, 'FEATURES', {}).get('ENABLE_EXAM_SETTINGS_HTML_VIEW', False) to decide whether the legacy proctored/timed-exam CourseBlock fields are marked deprecated in the advanced settings editor. The flag had no flat definition anywhere. Add an annotated ENABLE_EXAM_SETTINGS_HTML_VIEW = False to openedx/envs/common.py and read it with getattr(settings, 'ENABLE_EXAM_SETTINGS_HTML_VIEW', False) -- matching the adjacent DEFAULT_MOBILE_AVAILABLE / ENABLE_SPECIAL_EXAMS readers. getattr is kept (rather than bare settings.X) because this module is imported in contexts where settings may not be fully configured, e.g. static asset compilation. Co-Authored-By: Claude Opus 4.8 --- openedx/envs/common.py | 11 +++++++++++ xmodule/course_block.py | 2 +- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/openedx/envs/common.py b/openedx/envs/common.py index e4326a9fa81d..bf18e1f901a8 100644 --- a/openedx/envs/common.py +++ b/openedx/envs/common.py @@ -1213,6 +1213,17 @@ def add_optional_apps(optional_apps, installed_apps): # .. toggle_tickets: https://github.com/openedx/edx-platform/pull/9744 ENABLE_SPECIAL_EXAMS = False +# .. toggle_name: ENABLE_EXAM_SETTINGS_HTML_VIEW +# .. toggle_implementation: DjangoSetting +# .. toggle_default: False +# .. toggle_description: Enable the "Exam Settings" view in Studio's course settings. When enabled, +# the corresponding legacy proctored/timed-exam fields on the course are marked deprecated in the +# advanced settings editor so they are edited via the dedicated view instead. +# .. toggle_use_cases: open_edx +# .. toggle_creation_date: 2020-07-09 +# .. toggle_tickets: https://github.com/openedx/edx-platform/pull/24405 +ENABLE_EXAM_SETTINGS_HTML_VIEW = False + # .. toggle_name: SHOW_HEADER_LANGUAGE_SELECTOR # .. toggle_implementation: DjangoSetting # .. toggle_default: False diff --git a/xmodule/course_block.py b/xmodule/course_block.py index eee5beab8a66..b54dce326309 100644 --- a/xmodule/course_block.py +++ b/xmodule/course_block.py @@ -53,7 +53,7 @@ DEFAULT_MOBILE_AVAILABLE = getattr(settings, 'DEFAULT_MOBILE_AVAILABLE', False) # Note: updating assets does not have settings defined, so using `getattr`. -EXAM_SETTINGS_HTML_VIEW_ENABLED = getattr(settings, 'FEATURES', {}).get('ENABLE_EXAM_SETTINGS_HTML_VIEW', False) +EXAM_SETTINGS_HTML_VIEW_ENABLED = getattr(settings, 'ENABLE_EXAM_SETTINGS_HTML_VIEW', False) SPECIAL_EXAMS_ENABLED = getattr(settings, 'ENABLE_SPECIAL_EXAMS', False) COURSE_VISIBILITY_PRIVATE = 'private' From c571a9f9c59ae8275e87a2c3c5f3a992b760cea6 Mon Sep 17 00:00:00 2001 From: Feanil Patel Date: Mon, 24 Aug 2026 11:13:29 -0400 Subject: [PATCH 2/3] refactor: migrate LICENSING off FEATURES-as-dict in production settings lms/envs/production.py and cms/envs/production.py set XBLOCK_SETTINGS["VideoBlock"]["licensing_enabled"] from FEATURES["LICENSING"]. LICENSING is a flat setting (defined in openedx/envs/common.py) and is available in these modules through the common star-import, so read it directly instead of through the FEATURES dict. Co-Authored-By: Claude Opus 4.8 --- cms/envs/production.py | 2 +- lms/envs/production.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/cms/envs/production.py b/cms/envs/production.py index 604d2753bccd..60d5f88c1b59 100644 --- a/cms/envs/production.py +++ b/cms/envs/production.py @@ -287,7 +287,7 @@ def get_env_setting(setting): # TODO: Once we have successfully upgraded to ES7, switch this back to ELASTIC_SEARCH_CONFIG. ELASTIC_SEARCH_CONFIG = _YAML_TOKENS.get('ELASTIC_SEARCH_CONFIG_ES7', [{}]) -XBLOCK_SETTINGS.setdefault("VideoBlock", {})["licensing_enabled"] = FEATURES["LICENSING"] # noqa: F405 +XBLOCK_SETTINGS.setdefault("VideoBlock", {})["licensing_enabled"] = LICENSING # noqa: F405 XBLOCK_SETTINGS.setdefault("VideoBlock", {})['YOUTUBE_API_KEY'] = YOUTUBE_API_KEY # noqa: F405 ############################ OAUTH2 Provider ################################### diff --git a/lms/envs/production.py b/lms/envs/production.py index b13f232f5bb7..035f282dbd6f 100644 --- a/lms/envs/production.py +++ b/lms/envs/production.py @@ -332,7 +332,7 @@ def get_env_setting(setting): # TODO: Once we have successfully upgraded to ES7, switch this back to ELASTIC_SEARCH_CONFIG. ELASTIC_SEARCH_CONFIG = _YAML_TOKENS.get('ELASTIC_SEARCH_CONFIG_ES7', [{}]) -XBLOCK_SETTINGS.setdefault("VideoBlock", {})["licensing_enabled"] = FEATURES["LICENSING"] # noqa: F405 +XBLOCK_SETTINGS.setdefault("VideoBlock", {})["licensing_enabled"] = LICENSING # noqa: F405 XBLOCK_SETTINGS.setdefault("VideoBlock", {})['YOUTUBE_API_KEY'] = YOUTUBE_API_KEY # noqa: F405 ##### Custom Courses for EdX ##### From bf6719a777bbecfc8ff53b21bc93410a16a3337e Mon Sep 17 00:00:00 2001 From: Feanil Patel Date: Wed, 2 Sep 2026 08:18:37 -0400 Subject: [PATCH 3/3] refactor: read guaranteed settings directly in course_block xmodule/course_block.py read six settings through getattr(settings, 'NAME', default): DEFAULT_COURSE_VISIBILITY_IN_CATALOG, DEFAULT_MOBILE_AVAILABLE, ENABLE_EXAM_SETTINGS_HTML_VIEW, ENABLE_SPECIAL_EXAMS, ENABLE_PROCTORED_EXAMS, and PROCTORING_BACKENDS. All six are defined in openedx/envs/common.py, which is star-imported into both lms/envs/common.py and cms/envs/common.py, so every settings module this block runs under (production, test, devstack, pact, docker-production) defines them. The getattr defaults are therefore unreachable. The defaults did not guard the "settings not configured at all" case either: getattr(settings, 'NAME', default) returns the default only on AttributeError, but an unconfigured LazySettings raises ImproperlyConfigured, which propagates regardless. So the accompanying "updating assets does not have settings defined" comment described a protection getattr never provided. Read all six directly as settings.NAME and drop the stale comment. Behavior is unchanged: the values match the previous getattr defaults, and where they did not (PROCTORING_BACKENDS), the setting is always defined so the default never fired. Co-Authored-By: Claude Opus 4.8 --- xmodule/course_block.py | 23 +++++++---------------- 1 file changed, 7 insertions(+), 16 deletions(-) diff --git a/xmodule/course_block.py b/xmodule/course_block.py index b54dce326309..3af54d8bde9b 100644 --- a/xmodule/course_block.py +++ b/xmodule/course_block.py @@ -45,16 +45,11 @@ CATALOG_VISIBILITY_ABOUT = "about" CATALOG_VISIBILITY_NONE = "none" -DEFAULT_COURSE_VISIBILITY_IN_CATALOG = getattr( - settings, - 'DEFAULT_COURSE_VISIBILITY_IN_CATALOG', - 'both' -) +DEFAULT_COURSE_VISIBILITY_IN_CATALOG = settings.DEFAULT_COURSE_VISIBILITY_IN_CATALOG -DEFAULT_MOBILE_AVAILABLE = getattr(settings, 'DEFAULT_MOBILE_AVAILABLE', False) -# Note: updating assets does not have settings defined, so using `getattr`. -EXAM_SETTINGS_HTML_VIEW_ENABLED = getattr(settings, 'ENABLE_EXAM_SETTINGS_HTML_VIEW', False) -SPECIAL_EXAMS_ENABLED = getattr(settings, 'ENABLE_SPECIAL_EXAMS', False) +DEFAULT_MOBILE_AVAILABLE = settings.DEFAULT_MOBILE_AVAILABLE +EXAM_SETTINGS_HTML_VIEW_ENABLED = settings.ENABLE_EXAM_SETTINGS_HTML_VIEW +SPECIAL_EXAMS_ENABLED = settings.ENABLE_SPECIAL_EXAMS COURSE_VISIBILITY_PRIVATE = 'private' COURSE_VISIBILITY_PUBLIC_OUTLINE = 'public_outline' @@ -236,7 +231,7 @@ def from_json(self, value, validate_providers=False): and include any inherited values from the platform default. """ value = super().from_json(value) - if getattr(settings, 'ENABLE_PROCTORED_EXAMS', False): + if settings.ENABLE_PROCTORED_EXAMS: # Only validate the provider value if ProctoredExams are enabled on the environment # Otherwise, the passed in provider does not matter. We should always return default if validate_providers: @@ -275,7 +270,7 @@ def default(self): """ default = super().default - proctoring_backend_settings = getattr(settings, 'PROCTORING_BACKENDS', None) + proctoring_backend_settings = settings.PROCTORING_BACKENDS if proctoring_backend_settings: return proctoring_backend_settings.get('DEFAULT', None) @@ -287,11 +282,7 @@ def get_available_providers() -> list[str]: """ Return list of available proctoring providers. """ - proctoring_backend_settings = getattr( - settings, - 'PROCTORING_BACKENDS', - {} - ) + proctoring_backend_settings = settings.PROCTORING_BACKENDS available_providers = [provider for provider in proctoring_backend_settings if provider != 'DEFAULT'] available_providers.append('lti_external')