Пример #1
0
    def test_psi_review_callback(self, psi_review_status, review_status,
                                 credit_requirement_status):
        """
        Simulates callbacks from SoftwareSecure with various statuses
        """
        test_payload = json.loads(
            create_test_review_payload(
                attempt_code=self.attempt['attempt_code'],
                external_id=self.attempt['external_id'],
                review_status=psi_review_status))

        self.attempt['proctored_exam']['backend'] = 'software_secure'
        if review_status is None:
            with self.assertRaises(ProctoredExamBadReviewStatus):
                ProctoredExamReviewCallback().make_review(
                    self.attempt, test_payload)
        else:
            ProctoredExamReviewCallback().make_review(self.attempt,
                                                      test_payload)
            # make sure that what we have in the Database matches what we expect
            review = ProctoredExamSoftwareSecureReview.get_review_by_attempt_code(
                self.attempt['attempt_code'])

            self.assertIsNotNone(review)
            self.assertEqual(review.review_status, review_status)
            self.assertFalse(review.video_url)

            self.assertIsNotNone(review.raw_data)
            self.assertIsNone(review.reviewed_by)

            # now check the comments that were stored
            comments = ProctoredExamSoftwareSecureComment.objects.filter(
                review_id=review.id)

            self.assertEqual(len(comments), 6)

            # check that we got credit requirement set appropriately

            credit_service = get_runtime_service('credit')
            credit_status = credit_service.get_credit_state(
                self.user.id, 'foo/bar/baz')

            self.assertEqual(
                credit_status['credit_requirement_status'][0]['status'],
                credit_requirement_status)

            instructor_service = get_runtime_service('instructor')
            notifications = instructor_service.notifications
            if psi_review_status == SoftwareSecureReviewStatus.suspicious:
                # check to see whether the zendesk ticket was created
                self.assertEqual(len(notifications), 1)
                exam = self.attempt['proctored_exam']
                review_url = 'http://testserver/edx_proctoring/v1/instructor/foo/bar/baz/1?attempt=testexternalid'
                self.assertEqual(notifications,
                                 [(exam['course_id'], exam['exam_name'],
                                   self.attempt['user']['username'],
                                   review.review_status, review_url)])
            else:
                self.assertEqual(len(notifications), 0)
Пример #2
0
    def test_psi_review_callback(self, psi_review_status, review_status, credit_requirement_status):
        """
        Simulates callbacks from SoftwareSecure with various statuses
        """
        test_payload = json.loads(create_test_review_payload(
            attempt_code=self.attempt['attempt_code'],
            external_id=self.attempt['external_id'],
            review_status=psi_review_status
        ))

        self.attempt['proctored_exam']['backend'] = 'software_secure'
        if review_status is None:
            with self.assertRaises(ProctoredExamBadReviewStatus):
                ProctoredExamReviewCallback().make_review(self.attempt, test_payload)
        else:
            ProctoredExamReviewCallback().make_review(self.attempt, test_payload)
            # make sure that what we have in the Database matches what we expect
            review = ProctoredExamSoftwareSecureReview.get_review_by_attempt_code(self.attempt['attempt_code'])

            self.assertIsNotNone(review)
            self.assertEqual(review.review_status, review_status)
            self.assertFalse(review.video_url)

            self.assertIsNotNone(review.raw_data)
            self.assertIsNone(review.reviewed_by)

            # now check the comments that were stored
            comments = ProctoredExamSoftwareSecureComment.objects.filter(review_id=review.id)

            self.assertEqual(len(comments), 6)

            # check that we got credit requirement set appropriately

            credit_service = get_runtime_service('credit')
            credit_status = credit_service.get_credit_state(self.user.id, 'foo/bar/baz')

            self.assertEqual(
                credit_status['credit_requirement_status'][0]['status'],
                credit_requirement_status
            )

            instructor_service = get_runtime_service('instructor')
            notifications = instructor_service.notifications
            if psi_review_status == SoftwareSecureReviewStatus.suspicious:
                # check to see whether the zendesk ticket was created
                self.assertEqual(len(notifications), 1)
                exam = self.attempt['proctored_exam']
                review_url = 'http://testserver/edx_proctoring/v1/instructor/foo/bar/baz/1?attempt=testexternalid'
                self.assertEqual(notifications,
                                 [(exam['course_id'],
                                   exam['exam_name'],
                                   self.attempt['user']['username'],
                                   review.review_status,
                                   review_url)])
            else:
                self.assertEqual(len(notifications), 0)
Пример #3
0
 def wrapped(request, *args, **kwargs):  # pylint: disable=missing-docstring
     instructor_service = get_runtime_service('instructor')
     course_id = kwargs['course_id'] if 'course_id' in kwargs else None
     exam_id = request.data.get('exam_id', None)
     attempt_id = kwargs['attempt_id'] if 'attempt_id' in kwargs else None
     if request.user.is_staff:
         return func(request, *args, **kwargs)
     else:
         if course_id is None:
             if exam_id is not None:
                 exam = ProctoredExam.get_exam_by_id(exam_id)
                 course_id = exam.course_id
             elif attempt_id is not None:
                 exam_attempt = ProctoredExamStudentAttempt.objects.get_exam_attempt_by_id(attempt_id)
                 course_id = exam_attempt.proctored_exam.course_id
             else:
                 response_message = _("could not determine the course_id")
                 return Response(
                     status=status.HTTP_403_FORBIDDEN,
                     data={"detail": response_message}
                 )
         if instructor_service.is_course_staff(request.user, course_id):
             return func(request, *args, **kwargs)
         else:
             return Response(
                 status=status.HTTP_403_FORBIDDEN,
                 data={"detail": _("Must be a Staff User to Perform this request.")}
             )
Пример #4
0
 def wrapped(request, *args, **kwargs):  # pylint: disable=missing-docstring
     instructor_service = get_runtime_service('instructor')
     course_id = kwargs['course_id'] if 'course_id' in kwargs else None
     exam_id = request.data.get('exam_id', None)
     attempt_id = kwargs['attempt_id'] if 'attempt_id' in kwargs else None
     if request.user.is_staff:
         return func(request, *args, **kwargs)
     else:
         if course_id is None:
             if exam_id is not None:
                 exam = ProctoredExam.get_exam_by_id(exam_id)
                 course_id = exam.course_id
             elif attempt_id is not None:
                 exam_attempt = ProctoredExamStudentAttempt.objects.get_exam_attempt_by_id(
                     attempt_id)
                 course_id = exam_attempt.proctored_exam.course_id
             else:
                 response_message = _("could not determine the course_id")
                 return Response(status=status.HTTP_403_FORBIDDEN,
                                 data={"detail": response_message})
         if instructor_service.is_course_staff(request.user, course_id):
             return func(request, *args, **kwargs)
         else:
             return Response(
                 status=status.HTTP_403_FORBIDDEN,
                 data={
                     "detail":
                     _("Must be a Staff User to Perform this request.")
                 })
