Commit 3189e027 by Ari Rizzitano

Adds an endpoint get_exam_violation_report

EDUCATOR-411

add report generation endpoint + tests
EDUCATOR-411

quality

add include_practice_exams parameter

bump version

added review_status

join exams with reviews, not comments

quality + one more assertion

select_related -> prefetch_related
parent 614b8853
...@@ -4,6 +4,6 @@ The exam proctoring subsystem for the Open edX platform. ...@@ -4,6 +4,6 @@ The exam proctoring subsystem for the Open edX platform.
from __future__ import absolute_import from __future__ import absolute_import
__version__ = '1.1.0' __version__ = '1.2.0'
default_app_config = 'edx_proctoring.apps.EdxProctoringConfig' # pylint: disable=invalid-name default_app_config = 'edx_proctoring.apps.EdxProctoringConfig' # pylint: disable=invalid-name
...@@ -37,6 +37,7 @@ from edx_proctoring.models import ( ...@@ -37,6 +37,7 @@ from edx_proctoring.models import (
ProctoredExamStudentAttempt, ProctoredExamStudentAttempt,
ProctoredExamStudentAttemptStatus, ProctoredExamStudentAttemptStatus,
ProctoredExamReviewPolicy, ProctoredExamReviewPolicy,
ProctoredExamSoftwareSecureReview,
) )
from edx_proctoring.serializers import ( from edx_proctoring.serializers import (
ProctoredExamSerializer, ProctoredExamSerializer,
...@@ -1947,3 +1948,48 @@ def get_student_view(user_id, course_id, content_id, ...@@ -1947,3 +1948,48 @@ def get_student_view(user_id, course_id, content_id,
if sub_view_func: if sub_view_func:
return sub_view_func(exam, context, exam_id, user_id, course_id) return sub_view_func(exam, context, exam_id, user_id, course_id)
return None return None
def get_exam_violation_report(course_id, include_practice_exams=False):
"""
Returns proctored exam attempts for the course id, including review details.
Violation status messages are aggregated as a list per attempt for each
violation type.
"""
attempts_by_code = {
attempt['attempt_code']: {
'course_id': attempt['proctored_exam']['course_id'],
'exam_name': attempt['proctored_exam']['exam_name'],
'username': attempt['user']['username'],
'email': attempt['user']['email'],
'attempt_code': attempt['attempt_code'],
'allowed_time_limit_mins': attempt['allowed_time_limit_mins'],
'is_sample_attempt': attempt['is_sample_attempt'],
'started_at': attempt['started_at'],
'completed_at': attempt['completed_at'],
'status': attempt['status'],
'review_status': None
} for attempt in get_all_exam_attempts(course_id)
}
reviews = ProctoredExamSoftwareSecureReview.objects.prefetch_related(
'proctoredexamsoftwaresecurecomment_set'
).filter(
exam__course_id=course_id,
exam__is_practice_exam=include_practice_exams
)
for review in reviews:
attempt_code = review.attempt_code
attempts_by_code[attempt_code]['review_status'] = review.review_status
for comment in review.proctoredexamsoftwaresecurecomment_set.all():
comments_key = '{status} Comments'.format(status=comment.status)
if comments_key not in attempts_by_code[attempt_code]:
attempts_by_code[attempt_code][comments_key] = []
attempts_by_code[attempt_code][comments_key].append(comment.comment)
return sorted(attempts_by_code.values(), key=lambda a: a['exam_name'])
...@@ -31,6 +31,7 @@ from edx_proctoring.api import ( ...@@ -31,6 +31,7 @@ from edx_proctoring.api import (
get_exam_attempt_by_id, get_exam_attempt_by_id,
remove_exam_attempt, remove_exam_attempt,
get_all_exam_attempts, get_all_exam_attempts,
get_exam_violation_report,
get_filtered_exam_attempts, get_filtered_exam_attempts,
get_last_exam_completion_date, get_last_exam_completion_date,
mark_exam_attempt_timeout, mark_exam_attempt_timeout,
...@@ -62,7 +63,10 @@ from edx_proctoring.exceptions import ( ...@@ -62,7 +63,10 @@ from edx_proctoring.exceptions import (
) )
from edx_proctoring.models import ( from edx_proctoring.models import (
ProctoredExam, ProctoredExam,
ProctoredExamSoftwareSecureReview,
ProctoredExamSoftwareSecureComment,
ProctoredExamStudentAllowance, ProctoredExamStudentAllowance,
ProctoredExamStudentAttempt,
ProctoredExamStudentAttemptStatus, ProctoredExamStudentAttemptStatus,
ProctoredExamReviewPolicy, ProctoredExamReviewPolicy,
) )
...@@ -1663,3 +1667,153 @@ class ProctoredExamApiTests(ProctoredExamTestCase): ...@@ -1663,3 +1667,153 @@ class ProctoredExamApiTests(ProctoredExamTestCase):
timed_exam['content_id'] timed_exam['content_id']
) )
self.assertIsNone(summary) self.assertIsNone(summary)
def test_get_exam_violation_report(self):
"""
Test to get all the exam attempts.
"""
# attempt with comments in multiple categories
exam1_id = create_exam(
course_id=self.course_id,
content_id='test_content_1',
exam_name='DDDDDD',
time_limit_mins=self.default_time_limit
)
exam1_attempt_id = create_exam_attempt(
exam_id=exam1_id,
user_id=self.user_id
)
exam1_attempt = ProctoredExamStudentAttempt.objects.get_exam_attempt_by_id(
exam1_attempt_id
)
exam1_review = ProctoredExamSoftwareSecureReview.objects.create(
exam=ProctoredExam.get_exam_by_id(exam1_id),
attempt_code=exam1_attempt.attempt_code,
review_status="Suspicious"
)
ProctoredExamSoftwareSecureComment.objects.create(
review=exam1_review,
status="Rules Violation",
comment="foo",
start_time=0,
stop_time=1,
duration=1
)
ProctoredExamSoftwareSecureComment.objects.create(
review=exam1_review,
status="Suspicious",
comment="bar",
start_time=0,
stop_time=1,
duration=1
)
ProctoredExamSoftwareSecureComment.objects.create(
review=exam1_review,
status="Suspicious",
comment="baz",
start_time=0,
stop_time=1,
duration=1
)
# attempt with comments in only one category
exam2_id = create_exam(
course_id=self.course_id,
content_id='test_content_2',
exam_name='CCCCCC',
time_limit_mins=self.default_time_limit
)
exam2_attempt_id = create_exam_attempt(
exam_id=exam2_id,
user_id=self.user_id
)
exam2_attempt = ProctoredExamStudentAttempt.objects.get_exam_attempt_by_id(
exam2_attempt_id
)
exam2_review = ProctoredExamSoftwareSecureReview.objects.create(
exam=ProctoredExam.get_exam_by_id(exam2_id),
attempt_code=exam2_attempt.attempt_code,
review_status="Rules Violation"
)
ProctoredExamSoftwareSecureComment.objects.create(
review=exam2_review,
status="Rules Violation",
comment="bar",
start_time=0,
stop_time=1,
duration=1
)
# attempt with no comments, on a different exam
exam3_id = create_exam(
course_id=self.course_id,
content_id='test_content_3',
exam_name='BBBBBB',
time_limit_mins=self.default_time_limit
)
exam3_attempt_id = create_exam_attempt(
exam_id=exam3_id,
user_id=self.user_id
)
exam3_attempt = ProctoredExamStudentAttempt.objects.get_exam_attempt_by_id(
exam3_attempt_id
)
ProctoredExamSoftwareSecureReview.objects.create(
exam=ProctoredExam.get_exam_by_id(exam3_id),
attempt_code=exam3_attempt.attempt_code,
review_status="Clean"
)
# attempt with no comments or review
exam4_id = create_exam(
course_id=self.course_id,
content_id='test_content_4',
exam_name='AAAAAA',
time_limit_mins=self.default_time_limit
)
exam4_attempt_id = create_exam_attempt(
exam_id=exam4_id,
user_id=self.user_id
)
ProctoredExamStudentAttempt.objects.get_exam_attempt_by_id(
exam4_attempt_id
)
report = get_exam_violation_report(self.course_id)
self.assertEqual(len(report), 4)
self.assertEqual([attempt['exam_name'] for attempt in report], [
'AAAAAA',
'BBBBBB',
'CCCCCC',
'DDDDDD'
])
self.assertTrue('Rules Violation Comments' in report[3])
self.assertEqual(len(report[3]['Rules Violation Comments']), 1)
self.assertTrue('Suspicious Comments' in report[3])
self.assertEqual(len(report[3]['Suspicious Comments']), 2)
self.assertEqual(report[3]['review_status'], 'Suspicious')
self.assertTrue('Suspicious Comments' not in report[2])
self.assertTrue('Rules Violation Comments' in report[2])
self.assertEqual(len(report[2]['Rules Violation Comments']), 1)
self.assertEqual(report[2]['review_status'], 'Rules Violation')
self.assertEqual(report[1]['review_status'], 'Clean')
self.assertIsNone(report[0]['review_status'])
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment