def get(self, request, attempt_id):
        """
        HTTP GET Handler. Returns the status of the exam attempt.
        """
        attempt = get_exam_attempt_by_id(attempt_id)

        if not attempt:
            err_msg = (u'Attempted to access attempt_id {attempt_id} but '
                       u'it does not exist.'.format(attempt_id=attempt_id))
            raise StudentExamAttemptDoesNotExistsException(err_msg)
        # make sure the the attempt belongs to the calling user_id
        if attempt['user']['id'] != request.user.id:
            err_msg = (u'Attempted to access attempt_id {attempt_id} but '
                       u'does not have access to it.'.format(
                           attempt_id=attempt_id))
            raise ProctoredExamPermissionDenied(err_msg)

        # add in the computed time remaining as a helper
        time_remaining_seconds = get_time_remaining_for_attempt(attempt)

        attempt['time_remaining_seconds'] = time_remaining_seconds

        accessibility_time_string = _(
            u'you have {remaining_time} remaining').format(
                remaining_time=humanized_time(
                    int(round(time_remaining_seconds / 60.0, 0))))

        # special case if we are less than a minute, since we don't produce
        # text translations of granularity at the seconds range
        if time_remaining_seconds < 60:
            accessibility_time_string = _(
                u'you have less than a minute remaining')

        attempt['accessibility_time_string'] = accessibility_time_string
        return Response(attempt)
Beispiel #2
0
    def get(self, request):  # pylint: disable=unused-argument
        """
        HTTP GET Handler. Returns the status of the exam attempt.
        """

        exams = get_active_exams_for_user(request.user.id)

        if exams:
            exam_info = exams[0]

            exam = exam_info["exam"]
            attempt = exam_info["attempt"]

            time_remaining_seconds = get_time_remaining_for_attempt(attempt)

            proctoring_settings = getattr(settings, "PROCTORING_SETTINGS", {})
            low_threshold_pct = proctoring_settings.get("low_threshold_pct", 0.2)
            critically_low_threshold_pct = proctoring_settings.get("critically_low_threshold_pct", 0.05)

            low_threshold = int(low_threshold_pct * float(attempt["allowed_time_limit_mins"]) * 60)
            critically_low_threshold = int(
                critically_low_threshold_pct * float(attempt["allowed_time_limit_mins"]) * 60
            )

            exam_url_path = ""
            try:
                # resolve the LMS url, note we can't assume we're running in
                # a same process as the LMS
                exam_url_path = reverse("courseware.views.jump_to", args=[exam["course_id"], exam["content_id"]])
            except NoReverseMatch:
                pass

            response_dict = {
                "in_timed_exam": True,
                "taking_as_proctored": attempt["taking_as_proctored"],
                "exam_type": (
                    _("timed")
                    if not attempt["taking_as_proctored"]
                    else (_("practice") if attempt["is_sample_attempt"] else _("proctored"))
                ),
                "exam_display_name": exam["exam_name"],
                "exam_url_path": exam_url_path,
                "time_remaining_seconds": time_remaining_seconds,
                "low_threshold_sec": low_threshold,
                "critically_low_threshold_sec": critically_low_threshold,
                "course_id": exam["course_id"],
                "attempt_id": attempt["id"],
                "accessibility_time_string": _("you have {remaining_time} remaining").format(
                    remaining_time=humanized_time(int(round(time_remaining_seconds / 60.0, 0)))
                ),
                "attempt_status": attempt["status"],
            }
        else:
            response_dict = {"in_timed_exam": False, "is_proctored": False}

        return Response(data=response_dict, status=status.HTTP_200_OK)
Beispiel #3
0
    def get(self, request, attempt_id):
        """
        HTTP GET Handler. Returns the status of the exam attempt.
        """

        try:
            attempt = get_exam_attempt_by_id(attempt_id)

            if not attempt:
                err_msg = (
                    'Attempted to access attempt_id {attempt_id} but '
                    'it does not exist.'.format(
                        attempt_id=attempt_id
                    )
                )
                return Response(
                    status=status.HTTP_400_BAD_REQUEST
                )

            # make sure the the attempt belongs to the calling user_id
            if attempt['user']['id'] != request.user.id:
                err_msg = (
                    'Attempted to access attempt_id {attempt_id} but '
                    'does not have access to it.'.format(
                        attempt_id=attempt_id
                    )
                )
                raise ProctoredExamPermissionDenied(err_msg)

            # add in the computed time remaining as a helper
            time_remaining_seconds = get_time_remaining_for_attempt(attempt)

            attempt['time_remaining_seconds'] = time_remaining_seconds

            accessibility_time_string = _('you have {remaining_time} remaining').format(
                remaining_time=humanized_time(int(round(time_remaining_seconds / 60.0, 0))))

            # special case if we are less than a minute, since we don't produce
            # text translations of granularity at the seconds range
            if time_remaining_seconds < 60:
                accessibility_time_string = _('you have less than a minute remaining')

            attempt['accessibility_time_string'] = accessibility_time_string

            return Response(
                data=attempt,
                status=status.HTTP_200_OK
            )

        except ProctoredBaseException, ex:
            LOG.exception(ex)
            return Response(
                status=status.HTTP_400_BAD_REQUEST,
                data={"detail": str(ex)}
            )
