Esempio n. 1
0
    def test_exam_attempt_is_resumable(self):
        # Create an exam.
        proctored_exam = ProctoredExam.objects.create(
            course_id='a/b/c',
            content_id='test_content',
            exam_name='Test Exam',
            external_id='123aXqe3',
            time_limit_mins=90
        )
        # Create a user and their attempt
        user = User.objects.create(username='******', email='*****@*****.**')
        ProctoredExamStudentAttempt.create_exam_attempt(
            proctored_exam.id, user.id, 'test_name_resumable',
            'test_attempt_code_resumable', True, False, 'test_external_id_resumable'
        )

        filter_query = {
            'user_id': user.id,
            'proctored_exam_id': proctored_exam.id
        }

        attempt = ProctoredExamStudentAttempt.objects.get(**filter_query)
        self.assertFalse(attempt.is_resumable)

        # No entry in the History table on creation of the Allowance entry.
        attempt_history = ProctoredExamStudentAttemptHistory.objects.filter(**filter_query)
        self.assertEqual(len(attempt_history), 0)

        # Saving as an error status
        attempt.is_resumable = True
        attempt.status = ProctoredExamStudentAttemptStatus.error
        attempt.save()

        attempt = ProctoredExamStudentAttempt.objects.get(**filter_query)
        self.assertTrue(attempt.is_resumable)

        attempt_history = ProctoredExamStudentAttemptHistory.objects.filter(**filter_query)
        self.assertEqual(len(attempt_history), 1)
        self.assertFalse(attempt_history.first().is_resumable)

        # Saving as a Reviewed status, but not changing is_resumable
        attempt.status = ProctoredExamStudentAttemptStatus.verified
        attempt.save()
        attempt = ProctoredExamStudentAttempt.objects.get(**filter_query)
        self.assertTrue(attempt.is_resumable)

        attempt_history = ProctoredExamStudentAttemptHistory.objects.filter(**filter_query)
        self.assertEqual(len(attempt_history), 2)
        self.assertTrue(attempt_history.last().is_resumable)
Esempio n. 2
0
    def _create_exam_attempt(self,
                             exam_id,
                             status=ProctoredExamStudentAttemptStatus.created,
                             is_practice_exam=False,
                             time_remaining_seconds=None):
        """
        Creates the ProctoredExamStudentAttempt object.
        """
        attempt = ProctoredExamStudentAttempt(
            proctored_exam_id=exam_id,
            user_id=self.user_id,
            external_id=self.external_id,
            status=status,
            allowed_time_limit_mins=10,
            taking_as_proctored=True,
            is_sample_attempt=is_practice_exam,
            time_remaining_seconds=time_remaining_seconds,
        )

        if status in (ProctoredExamStudentAttemptStatus.started,
                      ProctoredExamStudentAttemptStatus.ready_to_submit,
                      ProctoredExamStudentAttemptStatus.submitted):
            attempt.started_at = datetime.now(pytz.UTC)

        if ProctoredExamStudentAttemptStatus.is_completed_status(status):
            attempt.completed_at = datetime.now(pytz.UTC)

        if status == ProctoredExamStudentAttemptStatus.error:
            attempt.is_resumable = True

        attempt.save()

        return attempt
    def test_get_student_exam_attempt_features(self):
        query_features = [
            'email', 'exam_name', 'allowed_time_limit_mins',
            'is_sample_attempt', 'started_at', 'completed_at', 'status',
            'Suspicious Count', 'Suspicious Comments', 'Rules Violation Count',
            'Rules Violation Comments', 'track'
        ]

        proctored_exam_id = create_exam(self.course_key, 'Test Content',
                                        'Test Exam', 1)
        ProctoredExamStudentAttempt.create_exam_attempt(
            proctored_exam_id, self.users[0].id, 'Test Code 1', True, False,
            'ad13')
        ProctoredExamStudentAttempt.create_exam_attempt(
            proctored_exam_id, self.users[1].id, 'Test Code 2', True, False,
            'ad13')
        ProctoredExamStudentAttempt.create_exam_attempt(
            proctored_exam_id, self.users[2].id, 'Test Code 3', True, False,
            'asd')

        proctored_exam_attempts = get_proctored_exam_results(
            self.course_key, query_features)
        assert len(proctored_exam_attempts) == 3
        for proctored_exam_attempt in proctored_exam_attempts:
            assert set(proctored_exam_attempt.keys()) == set(query_features)