Пример #5
0
    def test_send_email(self, status, expected_subject, expected_message_string):
        """
        Assert that email is sent on the following statuses of proctoring attempt.
        """

        exam_attempt = self._create_started_exam_attempt()
        credit_state = get_runtime_service('credit').get_credit_state(self.user_id, self.course_id)
        update_attempt_status(
            exam_attempt.proctored_exam_id,
            self.user.id,
            status
        )
        self.assertEqual(len(mail.outbox), 1)

        # Verify the subject
        actual_subject = self._normalize_whitespace(mail.outbox[0].subject)
        self.assertIn(expected_subject, actual_subject)
        self.assertIn(self.exam_name, actual_subject)

        # Verify the body
        actual_body = self._normalize_whitespace(mail.outbox[0].body)
        self.assertIn('Hello tester,', actual_body)
        self.assertIn('Your proctored exam "Test Exam"', actual_body)
        self.assertIn(credit_state['course_name'], actual_body)
        self.assertIn(expected_message_string, actual_body)
Пример #6
0
def remove_exam_attempt(attempt_id):
    """
    Removes an exam attempt given the attempt id.
    """

    existing_attempt = ProctoredExamStudentAttempt.objects.get_exam_attempt_by_id(
        attempt_id)
    if not existing_attempt:
        err_msg = ('Cannot remove attempt for attempt_id = {attempt_id} '
                   'because it does not exist!').format(attempt_id=attempt_id)

        raise StudentExamAttemptDoesNotExistsException(err_msg)

    username = existing_attempt.user.username
    user_id = existing_attempt.user.id
    course_id = existing_attempt.proctored_exam.course_id
    content_id = existing_attempt.proctored_exam.content_id
    to_status = existing_attempt.status

    log_msg = ('{attempt_code} - {username} ({email}) '
               'Removing exam attempt {attempt_id}'.format(
                   attempt_id=attempt_id,
                   attempt_code=existing_attempt.attempt_code,
                   username=username,
                   email=existing_attempt.user.email))
    log.info(log_msg)

    existing_attempt.delete_exam_attempt()
    instructor_service = get_runtime_service('instructor')

    if instructor_service:
        instructor_service.delete_student_attempt(username, course_id,
                                                  content_id)
Пример #7
0
    def test_send_email_unicode(self):
        """
        Assert that email can be sent with a unicode course name.
        """

        course_name = u'अआईउऊऋऌ अआईउऊऋऌ'
        set_runtime_service('credit', MockCreditService(course_name=course_name))

        exam_attempt = self._create_started_exam_attempt()
        credit_state = get_runtime_service('credit').get_credit_state(self.user_id, self.course_id)
        update_attempt_status(
            exam_attempt.proctored_exam_id,
            self.user.id,
            ProctoredExamStudentAttemptStatus.submitted
        )
        self.assertEqual(len(mail.outbox), 1)

        # Verify the subject
        actual_subject = self._normalize_whitespace(mail.outbox[0].subject)
        self.assertIn('Proctoring Review In Progress', actual_subject)
        self.assertIn(course_name, actual_subject)

        # Verify the body
        actual_body = self._normalize_whitespace(mail.outbox[0].body)
        self.assertIn('was submitted successfully', actual_body)
        self.assertIn(credit_state['course_name'], actual_body)
Пример #8
0
    def test_send_email(self, status, expected_subject, expected_message_string):
        """
        Assert that email is sent on the following statuses of proctoring attempt.
        """

        exam_attempt = self._create_started_exam_attempt()
        credit_state = get_runtime_service('credit').get_credit_state(self.user_id, self.course_id)
        update_attempt_status(
            exam_attempt.proctored_exam_id,
            self.user.id,
            status
        )
        self.assertEqual(len(mail.outbox), 1)

        # Verify the subject
        actual_subject = self._normalize_whitespace(mail.outbox[0].subject)
        self.assertIn(expected_subject, actual_subject)
        self.assertIn(self.exam_name, actual_subject)

        # Verify the body
        actual_body = self._normalize_whitespace(mail.outbox[0].body)
        self.assertIn('Hi tester,', actual_body)
        self.assertIn('Your proctored exam "Test Exam"', actual_body)
        self.assertIn(credit_state['course_name'], actual_body)
        self.assertIn(expected_message_string, actual_body)
Пример #9
0
    def test_send_email_unicode(self):
        """
        Assert that email can be sent with a unicode course name.
        """

        course_name = u'अआईउऊऋऌ अआईउऊऋऌ'
        set_runtime_service('credit',
                            MockCreditService(course_name=course_name))

        exam_attempt = self._create_started_exam_attempt()
        credit_state = get_runtime_service('credit').get_credit_state(
            self.user_id, self.course_id)
        update_attempt_status(exam_attempt.proctored_exam_id, self.user.id,
                              ProctoredExamStudentAttemptStatus.submitted)
        self.assertEqual(len(mail.outbox), 1)

        # Verify the subject
        actual_subject = self._normalize_whitespace(mail.outbox[0].subject)
        self.assertIn('Proctoring Review In Progress', actual_subject)
        self.assertIn(course_name, actual_subject)

        # Verify the body
        actual_body = self._normalize_whitespace(mail.outbox[0].body)
        self.assertIn('was submitted successfully', actual_body)
        self.assertIn(credit_state['course_name'], actual_body)
Пример #10
0
def remove_exam_attempt(attempt_id):
    """
    Removes an exam attempt given the attempt id.
    """

    log_msg = (
        'Removing exam attempt {attempt_id}'.format(attempt_id=attempt_id)
    )
    log.info(log_msg)

    existing_attempt = ProctoredExamStudentAttempt.objects.get_exam_attempt_by_id(attempt_id)
    if not existing_attempt:
        err_msg = (
            'Cannot remove attempt for attempt_id = {attempt_id} '
            'because it does not exist!'
        ).format(attempt_id=attempt_id)

        raise StudentExamAttemptDoesNotExistsException(err_msg)

    username = existing_attempt.user.username
    course_id = existing_attempt.proctored_exam.course_id
    content_id = existing_attempt.proctored_exam.content_id
    existing_attempt.delete_exam_attempt()
    instructor_service = get_runtime_service('instructor')
    if instructor_service:
        instructor_service.delete_student_attempt(username, course_id, content_id)
    def test_review_callback(self, review_status, credit_requirement_status):
        """
        Simulates callbacks from SoftwareSecure with various statuses
        """

        provider = get_backend_provider()

        exam_id = create_exam(
            course_id='foo/bar/baz',
            content_id='content',
            exam_name='Sample Exam',
            time_limit_mins=10,
            is_proctored=True
        )

        # be sure to use the mocked out SoftwareSecure handlers
        with HTTMock(mock_response_content):
            attempt_id = create_exam_attempt(
                exam_id,
                self.user.id,
                taking_as_proctored=True
            )

        attempt = get_exam_attempt_by_id(attempt_id)
        self.assertIsNotNone(attempt['external_id'])

        test_payload = Template(TEST_REVIEW_PAYLOAD).substitute(
            attempt_code=attempt['attempt_code'],
            external_id=attempt['external_id']
        )
        test_payload = test_payload.replace('Clean', review_status)

        provider.on_review_callback(json.loads(test_payload))

        # make sure that what we have in the Database matches what we expect
        review = ProctoredExamSoftwareSecureReview.get_review_by_attempt_code(attempt['attempt_code'])

        self.assertIsNotNone(review)
        self.assertEqual(review.review_status, review_status)
        self.assertEqual(
            review.video_url,
            'http://www.remoteproctor.com/AdminSite/Account/Reviewer/DirectLink-Generic.aspx?ID=foo'
        )
        self.assertIsNotNone(review.raw_data)

        # now check the comments that were stored
        comments = ProctoredExamSoftwareSecureComment.objects.filter(review_id=review.id)

        self.assertEqual(len(comments), 6)

        # check that we got credit requirement set appropriately

        credit_service = get_runtime_service('credit')
        credit_status = credit_service.get_credit_state(self.user.id, 'foo/bar/baz')

        self.assertEqual(
            credit_status['credit_requirement_status'][0]['status'],
            credit_requirement_status
        )