Beispiel #4
0
    def get(self, request, attempt_id):
        """
        HTTP GET Handler. Returns the status of the exam attempt.
        """
        attempt = get_exam_attempt_by_id(attempt_id)

        if not attempt:
            err_msg = (
                'Attempted to access attempt_id {attempt_id} but '
                'it does not exist.'.format(
                    attempt_id=attempt_id
                )
            )
            raise StudentExamAttemptDoesNotExistsException(err_msg)
        else:
            # make sure the the attempt belongs to the calling user_id
            if attempt['user']['id'] != request.user.id:
                err_msg = (
                    'Attempted to access attempt_id {attempt_id} but '
                    'does not have access to it.'.format(
                        attempt_id=attempt_id
                    )
                )
                raise ProctoredExamPermissionDenied(err_msg)

            # add in the computed time remaining as a helper
            time_remaining_seconds = get_time_remaining_for_attempt(attempt)

            attempt['time_remaining_seconds'] = time_remaining_seconds

            accessibility_time_string = _('you have {remaining_time} remaining').format(
                remaining_time=humanized_time(int(round(time_remaining_seconds / 60.0, 0))))

            # special case if we are less than a minute, since we don't produce
            # text translations of granularity at the seconds range
            if time_remaining_seconds < 60:
                accessibility_time_string = _('you have less than a minute remaining')

            attempt['accessibility_time_string'] = accessibility_time_string
        return Response(attempt)
Beispiel #5
0
    def get(self, request):  # pylint: disable=unused-argument
        """
        HTTP GET Handler. Returns the status of the exam attempt.
        """

        exams = get_active_exams_for_user(request.user.id)

        if exams:
            exam_info = exams[0]

            exam = exam_info['exam']
            attempt = exam_info['attempt']

            time_remaining_seconds = get_time_remaining_for_attempt(attempt)

            proctoring_settings = getattr(settings, 'PROCTORING_SETTINGS', {})
            low_threshold_pct = proctoring_settings.get(
                'low_threshold_pct', .2)
            critically_low_threshold_pct = proctoring_settings.get(
                'critically_low_threshold_pct', .05)

            low_threshold = int(low_threshold_pct *
                                float(attempt['allowed_time_limit_mins']) * 60)
            critically_low_threshold = int(
                critically_low_threshold_pct *
                float(attempt['allowed_time_limit_mins']) * 60)

            exam_url_path = ''
            try:
                # resolve the LMS url, note we can't assume we're running in
                # a same process as the LMS
                exam_url_path = reverse(
                    'courseware.views.jump_to',
                    args=[exam['course_id'], exam['content_id']])
            except NoReverseMatch:
                pass

            response_dict = {
                'in_timed_exam':
                True,
                'taking_as_proctored':
                attempt['taking_as_proctored'],
                'exam_type':
                (_('timed') if not attempt['taking_as_proctored'] else
                 (_('practice')
                  if attempt['is_sample_attempt'] else _('proctored'))),
                'exam_display_name':
                exam['exam_name'],
                'exam_url_path':
                exam_url_path,
                'time_remaining_seconds':
                time_remaining_seconds,
                'low_threshold_sec':
                low_threshold,
                'critically_low_threshold_sec':
                critically_low_threshold,
                'course_id':
                exam['course_id'],
                'attempt_id':
                attempt['id'],
                'accessibility_time_string':
                _('you have {remaining_time} remaining').format(
                    remaining_time=humanized_time(
                        int(round(time_remaining_seconds / 60.0, 0)))),
                'attempt_status':
                attempt['status']
            }
        else:
            response_dict = {'in_timed_exam': False, 'is_proctored': False}

        return Response(data=response_dict, status=status.HTTP_200_OK)