Esempio n. 4
0
    def test_get_student_exam_attempt_features(self):
        query_features = [
            'email',
            'exam_name',
            'allowed_time_limit_mins',
            'is_sample_attempt',
            'started_at',
            'completed_at',
            'status',
            'Suspicious Count',
            'Suspicious Comments',
            'Rules Violation Count',
            'Rules Violation Comments',
        ]

        proctored_exam_id = create_exam(self.course_key, 'Test Content', 'Test Exam', 1)
        ProctoredExamStudentAttempt.create_exam_attempt(
            proctored_exam_id, self.users[0].id, '',
            'Test Code 1', True, False, 'ad13'
        )
        ProctoredExamStudentAttempt.create_exam_attempt(
            proctored_exam_id, self.users[1].id, '',
            'Test Code 2', True, False, 'ad13'
        )
        ProctoredExamStudentAttempt.create_exam_attempt(
            proctored_exam_id, self.users[2].id, '',
            'Test Code 3', True, False, 'asd'
        )

        proctored_exam_attempts = get_proctored_exam_results(self.course_key, query_features)
        self.assertEqual(len(proctored_exam_attempts), 3)
        for proctored_exam_attempt in proctored_exam_attempts:
            self.assertEqual(set(proctored_exam_attempt.keys()), set(query_features))
Esempio n. 5
0
    def test_get_student_exam_attempt_features(self):
        query_features = [
            'user_email',
            'exam_name',
            'allowed_time_limit_mins',
            'is_sample_attempt',
            'started_at',
            'completed_at',
            'status',
        ]

        proctored_exam_id = create_exam(self.course_key, 'Test Content', 'Test Exam', 1)
        ProctoredExamStudentAttempt.create_exam_attempt(
            proctored_exam_id, self.users[0].id, '', 1,
            'Test Code 1', True, False, 'ad13'
        )
        ProctoredExamStudentAttempt.create_exam_attempt(
            proctored_exam_id, self.users[1].id, '', 2,
            'Test Code 2', True, False, 'ad13'
        )
        ProctoredExamStudentAttempt.create_exam_attempt(
            proctored_exam_id, self.users[2].id, '', 3,
            'Test Code 3', True, False, 'asd'
        )

        proctored_exam_attempts = get_proctored_exam_results(self.course_key, query_features)
        self.assertEqual(len(proctored_exam_attempts), 3)
        for proctored_exam_attempt in proctored_exam_attempts:
            self.assertEqual(set(proctored_exam_attempt.keys()), set(query_features))
Esempio n. 6
0
    def _create_exam_attempt(self,
                             exam_id,
                             status=ProctoredExamStudentAttemptStatus.created):
        """
        Creates the ProctoredExamStudentAttempt object.
        """
        attempt = ProctoredExamStudentAttempt(
            proctored_exam_id=exam_id,
            user_id=self.user_id,
            external_id=self.external_id,
            status=status,
            allowed_time_limit_mins=10,
            taking_as_proctored=True,
        )

        if status in (ProctoredExamStudentAttemptStatus.started,
                      ProctoredExamStudentAttemptStatus.ready_to_submit,
                      ProctoredExamStudentAttemptStatus.submitted):
            attempt.started_at = datetime.now(pytz.UTC)

        if ProctoredExamStudentAttemptStatus.is_completed_status(status):
            attempt.completed_at = datetime.now(pytz.UTC)

        attempt.save()

        return attempt
