diff --git a/cms/djangoapps/contentstore/exams.py b/cms/djangoapps/contentstore/exams.py index 314f1df1f2..1e9f1b75d3 100644 --- a/cms/djangoapps/contentstore/exams.py +++ b/cms/djangoapps/contentstore/exams.py @@ -70,12 +70,20 @@ def register_exams(course_key): timed_exam.is_practice_exam, timed_exam.is_onboarding_exam ) + exams_list.append({ 'course_id': str(course_key), 'content_id': str(timed_exam.location), 'exam_name': timed_exam.display_name, 'time_limit_mins': timed_exam.default_time_limit_minutes, - 'due_date': timed_exam.due.isoformat() if timed_exam.due and not course.self_paced else None, + # If the subsection has no due date, then infer a due date from the course end date. This behavior is a + # departure from the legacy register_exams function used by the edx-proctoring plugin because + # edx-proctoring makes a direct call to edx-when API when computing an exam's due date. + # By sending the course end date when registering exams, we can avoid calling to the platform from the + # exam service. Also note that we no longer consider the pacing type of the course - this applies to both + # self-paced and indstructor-paced courses. Therefore, this effectively opts out exams powered by edx-exams + # from personalized learner schedules/relative dates. + 'due_date': timed_exam.due.isoformat() if timed_exam.due else course.end.isoformat(), 'exam_type': exam_type, 'is_active': True, 'hide_after_due': timed_exam.hide_after_due, diff --git a/cms/djangoapps/contentstore/tests/test_exams.py b/cms/djangoapps/contentstore/tests/test_exams.py index 2a1a6e83a3..6b2f39687e 100644 --- a/cms/djangoapps/contentstore/tests/test_exams.py +++ b/cms/djangoapps/contentstore/tests/test_exams.py @@ -1,7 +1,7 @@ """ Test the exams service integration into Studio """ -from datetime import datetime, timedelta +from datetime import datetime, timedelta, timezone from unittest.mock import patch, Mock import ddt @@ -147,8 +147,15 @@ class TestExamService(ModuleStoreTestCase): listen_for_course_publish(self, self.course.id) mock_patch_course_exams.assert_not_called() - def test_self_paced_no_due_dates(self, mock_patch_course_exams): - self.course.self_paced = True + @ddt.data(True, False) + def test_no_due_dates(self, is_self_paced, mock_patch_course_exams): + """ + Test that the coures end date is registered as the due date when the subsection does not have a due date for + both self-paced and instructor-paced exams. + """ + self.course.self_paced = is_self_paced + end_date = datetime(2035, 1, 1, 0, 0, tzinfo=timezone.utc) + self.course.end = end_date self.course = self.update_course(self.course, 1) BlockFactory.create( parent=self.chapter, @@ -159,18 +166,40 @@ class TestExamService(ModuleStoreTestCase): default_time_limit_minutes=60, is_proctored_enabled=False, is_practice_exam=False, - due=datetime.now(UTC) + timedelta(minutes=60), + due=None, hide_after_due=True, is_onboarding_exam=False, ) - listen_for_course_publish(self, self.course.id) - called_exams, called_course = mock_patch_course_exams.call_args[0] - assert called_exams[0]['due_date'] is None - # now switch to instructor paced - # the exam will be updated with a due date - self.course.self_paced = False - self.course = self.update_course(self.course, 1) listen_for_course_publish(self, self.course.id) called_exams, called_course = mock_patch_course_exams.call_args[0] - assert called_exams[0]['due_date'] is not None + assert called_exams[0]['due_date'] == end_date.isoformat() + + @ddt.data(True, False) + def test_subsection_due_date_prioritized(self, is_self_paced, mock_patch_course_exams): + """ + Test that the subsection due date is registered as the due date when both the subsection has a due date and the + course has an end date for both self-paced and instructor-paced exams. + """ + self.course.self_paced = is_self_paced + self.course.end = datetime(2035, 1, 1, 0, 0) + self.course = self.update_course(self.course, 1) + + sequential_due_date = datetime.now(UTC) + timedelta(minutes=60) + BlockFactory.create( + parent=self.chapter, + category='sequential', + display_name='Test Proctored Exam', + graded=True, + is_time_limited=True, + default_time_limit_minutes=60, + is_proctored_enabled=False, + is_practice_exam=False, + due=sequential_due_date, + hide_after_due=True, + is_onboarding_exam=False, + ) + + listen_for_course_publish(self, self.course.id) + called_exams, called_course = mock_patch_course_exams.call_args[0] + assert called_exams[0]['due_date'] == sequential_due_date.isoformat()