Beispiel #6
0
    def get(self, request, attempt_id):
        """
        HTTP GET Handler. Returns the status of the exam attempt.
        """

        try:
            attempt = get_exam_attempt_by_id(attempt_id)

            if not attempt:
                err_msg = ('Attempted to access attempt_id {attempt_id} but '
                           'it does not exist.'.format(attempt_id=attempt_id))
                return Response(status=status.HTTP_400_BAD_REQUEST)

            # make sure the the attempt belongs to the calling user_id
            if attempt['user']['id'] != request.user.id:
                err_msg = ('Attempted to access attempt_id {attempt_id} but '
                           'does not have access to it.'.format(
                               attempt_id=attempt_id))
                raise ProctoredExamPermissionDenied(err_msg)

            # check if the last_poll_timestamp is not None
            # and if it is older than SOFTWARE_SECURE_CLIENT_TIMEOUT
            # then attempt status should be marked as error.
            last_poll_timestamp = attempt['last_poll_timestamp']

            # if we never heard from the client, then we assume it is shut down
            attempt['client_has_shutdown'] = last_poll_timestamp is None

            if last_poll_timestamp is not None:
                # Let's pass along information if we think the SoftwareSecure has completed
                # a healthy shutdown which is when our attempt is in a 'submitted' status
                if attempt[
                        'status'] == ProctoredExamStudentAttemptStatus.submitted:
                    attempt['client_has_shutdown'] = has_client_app_shutdown(
                        attempt)
                else:
                    # otherwise, let's see if the shutdown happened in error
                    # e.g. a crash
                    time_passed_since_last_poll = (
                        datetime.now(pytz.UTC) -
                        last_poll_timestamp).total_seconds()
                    if time_passed_since_last_poll > constants.SOFTWARE_SECURE_CLIENT_TIMEOUT:
                        try:
                            update_attempt_status(
                                attempt['proctored_exam']['id'],
                                attempt['user']['id'],
                                ProctoredExamStudentAttemptStatus.error)
                            attempt[
                                'status'] = ProctoredExamStudentAttemptStatus.error
                        except ProctoredExamIllegalStatusTransition:
                            # don't transition a completed state to an error state
                            pass

            # add in the computed time remaining as a helper to a client app
            time_remaining_seconds = get_time_remaining_for_attempt(attempt)

            attempt['time_remaining_seconds'] = time_remaining_seconds

            accessibility_time_string = _(
                'you have {remaining_time} remaining').format(
                    remaining_time=humanized_time(
                        int(round(time_remaining_seconds / 60.0, 0))))

            # special case if we are less than a minute, since we don't produce
            # text translations of granularity at the seconds range
            if time_remaining_seconds < 60:
                accessibility_time_string = _(
                    'you have less than a minute remaining')

            attempt['accessibility_time_string'] = accessibility_time_string

            return Response(data=attempt, status=status.HTTP_200_OK)

        except ProctoredBaseException, ex:
            LOG.exception(ex)
            return Response(status=status.HTTP_400_BAD_REQUEST,
                            data={"detail": str(ex)})
Beispiel #7
0
    def test_humanized_time(self):
        """
        tests the humanized_time utility function against different values.
        """
        human_time = humanized_time(0)
        self.assertEqual(human_time, "0 minutes")

        human_time = humanized_time(1)
        self.assertEqual(human_time, "1 minute")

        human_time = humanized_time(10)
        self.assertEqual(human_time, "10 minutes")

        human_time = humanized_time(60)
        self.assertEqual(human_time, "1 hour")

        human_time = humanized_time(61)
        self.assertEqual(human_time, "1 hour and 1 minute")

        human_time = humanized_time(62)
        self.assertEqual(human_time, "1 hour and 2 minutes")

        human_time = humanized_time(120)
        self.assertEqual(human_time, "2 hours")

        human_time = humanized_time(121)
        self.assertEqual(human_time, "2 hours and 1 minute")

        human_time = humanized_time(150)
        self.assertEqual(human_time, "2 hours and 30 minutes")

        human_time = humanized_time(180)
        self.assertEqual(human_time, "3 hours")

        human_time = humanized_time(-60)
        self.assertEqual(human_time, "error")