Пример #12
0
def is_user_course_or_global_staff(user, course_id):
    """
    Return whether a user is course staff for a given course, described by the course_id,
    or is global staff.
    """
    instructor_service = get_runtime_service('instructor')

    return user.is_staff or instructor_service.is_course_staff(user, course_id)
Пример #13
0
def is_user_course_or_global_staff(user, course_id):
    """
        Return whether a user is course staff for a given course, described by the course_id,
        or is global staff.
    """
    instructor_service = get_runtime_service('instructor')

    return user.is_staff or instructor_service.is_course_staff(user, course_id)
Пример #14
0
    def test_review_callback(self, review_status, credit_requirement_status):
        """
        Simulates callbacks from SoftwareSecure with various statuses
        """

        provider = get_backend_provider()

        exam_id = create_exam(course_id='foo/bar/baz',
                              content_id='content',
                              exam_name='Sample Exam',
                              time_limit_mins=10,
                              is_proctored=True)

        # be sure to use the mocked out SoftwareSecure handlers
        with HTTMock(mock_response_content):
            attempt_id = create_exam_attempt(exam_id,
                                             self.user.id,
                                             taking_as_proctored=True)

        attempt = get_exam_attempt_by_id(attempt_id)
        self.assertIsNotNone(attempt['external_id'])

        test_payload = Template(TEST_REVIEW_PAYLOAD).substitute(
            attempt_code=attempt['attempt_code'],
            external_id=attempt['external_id'])
        test_payload = test_payload.replace('Clean', review_status)

        provider.on_review_callback(json.loads(test_payload))

        # make sure that what we have in the Database matches what we expect
        review = ProctoredExamSoftwareSecureReview.get_review_by_attempt_code(
            attempt['attempt_code'])

        self.assertIsNotNone(review)
        self.assertEqual(review.review_status, review_status)
        self.assertEqual(
            review.video_url,
            'http://www.remoteproctor.com/AdminSite/Account/Reviewer/DirectLink-Generic.aspx?ID=foo'
        )
        self.assertIsNotNone(review.raw_data)
        self.assertIsNone(review.reviewed_by)

        # now check the comments that were stored
        comments = ProctoredExamSoftwareSecureComment.objects.filter(
            review_id=review.id)

        self.assertEqual(len(comments), 6)

        # check that we got credit requirement set appropriately

        credit_service = get_runtime_service('credit')
        credit_status = credit_service.get_credit_state(
            self.user.id, 'foo/bar/baz')

        self.assertEqual(
            credit_status['credit_requirement_status'][0]['status'],
            credit_requirement_status)
Пример #15
0
def remove_exam_attempt(attempt_id):
    """
    Removes an exam attempt given the attempt id.
    """

    log_msg = (
        'Removing exam attempt {attempt_id}'.format(attempt_id=attempt_id)
    )
    log.info(log_msg)

    existing_attempt = ProctoredExamStudentAttempt.objects.get_exam_attempt_by_id(attempt_id)
    if not existing_attempt:
        err_msg = (
            'Cannot remove attempt for attempt_id = {attempt_id} '
            'because it does not exist!'
        ).format(attempt_id=attempt_id)

        raise StudentExamAttemptDoesNotExistsException(err_msg)

    username = existing_attempt.user.username
    user_id = existing_attempt.user.id
    course_id = existing_attempt.proctored_exam.course_id
    content_id = existing_attempt.proctored_exam.content_id
    to_status = existing_attempt.status

    existing_attempt.delete_exam_attempt()
    instructor_service = get_runtime_service('instructor')

    if instructor_service:
        instructor_service.delete_student_attempt(username, course_id, content_id)

    # see if the status transition this changes credit requirement status
    if ProctoredExamStudentAttemptStatus.needs_credit_status_update(to_status):
        # trigger credit workflow, as needed
        credit_service = get_runtime_service('credit')
        credit_service.remove_credit_requirement_status(
            user_id=user_id,
            course_key_or_id=course_id,
            req_namespace=u'proctored_exam',
            req_name=content_id
        )
Пример #16
0
 def _create_zendesk_ticket(self, review, serialized_exam_object, serialized_attempt_obj):
     """
     Creates a Zendesk ticket for reviews with status listed in self.notify_support_for_status
     """
     if review.review_status in self.notify_support_for_status:
         instructor_service = get_runtime_service('instructor')
         if instructor_service:
             instructor_service.send_support_notification(
                 course_id=serialized_exam_object["course_id"],
                 exam_name=serialized_exam_object["exam_name"],
                 student_username=serialized_attempt_obj["user"]["username"],
                 review_status=review.review_status
             )
    def test_review_callback(self, review_status, credit_requirement_status):
        """
        Simulates callbacks from SoftwareSecure with various statuses
        """

        provider = get_backend_provider()

        exam_id = create_exam(
            course_id="foo/bar/baz",
            content_id="content",
            exam_name="Sample Exam",
            time_limit_mins=10,
            is_proctored=True,
        )

        # be sure to use the mocked out SoftwareSecure handlers
        with HTTMock(mock_response_content):
            attempt_id = create_exam_attempt(exam_id, self.user.id, taking_as_proctored=True)

        attempt = get_exam_attempt_by_id(attempt_id)
        self.assertIsNotNone(attempt["external_id"])

        test_payload = Template(TEST_REVIEW_PAYLOAD).substitute(
            attempt_code=attempt["attempt_code"], external_id=attempt["external_id"]
        )
        test_payload = test_payload.replace("Clean", review_status)

        provider.on_review_callback(json.loads(test_payload))

        # make sure that what we have in the Database matches what we expect
        review = ProctoredExamSoftwareSecureReview.get_review_by_attempt_code(attempt["attempt_code"])

        self.assertIsNotNone(review)
        self.assertEqual(review.review_status, review_status)
        self.assertFalse(review.video_url)

        self.assertIsNotNone(review.raw_data)
        self.assertIsNone(review.reviewed_by)

        # now check the comments that were stored
        comments = ProctoredExamSoftwareSecureComment.objects.filter(review_id=review.id)

        self.assertEqual(len(comments), 6)

        # check that we got credit requirement set appropriately

        credit_service = get_runtime_service("credit")
        credit_status = credit_service.get_credit_state(self.user.id, "foo/bar/baz")

        self.assertEqual(credit_status["credit_requirement_status"][0]["status"], credit_requirement_status)
Пример #18
0
    def test_proctoring_escalation_email_exceptions(self, error):
        """
        Test that when an error is raised when trying to retrieve a proctoring escalation
        email, it sets `proctoring_escalation_email` to None and link to support is used instead
        """
        instructor_service = get_runtime_service('instructor')
        instructor_service.mock_proctoring_escalation_email_error(error)

        exam_attempt = self._create_started_exam_attempt()
        update_attempt_status(exam_attempt.id,
                              ProctoredExamStudentAttemptStatus.verified)

        actual_body = self._normalize_whitespace(mail.outbox[0].body)
        self.assertIn('support/contact_us', actual_body)
