From d845dedcc7b4d36ef8f4fc240455bdf89c7a4fb1 Mon Sep 17 00:00:00 2001 From: Brian Buck Date: Fri, 1 May 2026 09:37:20 -0600 Subject: [PATCH 1/2] fix: Add exam_name and ready_to_resume to Inst Dash Special Exams tab --- .../references/instructor-v2-special-exams-api-spec.yaml | 7 +++++++ .../instructor/tests/views/test_special_exams_api_v2.py | 4 ++++ lms/djangoapps/instructor/views/serializers_v2.py | 2 ++ 3 files changed, 13 insertions(+) diff --git a/lms/djangoapps/instructor/docs/references/instructor-v2-special-exams-api-spec.yaml b/lms/djangoapps/instructor/docs/references/instructor-v2-special-exams-api-spec.yaml index a778b04afd2b..5620c1eb75e8 100644 --- a/lms/djangoapps/instructor/docs/references/instructor-v2-special-exams-api-spec.yaml +++ b/lms/djangoapps/instructor/docs/references/instructor-v2-special-exams-api-spec.yaml @@ -488,6 +488,11 @@ definitions: type: string exam_id: type: integer + exam_name: + type: string + exam_type: + type: string + enum: [timed, proctored, practice] status: type: string start_time: @@ -501,6 +506,8 @@ definitions: allowed_time_limit_mins: type: integer x-nullable: true + ready_to_resume: + type: boolean ProctoringSettings: type: object diff --git a/lms/djangoapps/instructor/tests/views/test_special_exams_api_v2.py b/lms/djangoapps/instructor/tests/views/test_special_exams_api_v2.py index 397a16469f3c..24de8d1d569f 100644 --- a/lms/djangoapps/instructor/tests/views/test_special_exams_api_v2.py +++ b/lms/djangoapps/instructor/tests/views/test_special_exams_api_v2.py @@ -264,7 +264,9 @@ def test_attempt_exam_type(self, is_proctored, is_practice_exam, expected_type): data = response.json() assert data['count'] == 1 assert data['results'][0]['exam_id'] == exam_id + assert data['results'][0]['exam_name'] == 'Test Exam' assert data['results'][0]['exam_type'] == expected_type + assert data['results'][0]['ready_to_resume'] is False assert data['results'][0]['user']['username'] == 'student1' def test_list_attempts_filters_by_exam(self): @@ -682,6 +684,8 @@ def test_list_all_attempts(self): data = response.json() assert data['count'] == 1 assert data['results'][0]['exam_id'] == self.exam_id + assert data['results'][0]['exam_name'] == 'Midterm Exam' + assert data['results'][0]['ready_to_resume'] is False def test_search_attempts_by_username(self): create_exam_attempt(self.exam_id, self.student.id) diff --git a/lms/djangoapps/instructor/views/serializers_v2.py b/lms/djangoapps/instructor/views/serializers_v2.py index c345f7d67fd4..b9e514ce9e84 100644 --- a/lms/djangoapps/instructor/views/serializers_v2.py +++ b/lms/djangoapps/instructor/views/serializers_v2.py @@ -1224,11 +1224,13 @@ class ExamAttemptSerializer(serializers.Serializer): id = serializers.IntegerField() user = ExamAttemptUserSerializer() exam_id = serializers.IntegerField(source='proctored_exam.id') + exam_name = serializers.CharField(source='proctored_exam.exam_name') exam_type = serializers.SerializerMethodField() status = serializers.CharField() start_time = serializers.DateTimeField(source='started_at', allow_null=True, required=False) end_time = serializers.DateTimeField(source='completed_at', allow_null=True, required=False) allowed_time_limit_mins = serializers.IntegerField(allow_null=True, required=False) + ready_to_resume = serializers.BooleanField(default=False) def get_exam_type(self, obj): """Derive exam type from proctored_exam flags.""" From 16ec12e63145df5d8ae51a50b48b873401dd9dfe Mon Sep 17 00:00:00 2001 From: Brian Buck Date: Fri, 1 May 2026 09:59:48 -0600 Subject: [PATCH 2/2] fix: Address Copilot feedback --- .../tests/views/test_special_exams_api_v2.py | 23 +++++++++++++++++++ .../instructor/views/serializers_v2.py | 2 +- 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/lms/djangoapps/instructor/tests/views/test_special_exams_api_v2.py b/lms/djangoapps/instructor/tests/views/test_special_exams_api_v2.py index 24de8d1d569f..4304b004ccc6 100644 --- a/lms/djangoapps/instructor/tests/views/test_special_exams_api_v2.py +++ b/lms/djangoapps/instructor/tests/views/test_special_exams_api_v2.py @@ -282,6 +282,18 @@ def test_list_attempts_filters_by_exam(self): assert data['count'] == 1 assert data['results'][0]['exam_id'] == exam_id + def test_ready_to_resume_true(self): + """Verify ready_to_resume reflects the actual attempt state.""" + exam_id = self._create_exam() + attempt_id = create_exam_attempt(exam_id, self.student.id) + attempt = ProctoredExamStudentAttempt.objects.get(id=attempt_id) + attempt.ready_to_resume = True + attempt.save() + + response = self.client.get(self._url(exam_id)) + assert response.status_code == status.HTTP_200_OK + assert response.json()['results'][0]['ready_to_resume'] is True + @override_settings(**PROCTORING_SETTINGS) @patch.dict(settings.FEATURES, {'ENABLE_SPECIAL_EXAMS': True}) @@ -754,3 +766,14 @@ def test_sort_attempts_descending(self): results = response.json()['results'] assert results[0]['user']['username'] == 'student2' assert results[1]['user']['username'] == 'student1' + + def test_ready_to_resume_true(self): + """Verify ready_to_resume reflects the actual attempt state.""" + attempt_id = create_exam_attempt(self.exam_id, self.student.id) + attempt = ProctoredExamStudentAttempt.objects.get(id=attempt_id) + attempt.ready_to_resume = True + attempt.save() + + response = self.client.get(self._url()) + assert response.status_code == status.HTTP_200_OK + assert response.json()['results'][0]['ready_to_resume'] is True diff --git a/lms/djangoapps/instructor/views/serializers_v2.py b/lms/djangoapps/instructor/views/serializers_v2.py index b9e514ce9e84..de4d7f647dd9 100644 --- a/lms/djangoapps/instructor/views/serializers_v2.py +++ b/lms/djangoapps/instructor/views/serializers_v2.py @@ -1230,7 +1230,7 @@ class ExamAttemptSerializer(serializers.Serializer): start_time = serializers.DateTimeField(source='started_at', allow_null=True, required=False) end_time = serializers.DateTimeField(source='completed_at', allow_null=True, required=False) allowed_time_limit_mins = serializers.IntegerField(allow_null=True, required=False) - ready_to_resume = serializers.BooleanField(default=False) + ready_to_resume = serializers.BooleanField() def get_exam_type(self, obj): """Derive exam type from proctored_exam flags."""