Beispiel #8
0
    def get(self, request):  # pylint: disable=unused-argument
        """
        HTTP GET Handler. Returns the status of the exam attempt.
        """

        exams = get_active_exams_for_user(request.user.id)

        if exams:
            exam_info = exams[0]

            exam = exam_info['exam']
            attempt = exam_info['attempt']

            provider = get_backend_provider(exam)

            time_remaining_seconds = get_time_remaining_for_attempt(attempt)

            proctoring_settings = getattr(settings, 'PROCTORING_SETTINGS', {})
            low_threshold_pct = proctoring_settings.get('low_threshold_pct', .2)
            critically_low_threshold_pct = proctoring_settings.get('critically_low_threshold_pct', .05)

            low_threshold = int(low_threshold_pct * float(attempt['allowed_time_limit_mins']) * 60)
            critically_low_threshold = int(
                critically_low_threshold_pct * float(attempt['allowed_time_limit_mins']) * 60
            )

            exam_url_path = ''
            try:
                # resolve the LMS url, note we can't assume we're running in
                # a same process as the LMS
                exam_url_path = reverse('jump_to', args=[exam['course_id'], exam['content_id']])
            except NoReverseMatch:
                LOG.exception(u"Can't find exam url for course %s", exam['course_id'])

            response_dict = {
                'in_timed_exam': True,
                'taking_as_proctored': attempt['taking_as_proctored'],
                'exam_type': (
                    _('a timed exam') if not attempt['taking_as_proctored'] else
                    (_('a proctored exam') if not attempt['is_sample_attempt'] else
                     (_('an onboarding exam') if provider.supports_onboarding else _('a practice exam')))
                ),
                'exam_display_name': exam['exam_name'],
                'exam_url_path': exam_url_path,
                'time_remaining_seconds': time_remaining_seconds,
                'low_threshold_sec': low_threshold,
                'critically_low_threshold_sec': critically_low_threshold,
                'course_id': exam['course_id'],
                'attempt_id': attempt['id'],
                'accessibility_time_string': _(u'you have {remaining_time} remaining').format(
                    remaining_time=humanized_time(int(round(time_remaining_seconds / 60.0, 0)))
                ),
                'attempt_status': attempt['status'],
                'exam_started_poll_url': reverse(
                    'edx_proctoring:proctored_exam.attempt',
                    args=[attempt['id']]
                ),

            }

            if provider:
                response_dict['desktop_application_js_url'] = provider.get_javascript()
                response_dict['ping_interval'] = provider.ping_interval
            else:
                response_dict['desktop_application_js_url'] = ''

        else:
            response_dict = {
                'in_timed_exam': False,
                'is_proctored': False
            }

        return Response(data=response_dict, status=status.HTTP_200_OK)
    def test_humanized_time(self):
        """
        tests the humanized_time utility function against different values.
        """
        human_time = humanized_time(0)
        self.assertEqual(human_time, "0 minutes")

        human_time = humanized_time(1)
        self.assertEqual(human_time, "1 minute")

        human_time = humanized_time(10)
        self.assertEqual(human_time, "10 minutes")

        human_time = humanized_time(60)
        self.assertEqual(human_time, "1 hour")

        human_time = humanized_time(61)
        self.assertEqual(human_time, "1 hour and 1 minute")

        human_time = humanized_time(62)
        self.assertEqual(human_time, "1 hour and 2 minutes")

        human_time = humanized_time(120)
        self.assertEqual(human_time, "2 hours")

        human_time = humanized_time(121)
        self.assertEqual(human_time, "2 hours and 1 minute")

        human_time = humanized_time(150)
        self.assertEqual(human_time, "2 hours and 30 minutes")

        human_time = humanized_time(180)
        self.assertEqual(human_time, "3 hours")

        human_time = humanized_time(-60)
        self.assertEqual(human_time, "error")