Пример #19
0
    def test_escalation_email_included(self, status):
        """
        Test that verified and rejected emails include a proctoring escalation email if given.
        """
        instructor_service = get_runtime_service('instructor')
        mock_escalation_email = '*****@*****.**'
        instructor_service.mock_proctoring_escalation_email(
            mock_escalation_email)

        exam_attempt = self._create_started_exam_attempt()
        update_attempt_status(exam_attempt.id, status)

        actual_body = self._normalize_whitespace(mail.outbox[0].body)
        self.assertIn(mock_escalation_email, actual_body)
        self.assertNotIn('support/contact_us', actual_body)
Пример #20
0
    def test_send_email(self, status):
        """
        Assert that email is sent on the following statuses of proctoring attempt.
        """

        exam_attempt = self._create_started_exam_attempt()
        credit_state = get_runtime_service('credit').get_credit_state(
            self.user_id, self.course_id)
        update_attempt_status(exam_attempt.proctored_exam_id, self.user.id,
                              status)
        self.assertEquals(len(mail.outbox), 1)
        self.assertIn(self.proctored_exam_email_subject,
                      mail.outbox[0].subject)
        self.assertIn(self.proctored_exam_email_body, mail.outbox[0].body)
        self.assertIn(
            ProctoredExamStudentAttemptStatus.get_status_alias(status),
            mail.outbox[0].body)
        self.assertIn(credit_state['course_name'], mail.outbox[0].body)
Пример #21
0
    def test_send_email_unicode(self):
        """
        Assert that email can be sent with a unicode course name.
        """

        course_name = u'अआईउऊऋऌ अआईउऊऋऌ'
        set_runtime_service('credit',
                            MockCreditService(course_name=course_name))

        exam_attempt = self._create_started_exam_attempt()
        credit_state = get_runtime_service('credit').get_credit_state(
            self.user_id, self.course_id)
        update_attempt_status(exam_attempt.proctored_exam_id, self.user.id,
                              ProctoredExamStudentAttemptStatus.submitted)
        self.assertEquals(len(mail.outbox), 1)
        self.assertIn(self.proctored_exam_email_subject,
                      mail.outbox[0].subject)
        self.assertIn(course_name, mail.outbox[0].subject)
        self.assertIn(self.proctored_exam_email_body, mail.outbox[0].body)
        self.assertIn(
            ProctoredExamStudentAttemptStatus.get_status_alias(
                ProctoredExamStudentAttemptStatus.submitted),
            mail.outbox[0].body)
        self.assertIn(credit_state['course_name'], mail.outbox[0].body)
