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 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 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")
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_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
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_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
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)