fix: prevent None entrance_exam_minimum_score_pct from breaking CourseOverview sync (#37339)

* 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#

* 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

* 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)
This commit is contained in:
Abdul-Muqadim-Arbisoft
2025-10-08 15:26:00 +05:00
committed by GitHub
parent e46cfa6b32
commit e5b497cbba
3 changed files with 19 additions and 5 deletions

View File

@@ -501,7 +501,11 @@ class CourseDetailsViewTest(CourseTestCase, MilestonesTestCaseMixin):
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')

View File

@@ -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)

View File

@@ -266,10 +266,20 @@ class CourseOverview(TimeStampedModel):
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
# 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)
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