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)
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)
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)} )
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)
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)
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)
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)
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)})
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 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 )
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)} )
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)