def post(self, request): """ Post callback handler """ provider = get_backend_provider({'backend': 'software_secure'}) # call down into the underlying provider code attempt_code = request.data.get('examMetaData', {}).get('examCode') attempt_obj, is_archived = locate_attempt_by_attempt_code(attempt_code) if not attempt_obj: # still can't find, error out err_msg = ( u'Could not locate attempt_code: {attempt_code}'.format(attempt_code=attempt_code) ) raise StudentExamAttemptDoesNotExistsException(err_msg) serialized = ProctoredExamStudentAttemptSerializer(attempt_obj).data serialized['is_archived'] = is_archived self.make_review(serialized, request.data, backend=provider) return Response('OK')
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" """ # redact the videoReviewLink from the payload if 'videoReviewLink' in payload: del payload['videoReviewLink'] 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 self.passing_review_status + self.failing_review_status 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, is_archived_attempt) = locate_attempt_by_attempt_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)) 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?!? We may not allow updates review = ProctoredExamSoftwareSecureReview.get_review_by_attempt_code( attempt_code) if review: if not constants.ALLOW_REVIEW_UPDATES: 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) # we allow updates warn_msg = ( 'We already have a review submitted from SoftwareSecure regarding ' 'attempt_code {attempt_code}. We have been configured to allow for ' 'updates and will continue...'.format( attempt_code=attempt_code)) log.warn(warn_msg) else: # this is first time we've received this attempt_code, so # make a new record in the review table review = ProctoredExamSoftwareSecureReview() review.attempt_code = attempt_code review.raw_data = json.dumps(payload) review.review_status = review_status review.student = attempt_obj.user review.exam = attempt_obj.proctored_exam # set reviewed_by to None because it was reviewed by our 3rd party # service provider, not a user in our database review.reviewed_by = None 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 gotten 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 allow_rejects = not constants.REQUIRE_FAILURE_SECOND_REVIEWS self.on_review_saved(review, allow_rejects=allow_rejects) # emit an event for 'review_received' data = { 'review_attempt_code': review.attempt_code, 'review_status': review.review_status, } attempt = ProctoredExamStudentAttemptSerializer(attempt_obj).data exam = ProctoredExamSerializer(attempt_obj.proctored_exam).data emit_event(exam, 'review_received', attempt=attempt, override_data=data) self._create_zendesk_ticket(review, exam, attempt)