Пример #22
0
    }
    """

    try:
        exam = get_exam_by_content_id(course_id, content_id)
    except ProctoredExamNotFoundException, ex:
        # this really shouldn't happen, but log it at least
        log.exception(ex)
        return None

    # check if the exam is not proctored
    if not exam['is_proctored']:
        return TIMED_EXAM_STATUS_SUMMARY_MAP['_default']

    # let's check credit eligibility
    credit_service = get_runtime_service('credit')
    # practice exams always has an attempt status regardless of
    # eligibility
    if credit_service and not exam['is_practice_exam']:
        credit_state = credit_service.get_credit_state(user_id, unicode(course_id))
        if not _check_credit_eligibility(credit_state):
            return None

    attempt = get_exam_attempt(exam['id'], user_id)
    status = attempt['status'] if attempt else ProctoredExamStudentAttemptStatus.eligible

    status_map = STATUS_SUMMARY_MAP if not exam['is_practice_exam'] else PRACTICE_STATUS_SUMMARY_MAP

    summary = None
    if status in status_map:
        summary = status_map[status]
Пример #23
0
    def make_review(self, attempt, data, backend=None):
        """
        Save the review and review comments
        """
        attempt_code = attempt['attempt_code']
        if not backend:
            backend = get_backend_provider(attempt['proctored_exam'])

        # this method should convert the payload into a normalized format
        backend_review = backend.on_review_callback(attempt, data)

        # 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 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 our proctoring provider regarding '
                'attempt_code {attempt_code}. We have been configured to allow for '
                'updates and will continue...'.format(
                    attempt_code=attempt_code
                )
            )
            LOG.warning(warn_msg)
        else:
            # this is first time we've received this attempt_code, so
            # make a new record in the review table
            review = ProctoredExamSoftwareSecureReview()

        # first, validate that the backend review status is valid
        ReviewStatus.validate(backend_review['status'])
        # For now, we'll convert the standard review status to the old
        # software secure review status.
        # In the future, the old data should be standardized.
        review.review_status = SoftwareSecureReviewStatus.from_standard_status.get(backend_review['status'])

        review.attempt_code = attempt_code
        review.raw_data = json.dumps(data)
        review.student_id = attempt['user']['id']
        review.exam_id = attempt['proctored_exam']['id']

        try:
            review.reviewed_by = get_user_model().objects.get(email=data['reviewed_by'])
        except (ObjectDoesNotExist, KeyError):
            review.reviewed_by = None

        # If the reviewing user is a user in the system (user may be None for automated reviews) and does
        # not have permission to submit a review, log a warning.
        course_id = attempt['proctored_exam']['course_id']
        if review.reviewed_by is not None and not is_user_course_or_global_staff(review.reviewed_by, course_id):
            LOG.warning(
                'User %(user)s does not have the required permissions to submit '
                'a review for attempt_code %(attempt_code)s.',
                {'user': review.reviewed_by, 'attempt_code': attempt_code}
            )

        review.save()

        # go through and populate all of the specific comments
        for comment in backend_review.get('comments', []):
            comment = ProctoredExamSoftwareSecureComment(
                review=review,
                start_time=comment.get('start', 0),
                stop_time=comment.get('stop', 0),
                duration=comment.get('duration', 0),
                comment=comment['comment'],
                status=comment['status']
            )
            comment.save()

        if review.should_notify:
            instructor_service = 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,
                )
Пример #24
0
def create_exam_attempt(exam_id, user_id, taking_as_proctored=False):
    """
    Creates an exam attempt for user_id against exam_id. There should only be
    one exam_attempt per user per exam. Multiple attempts by user will be archived
    in a separate table
    """
    # for now the student is allowed the exam default

    exam = get_exam_by_id(exam_id)
    existing_attempt = ProctoredExamStudentAttempt.objects.get_exam_attempt(exam_id, user_id)

    if existing_attempt:
        log_msg = (
            'Creating exam attempt for exam_id {exam_id} for '
            'user_id {user_id} with taking as proctored = {taking_as_proctored}'.format(
                exam_id=exam_id, user_id=user_id, taking_as_proctored=taking_as_proctored
            )
        )
        log.info(log_msg)

        if existing_attempt.is_sample_attempt:
            # Archive the existing attempt by deleting it.
            existing_attempt.delete_exam_attempt()
        else:
            err_msg = (
                'Cannot create new exam attempt for exam_id = {exam_id} and '
                'user_id = {user_id} because it already exists!'
            ).format(exam_id=exam_id, user_id=user_id)

            raise StudentExamAttemptAlreadyExistsException(err_msg)

    allowed_time_limit_mins = exam['time_limit_mins']

    # add in the allowed additional time
    allowance_extra_mins = ProctoredExamStudentAllowance.get_additional_time_granted(exam_id, user_id)
    if allowance_extra_mins:
        allowed_time_limit_mins += allowance_extra_mins

    attempt_code = unicode(uuid.uuid4()).upper()

    external_id = None
    review_policy = ProctoredExamReviewPolicy.get_review_policy_for_exam(exam_id)
    review_policy_exception = ProctoredExamStudentAllowance.get_review_policy_exception(exam_id, user_id)

    if taking_as_proctored:
        content_id = exam['content_id'].split('@')[-1]  # get hash
        scheme = 'https' if getattr(settings, 'HTTPS', 'on') == 'on' else 'http'
        callback_url = '{scheme}://{hostname}{path}'.format(
            scheme=scheme,
            hostname=settings.SITE_NAME,
            path=reverse(
                'jump_to_id', 
                kwargs={'course_id': exam['course_id'], 'module_id': content_id}
            )
        )

        # get the name of the user, if the service is available
        full_name = None

        credit_service = get_runtime_service('credit')
        user = User.objects.get(pk=user_id)


        context = {
            'time_limit_mins': allowed_time_limit_mins,
            'attempt_code': attempt_code,
            'is_sample_attempt': exam['is_practice_exam'],
            'callback_url': callback_url,
            'user_id': user_id,
            'full_name': " ".join((user.first_name,user.last_name)),
            'username': user.username,
            'email': user.email
        }

        # see if there is an exam review policy for this exam
        # if so, then pass it into the provider
        if review_policy:
            context.update({
                'review_policy': review_policy.review_policy
            })

        # see if there is a review policy exception for this *user*
        # exceptions are granted on a individual basis as an
        # allowance
        if review_policy_exception:
            context.update({
                'review_policy_exception': review_policy_exception
            })

        # now call into the backend provider to register exam attempt
        provider_name = get_provider_name_by_course_id(exam['course_id'])
        external_id = get_backend_provider(provider_name).register_exam_attempt(
            exam,
            context=context,
        )

    attempt = ProctoredExamStudentAttempt.create_exam_attempt(
        exam_id,
        user_id,
        '',  # student name is TBD
        allowed_time_limit_mins,
        attempt_code,
        taking_as_proctored,
        exam['is_practice_exam'],
        external_id,
        review_policy_id=review_policy.id if review_policy else None,
    )

    log_msg = (
        '{attempt_code} - {username} ({email}) '
        'Created exam attempt ({attempt_id}) for exam_id {exam_id} for '
        'user_id {user_id} with taking as proctored = {taking_as_proctored} '
        'with allowed time limit minutes of {allowed_time_limit_mins}. '
        'external_id of {external_id}'.format(
            attempt_id=attempt.id, exam_id=exam_id, user_id=user_id,
            taking_as_proctored=taking_as_proctored,
            allowed_time_limit_mins=allowed_time_limit_mins,
            attempt_code=attempt_code,
            external_id=external_id,
            username=attempt.user.username,
            email=attempt.user.email
        )
    )
    log.info(log_msg)

    return attempt.id
Пример #25
0
    def make_review(self, attempt, data, backend=None):
        """
        Save the review and review comments
        """
        attempt_code = attempt['attempt_code']
        if not backend:
            backend = get_backend_provider(attempt['proctored_exam'])

        # this method should convert the payload into a normalized format
        backend_review = backend.on_review_callback(attempt, data)

        # 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 = (
                    u'We already have a review submitted regarding '
                    u'attempt_code {attempt_code}. We do not allow for updates!'.format(
                        attempt_code=attempt_code
                    )
                )
                raise ProctoredExamReviewAlreadyExists(err_msg)

            # we allow updates
            warn_msg = (
                u'We already have a review submitted from our proctoring provider regarding '
                u'attempt_code {attempt_code}. We have been configured to allow for '
                u'updates and will continue...'.format(
                    attempt_code=attempt_code
                )
            )
            LOG.warning(warn_msg)
        else:
            # this is first time we've received this attempt_code, so
            # make a new record in the review table
            review = ProctoredExamSoftwareSecureReview()

        # first, validate that the backend review status is valid
        ReviewStatus.validate(backend_review['status'])
        # For now, we'll convert the standard review status to the old
        # software secure review status.
        # In the future, the old data should be standardized.
        review.review_status = SoftwareSecureReviewStatus.from_standard_status.get(backend_review['status'])

        review.attempt_code = attempt_code
        review.raw_data = json.dumps(data)
        review.student_id = attempt['user']['id']
        review.exam_id = attempt['proctored_exam']['id']

        try:
            review.reviewed_by = get_user_model().objects.get(email=data['reviewed_by'])
        except (ObjectDoesNotExist, KeyError):
            review.reviewed_by = None

        # If the reviewing user is a user in the system (user may be None for automated reviews) and does
        # not have permission to submit a review, log a warning.
        course_id = attempt['proctored_exam']['course_id']
        if review.reviewed_by is not None and not is_user_course_or_global_staff(review.reviewed_by, course_id):
            LOG.warning(
                u'User %(user)s does not have the required permissions to submit '
                u'a review for attempt_code %(attempt_code)s.',
                {'user': review.reviewed_by, 'attempt_code': attempt_code}
            )

        review.save()

        # go through and populate all of the specific comments
        for comment in backend_review.get('comments', []):
            comment = ProctoredExamSoftwareSecureComment(
                review=review,
                start_time=comment.get('start', 0),
                stop_time=comment.get('stop', 0),
                duration=comment.get('duration', 0),
                comment=comment['comment'],
                status=comment['status']
            )
            comment.save()

        if review.should_notify:
            instructor_service = 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,
                )
Пример #26
0
    }
    """

    try:
        exam = get_exam_by_content_id(course_id, content_id)
    except ProctoredExamNotFoundException, ex:
        # this really shouldn't happen, but log it at least
        log.exception(ex)
        return None

    # check if the exam is not proctored
    if not exam['is_proctored']:
        return TIMED_EXAM_STATUS_SUMMARY_MAP['_default']

    # let's check credit eligibility
    credit_service = get_runtime_service('credit')
    # practice exams always has an attempt status regardless of
    # eligibility
    if credit_service and not exam['is_practice_exam']:
        credit_state = credit_service.get_credit_state(user_id,
                                                       unicode(course_id))
        if not _check_credit_eligibility(credit_state):
            return None

    attempt = get_exam_attempt(exam['id'], user_id)
    status = attempt[
        'status'] if attempt else ProctoredExamStudentAttemptStatus.eligible

    status_map = STATUS_SUMMARY_MAP if not exam[
        'is_practice_exam'] else PRACTICE_STATUS_SUMMARY_MAP