Esempio n. 7
0
    def test_get_exam_attempts(self):
        """
        Test to get all the exam attempts for a course
        """
        # Create an exam.
        proctored_exam = ProctoredExam.objects.create(
            course_id='a/b/c',
            content_id='test_content',
            exam_name='Test Exam',
            external_id='123aXqe3',
            time_limit_mins=90)

        # create number of exam attempts
        for i in range(90):
            ProctoredExamStudentAttempt.create_exam_attempt(
                proctored_exam.id, i, 'test_name{0}'.format(i),
                'test_attempt_code{0}'.format(i), True, False,
                'test_external_id{0}'.format(i))

        with self.assertNumQueries(1):
            exam_attempts = ProctoredExamStudentAttempt.objects.get_all_exam_attempts(
                'a/b/c')
            self.assertEqual(len(exam_attempts), 90)
Esempio n. 8
0
    def test_get_exam_attempts(self):
        """
        Test to get all the exam attempts for a course
        """
        # Create an exam.
        proctored_exam = ProctoredExam.objects.create(
            course_id='a/b/c',
            content_id='test_content',
            exam_name='Test Exam',
            external_id='123aXqe3',
            time_limit_mins=90
        )

        # create number of exam attempts
        for i in range(90):
            ProctoredExamStudentAttempt.create_exam_attempt(
                proctored_exam.id, i, 'test_name{0}'.format(i), i + 1,
                'test_attempt_code{0}'.format(i), True, False, 'test_external_id{0}'.format(i)
            )

        with self.assertNumQueries(1):
            exam_attempts = ProctoredExamStudentAttempt.objects.get_all_exam_attempts('a/b/c')
            self.assertEqual(len(exam_attempts), 90)
Esempio n. 9
0
    def _create_exam_attempt(self, exam_id, status='created'):
        """
        Creates the ProctoredExamStudentAttempt object.
        """

        attempt = ProctoredExamStudentAttempt(
            proctored_exam_id=exam_id,
            user_id=self.user_id,
            external_id=self.external_id,
            allowed_time_limit_mins=10,
            status=status
        )

        if status in (ProctoredExamStudentAttemptStatus.started,
                      ProctoredExamStudentAttemptStatus.ready_to_submit, ProctoredExamStudentAttemptStatus.submitted):
            attempt.started_at = datetime.now(pytz.UTC)

        if ProctoredExamStudentAttemptStatus.is_completed_status(status):
            attempt.completed_at = datetime.now(pytz.UTC)

        attempt.save()

        return attempt
Esempio n. 10
0
    def test_exam_review_policy(self):
        """
        Assert correct behavior of the Exam Policy model including archiving of updates and deletes
        """

        # Create an exam.
        proctored_exam = ProctoredExam.objects.create(
            course_id='a/b/c',
            content_id='test_content',
            exam_name='Test Exam',
            external_id='123aXqe3',
            time_limit_mins=90)

        policy = ProctoredExamReviewPolicy.objects.create(
            set_by_user_id=self.user.id,
            proctored_exam=proctored_exam,
            review_policy='Foo Policy',
        )

        attempt = ProctoredExamStudentAttempt.create_exam_attempt(
            proctored_exam.id, self.user.id,
            'test_name{0}'.format(self.user.id),
            'test_attempt_code{0}'.format(self.user.id), True, False,
            'test_external_id{0}'.format(self.user.id))
        attempt.review_policy_id = policy.id
        attempt.save()

        history = ProctoredExamReviewPolicyHistory.objects.all()
        self.assertEqual(len(history), 0)

        # now update it
        policy.review_policy = 'Updated Foo Policy'
        policy.save()

        # look in history
        history = ProctoredExamReviewPolicyHistory.objects.all()
        self.assertEqual(len(history), 1)
        previous = history[0]
        self.assertEqual(previous.set_by_user_id, self.user.id)
        self.assertEqual(previous.proctored_exam_id, proctored_exam.id)
        self.assertEqual(previous.original_id, policy.id)
        self.assertEqual(previous.review_policy, 'Foo Policy')

        # now delete updated one
        deleted_id = policy.id
        policy.delete()

        # look in history
        history = ProctoredExamReviewPolicyHistory.objects.all()
        self.assertEqual(len(history), 2)
        previous = history[0]
        self.assertEqual(previous.set_by_user_id, self.user.id)
        self.assertEqual(previous.proctored_exam_id, proctored_exam.id)
        self.assertEqual(previous.original_id, deleted_id)
        self.assertEqual(previous.review_policy, 'Foo Policy')

        previous = history[1]
        self.assertEqual(previous.set_by_user_id, self.user.id)
        self.assertEqual(previous.proctored_exam_id, proctored_exam.id)
        self.assertEqual(previous.original_id, deleted_id)
        self.assertEqual(previous.review_policy, 'Updated Foo Policy')

        # assert that we cannot delete history!
        with self.assertRaises(NotImplementedError):
            previous.delete()

        # now delete attempt, to make sure we preserve the policy_id in the archive table
        attempt.delete()

        attempts = ProctoredExamStudentAttemptHistory.objects.all()
        self.assertEqual(len(attempts), 1)
        self.assertEqual(attempts[0].review_policy_id, deleted_id)