Beispiel #10
0
def get_student_view(user_id, course_id, content_id,
                     context, user_role='student'):
    """
    Helper method that will return the view HTML related to the exam control
    flow (i.e. entering, expired, completed, etc.) If there is no specific
    content to display, then None will be returned and the caller should
    render it's own view
    """

    # non-student roles should never see any proctoring related
    # screens
    if user_role != 'student':
        return None

    student_view_template = None

    exam_id = None
    try:
        exam = get_exam_by_content_id(course_id, content_id)
        if not exam['is_active']:
            # Exam is no longer active
            # Note, we don't hard delete exams since we need to retain
            # data
            return None

        exam_id = exam['id']
    except ProctoredExamNotFoundException:
        # This really shouldn't happen
        # as Studio will be setting this up
        exam_id = create_exam(
            course_id=course_id,
            content_id=unicode(content_id),
            exam_name=context['display_name'],
            time_limit_mins=context['default_time_limit_mins'],
            is_proctored=context.get('is_proctored', False),
            is_practice_exam=context.get('is_practice_exam', False)
        )
        exam = get_exam_by_content_id(course_id, content_id)

    is_proctored = exam['is_proctored']

    # see if only 'verified' track students should see this *except* if it is a practice exam
    check_mode_and_eligibility = (
        settings.PROCTORING_SETTINGS.get('MUST_BE_VERIFIED_TRACK', True) and
        'credit_state' in context and
        context['credit_state'] and not
        exam['is_practice_exam']
    )

    attempt = None
    if check_mode_and_eligibility:
        credit_state = context['credit_state']

        has_mode = _check_eligibility_of_enrollment_mode(credit_state)
        has_prerequisites = False
        if has_mode:
            has_prerequisites = _check_eligibility_of_prerequisites(credit_state)

        # see if the user has passed all pre-requisite credit eligibility
        # checks, otherwise just show the user the exam unproctored
        if not has_mode or not has_prerequisites:
            # if we are in the right mode and if we don't have
            # pre-requisites, then we implicitly decline the exam
            if has_mode:
                attempt = get_exam_attempt(exam_id, user_id)
                if not attempt:
                    # user hasn't a record of attempt, create one now
                    # so we can mark it as declined
                    create_exam_attempt(exam_id, user_id)

                    update_attempt_status(
                        exam_id,
                        user_id,
                        ProctoredExamStudentAttemptStatus.declined,
                        raise_if_not_found=False
                    )

            # don't override context, let the courseware show
            return None

    attempt = get_exam_attempt(exam_id, user_id)

    # if user has declined the attempt, then we don't show the
    # proctored exam
    if attempt and attempt['status'] == ProctoredExamStudentAttemptStatus.declined:
        return None

    does_time_remain = False
    has_started_exam = attempt and attempt.get('started_at')
    if has_started_exam:
        expires_at = attempt['started_at'] + timedelta(minutes=attempt['allowed_time_limit_mins'])
        does_time_remain = datetime.now(pytz.UTC) < expires_at

    if not attempt:
        # determine whether to show a timed exam only entrance screen
        # or a screen regarding proctoring

        if is_proctored:
            if exam['is_practice_exam']:
                student_view_template = 'proctoring/seq_proctored_practice_exam_entrance.html'
            else:
                student_view_template = 'proctoring/seq_proctored_exam_entrance.html'
        else:
            student_view_template = 'proctoring/seq_timed_exam_entrance.html'

    elif attempt['status'] == ProctoredExamStudentAttemptStatus.created:
        course_id = exam['course_id']
        course_key = CourseKey.from_string(course_id)
        course = modulestore().get_course(course_key)
        provider_name = course.proctoring_service
        provider = get_backend_provider(provider_name)
        student_view_template = 'proctoring/seq_proctored_exam_instructions.html'
        context.update({
            'exam_code': attempt['attempt_code'],
            'software_download_url': provider.get_software_download_url(),
        })
    elif attempt['status'] == ProctoredExamStudentAttemptStatus.ready_to_start:
        student_view_template = 'proctoring/seq_proctored_exam_ready_to_start.html'
    elif attempt['status'] == ProctoredExamStudentAttemptStatus.error:
        if attempt['is_sample_attempt']:
            student_view_template = 'proctoring/seq_proctored_practice_exam_error.html'
        else:
            student_view_template = 'proctoring/seq_proctored_exam_error.html'
    elif attempt['status'] == ProctoredExamStudentAttemptStatus.timed_out:
        student_view_template = 'proctoring/seq_timed_exam_expired.html'
    elif attempt['status'] == ProctoredExamStudentAttemptStatus.submitted:
        if attempt['is_sample_attempt']:
            student_view_template = 'proctoring/seq_proctored_practice_exam_submitted.html'
        else:
            student_view_template = 'proctoring/seq_proctored_exam_submitted.html'
    elif attempt['status'] == ProctoredExamStudentAttemptStatus.verified:
        student_view_template = 'proctoring/seq_proctored_exam_verified.html'
    elif attempt['status'] == ProctoredExamStudentAttemptStatus.rejected:
        student_view_template = 'proctoring/seq_proctored_exam_rejected.html'
    elif attempt['status'] == ProctoredExamStudentAttemptStatus.ready_to_submit:
        if is_proctored:
            student_view_template = 'proctoring/seq_proctored_exam_ready_to_submit.html'
        else:
            student_view_template = 'proctoring/seq_timed_exam_ready_to_submit.html'

    if student_view_template:
        template = loader.get_template(student_view_template)
        django_context = Context(context)
        attempt_time = attempt['allowed_time_limit_mins'] if attempt else exam['time_limit_mins']
        total_time = humanized_time(attempt_time)
        progress_page_url = ''
        try:
            progress_page_url = reverse(
                'courseware.views.progress',
                args=[course_id]
            )
        except NoReverseMatch:
            # we are allowing a failure here since we can't guarantee
            # that we are running in-proc with the edx-platform LMS
            # (for example unit tests)
            pass

        django_context.update({
            'platform_name': settings.PLATFORM_NAME,
            'total_time': total_time,
            'exam_id': exam_id,
            'progress_page_url': progress_page_url,
            'is_sample_attempt': attempt['is_sample_attempt'] if attempt else False,
            'does_time_remain': does_time_remain,
            'enter_exam_endpoint': reverse('edx_proctoring.proctored_exam.attempt.collection'),
            'exam_started_poll_url': reverse(
                'edx_proctoring.proctored_exam.attempt',
                args=[attempt['id']]
            ) if attempt else '',
            'change_state_url': reverse(
                'edx_proctoring.proctored_exam.attempt',
                args=[attempt['id']]
            ) if attempt else '',
            'link_urls': settings.PROCTORING_SETTINGS.get('LINK_URLS', {}),
        })
        return template.render(django_context)

    return None