Пример #27
0
def create_exam_attempt(exam_id, user_id, taking_as_proctored=False):
    """
    Creates an exam attempt for user_id against exam_id. There should only be
    one exam_attempt per user per exam. Multiple attempts by user will be archived
    in a separate table
    """
    # for now the student is allowed the exam default

    exam = get_exam_by_id(exam_id)
    existing_attempt = ProctoredExamStudentAttempt.objects.get_exam_attempt(
        exam_id, user_id)

    if existing_attempt:
        log_msg = (
            'Creating exam attempt for exam_id {exam_id} for '
            'user_id {user_id} with taking as proctored = {taking_as_proctored}'
            .format(exam_id=exam_id,
                    user_id=user_id,
                    taking_as_proctored=taking_as_proctored))
        log.info(log_msg)

        if existing_attempt.is_sample_attempt:
            # Archive the existing attempt by deleting it.
            existing_attempt.delete_exam_attempt()
        else:
            err_msg = (
                'Cannot create new exam attempt for exam_id = {exam_id} and '
                'user_id = {user_id} because it already exists!').format(
                    exam_id=exam_id, user_id=user_id)

            raise StudentExamAttemptAlreadyExistsException(err_msg)

    allowed_time_limit_mins = exam['time_limit_mins']

    # add in the allowed additional time
    allowance_extra_mins = ProctoredExamStudentAllowance.get_additional_time_granted(
        exam_id, user_id)
    if allowance_extra_mins:
        allowed_time_limit_mins += allowance_extra_mins

    attempt_code = unicode(uuid.uuid4()).upper()

    external_id = None
    review_policy = ProctoredExamReviewPolicy.get_review_policy_for_exam(
        exam_id)
    review_policy_exception = ProctoredExamStudentAllowance.get_review_policy_exception(
        exam_id, user_id)

    if taking_as_proctored:
        content_id = exam['content_id'].split('@')[-1]  # get hash
        scheme = 'https' if getattr(settings, 'HTTPS',
                                    'on') == 'on' else 'http'
        callback_url = '{scheme}://{hostname}{path}'.format(
            scheme=scheme,
            hostname=settings.SITE_NAME,
            path=reverse('jump_to_id',
                         kwargs={
                             'course_id': exam['course_id'],
                             'module_id': content_id
                         }))

        # get the name of the user, if the service is available
        full_name = None

        credit_service = get_runtime_service('credit')
        user = User.objects.get(pk=user_id)

        context = {
            'time_limit_mins': allowed_time_limit_mins,
            'attempt_code': attempt_code,
            'is_sample_attempt': exam['is_practice_exam'],
            'callback_url': callback_url,
            'user_id': user_id,
            'full_name': " ".join((user.first_name, user.last_name)),
            'username': user.username,
            'email': user.email
        }

        # see if there is an exam review policy for this exam
        # if so, then pass it into the provider
        if review_policy:
            context.update({'review_policy': review_policy.review_policy})

        # see if there is a review policy exception for this *user*
        # exceptions are granted on a individual basis as an
        # allowance
        if review_policy_exception:
            context.update(
                {'review_policy_exception': review_policy_exception})

        # now call into the backend provider to register exam attempt
        provider_name = get_provider_name_by_course_id(exam['course_id'])
        external_id = get_backend_provider(
            provider_name).register_exam_attempt(
                exam,
                context=context,
            )

    attempt = ProctoredExamStudentAttempt.create_exam_attempt(
        exam_id,
        user_id,
        '',  # student name is TBD
        allowed_time_limit_mins,
        attempt_code,
        taking_as_proctored,
        exam['is_practice_exam'],
        external_id,
        review_policy_id=review_policy.id if review_policy else None,
    )

    log_msg = (
        '{attempt_code} - {username} ({email}) '
        'Created exam attempt ({attempt_id}) for exam_id {exam_id} for '
        'user_id {user_id} with taking as proctored = {taking_as_proctored} '
        'with allowed time limit minutes of {allowed_time_limit_mins}. '
        'external_id of {external_id}'.format(
            attempt_id=attempt.id,
            exam_id=exam_id,
            user_id=user_id,
            taking_as_proctored=taking_as_proctored,
            allowed_time_limit_mins=allowed_time_limit_mins,
            attempt_code=attempt_code,
            external_id=external_id,
            username=attempt.user.username,
            email=attempt.user.email))
    log.info(log_msg)

    return attempt.id
Пример #28
0
def update_attempt_status(exam_id,
                          user_id,
                          to_status,
                          raise_if_not_found=True,
                          cascade_effects=True):
    """
    Internal helper to handle state transitions of attempt status
    """

    exam = get_exam_by_id(exam_id)
    provider_name = get_provider_name_by_course_id(exam['course_id'])
    proctoring_settings = get_proctoring_settings(provider_name)
    exam_attempt_obj = ProctoredExamStudentAttempt.objects.get_exam_attempt(
        exam_id, user_id)

    if exam_attempt_obj is None:
        if raise_if_not_found:
            raise StudentExamAttemptDoesNotExistsException(
                'Error. Trying to look up an exam that does not exist.')
        else:
            return
    else:
        log_msg = ('{attempt_code} - {username} ({email}) '
                   'Updating attempt status for exam_id {exam_id} '
                   'for user_id {user_id} to status {to_status}'.format(
                       exam_id=exam_id,
                       user_id=user_id,
                       to_status=to_status,
                       attempt_code=exam_attempt_obj.attempt_code,
                       username=exam_attempt_obj.user.username,
                       email=exam_attempt_obj.user.email))
        log.info(log_msg)

    timed_out_state = False
    if exam_attempt_obj.status == ProctoredExamStudentAttemptStatus.created:
        timed_out_state = True
    # In some configuration we may treat timeouts the same
    # as the user saying he/she wises to submit the exam
    alias_timeout = (to_status == ProctoredExamStudentAttemptStatus.timed_out
                     and not proctoring_settings.get('ALLOW_TIMED_OUT_STATE',
                                                     timed_out_state))
    if alias_timeout:
        to_status = ProctoredExamStudentAttemptStatus.submitted

    #
    # don't allow state transitions from a completed state to an incomplete state
    # if a re-attempt is desired then the current attempt must be deleted
    #
    in_completed_status = ProctoredExamStudentAttemptStatus.is_completed_status(
        exam_attempt_obj.status)
    to_incompleted_status = ProctoredExamStudentAttemptStatus.is_incomplete_status(
        to_status)

    if in_completed_status and to_incompleted_status:
        err_msg = (
            'A status transition from {from_status} to {to_status} was attempted '
            'on exam_id {exam_id} for user_id {user_id}. This is not '
            'allowed!'.format(from_status=exam_attempt_obj.status,
                              to_status=to_status,
                              exam_id=exam_id,
                              user_id=user_id))
        raise ProctoredExamIllegalStatusTransition(err_msg)

    # special case logic, if we are in a completed status we shouldn't allow
    # for a transition to 'Error' state
    if in_completed_status and to_status == ProctoredExamStudentAttemptStatus.error:
        err_msg = (
            'A status transition from {from_status} to {to_status} was attempted '
            'on exam_id {exam_id} for user_id {user_id}. This is not '
            'allowed!'.format(from_status=exam_attempt_obj.status,
                              to_status=to_status,
                              exam_id=exam_id,
                              user_id=user_id))
        raise ProctoredExamIllegalStatusTransition(err_msg)

    if to_status == exam_attempt_obj.status:
        log_msg = (
            '{attempt_code} - {username} ({email}) '
            'Try to change attempt status for exam_id {exam_id} for user_id '
            '{user_id} to the same status. Rejected'.format(
                exam_id=exam_id,
                user_id=user_id,
                attempt_code=exam_attempt_obj.attempt_code,
                username=exam_attempt_obj.user.username,
                email=exam_attempt_obj.user.email))
        log.info(log_msg)
        return exam_attempt_obj.id
    # OK, state transition is fine, we can proceed
    exam_attempt_obj.status = to_status
    exam_attempt_obj.save()

    # see if the status transition this changes credit requirement status
    if ProctoredExamStudentAttemptStatus.needs_credit_status_update(to_status):

        # trigger credit workflow, as needed
        credit_service = get_runtime_service('credit')

        exam = get_exam_by_id(exam_id)
        if to_status == ProctoredExamStudentAttemptStatus.verified:
            verification = 'satisfied'
        elif to_status == ProctoredExamStudentAttemptStatus.submitted:
            verification = 'submitted'
        else:
            verification = 'failed'

        log_msg = ('{attempt_code} - {username} ({email}) '
                   'Calling set_credit_requirement_status for '
                   'user_id {user_id} on {course_id} for '
                   'content_id {content_id}. Status: {status}'.format(
                       user_id=exam_attempt_obj.user_id,
                       course_id=exam['course_id'],
                       content_id=exam_attempt_obj.proctored_exam.content_id,
                       status=verification,
                       attempt_code=exam_attempt_obj.attempt_code,
                       username=exam_attempt_obj.user.username,
                       email=exam_attempt_obj.user.email))
        log.info(log_msg)

        credit_service.set_credit_requirement_status(
            user_id=exam_attempt_obj.user_id,
            course_key_or_id=exam['course_id'],
            req_namespace='proctored_exam',
            req_name=exam_attempt_obj.proctored_exam.content_id,
            status=verification)

    if cascade_effects and ProctoredExamStudentAttemptStatus.is_a_cascadable_failure(
            to_status):
        if to_status == ProctoredExamStudentAttemptStatus.declined:
            # if user declines attempt, make sure we clear out the external_id and
            # taking_as_proctored fields
            exam_attempt_obj.taking_as_proctored = False
            exam_attempt_obj.external_id = None
            exam_attempt_obj.save()

        # some state transitions (namely to a rejected or declined status)
        # will mark other exams as declined because once we fail or decline
        # one exam all other (un-completed) proctored exams will be likewise
        # updated to reflect a declined status
        # get all other unattempted exams and mark also as declined
        _exams = ProctoredExam.get_all_exams_for_course(
            exam_attempt_obj.proctored_exam.course_id, active_only=True)

        # we just want other exams which are proctored and are not practice
        exams = [
            exam for exam in _exams
            if (exam.content_id != exam_attempt_obj.proctored_exam.content_id
                and exam.is_proctored and not exam.is_practice_exam)
        ]

        for exam in exams:
            # see if there was an attempt on those other exams already
            attempt = get_exam_attempt(exam.id, user_id)
            if attempt and ProctoredExamStudentAttemptStatus.is_completed_status(
                    attempt['status']):
                # don't touch any completed statuses
                # we won't revoke those
                continue

            if not attempt:
                create_exam_attempt(exam.id,
                                    user_id,
                                    taking_as_proctored=False)

            # update any new or existing status to declined
            update_attempt_status(exam.id,
                                  user_id,
                                  ProctoredExamStudentAttemptStatus.declined,
                                  cascade_effects=False)

    if to_status == ProctoredExamStudentAttemptStatus.submitted:
        # also mark the exam attempt completed_at timestamp
        # after we submit the attempt
        exam_attempt_obj.completed_at = datetime.now(pytz.UTC)
        exam_attempt_obj.save()

    # if we have transitioned to started and haven't set our
    # started_at timestamp, do so now
    add_start_time = (to_status == ProctoredExamStudentAttemptStatus.started
                      and not exam_attempt_obj.started_at)
    if add_start_time:
        exam_attempt_obj.started_at = datetime.now(pytz.UTC)
        exam_attempt_obj.save()

    # email will be send when the exam is proctored and not practice exam
    # and the status is verified, submitted or rejected
    should_send_status_email = (
        exam_attempt_obj.taking_as_proctored
        and not exam_attempt_obj.is_sample_attempt
        and ProctoredExamStudentAttemptStatus.needs_status_change_email(
            exam_attempt_obj.status))
    if should_send_status_email:
        # trigger credit workflow, as needed
        credit_service = get_runtime_service('credit')

        # call service to get course name.
        credit_state = credit_service.get_credit_state(
            exam_attempt_obj.user_id,
            exam_attempt_obj.proctored_exam.course_id,
            #return_course_name=True
        )

        send_proctoring_attempt_status_email(
            exam_attempt_obj, credit_state.get('course_name',
                                               _('your course')))

    return exam_attempt_obj.id
