Example #1
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 = (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)
Example #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)
Example #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)}
            )
Example #4
0
 def test_not_started(self):
     """
     Test to return 0 if the exam attempt has not been started.
     """
     attempt = {
         'started_at': None,
         'allowed_time_limit_mins': 10,
         'time_remaining_seconds': None
     }
     time_remaining_seconds = get_time_remaining_for_attempt(attempt)
     self.assertEqual(time_remaining_seconds, 0)
Example #5
0
 def test_get_time_remaining_started(self):
     """
     Test to get the time remaining on an attempt after the exam has started.
     """
     with freeze_time(self.now_utc):
         attempt = {
             'started_at': self.now_utc,
             'allowed_time_limit_mins': 10,
             'time_remaining_seconds': None
         }
         time_remaining_seconds = get_time_remaining_for_attempt(attempt)
         self.assertEqual(time_remaining_seconds, 600)
Example #6
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)
Example #7
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)
Example #8
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)})
Example #9
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)
Example #10
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
        )
Example #11
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)}
            )
Example #12
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)