Skip to content

Commit

Permalink
Adds an endpoint get_exam_violation_report
Browse files Browse the repository at this point in the history
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
  • Loading branch information
arizzitano committed Aug 23, 2017
1 parent 614b885 commit 3189e02
Show file tree
Hide file tree
Showing 3 changed files with 201 additions and 1 deletion.
2 changes: 1 addition & 1 deletion edx_proctoring/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,6 @@

from __future__ import absolute_import

__version__ = '1.1.0'
__version__ = '1.2.0'

default_app_config = 'edx_proctoring.apps.EdxProctoringConfig' # pylint: disable=invalid-name
46 changes: 46 additions & 0 deletions edx_proctoring/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
ProctoredExamStudentAttempt,
ProctoredExamStudentAttemptStatus,
ProctoredExamReviewPolicy,
ProctoredExamSoftwareSecureReview,
)
from edx_proctoring.serializers import (
ProctoredExamSerializer,
Expand Down Expand Up @@ -1947,3 +1948,48 @@ def get_student_view(user_id, course_id, content_id,
if sub_view_func:
return sub_view_func(exam, context, exam_id, user_id, course_id)
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'])
154 changes: 154 additions & 0 deletions edx_proctoring/tests/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
get_exam_attempt_by_id,
remove_exam_attempt,
get_all_exam_attempts,
get_exam_violation_report,
get_filtered_exam_attempts,
get_last_exam_completion_date,
mark_exam_attempt_timeout,
Expand Down Expand Up @@ -62,7 +63,10 @@
)
from edx_proctoring.models import (
ProctoredExam,
ProctoredExamSoftwareSecureReview,
ProctoredExamSoftwareSecureComment,
ProctoredExamStudentAllowance,
ProctoredExamStudentAttempt,
ProctoredExamStudentAttemptStatus,
ProctoredExamReviewPolicy,
)
Expand Down Expand Up @@ -1663,3 +1667,153 @@ def test_summary_without_credit_state(self):
timed_exam['content_id']
)
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'])

0 comments on commit 3189e02

Please sign in to comment.