def test_delete_proctored_exam_attempt(self): # pylint: disable=invalid-name """ Deleting the proctored exam attempt creates an entry in the history table. """ proctored_exam = ProctoredExam.objects.create( course_id='test_course', content_id='test_content', exam_name='Test Exam', external_id='123aXqe3', time_limit_mins=90) attempt = ProctoredExamStudentAttempt.objects.create( proctored_exam_id=proctored_exam.id, user_id=1, student_name="John. D", allowed_time_limit_mins=10, attempt_code="123456", taking_as_proctored=True, is_sample_attempt=True, external_id=1) # No entry in the History table on creation of the Allowance entry. attempt_history = ProctoredExamStudentAttemptHistory.objects.filter( user_id=1) self.assertEqual(len(attempt_history), 0) attempt.delete_exam_attempt() attempt_history = ProctoredExamStudentAttemptHistory.objects.filter( user_id=1) self.assertEqual(len(attempt_history), 1) # make sure we can ready it back with helper class method deleted_item = ProctoredExamStudentAttemptHistory.get_exam_attempt_by_code( "123456") self.assertEqual(deleted_item.student_name, "John. D") # re-create and delete again using same attempt_cde attempt = ProctoredExamStudentAttempt.objects.create( proctored_exam_id=proctored_exam.id, user_id=1, student_name="John. D Updated", allowed_time_limit_mins=10, attempt_code="123456", taking_as_proctored=True, is_sample_attempt=True, external_id=1) attempt.delete_exam_attempt() attempt_history = ProctoredExamStudentAttemptHistory.objects.filter( user_id=1) self.assertEqual(len(attempt_history), 2) deleted_item = ProctoredExamStudentAttemptHistory.get_exam_attempt_by_code( "123456") self.assertEqual(deleted_item.student_name, "John. D Updated")
def test_delete_proctored_exam_attempt(self): # pylint: disable=invalid-name """ Deleting the proctored exam attempt creates an entry in the history table. """ proctored_exam = ProctoredExam.objects.create( course_id='test_course', content_id='test_content', exam_name='Test Exam', external_id='123aXqe3', time_limit_mins=90 ) attempt = ProctoredExamStudentAttempt.objects.create( proctored_exam_id=proctored_exam.id, user_id=1, student_name="John. D", allowed_time_limit_mins=10, attempt_code="123456", taking_as_proctored=True, is_sample_attempt=True, external_id=1 ) # No entry in the History table on creation of the Allowance entry. attempt_history = ProctoredExamStudentAttemptHistory.objects.filter(user_id=1) self.assertEqual(len(attempt_history), 0) attempt.delete_exam_attempt() attempt_history = ProctoredExamStudentAttemptHistory.objects.filter(user_id=1) self.assertEqual(len(attempt_history), 1) # make sure we can ready it back with helper class method deleted_item = ProctoredExamStudentAttemptHistory.get_exam_attempt_by_code("123456") self.assertEqual(deleted_item.student_name, "John. D") # re-create and delete again using same attempt_cde attempt = ProctoredExamStudentAttempt.objects.create( proctored_exam_id=proctored_exam.id, user_id=1, student_name="John. D Updated", allowed_time_limit_mins=10, attempt_code="123456", taking_as_proctored=True, is_sample_attempt=True, external_id=1 ) attempt.delete_exam_attempt() attempt_history = ProctoredExamStudentAttemptHistory.objects.filter(user_id=1) self.assertEqual(len(attempt_history), 2) deleted_item = ProctoredExamStudentAttemptHistory.get_exam_attempt_by_code("123456") self.assertEqual(deleted_item.student_name, "John. D Updated")
def locate_attempt_by_attempt_code(attempt_code): """ Helper method to look up an attempt by attempt_code. This can be either in the ProctoredExamStudentAttempt *OR* ProctoredExamStudentAttemptHistory tables we will return a tuple of (attempt, is_archived_attempt) """ attempt_obj = ProctoredExamStudentAttempt.objects.get_exam_attempt_by_code( attempt_code) if not attempt_obj: # try archive table attempt_obj = ProctoredExamStudentAttemptHistory.get_exam_attempt_by_code( attempt_code) if not attempt_obj: # still can't find, error out err_msg = ('Could not locate attempt_code: {attempt_code}'.format( attempt_code=attempt_code)) log.error(err_msg) is_archived = None else: is_archived = True else: is_archived = False return attempt_obj, is_archived
def test_update_proctored_exam_attempt(self): """ Deleting the proctored exam attempt creates an entry in the history table. """ proctored_exam = ProctoredExam.objects.create( course_id='test_course', content_id='test_content', exam_name='Test Exam', external_id='123aXqe3', time_limit_mins=90) attempt = ProctoredExamStudentAttempt.objects.create( proctored_exam_id=proctored_exam.id, user_id=1, status=ProctoredExamStudentAttemptStatus.created, student_name="John. D", allowed_time_limit_mins=10, attempt_code="123456", taking_as_proctored=True, is_sample_attempt=True, external_id=1) # No entry in the History table on creation of the Allowance entry. attempt_history = ProctoredExamStudentAttemptHistory.objects.filter( user_id=1) self.assertEqual(len(attempt_history), 0) # re-saving, but not changing status should not make an archive copy attempt.student_name = 'John. D Updated' attempt.save() attempt_history = ProctoredExamStudentAttemptHistory.objects.filter( user_id=1) self.assertEqual(len(attempt_history), 0) # change status... attempt.status = ProctoredExamStudentAttemptStatus.started attempt.save() attempt_history = ProctoredExamStudentAttemptHistory.objects.filter( user_id=1) self.assertEqual(len(attempt_history), 1) # make sure we can ready it back with helper class method updated_item = ProctoredExamStudentAttemptHistory.get_exam_attempt_by_code( "123456") self.assertEqual(updated_item.student_name, "John. D Updated") self.assertEqual(updated_item.status, ProctoredExamStudentAttemptStatus.created)
def test_update_proctored_exam_attempt(self): """ Deleting the proctored exam attempt creates an entry in the history table. """ proctored_exam = ProctoredExam.objects.create( course_id='test_course', content_id='test_content', exam_name='Test Exam', external_id='123aXqe3', time_limit_mins=90 ) attempt = ProctoredExamStudentAttempt.objects.create( proctored_exam_id=proctored_exam.id, user_id=1, status=ProctoredExamStudentAttemptStatus.created, student_name="John. D", allowed_time_limit_mins=10, attempt_code="123456", taking_as_proctored=True, is_sample_attempt=True, external_id=1 ) # No entry in the History table on creation of the Allowance entry. attempt_history = ProctoredExamStudentAttemptHistory.objects.filter(user_id=1) self.assertEqual(len(attempt_history), 0) # re-saving, but not changing status should not make an archive copy attempt.student_name = 'John. D Updated' attempt.save() attempt_history = ProctoredExamStudentAttemptHistory.objects.filter(user_id=1) self.assertEqual(len(attempt_history), 0) # change status... attempt.status = ProctoredExamStudentAttemptStatus.started attempt.save() attempt_history = ProctoredExamStudentAttemptHistory.objects.filter(user_id=1) self.assertEqual(len(attempt_history), 1) # make sure we can ready it back with helper class method updated_item = ProctoredExamStudentAttemptHistory.get_exam_attempt_by_code("123456") self.assertEqual(updated_item.student_name, "John. D Updated") self.assertEqual(updated_item.status, ProctoredExamStudentAttemptStatus.created)
def locate_attempt_by_attempt_code(attempt_code): """ Helper method to look up an attempt by attempt_code. This can be either in the ProctoredExamStudentAttempt *OR* ProctoredExamStudentAttemptHistory tables we will return a tuple of (attempt, is_archived_attempt) """ attempt_obj = ProctoredExamStudentAttempt.objects.get_exam_attempt_by_code(attempt_code) is_archived_attempt = False if not attempt_obj: # try archive table attempt_obj = ProctoredExamStudentAttemptHistory.get_exam_attempt_by_code(attempt_code) is_archived_attempt = True if not attempt_obj: # still can't find, error out err_msg = "Could not locate attempt_code: {attempt_code}".format(attempt_code=attempt_code) log.error(err_msg) return (attempt_obj, is_archived_attempt)
def on_review_callback(self, payload): """ Called when the reviewing 3rd party service posts back the results Documentation on the data format can be found from SoftwareSecure's documentation named "Reviewer Data Transfer" """ log_msg = ( 'Received callback from SoftwareSecure with review data: {payload}'.format( payload=payload ) ) log.info(log_msg) # what we consider the external_id is SoftwareSecure's 'ssiRecordLocator' external_id = payload['examMetaData']['ssiRecordLocator'] # what we consider the attempt_code is SoftwareSecure's 'examCode' attempt_code = payload['examMetaData']['examCode'] # get the SoftwareSecure status on this attempt review_status = payload['reviewStatus'] bad_status = review_status not in [ 'Not Reviewed', 'Suspicious', 'Rules Violation', 'Clean' ] if bad_status: err_msg = ( 'Received unexpected reviewStatus field calue from payload. ' 'Was {review_status}.'.format(review_status=review_status) ) raise ProctoredExamBadReviewStatus(err_msg) # do a lookup on the attempt by examCode, and compare the # passed in ssiRecordLocator and make sure it matches # what we recorded as the external_id. We need to look in both # the attempt table as well as the archive table attempt_obj = ProctoredExamStudentAttempt.objects.get_exam_attempt_by_code(attempt_code) is_archived_attempt = False if not attempt_obj: # try archive table attempt_obj = ProctoredExamStudentAttemptHistory.get_exam_attempt_by_code(attempt_code) is_archived_attempt = True if not attempt_obj: # still can't find, error out err_msg = ( 'Could not locate attempt_code: {attempt_code}'.format(attempt_code=attempt_code) ) raise StudentExamAttemptDoesNotExistsException(err_msg) # then make sure we have the right external_id # note that SoftwareSecure might send a case insensitive # ssiRecordLocator than what it returned when we registered the # exam match = ( attempt_obj.external_id.lower() == external_id.lower() or settings.PROCTORING_SETTINGS.get('ALLOW_CALLBACK_SIMULATION', False) ) if not match: err_msg = ( 'Found attempt_code {attempt_code}, but the recorded external_id did not ' 'match the ssiRecordLocator that had been recorded previously. Has {existing} ' 'but received {received}!'.format( attempt_code=attempt_code, existing=attempt_obj.external_id, received=external_id ) ) raise ProctoredExamSuspiciousLookup(err_msg) # do we already have a review for this attempt?!? It should not be updated! review = ProctoredExamSoftwareSecureReview.get_review_by_attempt_code(attempt_code) if review: err_msg = ( 'We already have a review submitted from SoftwareSecure regarding ' 'attempt_code {attempt_code}. We do not allow for updates!'.format( attempt_code=attempt_code ) ) raise ProctoredExamReviewAlreadyExists(err_msg) # do some limited parsing of the JSON payload review_status = payload['reviewStatus'] video_review_link = payload['videoReviewLink'] # make a new record in the review table review = ProctoredExamSoftwareSecureReview( attempt_code=attempt_code, raw_data=json.dumps(payload), review_status=review_status, video_url=video_review_link, ) review.save() # go through and populate all of the specific comments for comment in payload.get('webCamComments', []): self._save_review_comment(review, comment) for comment in payload.get('desktopComments', []): self._save_review_comment(review, comment) # we could have gottent a review for an archived attempt # this should *not* cause an update in our credit # eligibility table if not is_archived_attempt: # update our attempt status, note we have to import api.py here because # api.py imports software_secure.py, so we'll get an import circular reference from edx_proctoring.api import update_attempt_status # only 'Clean' and 'Rules Violation' could as passing status = ( ProctoredExamStudentAttemptStatus.verified if review_status in ['Clean', 'Suspicious'] else ProctoredExamStudentAttemptStatus.rejected ) update_attempt_status( attempt_obj.proctored_exam_id, attempt_obj.user_id, status )