Пример #29
0
def create_exam_attempt(exam_id, user_id, taking_as_proctored=False):
    """
    Creates an exam attempt for user_id against exam_id. There should only be
    one exam_attempt per user per exam. Multiple attempts by user will be archived
    in a separate table
    """
    # for now the student is allowed the exam default

    log_msg = (
        'Creating exam attempt for exam_id {exam_id} for '
        'user_id {user_id} with taking as proctored = {taking_as_proctored}'.format(
            exam_id=exam_id, user_id=user_id, taking_as_proctored=taking_as_proctored
        )
    )
    log.info(log_msg)

    exam = get_exam_by_id(exam_id)
    existing_attempt = ProctoredExamStudentAttempt.objects.get_exam_attempt(exam_id, user_id)
    if existing_attempt:
        if existing_attempt.is_sample_attempt:
            # Archive the existing attempt by deleting it.
            existing_attempt.delete_exam_attempt()
        else:
            err_msg = (
                'Cannot create new exam attempt for exam_id = {exam_id} and '
                'user_id = {user_id} because it already exists!'
            ).format(exam_id=exam_id, user_id=user_id)

            raise StudentExamAttemptAlreadyExistsException(err_msg)

    allowed_time_limit_mins = exam['time_limit_mins']

    # add in the allowed additional time
    allowance_extra_mins = ProctoredExamStudentAllowance.get_additional_time_granted(exam_id, user_id)
    if allowance_extra_mins:
        allowed_time_limit_mins += allowance_extra_mins

    attempt_code = unicode(uuid.uuid4()).upper()

    external_id = None
    review_policy = ProctoredExamReviewPolicy.get_review_policy_for_exam(exam_id)
    review_policy_exception = ProctoredExamStudentAllowance.get_review_policy_exception(exam_id, user_id)

    if taking_as_proctored:
        scheme = 'https' if getattr(settings, 'HTTPS', 'on') == 'on' else 'http'
        callback_url = '{scheme}://{hostname}{path}'.format(
            scheme=scheme,
            hostname=settings.SITE_NAME,
            path=reverse(
                'edx_proctoring.anonymous.proctoring_launch_callback.start_exam',
                args=[attempt_code]
            )
        )

        # get the name of the user, if the service is available
        full_name = None

        credit_service = get_runtime_service('credit')
        if credit_service:
            credit_state = credit_service.get_credit_state(user_id, exam['course_id'])
            full_name = credit_state['profile_fullname']

        context = {
            'time_limit_mins': allowed_time_limit_mins,
            'attempt_code': attempt_code,
            'is_sample_attempt': exam['is_practice_exam'],
            'callback_url': callback_url,
            'full_name': full_name,
        }

        # see if there is an exam review policy for this exam
        # if so, then pass it into the provider
        if review_policy:
            context.update({
                'review_policy': review_policy.review_policy
            })

        # see if there is a review policy exception for this *user*
        # exceptions are granted on a individual basis as an
        # allowance
        if review_policy_exception:
            context.update({
                'review_policy_exception': review_policy_exception
            })

        # now call into the backend provider to register exam attempt
        course_id = exam['course_id']
        course_key = CourseKey.from_string(course_id)
        course = modulestore().get_course(course_key)
        provider_name = course.proctoring_service
        external_id = get_backend_provider(provider_name).register_exam_attempt(
            exam,
            context=context,
        )

    attempt = ProctoredExamStudentAttempt.create_exam_attempt(
        exam_id,
        user_id,
        '',  # student name is TBD
        allowed_time_limit_mins,
        attempt_code,
        taking_as_proctored,
        exam['is_practice_exam'],
        external_id,
        review_policy_id=review_policy.id if review_policy else None,
    )

    log_msg = (
        'Created exam attempt ({attempt_id}) for exam_id {exam_id} for '
        'user_id {user_id} with taking as proctored = {taking_as_proctored} '
        'with allowed time limit minutes of {allowed_time_limit_mins}. '
        'Attempt_code {attempt_code} was generated which has a '
        'external_id of {external_id}'.format(
            attempt_id=attempt.id, exam_id=exam_id, user_id=user_id,
            taking_as_proctored=taking_as_proctored,
            allowed_time_limit_mins=allowed_time_limit_mins,
            attempt_code=attempt_code,
            external_id=external_id
        )
    )
    log.info(log_msg)

    return attempt.id