Beispiel #11
0
    def get(self, request):  # pylint: disable=unused-argument
        """
        HTTP GET Handler. Returns the status of the exam attempt.
        """

        exams = get_active_exams_for_user(request.user.id)

        if exams:
            exam_info = exams[0]

            exam = exam_info['exam']
            attempt = exam_info['attempt']

            time_remaining_seconds = get_time_remaining_for_attempt(attempt)

            proctoring_settings = getattr(settings, 'PROCTORING_SETTINGS', {})
            low_threshold_pct = proctoring_settings.get('low_threshold_pct', .2)
            critically_low_threshold_pct = proctoring_settings.get('critically_low_threshold_pct', .05)

            low_threshold = int(low_threshold_pct * float(attempt['allowed_time_limit_mins']) * 60)
            critically_low_threshold = int(
                critically_low_threshold_pct * float(attempt['allowed_time_limit_mins']) * 60
            )

            exam_url_path = ''
            try:
                # resolve the LMS url, note we can't assume we're running in
                # a same process as the LMS
                exam_url_path = reverse(
                    'courseware.views.jump_to',
                    args=[exam['course_id'], exam['content_id']]
                )
            except NoReverseMatch:
                pass

            response_dict = {
                'in_timed_exam': True,
                'taking_as_proctored': attempt['taking_as_proctored'],
                'exam_type': (
                    _('timed') if not attempt['taking_as_proctored'] else
                    (_('practice') if attempt['is_sample_attempt'] else _('proctored'))
                ),
                'exam_display_name': exam['exam_name'],
                'exam_url_path': exam_url_path,
                'time_remaining_seconds': time_remaining_seconds,
                'low_threshold_sec': low_threshold,
                'critically_low_threshold_sec': critically_low_threshold,
                'course_id': exam['course_id'],
                'attempt_id': attempt['id'],
                'accessibility_time_string': _('you have {remaining_time} remaining').format(
                    remaining_time=humanized_time(int(round(time_remaining_seconds / 60.0, 0)))
                ),
                'attempt_status': attempt['status']
            }
        else:
            response_dict = {
                'in_timed_exam': False,
                'is_proctored': False
            }

        return Response(
            data=response_dict,
            status=status.HTTP_200_OK
        )
Beispiel #12
0
    def get(self, request, attempt_id):
        """
        HTTP GET Handler. Returns the status of the exam attempt.
        """

        try:
            attempt = get_exam_attempt_by_id(attempt_id)

            if not attempt:
                err_msg = (
                    'Attempted to access attempt_id {attempt_id} but '
                    'it does not exist.'.format(
                        attempt_id=attempt_id
                    )
                )
                return Response(
                    status=status.HTTP_400_BAD_REQUEST
                )

            # make sure the the attempt belongs to the calling user_id
            if attempt['user']['id'] != request.user.id:
                err_msg = (
                    'Attempted to access attempt_id {attempt_id} but '
                    'does not have access to it.'.format(
                        attempt_id=attempt_id
                    )
                )
                raise ProctoredExamPermissionDenied(err_msg)

            # check if the last_poll_timestamp is not None
            # and if it is older than SOFTWARE_SECURE_CLIENT_TIMEOUT
            # then attempt status should be marked as error.
            last_poll_timestamp = attempt['last_poll_timestamp']

            # if we never heard from the client, then we assume it is shut down
            attempt['client_has_shutdown'] = last_poll_timestamp is None

            if last_poll_timestamp is not None:
                # Let's pass along information if we think the SoftwareSecure has completed
                # a healthy shutdown which is when our attempt is in a 'submitted' status
                if attempt['status'] == ProctoredExamStudentAttemptStatus.submitted:
                    attempt['client_has_shutdown'] = has_client_app_shutdown(attempt)
                else:
                    # otherwise, let's see if the shutdown happened in error
                    # e.g. a crash
                    time_passed_since_last_poll = (datetime.now(pytz.UTC) - last_poll_timestamp).total_seconds()
                    if time_passed_since_last_poll > constants.SOFTWARE_SECURE_CLIENT_TIMEOUT:
                        try:
                            update_attempt_status(
                                attempt['proctored_exam']['id'],
                                attempt['user']['id'],
                                ProctoredExamStudentAttemptStatus.error
                            )
                            attempt['status'] = ProctoredExamStudentAttemptStatus.error
                        except ProctoredExamIllegalStatusTransition:
                            # don't transition a completed state to an error state
                            pass

            # add in the computed time remaining as a helper to a client app
            time_remaining_seconds = get_time_remaining_for_attempt(attempt)

            attempt['time_remaining_seconds'] = time_remaining_seconds

            accessibility_time_string = _('you have {remaining_time} remaining').format(
                remaining_time=humanized_time(int(round(time_remaining_seconds / 60.0, 0))))

            # special case if we are less than a minute, since we don't produce
            # text translations of granularity at the seconds range
            if time_remaining_seconds < 60:
                accessibility_time_string = _('you have less than a minute remaining')

            attempt['accessibility_time_string'] = accessibility_time_string

            return Response(
                data=attempt,
                status=status.HTTP_200_OK
            )

        except ProctoredBaseException, ex:
            LOG.exception(ex)
            return Response(
                status=status.HTTP_400_BAD_REQUEST,
                data={"detail": str(ex)}
            )
