From a5f192872970c3f3c566762f7b5fce8770e58b0b Mon Sep 17 00:00:00 2001 From: Abdul-Muqadim-Arbisoft Date: Tue, 9 Sep 2025 21:55:05 +0500 Subject: [PATCH 1/3] fix: prevent None entrance_exam_minimum_score_pct from breaking CourseOverview sync When entrance exams are disabled in Studio, the field `entrance_exam_minimum_score_pct` was set to `None`. This caused silent failures when saving `CourseOverview` because the database column requires a float (NOT NULL). This patch ensures that: - CourseOverview sanitizes None values by falling back to `settings.ENTRANCE_EXAM_MIN_SCORE_PCT` (default=50). - Studio avoids writing `None` and instead applies the configured default. Impact: - Prevents IntegrityErrors and silent failures when updating course settings. - Restores proper syncing between modulestore (Mongo) and CourseOverview (MySQL). - Fixes reported issues such as display name changes not persisting and course start dates not syncing. Closes: https://github.com/openedx/edx-platform/issues/37319# --- cms/djangoapps/contentstore/views/entrance_exam.py | 2 +- .../core/djangoapps/content/course_overviews/models.py | 9 +++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/cms/djangoapps/contentstore/views/entrance_exam.py b/cms/djangoapps/contentstore/views/entrance_exam.py index bbefb0e9e876..5d914366bd9e 100644 --- a/cms/djangoapps/contentstore/views/entrance_exam.py +++ b/cms/djangoapps/contentstore/views/entrance_exam.py @@ -224,7 +224,7 @@ def _delete_entrance_exam(request, course_key): if course.entrance_exam_id: metadata = { 'entrance_exam_enabled': False, - 'entrance_exam_minimum_score_pct': None, + 'entrance_exam_minimum_score_pct': _get_default_entrance_exam_minimum_pct(), 'entrance_exam_id': None, } CourseMetadata.update_from_dict(metadata, course, request.user) diff --git a/openedx/core/djangoapps/content/course_overviews/models.py b/openedx/core/djangoapps/content/course_overviews/models.py index 10a56f0868fb..35c8a2dd0cc1 100644 --- a/openedx/core/djangoapps/content/course_overviews/models.py +++ b/openedx/core/djangoapps/content/course_overviews/models.py @@ -272,6 +272,15 @@ def _create_or_update(cls, course): # lint-amnesty, pylint: disable=too-many-st course_overview.entrance_exam_minimum_score_pct = course.entrance_exam_minimum_score_pct course_overview.force_on_flexible_peer_openassessments = course.force_on_flexible_peer_openassessments + if course.entrance_exam_minimum_score_pct is None: + entrance_exam_minimum_score_pct = float(settings.ENTRANCE_EXAM_MIN_SCORE_PCT) + if entrance_exam_minimum_score_pct.is_integer(): + entrance_exam_minimum_score_pct = entrance_exam_minimum_score_pct / 100 + course_overview.entrance_exam_minimum_score_pct = entrance_exam_minimum_score_pct + elif isinstance(course.entrance_exam_minimum_score_pct, int): + course_overview.entrance_exam_minimum_score_pct = course.entrance_exam_minimum_score_pct / 100 + else: + course_overview.entrance_exam_minimum_score_pct = course.entrance_exam_minimum_score_pct if not CatalogIntegration.is_enabled(): course_overview.language = course.language From 8f9b8f298ebd0851bb67e674fc415bef61f759b0 Mon Sep 17 00:00:00 2001 From: Abdul-Muqadim-Arbisoft Date: Thu, 11 Sep 2025 11:38:55 +0500 Subject: [PATCH 2/3] refactor: clean up entrance_exam_minimum_score_pct handling - Consolidate logic to avoid repeated assignments - Centralize None fallback and int/float normalization - Improve readability with inline comment and consistency with Open edX style --- .../content/course_overviews/models.py | 25 ++++++++++--------- 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/openedx/core/djangoapps/content/course_overviews/models.py b/openedx/core/djangoapps/content/course_overviews/models.py index 35c8a2dd0cc1..c7264fd9d94b 100644 --- a/openedx/core/djangoapps/content/course_overviews/models.py +++ b/openedx/core/djangoapps/content/course_overviews/models.py @@ -266,21 +266,22 @@ def _create_or_update(cls, course): # lint-amnesty, pylint: disable=too-many-st course_overview.entrance_exam_id = course.entrance_exam_id or '' # Despite it being a float, the course object defaults to an int. So we will detect that case and update # it to be a float like everything else. - if isinstance(course.entrance_exam_minimum_score_pct, int): - course_overview.entrance_exam_minimum_score_pct = course.entrance_exam_minimum_score_pct / 100 - else: - course_overview.entrance_exam_minimum_score_pct = course.entrance_exam_minimum_score_pct - - course_overview.force_on_flexible_peer_openassessments = course.force_on_flexible_peer_openassessments + # Extra handling: entrance_exam_minimum_score_pct can be None (e.g. when exams are disabled in Studio), + # so we fall back to settings.ENTRANCE_EXAM_MIN_SCORE_PCT to prevent CourseOverview save failures. if course.entrance_exam_minimum_score_pct is None: entrance_exam_minimum_score_pct = float(settings.ENTRANCE_EXAM_MIN_SCORE_PCT) - if entrance_exam_minimum_score_pct.is_integer(): - entrance_exam_minimum_score_pct = entrance_exam_minimum_score_pct / 100 - course_overview.entrance_exam_minimum_score_pct = entrance_exam_minimum_score_pct - elif isinstance(course.entrance_exam_minimum_score_pct, int): - course_overview.entrance_exam_minimum_score_pct = course.entrance_exam_minimum_score_pct / 100 else: - course_overview.entrance_exam_minimum_score_pct = course.entrance_exam_minimum_score_pct + entrance_exam_minimum_score_pct = course.entrance_exam_minimum_score_pct + + if ( + isinstance(entrance_exam_minimum_score_pct, int) + or (isinstance(entrance_exam_minimum_score_pct, float) and entrance_exam_minimum_score_pct.is_integer()) + ): + entrance_exam_minimum_score_pct = entrance_exam_minimum_score_pct / 100 + + course_overview.entrance_exam_minimum_score_pct = entrance_exam_minimum_score_pct + + course_overview.force_on_flexible_peer_openassessments = course.force_on_flexible_peer_openassessments if not CatalogIntegration.is_enabled(): course_overview.language = course.language From 2968281bed476b455f1cf700351b34eb376c3bbb Mon Sep 17 00:00:00 2001 From: Abdul-Muqadim-Arbisoft Date: Mon, 15 Sep 2025 12:23:29 +0500 Subject: [PATCH 3/3] test: update entrance exam deletion test to expect default min score - Adjusted `test_entrance_exam_created_updated_and_deleted_successfully` to check for `settings.ENTRANCE_EXAM_MIN_SCORE_PCT` instead of `None` after exam deletion - Added handling for both int and float defaults (`/100` for integer case) --- cms/djangoapps/contentstore/tests/test_course_settings.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/cms/djangoapps/contentstore/tests/test_course_settings.py b/cms/djangoapps/contentstore/tests/test_course_settings.py index a72d14e931f7..44c5891a85bd 100644 --- a/cms/djangoapps/contentstore/tests/test_course_settings.py +++ b/cms/djangoapps/contentstore/tests/test_course_settings.py @@ -499,7 +499,11 @@ def test_entrance_exam_created_updated_and_deleted_successfully(self): course = modulestore().get_course(self.course.id) self.assertEqual(response.status_code, 200) self.assertFalse(course.entrance_exam_enabled) - self.assertEqual(course.entrance_exam_minimum_score_pct, None) + entrance_exam_minimum_score_pct = float(settings.ENTRANCE_EXAM_MIN_SCORE_PCT) + if entrance_exam_minimum_score_pct.is_integer(): + entrance_exam_minimum_score_pct = entrance_exam_minimum_score_pct / 100 + + self.assertEqual(course.entrance_exam_minimum_score_pct, entrance_exam_minimum_score_pct) self.assertFalse(milestones_helpers.any_unfulfilled_milestones(self.course.id, self.user.id), msg='The entrance exam should not be required anymore')