def student_username_for_review(self, obj): """Return username of student who took the test""" if obj.student: return obj.student.username else: attempt = locate_attempt_by_attempt_code(obj.attempt_code) return attempt.user.username if attempt else '(None)'
class ExamReviewCallback(APIView): """ This endpoint is called by a 3rd party proctoring review service when there are results available for us to record IMPORTANT: This is an unauthenticated endpoint, so be VERY CAREFUL about extending this endpoint """ content_negotiation_class = IgnoreClientContentNegotiation def post(self, request): """ Post callback handler """ try: attempt_code = request.DATA['examMetaData']['examCode'] except KeyError, ex: log.exception(ex) return Response(data={'reason': unicode(ex)}, status=400) attempt_obj, is_archived_attempt = locate_attempt_by_attempt_code( attempt_code) course_id = attempt_obj.proctored_exam.course_id provider_name = get_provider_name_by_course_id(course_id) provider = get_backend_provider(provider_name) # call down into the underlying provider code try: provider.on_review_callback(request.DATA) except ProctoredBaseException, ex: log.exception(ex) return Response(data={'reason': unicode(ex)}, status=400)
def finish_review_workflow(sender, instance, signal, **kwargs): # pylint: disable=unused-argument """ Updates the attempt status based on the review status Also notifies support about suspicious reviews. """ review = instance attempt_obj, is_archived = locate_attempt_by_attempt_code(review.attempt_code) attempt = api.ProctoredExamStudentAttemptSerializer(attempt_obj).data # we could have gotten a review for an archived attempt # this should *not* cause an update in our credit # eligibility table if review.review_status in SoftwareSecureReviewStatus.passing_statuses: attempt_status = ProctoredExamStudentAttemptStatus.verified elif review.reviewed_by or not constants.REQUIRE_FAILURE_SECOND_REVIEWS: # reviews from the django admin have a reviewer set. They should be allowed to # reject an attempt attempt_status = ProctoredExamStudentAttemptStatus.rejected else: # if we are not allowed to store 'rejected' on this # code path, then put status into 'second_review_required' attempt_status = ProctoredExamStudentAttemptStatus.second_review_required if review.review_status in SoftwareSecureReviewStatus.notify_support_for_status: instructor_service = api.get_runtime_service('instructor') request = get_current_request() if instructor_service and request: course_id = attempt['proctored_exam']['course_id'] exam_id = attempt['proctored_exam']['id'] review_url = request.build_absolute_uri( u'{}?attempt={}'.format( reverse('edx_proctoring:instructor_dashboard_exam', args=[course_id, exam_id]), attempt['external_id'] )) instructor_service.send_support_notification( course_id=attempt['proctored_exam']['course_id'], exam_name=attempt['proctored_exam']['exam_name'], student_username=attempt['user']['username'], review_status=review.review_status, review_url=review_url, ) if not is_archived: # updating attempt status will trigger workflow # (i.e. updating credit eligibility table) # archived attempts should not trigger the workflow api.update_attempt_status( attempt['proctored_exam']['id'], attempt['user']['id'], attempt_status, raise_if_not_found=False ) # emit an event for 'review_received' data = { 'review_attempt_code': review.attempt_code, 'review_status': review.review_status, } emit_event(attempt['proctored_exam'], 'review_received', attempt=attempt, override_data=data)
def finish_review_workflow(sender, instance, signal, **kwargs): # pylint: disable=unused-argument """ Updates the attempt status based on the review status """ review = instance attempt_obj, is_archived = locate_attempt_by_attempt_code( review.attempt_code) attempt = api.ProctoredExamStudentAttemptSerializer(attempt_obj).data backend = get_backend_provider(attempt['proctored_exam']) # we could have gotten a review for an archived attempt # this should *not* cause an update in our credit # eligibility table if review.is_passing: attempt_status = ProctoredExamStudentAttemptStatus.verified elif review.review_status == SoftwareSecureReviewStatus.not_reviewed: attempt_status = ProctoredExamStudentAttemptStatus.error elif review.reviewed_by or not constants.REQUIRE_FAILURE_SECOND_REVIEWS: # reviews from the django admin have a reviewer set. They should be allowed to # reject an attempt attempt_status = ProctoredExamStudentAttemptStatus.rejected elif backend and backend.supports_onboarding and attempt[ 'is_sample_attempt']: attempt_status = ProctoredExamStudentAttemptStatus.rejected else: # if we are not allowed to store 'rejected' on this # code path, then put status into 'second_review_required' attempt_status = ProctoredExamStudentAttemptStatus.second_review_required if not is_archived: # updating attempt status will trigger workflow # (i.e. updating credit eligibility table) # archived attempts should not trigger the workflow api.update_attempt_status(attempt['proctored_exam']['id'], attempt['user']['id'], attempt_status, raise_if_not_found=False, update_attributable_to=review.reviewed_by or None) # emit an event for 'review_received' data = { 'review_attempt_code': review.attempt_code, 'review_status': review.review_status, } emit_event(attempt['proctored_exam'], 'review_received', attempt=attempt, override_data=data)
def on_review_saved(self, review, allow_rejects=False): # pylint: disable=arguments-differ """ called when a review has been save - either through API (on_review_callback) or via Django Admin panel in order to trigger any workflow associated with proctoring review results """ (attempt_obj, is_archived_attempt) = locate_attempt_by_attempt_code(review.attempt_code) if not attempt_obj: # This should not happen, but it is logged in the help # method return if is_archived_attempt: # we don't trigger workflow on reviews on archived attempts err_msg = ( 'Got on_review_save() callback for an archived attempt with ' 'attempt_code {attempt_code}. Will not trigger workflow...'.format( attempt_code=review.attempt_code ) ) log.warn(err_msg) return # only 'Clean' and 'Rules Violation' count as passing status = ( ProctoredExamStudentAttemptStatus.verified if review.review_status in self.passing_review_status else ( # if we are not allowed to store 'rejected' on this # code path, then put status into 'second_review_required' ProctoredExamStudentAttemptStatus.rejected if allow_rejects else ProctoredExamStudentAttemptStatus.second_review_required ) ) # updating attempt status will trigger workflow # (i.e. updating credit eligibility table) from edx_proctoring.api import update_attempt_status update_attempt_status( attempt_obj.proctored_exam_id, attempt_obj.user_id, status )
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 finish_review_workflow(sender, instance, signal, **kwargs): # pylint: disable=unused-argument """ Updates the attempt status based on the review status """ review = instance attempt_obj, is_archived = locate_attempt_by_attempt_code(review.attempt_code) attempt = api.ProctoredExamStudentAttemptSerializer(attempt_obj).data backend = get_backend_provider(attempt['proctored_exam']) # we could have gotten a review for an archived attempt # this should *not* cause an update in our credit # eligibility table if review.is_passing: attempt_status = ProctoredExamStudentAttemptStatus.verified elif review.review_status == SoftwareSecureReviewStatus.not_reviewed: attempt_status = ProctoredExamStudentAttemptStatus.error elif review.reviewed_by or not constants.REQUIRE_FAILURE_SECOND_REVIEWS: # reviews from the django admin have a reviewer set. They should be allowed to # reject an attempt attempt_status = ProctoredExamStudentAttemptStatus.rejected elif backend and backend.supports_onboarding and attempt['is_sample_attempt']: attempt_status = ProctoredExamStudentAttemptStatus.rejected else: # if we are not allowed to store 'rejected' on this # code path, then put status into 'second_review_required' attempt_status = ProctoredExamStudentAttemptStatus.second_review_required if not is_archived: # updating attempt status will trigger workflow # (i.e. updating credit eligibility table) # archived attempts should not trigger the workflow api.update_attempt_status( attempt['proctored_exam']['id'], attempt['user']['id'], attempt_status, raise_if_not_found=False ) # emit an event for 'review_received' data = { 'review_attempt_code': review.attempt_code, 'review_status': review.review_status, } emit_event(attempt['proctored_exam'], 'review_received', attempt=attempt, override_data=data)
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 = ( '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 post(self, request): """ Post callback handler """ data = request.DATA course_id = "" for review in data: try: attempt_code = review['examMetaData']['examCode'] except KeyError, ex: continue attempt_obj, is_archived_attempt = locate_attempt_by_attempt_code(attempt_code) if course_id != attempt_obj.proctored_exam.course_id: course_id = attempt_obj.proctored_exam.course_id provider_name = get_provider_name_by_course_id(course_id) provider = get_backend_provider(provider_name) # call down into the underlying provider code try: provider.on_review_callback(review) except ProctoredBaseException, ex: log.exception(ex)
def test_update_archived_attempt(self): """ Test calling the interface point with an attempt_code that was archived """ test_payload = self.get_review_payload() # now process the report ProctoredExamReviewCallback().make_review(self.attempt, test_payload) # now look at the attempt and make sure it did not # transition to failure on the callback, # as we'll need a manual confirmation via Django Admin pages attempt = get_exam_attempt_by_id(self.attempt_id) self.assertEqual(attempt['status'], 'verified') # now delete the attempt, which puts it into the archive table remove_exam_attempt(self.attempt_id, requesting_user=self.user) review = ProctoredExamSoftwareSecureReview.objects.get( attempt_code=self.attempt['attempt_code']) # look at the attempt again, since it moved into Archived state # then it should still remain unchanged archived_attempt = ProctoredExamStudentAttemptHistory.objects.filter( attempt_code=self.attempt['attempt_code']).latest('created') self.assertEqual(archived_attempt.status, attempt['status']) self.assertEqual(review.review_status, SoftwareSecureReviewStatus.clean) # now we'll make another review for the archived attempt. It should NOT update the status test_payload = self.get_review_payload(ReviewStatus.suspicious) self.attempt['is_archived'] = True ProctoredExamReviewCallback().make_review(self.attempt, test_payload) attempt, is_archived = locate_attempt_by_attempt_code( self.attempt['attempt_code']) self.assertTrue(is_archived) self.assertEqual(attempt.status, 'verified')
def post(self, request): """ Post callback handler """ data = request.DATA course_id = "" for review in data: try: attempt_code = review['examMetaData']['examCode'] except KeyError, ex: continue attempt_obj, is_archived_attempt = locate_attempt_by_attempt_code( attempt_code) if course_id != attempt_obj.proctored_exam.course_id: course_id = attempt_obj.proctored_exam.course_id provider_name = get_provider_name_by_course_id(course_id) provider = get_backend_provider(provider_name) # call down into the underlying provider code try: provider.on_review_callback(review) except ProctoredBaseException, ex: log.exception(ex)
def test_update_archived_attempt(self): """ Test calling the interface point with an attempt_code that was archived """ test_payload = self.get_review_payload() # now process the report ProctoredExamReviewCallback().make_review(self.attempt, test_payload) # now look at the attempt and make sure it did not # transition to failure on the callback, # as we'll need a manual confirmation via Django Admin pages attempt = get_exam_attempt_by_id(self.attempt_id) self.assertEqual(attempt['status'], 'verified') # now delete the attempt, which puts it into the archive table remove_exam_attempt(self.attempt_id, requesting_user=self.user) review = ProctoredExamSoftwareSecureReview.objects.get(attempt_code=self.attempt['attempt_code']) # look at the attempt again, since it moved into Archived state # then it should still remain unchanged archived_attempt = ProctoredExamStudentAttemptHistory.objects.filter( attempt_code=self.attempt['attempt_code'] ).latest('created') self.assertEqual(archived_attempt.status, attempt['status']) self.assertEqual(review.review_status, SoftwareSecureReviewStatus.clean) # now we'll make another review for the archived attempt. It should NOT update the status test_payload = self.get_review_payload(ReviewStatus.suspicious) self.attempt['is_archived'] = True ProctoredExamReviewCallback().make_review(self.attempt, test_payload) attempt, is_archived = locate_attempt_by_attempt_code(self.attempt['attempt_code']) self.assertTrue(is_archived) self.assertEqual(attempt.status, 'verified')
def _get_exam_from_attempt_code(self, code): """Get exam from attempt code. Note that the attempt code could be an archived one""" (attempt_obj, __) = locate_attempt_by_attempt_code(code) return attempt_obj.proctored_exam if attempt_obj else None
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)
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 some limited parsing of the JSON payload review_status = payload["reviewStatus"] # 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_raw_data": review.raw_data, "review_status": review.review_status, } serialized_attempt_obj = ProctoredExamStudentAttemptSerializer(attempt_obj) attempt = serialized_attempt_obj.data serialized_exam_object = ProctoredExamSerializer(attempt_obj.proctored_exam) exam = serialized_exam_object.data emit_event(exam, "review-received", attempt=attempt, override_data=data)
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 ProctorWebassistant's documentation named "Reviewer Data Transfer" """ log_msg = ( 'Received callback from ProctorWebassistant with review data: {payload}'.format( payload=payload ) ) log.info(log_msg) # what we consider the external_id is ProctorWebassistant's 'ssiRecordLocator' external_id = payload['examMetaData']['ssiRecordLocator'] # what we consider the attempt_code is ProctorWebassistant's 'examCode' attempt_code = payload['examMetaData']['examCode'] # get the ProctorWebassistant 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 ProctorWebassistant 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 some limited parsing of the JSON payload review_status = payload['reviewStatus'] video_review_link = payload['videoReviewLink'] # 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 ProctorWebassistant 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 ProctorWebassistant 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.video_url = video_review_link 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 try: reviewer_username = payload['examMetaData']['proctor_username'] reviewer = User.objects.get(username=reviewer_username) except (User.DoesNotExist, KeyError): reviewer = None review.reviewed_by = reviewer 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: allow_status_update_on_fail = not constants.REQUIRE_FAILURE_SECOND_REVIEWS self.on_review_saved(review, allow_status_update_on_fail=allow_status_update_on_fail)
def _get_exam_from_attempt_code(self, code): """Get exam from attempt code. Note that the attempt code could be an archived one""" attempt = locate_attempt_by_attempt_code(code) return attempt.proctored_exam if attempt else None