Commit fe4464aa by Chris Dodge

if a verified user views a proctored exam without the prerequisites, then we…

if a verified user views a proctored exam without the prerequisites, then we should auto-mark the attempt as declined
parent c057412c
...@@ -498,7 +498,7 @@ def mark_exam_attempt_as_ready(exam_id, user_id): ...@@ -498,7 +498,7 @@ def mark_exam_attempt_as_ready(exam_id, user_id):
return update_attempt_status(exam_id, user_id, ProctoredExamStudentAttemptStatus.ready_to_start) return update_attempt_status(exam_id, user_id, ProctoredExamStudentAttemptStatus.ready_to_start)
def update_attempt_status(exam_id, user_id, to_status): def update_attempt_status(exam_id, user_id, to_status, raise_if_not_found=True):
""" """
Internal helper to handle state transitions of attempt status Internal helper to handle state transitions of attempt status
""" """
...@@ -522,7 +522,10 @@ def update_attempt_status(exam_id, user_id, to_status): ...@@ -522,7 +522,10 @@ def update_attempt_status(exam_id, user_id, to_status):
exam_attempt_obj = ProctoredExamStudentAttempt.objects.get_exam_attempt(exam_id, user_id) exam_attempt_obj = ProctoredExamStudentAttempt.objects.get_exam_attempt(exam_id, user_id)
if exam_attempt_obj is None: if exam_attempt_obj is None:
raise StudentExamAttemptDoesNotExistsException('Error. Trying to look up an exam that does not exist.') if raise_if_not_found:
raise StudentExamAttemptDoesNotExistsException('Error. Trying to look up an exam that does not exist.')
else:
return
# #
# don't allow state transitions from a completed state to an incomplete state # don't allow state transitions from a completed state to an incomplete state
...@@ -738,22 +741,39 @@ def _check_credit_eligibility(credit_state): ...@@ -738,22 +741,39 @@ def _check_credit_eligibility(credit_state):
been passed and False if otherwise been passed and False if otherwise
""" """
# check both conditions
return (
_check_eligibility_of_enrollment_mode(credit_state) and
_check_eligibility_of_prerequisites(credit_state)
)
def _check_eligibility_of_enrollment_mode(credit_state):
"""
Inspects that the enrollment mode of the user
is valid for proctoring
"""
# Allow only the verified students to take the exam as a proctored exam # Allow only the verified students to take the exam as a proctored exam
# Also make an exception for the honor students to take the "practice exam" as a proctored exam. # Also make an exception for the honor students to take the "practice exam" as a proctored exam.
# For the rest of the enrollment modes, None is returned which shows the exam content # For the rest of the enrollment modes, None is returned which shows the exam content
# to the student rather than the proctoring prompt. # to the student rather than the proctoring prompt.
if credit_state['enrollment_mode'] != 'verified': return credit_state['enrollment_mode'] == 'verified'
return False
def _check_eligibility_of_prerequisites(credit_state):
"""
Inspects that the user has completed all prerequiste
steps required to be allowed to take a proctored exam
"""
# also, if there are in-course reverifications requirements # also, if there are in-course reverifications requirements
# then make sure those has a 'satisfied' status # then make sure those has a 'satisfied' status
for requirement in credit_state['credit_requirement_status']: for requirement in credit_state['credit_requirement_status']:
if requirement['namespace'] == 'reverification': if requirement['namespace'] == 'reverification':
if requirement['status'] != 'satisfied': if requirement['status'] != 'satisfied':
return False return False
# passed everything, so we can return True
return True return True
...@@ -912,14 +932,35 @@ def get_student_view(user_id, course_id, content_id, ...@@ -912,14 +932,35 @@ def get_student_view(user_id, course_id, content_id,
exam['is_practice_exam'] exam['is_practice_exam']
) )
attempt = None
if check_mode_and_eligibility: if check_mode_and_eligibility:
credit_state = context['credit_state'] credit_state = context['credit_state']
has_mode = _check_eligibility_of_enrollment_mode(credit_state)
has_prerequisites = False
if has_mode:
has_prerequisites = _check_eligibility_of_prerequisites(credit_state)
# see if the user has passed all pre-requisite credit eligibility # see if the user has passed all pre-requisite credit eligibility
# checks, otherwise just show the user the exam unproctored # checks, otherwise just show the user the exam unproctored
if not _check_credit_eligibility(credit_state): if not has_mode or not has_prerequisites:
# Nope, has not fulfilled pre-requisites, thus we # if we are in the right mode and if we don't have
# just show the unproctored version # pre-requisites, then we implicitly decline the exam
if has_mode:
attempt = get_exam_attempt(exam_id, user_id)
if not attempt:
# user hasn't a record of attempt, create one now
# so we can mark it as declined
create_exam_attempt(exam_id, user_id)
update_attempt_status(
exam_id,
user_id,
ProctoredExamStudentAttemptStatus.declined,
raise_if_not_found=False
)
# don't override context, let the courseware show
return None return None
attempt = get_exam_attempt(exam_id, user_id) attempt = get_exam_attempt(exam_id, user_id)
......
...@@ -649,7 +649,15 @@ class ProctoredExamApiTests(LoggedInTestCase): ...@@ -649,7 +649,15 @@ class ProctoredExamApiTests(LoggedInTestCase):
) )
self.assertIsNone(rendered_response) self.assertIsNone(rendered_response)
def test_prerequirements_view(self): @ddt.data(
('reverification', None, False, True, ProctoredExamStudentAttemptStatus.declined),
('reverification', 'failed', False, False, ProctoredExamStudentAttemptStatus.declined),
('reverification', 'satisfied', True, True, None),
('grade', 'failed', True, False, None)
)
@ddt.unpack
def test_prereq_scarios(self, namespace, req_status, show_proctored,
pre_create_attempt, mark_as_declined):
""" """
This test asserts that proctoring will not be displayed under the following This test asserts that proctoring will not be displayed under the following
conditions: conditions:
...@@ -657,99 +665,42 @@ class ProctoredExamApiTests(LoggedInTestCase): ...@@ -657,99 +665,42 @@ class ProctoredExamApiTests(LoggedInTestCase):
- Verified student has not completed all 'reverification' requirements - Verified student has not completed all 'reverification' requirements
""" """
# user hasn't attempted reverifications exam = get_exam_by_id(self.proctored_exam_id)
rendered_response = get_student_view(
user_id=self.user_id,
course_id=self.course_id,
content_id=self.content_id,
context={
'is_proctored': True,
'display_name': self.exam_name,
'default_time_limit_mins': 90,
'credit_state': {
'enrollment_mode': 'verified',
'credit_requirement_status': [
{
'namespace': 'reverification',
'status': None,
}
]
},
'is_practice_exam': False
}
)
self.assertIsNone(rendered_response)
# user failed reverifications if pre_create_attempt:
rendered_response = get_student_view( create_exam_attempt(self.proctored_exam_id, self.user_id)
user_id=self.user_id,
course_id=self.course_id,
content_id=self.content_id,
context={
'is_proctored': True,
'display_name': self.exam_name,
'default_time_limit_mins': 90,
'credit_state': {
'enrollment_mode': 'verified',
'credit_requirement_status': [
{
'namespace': 'reverification',
'status': 'failed',
}
]
},
'is_practice_exam': False
}
)
self.assertIsNone(rendered_response)
# user passed reverifications # user hasn't attempted reverifications
rendered_response = get_student_view( rendered_response = get_student_view(
user_id=self.user_id, user_id=self.user_id,
course_id=self.course_id, course_id=exam['course_id'],
content_id=self.content_id, content_id=exam['content_id'],
context={ context={
'is_proctored': True, 'is_proctored': True,
'display_name': self.exam_name, 'display_name': self.exam_name,
'default_time_limit_mins': 90, 'default_time_limit_mins': 90,
'is_practice_exam': False,
'credit_state': { 'credit_state': {
'enrollment_mode': 'verified', 'enrollment_mode': 'verified',
'credit_requirement_status': [ 'credit_requirement_status': [
{ {
'namespace': 'reverification', 'namespace': namespace,
'status': 'satisfied', 'status': req_status,
} }
] ]
}, }
'is_practice_exam': False
} }
) )
# here, we should get proctoring content if show_proctored:
self.assertIsNotNone(rendered_response) self.assertIsNotNone(rendered_response)
else:
self.assertIsNone(rendered_response)
# user doesn't have pre-requisites on reverification # also the user should have been marked as declined in certain
rendered_response = get_student_view( # cases
user_id=self.user_id, if mark_as_declined:
course_id=self.course_id, attempt = get_exam_attempt(self.proctored_exam_id, self.user_id)
content_id=self.content_id, self.assertEqual(attempt['status'], ProctoredExamStudentAttemptStatus.declined)
context={
'is_proctored': True,
'display_name': self.exam_name,
'default_time_limit_mins': 90,
'credit_state': {
'enrollment_mode': 'verified',
'credit_requirement_status': [
{
'namespace': 'grade',
'status': 'failed',
}
]
},
'is_practice_exam': False
}
)
# here, we should get proctoring content
self.assertIsNotNone(rendered_response)
def test_student_view_non_student(self): def test_student_view_non_student(self):
""" """
...@@ -1182,6 +1133,24 @@ class ProctoredExamApiTests(LoggedInTestCase): ...@@ -1182,6 +1133,24 @@ class ProctoredExamApiTests(LoggedInTestCase):
ProctoredExamStudentAttemptStatus.ready_to_submit ProctoredExamStudentAttemptStatus.ready_to_submit
) )
def test_update_unexisting_attempt(self):
"""
Tests updating an non-existing attempt
"""
with self.assertRaises(StudentExamAttemptDoesNotExistsException):
update_attempt_status(0, 0, ProctoredExamStudentAttemptStatus.timed_out)
# also check the raise_if_not_found flag
self.assertIsNone(
update_attempt_status(
0,
0,
ProctoredExamStudentAttemptStatus.timed_out,
raise_if_not_found=False
)
)
@ddt.data( @ddt.data(
( (
ProctoredExamStudentAttemptStatus.eligible, { ProctoredExamStudentAttemptStatus.eligible, {
......
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