Beispiel #13
0
def get_student_view(user_id,
                     course_id,
                     content_id,
                     context,
                     user_role='student'):
    """
    Helper method that will return the view HTML related to the exam control
    flow (i.e. entering, expired, completed, etc.) If there is no specific
    content to display, then None will be returned and the caller should
    render it's own view
    """

    # non-student roles should never see any proctoring related
    # screens
    if user_role != 'student':
        return None

    student_view_template = None

    exam_id = None
    try:
        exam = get_exam_by_content_id(course_id, content_id)
        if not exam['is_active']:
            # Exam is no longer active
            # Note, we don't hard delete exams since we need to retain
            # data
            return None

        exam_id = exam['id']
    except ProctoredExamNotFoundException:
        # This really shouldn't happen
        # as Studio will be setting this up
        exam_id = create_exam(
            course_id=course_id,
            content_id=unicode(content_id),
            exam_name=context['display_name'],
            time_limit_mins=context['default_time_limit_mins'],
            is_proctored=context.get('is_proctored', False),
            is_practice_exam=context.get('is_practice_exam', False))
        exam = get_exam_by_content_id(course_id, content_id)

    is_proctored = exam['is_proctored']

    provider_name = get_provider_name_by_course_id(exam['course_id'])
    proctoring_settings = get_proctoring_settings(provider_name)
    # see if only 'verified' track students should see this *except* if it is a practice exam
    check_mode_and_eligibility = (proctoring_settings.get(
        'MUST_BE_VERIFIED_TRACK', True) and 'credit_state' in context
                                  and context['credit_state']
                                  and not exam['is_practice_exam'])

    attempt = None
    if check_mode_and_eligibility:
        credit_state = context['credit_state']

        has_mode = _check_eligibility_of_enrollment_mode(credit_state)
        has_prerequisites = False
        if has_mode:
            has_prerequisites = _check_eligibility_of_prerequisites(
                credit_state)

        # see if the user has passed all pre-requisite credit eligibility
        # checks, otherwise just show the user the exam unproctored
        if not has_mode or not has_prerequisites:
            # if we are in the right mode and if we don't have
            # pre-requisites, then we implicitly decline the exam
            if has_mode:
                attempt = get_exam_attempt(exam_id, user_id)
                if not attempt:
                    # user hasn't a record of attempt, create one now
                    # so we can mark it as declined
                    create_exam_attempt(exam_id, user_id)

                    update_attempt_status(
                        exam_id,
                        user_id,
                        ProctoredExamStudentAttemptStatus.declined,
                        raise_if_not_found=False)

            # don't override context, let the courseware show
            return None

    attempt = get_exam_attempt(exam_id, user_id)

    # if user has declined the attempt, then we don't show the
    # proctored exam
    if attempt and attempt[
            'status'] == ProctoredExamStudentAttemptStatus.declined:
        return None

    does_time_remain = False
    has_started_exam = attempt and attempt.get('started_at')
    if has_started_exam:
        expires_at = attempt['started_at'] + timedelta(
            minutes=attempt['allowed_time_limit_mins'])
        does_time_remain = datetime.now(pytz.UTC) < expires_at

    if not attempt:
        # determine whether to show a timed exam only entrance screen
        # or a screen regarding proctoring

        if is_proctored:
            if exam['is_practice_exam']:
                student_view_template = 'proctoring/seq_proctored_practice_exam_entrance.html'
            else:
                student_view_template = 'proctoring/seq_proctored_exam_entrance.html'
        else:
            student_view_template = 'proctoring/seq_timed_exam_entrance.html'

    elif attempt['status'] == ProctoredExamStudentAttemptStatus.created:
        provider_name = get_provider_name_by_course_id(exam['course_id'])
        provider = get_backend_provider(provider_name)
        student_view_template = 'proctoring/seq_proctored_exam_instructions.html'
        context.update({
            'exam_code':
            attempt['attempt_code'],
            'software_download_url':
            provider.get_software_download_url(),
        })
    elif attempt['status'] == ProctoredExamStudentAttemptStatus.ready_to_start:
        student_view_template = 'proctoring/seq_proctored_exam_ready_to_start.html'
    elif attempt['status'] == ProctoredExamStudentAttemptStatus.error:
        if attempt['is_sample_attempt']:
            student_view_template = 'proctoring/seq_proctored_practice_exam_error.html'
        else:
            student_view_template = 'proctoring/seq_proctored_exam_error.html'
    elif attempt['status'] == ProctoredExamStudentAttemptStatus.timed_out:
        student_view_template = 'proctoring/seq_timed_exam_expired.html'
    elif attempt['status'] == ProctoredExamStudentAttemptStatus.submitted:
        if attempt['is_sample_attempt']:
            student_view_template = 'proctoring/seq_proctored_practice_exam_submitted.html'
        else:
            student_view_template = 'proctoring/seq_proctored_exam_submitted.html'
    elif attempt['status'] == ProctoredExamStudentAttemptStatus.verified:
        student_view_template = 'proctoring/seq_proctored_exam_verified.html'
    elif attempt['status'] == ProctoredExamStudentAttemptStatus.rejected:
        student_view_template = 'proctoring/seq_proctored_exam_rejected.html'
    elif attempt[
            'status'] == ProctoredExamStudentAttemptStatus.ready_to_submit:
        if is_proctored:
            student_view_template = 'proctoring/seq_proctored_exam_ready_to_submit.html'
        else:
            student_view_template = 'proctoring/seq_timed_exam_ready_to_submit.html'

    if student_view_template:
        template = loader.get_template(student_view_template)
        django_context = Context(context)
        attempt_time = attempt['allowed_time_limit_mins'] if attempt else exam[
            'time_limit_mins']
        total_time = humanized_time(attempt_time)
        progress_page_url = ''
        try:
            progress_page_url = reverse('courseware.views.progress',
                                        args=[course_id])
        except NoReverseMatch:
            # we are allowing a failure here since we can't guarantee
            # that we are running in-proc with the edx-platform LMS
            # (for example unit tests)
            pass

        provider_name = get_provider_name_by_course_id(exam['course_id'])
        proctoring_settings = get_proctoring_settings(provider_name)

        django_context.update({
            'platform_name':
            settings.PLATFORM_NAME,
            'total_time':
            total_time,
            'exam_id':
            exam_id,
            'progress_page_url':
            progress_page_url,
            'is_sample_attempt':
            attempt['is_sample_attempt'] if attempt else False,
            'does_time_remain':
            does_time_remain,
            'enter_exam_endpoint':
            reverse('edx_proctoring.proctored_exam.attempt.collection'),
            'exam_started_poll_url':
            reverse('edx_proctoring.proctored_exam.attempt',
                    args=[attempt['id']]) if attempt else '',
            'change_state_url':
            reverse('edx_proctoring.proctored_exam.attempt',
                    args=[attempt['id']]) if attempt else '',
            'link_urls':
            proctoring_settings.get('LINK_URLS', {}),
        })
        return template.render(django_context)

    return None