Пример #30
0
def update_attempt_status(exam_id, user_id, to_status, raise_if_not_found=True, cascade_effects=True):
    """
    Internal helper to handle state transitions of attempt status
    """

    log_msg = (
        'Updating attempt status for exam_id {exam_id} '
        'for user_id {user_id} to status {to_status}'.format(
            exam_id=exam_id, user_id=user_id, to_status=to_status
        )
    )
    log.info(log_msg)

    # In some configuration we may treat timeouts the same
    # as the user saying he/she wises to submit the exam
    alias_timeout = (
        to_status == ProctoredExamStudentAttemptStatus.timed_out and
        not settings.PROCTORING_SETTINGS.get('ALLOW_TIMED_OUT_STATE', False)
    )
    if alias_timeout:
        to_status = ProctoredExamStudentAttemptStatus.submitted

    exam_attempt_obj = ProctoredExamStudentAttempt.objects.get_exam_attempt(exam_id, user_id)
    if exam_attempt_obj is None:
        if raise_if_not_found:
            raise StudentExamAttemptDoesNotExistsException('Error. Trying to look up an exam that does not exist.')
        else:
            return

    #
    # don't allow state transitions from a completed state to an incomplete state
    # if a re-attempt is desired then the current attempt must be deleted
    #
    in_completed_status = ProctoredExamStudentAttemptStatus.is_completed_status(exam_attempt_obj.status)
    to_incompleted_status = ProctoredExamStudentAttemptStatus.is_incomplete_status(to_status)

    if in_completed_status and to_incompleted_status:
        err_msg = (
            'A status transition from {from_status} to {to_status} was attempted '
            'on exam_id {exam_id} for user_id {user_id}. This is not '
            'allowed!'.format(
                from_status=exam_attempt_obj.status,
                to_status=to_status,
                exam_id=exam_id,
                user_id=user_id
            )
        )
        raise ProctoredExamIllegalStatusTransition(err_msg)

    # special case logic, if we are in a completed status we shouldn't allow
    # for a transition to 'Error' state
    if in_completed_status and to_status == ProctoredExamStudentAttemptStatus.error:
        err_msg = (
            'A status transition from {from_status} to {to_status} was attempted '
            'on exam_id {exam_id} for user_id {user_id}. This is not '
            'allowed!'.format(
                from_status=exam_attempt_obj.status,
                to_status=to_status,
                exam_id=exam_id,
                user_id=user_id
            )
        )
        raise ProctoredExamIllegalStatusTransition(err_msg)

    # OK, state transition is fine, we can proceed
    exam_attempt_obj.status = to_status
    exam_attempt_obj.save()

    # see if the status transition this changes credit requirement status
    if ProctoredExamStudentAttemptStatus.needs_credit_status_update(to_status):

        # trigger credit workflow, as needed
        credit_service = get_runtime_service('credit')

        exam = get_exam_by_id(exam_id)
        if to_status == ProctoredExamStudentAttemptStatus.verified:
            verification = 'satisfied'
        elif to_status == ProctoredExamStudentAttemptStatus.submitted:
            verification = 'submitted'
        else:
            verification = 'failed'

        log_msg = (
            'Calling set_credit_requirement_status for '
            'user_id {user_id} on {course_id} for '
            'content_id {content_id}. Status: {status}'.format(
                user_id=exam_attempt_obj.user_id,
                course_id=exam['course_id'],
                content_id=exam_attempt_obj.proctored_exam.content_id,
                status=verification
            )
        )
        log.info(log_msg)

        credit_service.set_credit_requirement_status(
            user_id=exam_attempt_obj.user_id,
            course_key_or_id=exam['course_id'],
            req_namespace='proctored_exam',
            req_name=exam_attempt_obj.proctored_exam.content_id,
            status=verification
        )

    if cascade_effects and ProctoredExamStudentAttemptStatus.is_a_cascadable_failure(to_status):
        if to_status == ProctoredExamStudentAttemptStatus.declined:
            # if user declines attempt, make sure we clear out the external_id and
            # taking_as_proctored fields
            exam_attempt_obj.taking_as_proctored = False
            exam_attempt_obj.external_id = None
            exam_attempt_obj.save()

        # some state transitions (namely to a rejected or declined status)
        # will mark other exams as declined because once we fail or decline
        # one exam all other (un-completed) proctored exams will be likewise
        # updated to reflect a declined status
        # get all other unattempted exams and mark also as declined
        _exams = ProctoredExam.get_all_exams_for_course(
            exam_attempt_obj.proctored_exam.course_id,
            active_only=True
        )

        # we just want other exams which are proctored and are not practice
        exams = [
            exam
            for exam in _exams
            if (
                exam.content_id != exam_attempt_obj.proctored_exam.content_id and
                exam.is_proctored and not exam.is_practice_exam
            )
        ]

        for exam in exams:
            # see if there was an attempt on those other exams already
            attempt = get_exam_attempt(exam.id, user_id)
            if attempt and ProctoredExamStudentAttemptStatus.is_completed_status(attempt['status']):
                # don't touch any completed statuses
                # we won't revoke those
                continue

            if not attempt:
                create_exam_attempt(exam.id, user_id, taking_as_proctored=False)

            # update any new or existing status to declined
            update_attempt_status(
                exam.id,
                user_id,
                ProctoredExamStudentAttemptStatus.declined,
                cascade_effects=False
            )

    if to_status == ProctoredExamStudentAttemptStatus.submitted:
        # also mark the exam attempt completed_at timestamp
        # after we submit the attempt
        exam_attempt_obj.completed_at = datetime.now(pytz.UTC)
        exam_attempt_obj.save()

    # if we have transitioned to started and haven't set our
    # started_at timestamp, do so now
    add_start_time = (
        to_status == ProctoredExamStudentAttemptStatus.started and
        not exam_attempt_obj.started_at
    )
    if add_start_time:
        exam_attempt_obj.started_at = datetime.now(pytz.UTC)
        exam_attempt_obj.save()

    # email will be send when the exam is proctored and not practice exam
    # and the status is verified, submitted or rejected
    should_send_status_email = (
        exam_attempt_obj.taking_as_proctored and
        not exam_attempt_obj.is_sample_attempt and
        ProctoredExamStudentAttemptStatus.needs_status_change_email(exam_attempt_obj.status)
    )
    if should_send_status_email:
        # trigger credit workflow, as needed
        credit_service = get_runtime_service('credit')

        # call service to get course name.
        credit_state = credit_service.get_credit_state(
            exam_attempt_obj.user_id,
            exam_attempt_obj.proctored_exam.course_id,
            return_course_name=True
        )

        send_proctoring_attempt_status_email(
            exam_attempt_obj,
            credit_state.get('course_name', _('your course'))
        )

    return exam_attempt_obj.id