Example #1
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)
Example #2
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)
Example #3
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)
Example #4
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)
    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)