Beispiel #14
0
    def get(self, request):  # pylint: disable=unused-argument
        """
        HTTP GET Handler. Returns the status of the exam attempt.
        """

        exams = get_active_exams_for_user(request.user.id)

        if exams:
            exam_info = exams[0]

            exam = exam_info['exam']
            attempt = exam_info['attempt']

            provider = get_backend_provider(exam)

            time_remaining_seconds = get_time_remaining_for_attempt(attempt)

            proctoring_settings = getattr(settings, 'PROCTORING_SETTINGS', {})
            low_threshold_pct = proctoring_settings.get('low_threshold_pct', .2)
            critically_low_threshold_pct = proctoring_settings.get('critically_low_threshold_pct', .05)

            low_threshold = int(low_threshold_pct * float(attempt['allowed_time_limit_mins']) * 60)
            critically_low_threshold = int(
                critically_low_threshold_pct * float(attempt['allowed_time_limit_mins']) * 60
            )

            exam_url_path = ''
            try:
                # resolve the LMS url, note we can't assume we're running in
                # a same process as the LMS
                exam_url_path = reverse('jump_to', args=[exam['course_id'], exam['content_id']])
            except NoReverseMatch:
                LOG.exception("Can't find exam url for course %s", exam['course_id'])

            response_dict = {
                'in_timed_exam': True,
                'taking_as_proctored': attempt['taking_as_proctored'],
                'exam_type': (
                    _('a timed exam') if not attempt['taking_as_proctored'] else
                    (_('a proctored exam') if not attempt['is_sample_attempt'] else
                     (_('an onboarding exam') if provider.supports_onboarding else _('a practice exam')))
                ),
                'exam_display_name': exam['exam_name'],
                'exam_url_path': exam_url_path,
                'time_remaining_seconds': time_remaining_seconds,
                'low_threshold_sec': low_threshold,
                'critically_low_threshold_sec': critically_low_threshold,
                'course_id': exam['course_id'],
                'attempt_id': attempt['id'],
                'accessibility_time_string': _('you have {remaining_time} remaining').format(
                    remaining_time=humanized_time(int(round(time_remaining_seconds / 60.0, 0)))
                ),
                'attempt_status': attempt['status'],
                'exam_started_poll_url': reverse(
                    'edx_proctoring:proctored_exam.attempt',
                    args=[attempt['id']]
                ),

            }

            if provider:
                response_dict['desktop_application_js_url'] = provider.get_javascript()
                response_dict['ping_interval'] = provider.ping_interval
            else:
                response_dict['desktop_application_js_url'] = ''

        else:
            response_dict = {
                'in_timed_exam': False,
                'is_proctored': False
            }

        return Response(data=response_dict, status=status.HTTP_200_OK)