Exemplo n.º 1
0
 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)'
Exemplo n.º 2
0
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)
Exemplo n.º 3
0
 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)'
Exemplo n.º 4
0
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)
Exemplo n.º 5
0
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)
Exemplo n.º 6
0
    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
        )
Exemplo n.º 7
0
    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
        )
Exemplo n.º 8
0
    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')
Exemplo n.º 9
0
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)
Exemplo n.º 10
0
    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')
Exemplo n.º 11
0
    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)
Exemplo n.º 12
0
    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')
Exemplo n.º 13
0
    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)
Exemplo n.º 14
0
    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')
Exemplo n.º 15
0
 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
Exemplo n.º 16
0
    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)
Exemplo n.º 17
0
    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)
Exemplo n.º 19
0
 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