Esempio n. 11
0
    def test_exam_review_policy(self):
        """
        Assert correct behavior of the Exam Policy model including archiving of updates and deletes
        """

        # Create an exam.
        proctored_exam = ProctoredExam.objects.create(
            course_id='a/b/c',
            content_id='test_content',
            exam_name='Test Exam',
            external_id='123aXqe3',
            time_limit_mins=90
        )

        policy = ProctoredExamReviewPolicy.objects.create(
            set_by_user_id=self.user.id,
            proctored_exam=proctored_exam,
            review_policy='Foo Policy'
        )

        attempt = ProctoredExamStudentAttempt.create_exam_attempt(
            proctored_exam.id,
            self.user.id,
            'test_name{0}'.format(self.user.id),
            self.user.id + 1,
            'test_attempt_code{0}'.format(self.user.id),
            True,
            False,
            'test_external_id{0}'.format(self.user.id)
        )
        attempt.review_policy_id = policy.id
        attempt.save()

        history = ProctoredExamReviewPolicyHistory.objects.all()
        self.assertEqual(len(history), 0)

        # now update it
        policy.review_policy = 'Updated Foo Policy'
        policy.save()

        # look in history
        history = ProctoredExamReviewPolicyHistory.objects.all()
        self.assertEqual(len(history), 1)
        previous = history[0]
        self.assertEqual(previous.set_by_user_id, self.user.id)
        self.assertEqual(previous.proctored_exam_id, proctored_exam.id)
        self.assertEqual(previous.original_id, policy.id)
        self.assertEqual(previous.review_policy, 'Foo Policy')

        # now delete updated one
        deleted_id = policy.id
        policy.delete()

        # look in history
        history = ProctoredExamReviewPolicyHistory.objects.all()
        self.assertEqual(len(history), 2)
        previous = history[0]
        self.assertEqual(previous.set_by_user_id, self.user.id)
        self.assertEqual(previous.proctored_exam_id, proctored_exam.id)
        self.assertEqual(previous.original_id, deleted_id)
        self.assertEqual(previous.review_policy, 'Foo Policy')

        previous = history[1]
        self.assertEqual(previous.set_by_user_id, self.user.id)
        self.assertEqual(previous.proctored_exam_id, proctored_exam.id)
        self.assertEqual(previous.original_id, deleted_id)
        self.assertEqual(previous.review_policy, 'Updated Foo Policy')

        # assert that we cannot delete history!
        with self.assertRaises(NotImplementedError):
            previous.delete()

        # now delete attempt, to make sure we preserve the policy_id in the archive table
        attempt.delete()

        attempts = ProctoredExamStudentAttemptHistory.objects.all()
        self.assertEqual(len(attempts), 1)
        self.assertEqual(attempts[0].review_policy_id, deleted_id)
Esempio n. 12
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
Esempio n. 13
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
Esempio n. 14
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