Пример #1
0
    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")
Пример #2
0
    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")
Пример #3
0
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
Пример #4
0
    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)
Пример #5
0
    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)
Пример #6
0
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)
Пример #7
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"
        """

        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
            )