Commit f3e1da2d by chrisndodge

Merge pull request #200 from edx/cdodge/declined-status

store declines in the CreditRequirementStatus table
parents 4f897783 d5de1d82
...@@ -600,11 +600,13 @@ def update_attempt_status(exam_id, user_id, to_status, raise_if_not_found=True, ...@@ -600,11 +600,13 @@ def update_attempt_status(exam_id, user_id, to_status, raise_if_not_found=True,
exam = get_exam_by_id(exam_id) exam = get_exam_by_id(exam_id)
if to_status == ProctoredExamStudentAttemptStatus.verified: if to_status == ProctoredExamStudentAttemptStatus.verified:
verification = 'satisfied' credit_requirement_status = 'satisfied'
elif to_status == ProctoredExamStudentAttemptStatus.submitted: elif to_status == ProctoredExamStudentAttemptStatus.submitted:
verification = 'submitted' credit_requirement_status = 'submitted'
elif to_status == ProctoredExamStudentAttemptStatus.declined:
credit_requirement_status = 'declined'
else: else:
verification = 'failed' credit_requirement_status = 'failed'
log_msg = ( log_msg = (
'Calling set_credit_requirement_status for ' 'Calling set_credit_requirement_status for '
...@@ -613,7 +615,7 @@ def update_attempt_status(exam_id, user_id, to_status, raise_if_not_found=True, ...@@ -613,7 +615,7 @@ def update_attempt_status(exam_id, user_id, to_status, raise_if_not_found=True,
user_id=exam_attempt_obj.user_id, user_id=exam_attempt_obj.user_id,
course_id=exam['course_id'], course_id=exam['course_id'],
content_id=exam_attempt_obj.proctored_exam.content_id, content_id=exam_attempt_obj.proctored_exam.content_id,
status=verification status=credit_requirement_status
) )
) )
log.info(log_msg) log.info(log_msg)
...@@ -623,7 +625,7 @@ def update_attempt_status(exam_id, user_id, to_status, raise_if_not_found=True, ...@@ -623,7 +625,7 @@ def update_attempt_status(exam_id, user_id, to_status, raise_if_not_found=True,
course_key_or_id=exam['course_id'], course_key_or_id=exam['course_id'],
req_namespace='proctored_exam', req_namespace='proctored_exam',
req_name=exam_attempt_obj.proctored_exam.content_id, req_name=exam_attempt_obj.proctored_exam.content_id,
status=verification status=credit_requirement_status
) )
if cascade_effects and ProctoredExamStudentAttemptStatus.is_a_cascadable_failure(to_status): if cascade_effects and ProctoredExamStudentAttemptStatus.is_a_cascadable_failure(to_status):
...@@ -963,6 +965,7 @@ def _are_prerequirements_satisfied(prerequisites_statuses, evaluate_for_requirem ...@@ -963,6 +965,7 @@ def _are_prerequirements_satisfied(prerequisites_statuses, evaluate_for_requirem
satisfied_prerequisites = [] satisfied_prerequisites = []
failed_prerequisites = [] failed_prerequisites = []
pending_prerequisites = [] pending_prerequisites = []
declined_prerequisites = []
# insure an ordered and filtered list # insure an ordered and filtered list
# we remove 'grade' requirements since those cannot be # we remove 'grade' requirements since those cannot be
...@@ -996,6 +999,8 @@ def _are_prerequirements_satisfied(prerequisites_statuses, evaluate_for_requirem ...@@ -996,6 +999,8 @@ def _are_prerequirements_satisfied(prerequisites_statuses, evaluate_for_requirem
satisfied_prerequisites.append(requirement) satisfied_prerequisites.append(requirement)
elif status == 'failed': elif status == 'failed':
failed_prerequisites.append(requirement) failed_prerequisites.append(requirement)
elif status == 'declined':
declined_prerequisites.append(requirement)
else: else:
pending_prerequisites.append(requirement) pending_prerequisites.append(requirement)
...@@ -1005,12 +1010,13 @@ def _are_prerequirements_satisfied(prerequisites_statuses, evaluate_for_requirem ...@@ -1005,12 +1010,13 @@ def _are_prerequirements_satisfied(prerequisites_statuses, evaluate_for_requirem
# all prequisites are satisfied if there are no failed or pending requirement # all prequisites are satisfied if there are no failed or pending requirement
# statuses # statuses
'are_prerequisites_satisifed': ( 'are_prerequisites_satisifed': (
not failed_prerequisites and not pending_prerequisites not failed_prerequisites and not pending_prerequisites and not declined_prerequisites
), ),
# note that we reverse the list here, because we assempled it by walking backwards # note that we reverse the list here, because we assempled it by walking backwards
'satisfied_prerequisites': list(reversed(satisfied_prerequisites)), 'satisfied_prerequisites': list(reversed(satisfied_prerequisites)),
'failed_prerequisites': list(reversed(failed_prerequisites)), 'failed_prerequisites': list(reversed(failed_prerequisites)),
'pending_prerequisites': list(reversed(pending_prerequisites)), 'pending_prerequisites': list(reversed(pending_prerequisites)),
'declined_prerequisites': list(reversed(declined_prerequisites))
} }
...@@ -1364,7 +1370,9 @@ def _get_proctored_exam_view(exam, context, exam_id, user_id, course_id): ...@@ -1364,7 +1370,9 @@ def _get_proctored_exam_view(exam, context, exam_id, user_id, course_id):
# so, show them: # so, show them:
# 1) If there are failed prerequisites then block user and say why # 1) If there are failed prerequisites then block user and say why
# 2) If there are pending prerequisites then block user and allow them to remediate them # 2) If there are pending prerequisites then block user and allow them to remediate them
# 3) Otherwise - all prerequisites are satisfied - then give user # 3) If there are declined prerequisites, then we auto-decline proctoring since user
# explicitly declined their interest in credit
# 4) Otherwise - all prerequisites are satisfied - then give user
# option to take exam as proctored # option to take exam as proctored
# get information about prerequisites # get information about prerequisites
...@@ -1386,7 +1394,23 @@ def _get_proctored_exam_view(exam, context, exam_id, user_id, course_id): ...@@ -1386,7 +1394,23 @@ def _get_proctored_exam_view(exam, context, exam_id, user_id, course_id):
}) })
if not prerequisite_status['are_prerequisites_satisifed']: if not prerequisite_status['are_prerequisites_satisifed']:
# do we have failed prerequisites? That takes priority # do we have any declined prerequisites, if so, then we
# will auto-decline this proctored exam
if prerequisite_status['declined_prerequisites']:
# 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
)
return None
# do we have failed prerequisites? That takes priority in terms of
# messaging
if prerequisite_status['failed_prerequisites']: if prerequisite_status['failed_prerequisites']:
# Let's resolve the URLs to jump to this prequisite # Let's resolve the URLs to jump to this prequisite
prerequisite_status['failed_prerequisites'] = _resolve_prerequisite_links( prerequisite_status['failed_prerequisites'] = _resolve_prerequisite_links(
......
...@@ -155,6 +155,39 @@ class ProctoredExamApiTests(LoggedInTestCase): ...@@ -155,6 +155,39 @@ class ProctoredExamApiTests(LoggedInTestCase):
}, },
] ]
self.declined_prerequisites = [
{
'namespace': 'proctoring',
'name': 'proc1',
'order': 2,
'status': 'satisfied',
},
{
'namespace': 'reverification',
'name': 'rever1',
'order': 1,
'status': 'satisfied',
},
{
'namespace': 'grade',
'name': 'grade1',
'order': 0,
'status': 'pending',
},
{
'namespace': 'reverification',
'name': 'rever2',
'order': 3,
'status': 'declined',
},
{
'namespace': 'proctoring',
'name': 'proc2',
'order': 4,
'status': 'pending',
},
]
def _create_proctored_exam(self): def _create_proctored_exam(self):
""" """
Calls the api's create_exam to create an exam object. Calls the api's create_exam to create an exam object.
...@@ -575,6 +608,7 @@ class ProctoredExamApiTests(LoggedInTestCase): ...@@ -575,6 +608,7 @@ class ProctoredExamApiTests(LoggedInTestCase):
@ddt.data( @ddt.data(
(ProctoredExamStudentAttemptStatus.verified, 'satisfied'), (ProctoredExamStudentAttemptStatus.verified, 'satisfied'),
(ProctoredExamStudentAttemptStatus.submitted, 'submitted'), (ProctoredExamStudentAttemptStatus.submitted, 'submitted'),
(ProctoredExamStudentAttemptStatus.declined, 'declined'),
(ProctoredExamStudentAttemptStatus.error, 'failed') (ProctoredExamStudentAttemptStatus.error, 'failed')
) )
@ddt.unpack @ddt.unpack
...@@ -797,11 +831,15 @@ class ProctoredExamApiTests(LoggedInTestCase): ...@@ -797,11 +831,15 @@ class ProctoredExamApiTests(LoggedInTestCase):
('reverification', 'pending', 'The following prerequisites are in a <strong>pending</strong> state', True), ('reverification', 'pending', 'The following prerequisites are in a <strong>pending</strong> state', True),
('reverification', 'failed', 'You did not satisfy the following prerequisites', True), ('reverification', 'failed', 'You did not satisfy the following prerequisites', True),
('reverification', 'satisfied', 'To be eligible to earn credit for this course', False), ('reverification', 'satisfied', 'To be eligible to earn credit for this course', False),
('reverification', 'declined', None, False),
('proctored_exam', None, 'The following prerequisites are in a <strong>pending</strong> state', True), ('proctored_exam', None, 'The following prerequisites are in a <strong>pending</strong> state', True),
('proctored_exam', 'pending', 'The following prerequisites are in a <strong>pending</strong> state', True), ('proctored_exam', 'pending', 'The following prerequisites are in a <strong>pending</strong> state', True),
('proctored_exam', 'failed', 'You did not satisfy the following prerequisites', True), ('proctored_exam', 'failed', 'You did not satisfy the following prerequisites', True),
('proctored_exam', 'satisfied', 'To be eligible to earn credit for this course', False), ('proctored_exam', 'satisfied', 'To be eligible to earn credit for this course', False),
('grade', 'failed', 'To be eligible to earn credit for this course', False) ('proctored_exam', 'declined', None, False),
('grade', 'failed', 'To be eligible to earn credit for this course', False),
# this is nonesense, but let's double check it
('grade', 'declined', 'To be eligible to earn credit for this course', False),
) )
@ddt.unpack @ddt.unpack
def test_prereq_scenarios(self, namespace, req_status, expected_content, should_see_prereq): def test_prereq_scenarios(self, namespace, req_status, expected_content, should_see_prereq):
...@@ -839,7 +877,16 @@ class ProctoredExamApiTests(LoggedInTestCase): ...@@ -839,7 +877,16 @@ class ProctoredExamApiTests(LoggedInTestCase):
} }
) )
self.assertIn(expected_content, rendered_response) if expected_content:
self.assertIn(expected_content, rendered_response)
else:
self.assertIsNone(rendered_response)
if req_status == 'declined' and not expected_content:
# also we should have auto-declined if a pre-requisite was declined
attempt = get_exam_attempt(exam['id'], self.user_id)
self.assertIsNotNone(attempt)
self.assertEqual(attempt['status'], ProctoredExamStudentAttemptStatus.declined)
if should_see_prereq: if should_see_prereq:
self.assertIn('Foo Requirement', rendered_response) self.assertIn('Foo Requirement', rendered_response)
...@@ -2134,19 +2181,20 @@ class ProctoredExamApiTests(LoggedInTestCase): ...@@ -2134,19 +2181,20 @@ class ProctoredExamApiTests(LoggedInTestCase):
self.assertEqual(ordered_list[3]['name'], 'proc2') self.assertEqual(ordered_list[3]['name'], 'proc2')
@ddt.data( @ddt.data(
('rever1', True, 0, 0, 0), ('rever1', True, 0, 0, 0, 0),
('proc1', True, 1, 0, 0), ('proc1', True, 1, 0, 0, 0),
('rever2', True, 2, 0, 0), ('rever2', True, 2, 0, 0, 0),
('proc2', False, 2, 1, 0), ('proc2', False, 2, 1, 0, 0),
('unknown', False, 2, 1, 1), ('unknown', False, 2, 1, 1, 0),
(None, False, 2, 1, 1), (None, False, 2, 1, 1, 0),
) )
@ddt.unpack @ddt.unpack
def test_are_prerequisite_satisifed(self, content_id, def test_are_prerequisite_satisifed(self, content_id,
expected_are_prerequisites_satisifed, expected_are_prerequisites_satisifed,
expected_len_satisfied_prerequisites, expected_len_satisfied_prerequisites,
expected_len_failed_prerequisites, expected_len_failed_prerequisites,
expected_len_pending_prerequisites): expected_len_pending_prerequisites,
expected_len_declined_prerequisites):
""" """
verify proper operation of the logic when computing is prerequisites are satisfied verify proper operation of the logic when computing is prerequisites are satisfied
""" """
...@@ -2161,3 +2209,35 @@ class ProctoredExamApiTests(LoggedInTestCase): ...@@ -2161,3 +2209,35 @@ class ProctoredExamApiTests(LoggedInTestCase):
self.assertEqual(len(results['satisfied_prerequisites']), expected_len_satisfied_prerequisites) self.assertEqual(len(results['satisfied_prerequisites']), expected_len_satisfied_prerequisites)
self.assertEqual(len(results['failed_prerequisites']), expected_len_failed_prerequisites) self.assertEqual(len(results['failed_prerequisites']), expected_len_failed_prerequisites)
self.assertEqual(len(results['pending_prerequisites']), expected_len_pending_prerequisites) self.assertEqual(len(results['pending_prerequisites']), expected_len_pending_prerequisites)
self.assertEqual(len(results['declined_prerequisites']), expected_len_declined_prerequisites)
@ddt.data(
('rever1', True, 0, 0, 0, 0),
('proc1', True, 1, 0, 0, 0),
('rever2', True, 2, 0, 0, 0),
('proc2', False, 2, 0, 0, 1),
('unknown', False, 2, 0, 1, 1),
(None, False, 2, 0, 1, 1),
)
@ddt.unpack
def test_declined_prerequisites(self, content_id,
expected_are_prerequisites_satisifed,
expected_len_satisfied_prerequisites,
expected_len_failed_prerequisites,
expected_len_pending_prerequisites,
expected_len_declined_prerequisites):
"""
verify proper operation of the logic when computing is prerequisites are satisfied
"""
results = _are_prerequirements_satisfied(
self.declined_prerequisites,
content_id,
filter_out_namespaces=['grade']
)
self.assertEqual(results['are_prerequisites_satisifed'], expected_are_prerequisites_satisifed)
self.assertEqual(len(results['satisfied_prerequisites']), expected_len_satisfied_prerequisites)
self.assertEqual(len(results['failed_prerequisites']), expected_len_failed_prerequisites)
self.assertEqual(len(results['pending_prerequisites']), expected_len_pending_prerequisites)
self.assertEqual(len(results['declined_prerequisites']), expected_len_declined_prerequisites)
...@@ -1573,7 +1573,7 @@ class TestStudentProctoredExamAttempt(LoggedInTestCase): ...@@ -1573,7 +1573,7 @@ class TestStudentProctoredExamAttempt(LoggedInTestCase):
) )
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
# make sure we failed the requirement status # make sure we declined the requirement status
credit_service = get_runtime_service('credit') credit_service = get_runtime_service('credit')
credit_status = credit_service.get_credit_state(self.user.id, proctored_exam.course_id) credit_status = credit_service.get_credit_state(self.user.id, proctored_exam.course_id)
...@@ -1581,7 +1581,7 @@ class TestStudentProctoredExamAttempt(LoggedInTestCase): ...@@ -1581,7 +1581,7 @@ class TestStudentProctoredExamAttempt(LoggedInTestCase):
self.assertEqual(len(credit_status['credit_requirement_status']), 1) self.assertEqual(len(credit_status['credit_requirement_status']), 1)
self.assertEqual( self.assertEqual(
credit_status['credit_requirement_status'][0]['status'], credit_status['credit_requirement_status'][0]['status'],
'failed' 'declined'
) )
def test_exam_callback(self): def test_exam_callback(self):
......
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