From 2767816028d5217fccea73e8bdd9026c6d461263 Mon Sep 17 00:00:00 2001 From: Michael Roytman Date: Mon, 5 Apr 2021 11:42:56 -0400 Subject: [PATCH] fix: Display Useful Status in InstructorDashboard StudentOnboardingPanel for "onboarding reset" Attempt Status JIRA: MST-736 Due to inconsistencies in the way we handle attempts in past due practice proctored/onboarding exams, learners can end up in an unintended liminal state after attempting to reset their onboarding attempt. If a learner attempts to reset their rejected onboarding attempt after the exam's due date, we process the reset request and move their attempt into the "onboarding_reset" state. Theoretically, a new exam attempt should be created immediately thereafter. However, we have code that prevents the creation of an exam attempt after the exam's due date, so the call to create a subsequent exam attempt fails, leaving the learner with an exam attempt with the "onboarding_reset" status. Theoretically, this situation should never occur, and the fact that it does is a bug. Because of this, we did not handle the "onboarding_reset" status in the StudentOnboardingStatus panel, and this status appears as "null". As an intermediate step, while we think about our due date logic, this pull request adds a new onboarding status "onboarding_status_past_due". This status is displayed as "Onboarding Reset Failed Due to Past Due Exam" in the StudentOnboardingPanel in the InstructorDashboard, which should provide course staff with a clearer explanation. JIRA: MST-745 tracks the removal of this intermediate code from the code base once we fix the underlying cause of this bug. JIRA: MST-749 tracks the fix for the behavior that allowed for this state to occur. --- CHANGELOG.rst | 6 ++ edx_proctoring/__init__.py | 2 +- .../views/proctored_exam_onboarding_view.js | 2 + edx_proctoring/statuses.py | 8 +++ edx_proctoring/tests/test_views.py | 63 +++++++++++++++++++ edx_proctoring/views.py | 20 +++++- package.json | 2 +- 7 files changed, 99 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index ed8643c0fbd..ed4dd26a6b7 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -14,6 +14,12 @@ Change Log Unreleased ~~~~~~~~~~ +[3.8.5] - 2021-04-07 +~~~~~~~~~~~~~~~~~~~~~ +* Add handling of the "onboarding_reset" attempt status to the + StudentOnboardingStatusByCourseView view and the StudentOnboardingStatus + panel in the Instructor Dashboard. + [3.8.4] - 2021-04-05 ~~~~~~~~~~~~~~~~~~~~~ * Add the request username to the proctoring info panel, allowing course staff to masquerade as diff --git a/edx_proctoring/__init__.py b/edx_proctoring/__init__.py index d4e7e4fc89b..6dd0fc787b5 100644 --- a/edx_proctoring/__init__.py +++ b/edx_proctoring/__init__.py @@ -3,6 +3,6 @@ """ # Be sure to update the version number in edx_proctoring/package.json -__version__ = '3.8.4' +__version__ = '3.8.5' default_app_config = 'edx_proctoring.apps.EdxProctoringConfig' # pylint: disable=invalid-name diff --git a/edx_proctoring/static/proctoring/js/views/proctored_exam_onboarding_view.js b/edx_proctoring/static/proctoring/js/views/proctored_exam_onboarding_view.js index 9e4776e0b68..46e62d24815 100644 --- a/edx_proctoring/static/proctoring/js/views/proctored_exam_onboarding_view.js +++ b/edx_proctoring/static/proctoring/js/views/proctored_exam_onboarding_view.js @@ -27,6 +27,8 @@ edx = edx || {}; verified: gettext('Verified'), rejected: gettext('Rejected'), error: gettext('Error'), + // TODO: remove as part of MST-745 + onboarding_reset_past_due: gettext('Onboarding Reset Failed Due to Past Due Exam'), // Enrollment modes (Note: 'verified' is both a status and enrollment mode) audit: gettext('Audit'), honor: gettext('Honor'), diff --git a/edx_proctoring/statuses.py b/edx_proctoring/statuses.py index d47733c42dc..1ccefe1d544 100644 --- a/edx_proctoring/statuses.py +++ b/edx_proctoring/statuses.py @@ -256,6 +256,14 @@ class InstructorDashboardOnboardingAttemptStatus: # user's onboarding profile is approved in a different course. other_course_approved = 'other_course_approved' + # The following status is not a true attempt status that has a corresponding database + # state. This is a consequence of a bug in our software that allows a learner to end up + # with their only or their most recent exam attempt being in the "onboarding_reset" state. + # The learner should not end up in this state, but while we work on a fix, we should not + # display "null" in the Instructor Dashboard Student Onboarding Panel. + # TODO: remove as part of MST-745 + onboarding_reset_past_due = 'onboarding_reset_past_due' + onboarding_statuses = { ProctoredExamStudentAttemptStatus.created: setup_started, ProctoredExamStudentAttemptStatus.download_software_clicked: setup_started, diff --git a/edx_proctoring/tests/test_views.py b/edx_proctoring/tests/test_views.py index fd3939e8b0e..70e026d3e73 100644 --- a/edx_proctoring/tests/test_views.py +++ b/edx_proctoring/tests/test_views.py @@ -1386,6 +1386,69 @@ def test_other_course_verified(self): } self.assertEqual(response_data, expected_data) + def test_onboarding_reset_failed_past_due(self): + # TODO: remove as part of MST-745 + onboarding_attempt_id = create_exam_attempt( + self.onboarding_exam.id, + self.user.id, + True, + ) + + # Update the exam attempt to rejected to allow onboarding exam to be reset. + update_attempt_status(onboarding_attempt_id, ProctoredExamStudentAttemptStatus.rejected) + + # Update the exam to have a due date in the past. + self.onboarding_exam.due_date = datetime.now(pytz.UTC) - timedelta(minutes=10) + self.onboarding_exam.save() + + # Reset the practice exam. + response = self.client.put( + reverse('edx_proctoring:proctored_exam.attempt', args=[onboarding_attempt_id]), + json.dumps({ + 'action': 'reset_attempt', + }), + content_type='application/json' + ) + + # Get serialized onboarding_attempt to get modified time. + serialized_onboarding_attempt = get_exam_attempt_by_id(onboarding_attempt_id) + + response = self.client.get(reverse( + 'edx_proctoring:user_onboarding.status.course', + kwargs={'course_id': self.onboarding_exam.course_id} + ) + ) + self.assertEqual(response.status_code, 200) + + response_data = json.loads(response.content.decode('utf-8')) + expected_data = { + 'results': [ + { + 'username': self.user.username, + 'enrollment_mode': self.enrollment_modes[0], + 'status': InstructorDashboardOnboardingAttemptStatus.onboarding_reset_past_due, + 'modified': serialized_onboarding_attempt.get('modified') + }, + { + 'username': self.learner_1.username, + 'enrollment_mode': self.enrollment_modes[1], + 'status': InstructorDashboardOnboardingAttemptStatus.not_started, + 'modified': None, + }, + { + 'username': self.learner_2.username, + 'enrollment_mode': self.enrollment_modes[2], + 'status': InstructorDashboardOnboardingAttemptStatus.not_started, + 'modified': None, + }, + ], + 'count': 3, + 'previous': None, + 'next': None, + 'num_pages': 1, + } + self.assertEqual(response_data, expected_data) + def test_not_staff_or_course_staff(self): self.user.is_staff = False self.user.save() diff --git a/edx_proctoring/views.py b/edx_proctoring/views.py index c7d16e8622e..6f5dc852399 100644 --- a/edx_proctoring/views.py +++ b/edx_proctoring/views.py @@ -523,8 +523,24 @@ def get(self, request, course_id): data['status'] = InstructorDashboardOnboardingAttemptStatus.other_course_approved data['modified'] = other_verified_attempt.modified else: - data['status'] = (InstructorDashboardOnboardingAttemptStatus - .get_onboarding_status_from_attempt_status(user_attempt.get('status'))) + attempt_status = user_attempt.get('status') + + # If the learner's most recent attempt is in the "onboarding_reset" state, + # return the onboarding_reset_past_due state. + # This is a consequence of a bug in our software that allows a learner to end up + # with their only or their most recent exam attempt being in the "onboarding_reset" state. + # The learner should not end up in this state, but while we work on a fix, we should not + # display "null" in the Instructor Dashboard Student Onboarding Panel. + # TODO: remove as part of MST-745 + if user_attempt.get('status') == ProctoredExamStudentAttemptStatus.onboarding_reset: + onboarding_status = InstructorDashboardOnboardingAttemptStatus.onboarding_reset_past_due + else: + onboarding_status = \ + InstructorDashboardOnboardingAttemptStatus.get_onboarding_status_from_attempt_status( + attempt_status + ) + + data['status'] = onboarding_status data['modified'] = user_attempt.get('modified') onboarding_data.append(data) diff --git a/package.json b/package.json index 90c7b1111bc..fbf1e24eb5c 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "@edx/edx-proctoring", "//": "Be sure to update the version number in edx_proctoring/__init__.py", "//": "Note that the version format is slightly different than that of the Python version when using prereleases.", - "version": "3.8.4", + "version": "3.8.5", "main": "edx_proctoring/static/index.js", "scripts":{ "test":"gulp test"