def test_dangling_exam(self): """ Make sure we filter out all dangling items """ chapter = ItemFactory.create(parent=self.course, category='chapter', display_name='Test Section') ItemFactory.create( parent=chapter, category='sequential', display_name='Test Proctored Exam', graded=True, is_time_limited=True, default_time_limit_minutes=10, is_proctored_exam=True, hide_after_due=False, ) listen_for_course_publish(self, self.course.id) exams = get_all_exams_for_course(unicode(self.course.id)) self.assertEqual(len(exams), 1) self.store.delete_item(chapter.location, self.user.id) # republish course listen_for_course_publish(self, self.course.id) # look through exam table, the dangling exam # should be disabled exams = get_all_exams_for_course(unicode(self.course.id)) self.assertEqual(len(exams), 1) exam = exams[0] self.assertEqual(exam['is_active'], False)
def test_dangling_exam(self): """ Make sure we filter out all dangling items """ chapter = ItemFactory.create(parent=self.course, category='chapter', display_name='Test Section') ItemFactory.create( parent=chapter, category='sequential', display_name='Test Proctored Exam', graded=True, is_time_limited=True, default_time_limit_minutes=10, is_proctored_exam=True, hide_after_due=False, ) listen_for_course_publish(self, self.course.id) exams = get_all_exams_for_course(six.text_type(self.course.id)) self.assertEqual(len(exams), 1) self.store.delete_item(chapter.location, self.user.id) # republish course listen_for_course_publish(self, self.course.id) # look through exam table, the dangling exam # should be disabled exams = get_all_exams_for_course(six.text_type(self.course.id)) self.assertEqual(len(exams), 1) exam = exams[0] self.assertEqual(exam['is_active'], False)
def to_representation(self, instance): """ Object instance -> Dict of primitive datatypes. """ specific_proctoring_system = False available_proctoring_service = instance.available_proctoring_services.split(',') proctoring_system = self.context['request'].GET.get('proctoring_system') if len(available_proctoring_service) > 1 and proctoring_system: specific_proctoring_system = proctoring_system ret = OrderedDict() fields = [field for field in self.fields.values() if not field.write_only] exams = get_all_exams_for_course(course_id=instance.id, dt_expired=True, proctoring_service=specific_proctoring_system) for field in fields: try: attribute = field.get_attribute(instance) except SkipField: continue except AttributeError: if isinstance(field, ExamSerializerField): ret[field.field_name] = field.to_representation(instance, exams) continue if attribute is None: ret[field.field_name] = None else: ret[field.field_name] = field.to_representation(attribute) return ret
def test_unpublishing_proctored_exam(self): """ Make sure that if we publish and then unpublish a proctored exam, the exam record stays, but is marked as is_active=False """ chapter = ItemFactory.create(parent=self.course, category='chapter', display_name='Test Section') sequence = ItemFactory.create( parent=chapter, category='sequential', display_name='Test Proctored Exam', graded=True, is_time_limited=True, default_time_limit_minutes=10, is_proctored_exam=True, hide_after_due=False, ) listen_for_course_publish(self, self.course.id) exams = get_all_exams_for_course(unicode(self.course.id)) self.assertEqual(len(exams), 1) sequence.is_time_limited = False sequence.is_proctored_exam = False self.store.update_item(sequence, self.user.id) listen_for_course_publish(self, self.course.id) self._verify_exam_data(sequence, False)
def _verify_exam_data(self, sequence, expected_active): """ Helper method to compare the sequence with the stored exam, which should just be a single one """ exams = get_all_exams_for_course(six.text_type(self.course.id)) self.assertEqual(len(exams), 1) exam = exams[0] if exam['is_proctored'] and not exam['is_practice_exam']: # get the review policy object exam_review_policy = get_review_policy_by_exam_id(exam['id']) self.assertEqual(exam_review_policy['review_policy'], sequence.exam_review_rules) if not exam['is_proctored'] and not exam['is_practice_exam']: # the hide after due value only applies to timed exams self.assertEqual(exam['hide_after_due'], sequence.hide_after_due) self.assertEqual(exam['course_id'], six.text_type(self.course.id)) self.assertEqual(exam['content_id'], six.text_type(sequence.location)) self.assertEqual(exam['exam_name'], sequence.display_name) self.assertEqual(exam['time_limit_mins'], sequence.default_time_limit_minutes) self.assertEqual(exam['is_proctored'], sequence.is_proctored_exam) self.assertEqual(exam['is_practice_exam'], sequence.is_practice_exam or sequence.is_onboarding_exam) self.assertEqual(exam['is_active'], expected_active) self.assertEqual(exam['backend'], self.course.proctoring_provider)
def _get_proctoring_requirements(course_key): """ Will return list of requirements regarding any exams that have been marked as proctored exams. For credit-bearing courses, all proctored exams must be validated and confirmed from a proctoring standpoint. The passing grade on an exam is not enough. Args: course_key: The key of the course in question Returns: list of requirements dictionary, one per active proctored exam """ # Note: Need to import here as there appears to be # a circular reference happening when launching Studio # process from edx_proctoring.api import get_all_exams_for_course requirements = [{ 'namespace': 'proctored_exam', 'name': 'proctored_exam_id:{id}'.format(id=exam['id']), 'display_name': exam['exam_name'], 'criteria': {}, } for exam in get_all_exams_for_course(unicode(course_key)) if exam['is_proctored'] and exam['is_active']] log_msg = ( 'Registering the following as \'proctored_exam\' credit requirements: {log_msg}' .format(log_msg=requirements)) LOGGER.info(log_msg) return requirements
def _verify_exam_data(self, sequence, expected_active): """ Helper method to compare the sequence with the stored exam, which should just be a single one """ exams = get_all_exams_for_course(unicode(self.course.id)) self.assertEqual(len(exams), 1) exam = exams[0] if exam['is_proctored'] and not exam['is_practice_exam']: # get the review policy object exam_review_policy = get_review_policy_by_exam_id(exam['id']) self.assertEqual(exam_review_policy['review_policy'], sequence.exam_review_rules) if not exam['is_proctored'] and not exam['is_practice_exam']: # the hide after due value only applies to timed exams self.assertEqual(exam['hide_after_due'], sequence.hide_after_due) self.assertEqual(exam['course_id'], unicode(self.course.id)) self.assertEqual(exam['content_id'], unicode(sequence.location)) self.assertEqual(exam['exam_name'], sequence.display_name) self.assertEqual(exam['time_limit_mins'], sequence.default_time_limit_minutes) self.assertEqual(exam['is_proctored'], sequence.is_proctored_exam) self.assertEqual(exam['is_practice_exam'], sequence.is_practice_exam) self.assertEqual(exam['is_active'], expected_active)
def to_representation(self, instance): """ Object instance -> Dict of primitive datatypes. """ specific_proctoring_system = False available_proctoring_service = instance.available_proctoring_services.split(',') proctoring_system = self.context['request'].GET.get('proctoring_system') if len(available_proctoring_service) > 1 and proctoring_system: specific_proctoring_system = proctoring_system ret = OrderedDict() fields = [field for field in self.fields.values() if not field.write_only] exams = get_all_exams_for_course(course_id=instance.id, dt_expired=True, proctoring_service=specific_proctoring_system) for field in fields: try: attribute = field.get_attribute(instance) except SkipField: continue except AttributeError: if isinstance(field, ExamSerializerField): ret[field.field_name] = field.to_representation(instance, exams) continue if attribute is None: ret[field.field_name] = None else: ret[field.field_name] = field.to_representation(attribute) return ret
def get_user_proctored_exams(username, request): enrolments = CourseEnrollment.objects.filter(is_active=True, user__username=username) result = {} for enrolment in enrolments: course = enrolment.course course_id = str(course.id) cohorts = CourseUserGroup.objects.filter( course_id=enrolment.course_id, users__username=username, group_type=CourseUserGroup.COHORT, name=VERIFIED ) if course_id not in result and cohorts.exists(): result[course_id] = { "id": course_id, "name": course.display_name, "uri": request.build_absolute_uri( reverse('course_structure_api:v0:detail', kwargs={'course_id': course_id})), "image_url": course.course_image_url, "start": course.start, "end": course.end, 'exams': [] } exams = get_all_exams_for_course(course_id=course.id) for exam in exams: if exam['is_proctored'] == True: result[course_id]['exams'].append(exam) result = {key: value for key, value in result.items() if len(value['exams']) > 0} return result
def _get_proctoring_requirements(course_key): """ Will return list of requirements regarding any exams that have been marked as proctored exams. For credit-bearing courses, all proctored exams must be validated and confirmed from a proctoring standpoint. The passing grade on an exam is not enough. Args: course_key: The key of the course in question Returns: list of requirements dictionary, one per active proctored exam """ # Note: Need to import here as there appears to be # a circular reference happening when launching Studio # process from edx_proctoring.api import get_all_exams_for_course requirements = [ {"namespace": "proctored_exam", "name": exam["content_id"], "display_name": exam["exam_name"], "criteria": {}} for exam in get_all_exams_for_course(unicode(course_key)) # practice exams do not count towards eligibility if exam["is_proctored"] and exam["is_active"] and not exam["is_practice_exam"] ] log_msg = "Registering the following as 'proctored_exam' credit requirements: {log_msg}".format( log_msg=requirements ) LOGGER.info(log_msg) return requirements
def test_unpublishing_proctored_exam(self): """ Make sure that if we publish and then unpublish a proctored exam, the exam record stays, but is marked as is_active=False """ chapter = ItemFactory.create(parent=self.course, category='chapter', display_name='Test Section') sequence = ItemFactory.create( parent=chapter, category='sequential', display_name='Test Proctored Exam', graded=True, is_time_limited=True, default_time_limit_minutes=10, is_proctored_exam=True, hide_after_due=False, is_onboarding_exam=False, ) listen_for_course_publish(self, self.course.id) exams = get_all_exams_for_course(six.text_type(self.course.id)) self.assertEqual(len(exams), 1) sequence.is_time_limited = False sequence.is_proctored_exam = False self.store.update_item(sequence, self.user.id) listen_for_course_publish(self, self.course.id) self._verify_exam_data(sequence, False)
def test_advanced_settings(self, enable_timed_exams, enable_proctored_exams, expected_count): """ Make sure the feature flag is honored """ self.course = CourseFactory.create( org='edX', course='901', run='test_run2', enable_proctored_exams=enable_proctored_exams, enable_timed_exams=enable_timed_exams ) chapter = ItemFactory.create(parent=self.course, category='chapter', display_name='Test Section') ItemFactory.create( parent=chapter, category='sequential', display_name='Test Proctored Exam', graded=True, is_time_limited=True, default_time_limit_minutes=10, is_proctored_exam=True, exam_review_rules="allow_use_of_paper", hide_after_due=False, ) listen_for_course_publish(self, self.course.id) # there shouldn't be any exams because we haven't enabled that # advanced setting flag exams = get_all_exams_for_course(six.text_type(self.course.id)) self.assertEqual(len(exams), expected_count)
def test_advanced_setting_off(self): """ Make sure the feature flag is honored """ self.course = CourseFactory.create(org='edX', course='901', run='test_run2', enable_proctored_exams=False) chapter = ItemFactory.create(parent=self.course, category='chapter', display_name='Test Section') ItemFactory.create(parent=chapter, category='sequential', display_name='Test Proctored Exam', graded=True, is_time_limited=True, default_time_limit_minutes=10, is_proctored_enabled=True) listen_for_course_publish(self, self.course.id) # there shouldn't be any exams because we haven't enabled that # advanced setting flag exams = get_all_exams_for_course(unicode(self.course.id)) self.assertEqual(len(exams), 0)
def _get_exams(self, course, is_proctored): exams = get_all_exams_for_course(course_id=course.id) result = [] for exam in exams: if exam.get('is_proctored') == is_proctored: result.append(exam) return result
def test_advanced_settings(self, enable_timed_exams, enable_proctored_exams, expected_count): """ Make sure the feature flag is honored """ self.course = CourseFactory.create( org='edX', course='901', run='test_run2', enable_proctored_exams=enable_proctored_exams, enable_timed_exams=enable_timed_exams ) chapter = ItemFactory.create(parent=self.course, category='chapter', display_name='Test Section') ItemFactory.create( parent=chapter, category='sequential', display_name='Test Proctored Exam', graded=True, is_time_limited=True, default_time_limit_minutes=10, is_proctored_exam=True, exam_review_rules="allow_use_of_paper", hide_after_due=False, ) listen_for_course_publish(self, self.course.id) # there shouldn't be any exams because we haven't enabled that # advanced setting flag exams = get_all_exams_for_course(unicode(self.course.id)) self.assertEqual(len(exams), expected_count)
def get(self, request, course_id, exam_id=None): """ Redirect to dashboard for a given course and optional exam_id """ exam = None attempt_id = None ext_exam_id = None show_configuration_dashboard = False if exam_id: exam = get_exam_by_id(exam_id) # the exam_id in the url is our database id (for ease of lookups) # but the backend needs its external id for the instructor dashboard ext_exam_id = exam['external_id'] attempt_id = request.GET.get('attempt', None) # only show the configuration dashboard if an exam_id is passed in show_configuration_dashboard = request.GET.get('config', '').lower() == 'true' else: found_backend = None for exam in get_all_exams_for_course(course_id, True): exam_backend = exam['backend'] or settings.PROCTORING_BACKENDS.get('DEFAULT', None) if found_backend and exam_backend != found_backend: # In this case, what are we supposed to do?! # It should not be possible to get in this state, because # course teams will be prevented from updating the backend after the course start date error_message = "Multiple backends for course %r %r != %r" % (course_id, found_backend, exam['backend']) return Response(data=error_message, status=400) else: found_backend = exam_backend if exam is None: error = _('No exams in course {course_id}.').format(course_id=course_id) else: backend = get_backend_provider(exam) if backend: user = { 'id': obscured_user_id(request.user.id, exam['backend']), 'full_name': request.user.get_full_name(), 'email': request.user.email } url = backend.get_instructor_url( exam['course_id'], user, exam_id=ext_exam_id, attempt_id=attempt_id, show_configuration_dashboard=show_configuration_dashboard ) if url: return redirect(url) else: error = _('No instructor dashboard for {proctor_service}').format( proctor_service=backend.verbose_name) else: error = _('No proctored exams in course {course_id}').format(course_id=course_id) return Response(data=error, status=404, headers={'X-Frame-Options': 'sameorigin'})
def get(self, request, course_id, exam_id=None): """ Redirect to dashboard for a given course and optional exam_id """ exam = None attempt_id = None ext_exam_id = None show_configuration_dashboard = False if exam_id: exam = get_exam_by_id(exam_id) # the exam_id in the url is our database id (for ease of lookups) # but the backend needs its external id for the instructor dashboard ext_exam_id = exam['external_id'] attempt_id = request.GET.get('attempt', None) # only show the configuration dashboard if an exam_id is passed in show_configuration_dashboard = request.GET.get('config', '').lower() == 'true' else: found_backend = None for exam in get_all_exams_for_course(course_id, True): exam_backend = exam['backend'] or settings.PROCTORING_BACKENDS.get('DEFAULT', None) if found_backend and exam_backend != found_backend: # In this case, what are we supposed to do?! # It should not be possible to get in this state, because # course teams will be prevented from updating the backend after the course start date error_message = "Multiple backends for course %r %r != %r" % (course_id, found_backend, exam['backend']) return Response(data=error_message, status=400) else: found_backend = exam_backend if exam is None: error = _('No exams in course {course_id}.').format(course_id=course_id) else: backend = get_backend_provider(exam) if backend: user = { 'id': obscured_user_id(request.user.id, exam['backend']), 'full_name': request.user.get_full_name(), 'email': request.user.email } url = backend.get_instructor_url( exam['course_id'], user, exam_id=ext_exam_id, attempt_id=attempt_id, show_configuration_dashboard=show_configuration_dashboard ) if url: return redirect(url) else: error = _('No instructor dashboard for {proctor_service}').format( proctor_service=backend.verbose_name) else: error = _('No proctored exams in course {course_id}').format(course_id=course_id) return Response(data=error, status=404, headers={'X-Frame-Options': 'sameorigin'})
def _get_proctoring_requirements(course_key): """ Will return list of requirements regarding any exams that have been marked as proctored exams. For credit-bearing courses, all proctored exams must be validated and confirmed from a proctoring standpoint. The passing grade on an exam is not enough. Args: course_key: The key of the course in question Returns: list of requirements dictionary, one per active proctored exam """ # Note: Need to import here as there appears to be # a circular reference happening when launching Studio # process from edx_proctoring.api import get_all_exams_for_course requirements = [] for exam in get_all_exams_for_course(unicode(course_key)): if exam['is_proctored'] and exam[ 'is_active'] and not exam['is_practice_exam']: try: usage_key = UsageKey.from_string(exam['content_id']) proctor_block = modulestore().get_item(usage_key) except (InvalidKeyError, ItemNotFoundError): LOGGER.info( u"Invalid content_id '%s' for proctored block '%s'", exam['content_id'], exam['exam_name']) proctor_block = None if proctor_block: requirements.append({ 'namespace': 'proctored_exam', 'name': exam['content_id'], 'display_name': exam['exam_name'], 'start_date': proctor_block.start if proctor_block.start else None, 'criteria': {}, }) if requirements: log_msg = ( u'Registering the following as \'proctored_exam\' credit requirements: {log_msg}' .format(log_msg=requirements)) LOGGER.info(log_msg) return requirements
def test_self_paced_no_due_dates(self): self.course = CourseFactory.create( org='edX', course='901', run='test_run2', enable_proctored_exams=True, enable_timed_exams=True, self_paced=True, ) chapter = ItemFactory.create(parent=self.course, category='chapter', display_name='Test Section') ItemFactory.create( parent=chapter, category='sequential', display_name='Test Proctored Exam', graded=True, is_time_limited=True, default_time_limit_minutes=60, is_proctored_exam=False, is_practice_exam=False, due=datetime.now(UTC) + timedelta(minutes=60), exam_review_rules="allow_use_of_paper", hide_after_due=True, is_onboarding_exam=False, ) listen_for_course_publish(self, self.course.id) exams = get_all_exams_for_course(six.text_type(self.course.id)) # self-paced courses should ignore due dates assert exams[0]['due_date'] is None # now switch to instructor paced # the exam will be updated with a due date self.course.self_paced = False self.course = self.update_course(self.course, 1) listen_for_course_publish(self, self.course.id) exams = get_all_exams_for_course(six.text_type(self.course.id)) assert exams[0]['due_date'] is not None
def _get_proctoring_requirements(course_key): """ Will return list of requirements regarding any exams that have been marked as proctored exams. For credit-bearing courses, all proctored exams must be validated and confirmed from a proctoring standpoint. The passing grade on an exam is not enough. Args: course_key: The key of the course in question Returns: list of requirements dictionary, one per active proctored exam """ # Note: Need to import here as there appears to be # a circular reference happening when launching Studio # process from edx_proctoring.api import get_all_exams_for_course requirements = [] for exam in get_all_exams_for_course(six.text_type(course_key)): if exam['is_proctored'] and exam['is_active'] and not exam['is_practice_exam']: try: usage_key = UsageKey.from_string(exam['content_id']) proctor_block = modulestore().get_item(usage_key) except (InvalidKeyError, ItemNotFoundError): LOGGER.info(u"Invalid content_id '%s' for proctored block '%s'", exam['content_id'], exam['exam_name']) proctor_block = None if proctor_block: requirements.append( { 'namespace': 'proctored_exam', 'name': exam['content_id'], 'display_name': exam['exam_name'], 'start_date': proctor_block.start if proctor_block.start else None, 'criteria': {}, }) if requirements: log_msg = ( u'Registering the following as \'proctored_exam\' credit requirements: {log_msg}'.format( log_msg=requirements ) ) LOGGER.info(log_msg) return requirements
def _verify_exam_data(self, sequence, expected_active): """ Helper method to compare the sequence with the stored exam, which should just be a single one """ exams = get_all_exams_for_course(unicode(self.course.id)) self.assertEqual(len(exams), 1) exam = exams[0] self.assertEqual(exam['course_id'], unicode(self.course.id)) self.assertEqual(exam['content_id'], unicode(sequence.location)) self.assertEqual(exam['exam_name'], sequence.display_name) self.assertEqual(exam['time_limit_mins'], sequence.default_time_limit_minutes) self.assertEqual(exam['is_proctored'], sequence.is_proctored_enabled) self.assertEqual(exam['is_active'], expected_active)
def _verify_exam_data(self, sequence, expected_active): """ Helper method to compare the sequence with the stored exam, which should just be a single one """ exams = get_all_exams_for_course(unicode(self.course.id)) self.assertEqual(len(exams), 1) exam = exams[0] self.assertEqual(exam['course_id'], unicode(self.course.id)) self.assertEqual(exam['content_id'], unicode(sequence.location)) self.assertEqual(exam['exam_name'], sequence.display_name) self.assertEqual(exam['time_limit_mins'], sequence.default_time_limit_minutes) self.assertEqual(exam['is_proctored'], sequence.is_proctored_exam) self.assertEqual(exam['is_active'], expected_active)
def get(self, request, exam_id=None, course_id=None, content_id=None): # pylint: disable=unused-argument """ HTTP GET handler. Scenarios: by exam_id: calls get_exam_by_id() by course_id, content_id: get_exam_by_content_id() """ if exam_id: data = get_exam_by_id(exam_id) elif course_id is not None: if content_id is not None: # get by course_id & content_id data = get_exam_by_content_id(course_id, content_id) else: data = get_all_exams_for_course(course_id=course_id, active_only=True) return Response(data)
def test_feature_flag_off(self): """ Make sure the feature flag is honored """ chapter = ItemFactory.create(parent=self.course, category='chapter', display_name='Test Section') ItemFactory.create( parent=chapter, category='sequential', display_name='Test Proctored Exam', graded=True, is_time_limited=True, default_time_limit_minutes=10, is_proctored_exam=True, hide_after_due=False, ) listen_for_course_publish(self, self.course.id) exams = get_all_exams_for_course(unicode(self.course.id)) self.assertEqual(len(exams), 0)
def get(self, request, exam_id=None, course_id=None, content_id=None): # pylint: disable=unused-argument """ HTTP GET handler. Scenarios: by exam_id: calls get_exam_by_id() by course_id, content_id: get_exam_by_content_id() """ if exam_id: data = get_exam_by_id(exam_id) elif course_id is not None: if content_id is not None: # get by course_id & content_id data = get_exam_by_content_id(course_id, content_id) else: data = get_all_exams_for_course( course_id=course_id, active_only=True ) return Response(data)
def test_feature_flag_off(self): """ Make sure the feature flag is honored """ chapter = ItemFactory.create(parent=self.course, category='chapter', display_name='Test Section') ItemFactory.create( parent=chapter, category='sequential', display_name='Test Proctored Exam', graded=True, is_time_limited=True, default_time_limit_minutes=10, is_proctored_exam=True, hide_after_due=False, ) listen_for_course_publish(self, self.course.id) exams = get_all_exams_for_course(six.text_type(self.course.id)) self.assertEqual(len(exams), 0)
def test_async_waffle_flag_publishes(self): chapter = ItemFactory.create(parent=self.course, category='chapter', display_name='Test Section') sequence = ItemFactory.create( parent=chapter, category='sequential', display_name='Test Proctored Exam', graded=True, is_time_limited=True, default_time_limit_minutes=10, is_proctored_exam=True, hide_after_due=False, is_onboarding_exam=False, exam_review_rules="allow_use_of_paper", ) listen_for_course_publish(self, self.course.id) exams = get_all_exams_for_course(str(self.course.id)) self.assertEqual(len(exams), 1) self._verify_exam_data(sequence, True)
def get_user_proctored_exams(username, request): enrolments = CourseEnrollment.objects.filter(is_active=True, user__username=username) result = {} for enrolment in enrolments: course = enrolment.course course_id = str(course.id) cohorts = CourseUserGroup.objects.filter( course_id=enrolment.course_id, users__username=username, group_type=CourseUserGroup.COHORT, name=VERIFIED) if course_id not in result and cohorts.exists(): result[course_id] = { "id": course_id, "name": course.display_name, "uri": request.build_absolute_uri( reverse('course_structure_api:v0:detail', kwargs={'course_id': course_id})), "image_url": course.course_image_url, "start": course.start, "end": course.end, 'exams': [] } exams = get_all_exams_for_course(course_id=course.id) for exam in exams: if exam['is_proctored'] == True: result[course_id]['exams'].append(exam) result = { key: value for key, value in result.items() if len(value['exams']) > 0 } return result
def _get_proctoring_requirements(course_key): """ Will return list of requirements regarding any exams that have been marked as proctored exams. For credit-bearing courses, all proctored exams must be validated and confirmed from a proctoring standpoint. The passing grade on an exam is not enough. Args: course_key: The key of the course in question Returns: list of requirements dictionary, one per active proctored exam """ # Note: Need to import here as there appears to be # a circular reference happening when launching Studio # process from edx_proctoring.api import get_all_exams_for_course requirements = [ { 'namespace': 'proctored_exam', 'name': 'proctored_exam_id:{id}'.format(id=exam['id']), 'display_name': exam['exam_name'], 'criteria': {}, } for exam in get_all_exams_for_course(unicode(course_key)) if exam['is_proctored'] and exam['is_active'] ] log_msg = ( 'Registering the following as \'proctored_exam\' credit requirements: {log_msg}'.format( log_msg=requirements ) ) LOGGER.info(log_msg) return requirements
def _verify_exam_data(self, sequence, expected_active): """ Helper method to compare the sequence with the stored exam, which should just be a single one """ exams = get_all_exams_for_course(unicode(self.course.id)) self.assertEqual(len(exams), 1) exam = exams[0] if exam['is_proctored'] and not exam['is_practice_exam']: # get the review policy object exam_review_policy = get_review_policy_by_exam_id(exam['id']) self.assertEqual(exam_review_policy['review_policy'], sequence.exam_review_rules) self.assertEqual(exam['course_id'], unicode(self.course.id)) self.assertEqual(exam['content_id'], unicode(sequence.location)) self.assertEqual(exam['exam_name'], sequence.display_name) self.assertEqual(exam['time_limit_mins'], sequence.default_time_limit_minutes) self.assertEqual(exam['is_proctored'], sequence.is_proctored_exam) self.assertEqual(exam['is_practice_exam'], sequence.is_practice_exam) self.assertEqual(exam['is_active'], expected_active)
try: return Response( data=get_exam_by_content_id(course_id, content_id), status=status.HTTP_200_OK ) except ProctoredExamNotFoundException, ex: LOG.exception(ex) return Response( status=status.HTTP_400_BAD_REQUEST, data={"detail": "The exam with course_id, content_id does not exist."} ) else: timed_exams_only = not request.user.is_staff result_set = get_all_exams_for_course( course_id=course_id, timed_exams_only=timed_exams_only, active_only=True ) return Response(result_set) class StudentProctoredExamAttempt(AuthenticatedAPIView): """ Endpoint for the StudentProctoredExamAttempt /edx_proctoring/v1/proctored_exam/attempt Supports: HTTP POST: Starts an exam attempt. HTTP PUT: Stops an exam attempt. HTTP GET: Returns the status of an exam attempt.
def get_user_proctored_exams(username, request): enrollments = CourseEnrollment.objects.filter(is_active=True, user__username=username) system = request.data.get('system') if not system: system = request.GET.get('system') if system and 'ITMO' in system: system = 'ITMO' result = {} for enrollment in enrollments: course = enrollment.course if course and course.end and course.end < timezone.now(): continue try: course_id = str(course.id) except AttributeError: continue cohorts = CourseUserGroup.objects.filter( course_id=enrollment.course_id, users__username=username, group_type=CourseUserGroup.COHORT, name__iexact=VERIFIED ) if course_id not in result and cohorts.exists(): proctoring_service = modulestore().get_course(CourseKey.from_string(course_id)).available_proctoring_services.split(",") if system and system not in proctoring_service: continue result[course_id] = { "id": course_id, "name": course.display_name, "uri": request.build_absolute_uri( reverse('course_structure_api:v0:detail', kwargs={'course_id': course_id})), "image_url": course.course_image_url, "start": course.start, "end": course.end, "system": system, 'exams': [] } exams = get_all_exams_for_course(course_id=course.id) for exam in exams: if exam['is_proctored']: item_id = UsageKey.from_string(exam['content_id']) try: item = modulestore().get_item(item_id) except: logging.warning("Item {} not found".format(item_id)) continue logging.warning("{} {}".format(proctoring_service, item.exam_proctoring_system)) if len(proctoring_service) > 1 and not item.exam_proctoring_system: logging.warning("For course {} and exam {} proctoring service not specified. Available are {}".format(course_id, exam, proctoring_service)) continue if len(proctoring_service) > 1 and item.exam_proctoring_system and system and item.exam_proctoring_system != system: logging.warning("For course {} and exam {} proctoring service is {}, but system is {}".format(course_id, exam, item.exam_proctoring_system, system)) continue exam['visible_to_staff_only'] = item.visible_to_staff_only if hasattr(item, "exam_review_checkbox"): exam_review_checkbox = item.exam_review_checkbox if 'voice' in exam_review_checkbox: exam_review_checkbox['voices'] = exam_review_checkbox.pop('voice') if 'aid' in exam_review_checkbox: exam_review_checkbox['human_assistant'] = exam_review_checkbox.pop('aid') exam['exam_review_checkbox'] = exam_review_checkbox else: exam['exam_review_checkbox'] = {} oldest = None due_dates = [] for vertical in item.get_children(): if vertical.due: due_dates.append(vertical.due) if due_dates: oldest = min(due_dates) exam['deadline'] = oldest exam['start'] = item.start result[course_id]['exams'].append(exam) result = {key: value for key, value in result.items() if len(value['exams']) > 0} return result
def register_special_exams(course_key): """ This is typically called on a course published signal. The course is examined for sequences that are marked as timed exams. Then these are registered with the edx-proctoring subsystem. Likewise, if formerly registered exams are unmarked, then those registered exams are marked as inactive """ if not settings.FEATURES.get("ENABLE_SPECIAL_EXAMS"): # if feature is not enabled then do a quick exit return course = modulestore().get_course(course_key) if not course.enable_proctored_exams and not course.enable_timed_exams: # likewise if course does not have these features turned on # then quickly exit return # get all sequences, since they can be marked as timed/proctored exams _timed_exams = modulestore().get_items( course_key, qualifiers={"category": "sequential"}, settings={"is_time_limited": True} ) # filter out any potential dangling sequences timed_exams = [timed_exam for timed_exam in _timed_exams if is_item_in_course_tree(timed_exam)] # enumerate over list of sequences which are time-limited and # add/update any exam entries in edx-proctoring for timed_exam in timed_exams: msg = "Found {location} as a timed-exam in course structure. Inspecting...".format( location=unicode(timed_exam.location) ) log.info(msg) try: exam = get_exam_by_content_id(unicode(course_key), unicode(timed_exam.location)) # update case, make sure everything is synced update_exam( exam_id=exam["id"], exam_name=timed_exam.display_name, time_limit_mins=timed_exam.default_time_limit_minutes, due_date=timed_exam.due, is_proctored=timed_exam.is_proctored_exam, is_practice_exam=timed_exam.is_practice_exam, is_active=True, ) msg = "Updated timed exam {exam_id}".format(exam_id=exam["id"]) log.info(msg) except ProctoredExamNotFoundException: exam_id = create_exam( course_id=unicode(course_key), content_id=unicode(timed_exam.location), exam_name=timed_exam.display_name, time_limit_mins=timed_exam.default_time_limit_minutes, due_date=timed_exam.due, is_proctored=timed_exam.is_proctored_exam, is_practice_exam=timed_exam.is_practice_exam, is_active=True, ) msg = "Created new timed exam {exam_id}".format(exam_id=exam_id) log.info(msg) # then see which exams we have in edx-proctoring that are not in # our current list. That means the the user has disabled it exams = get_all_exams_for_course(course_key) for exam in exams: if exam["is_active"]: # try to look up the content_id in the sequences location search = [timed_exam for timed_exam in timed_exams if unicode(timed_exam.location) == exam["content_id"]] if not search: # This means it was turned off in Studio, we need to mark # the exam as inactive (we don't delete!) msg = "Disabling timed exam {exam_id}".format(exam_id=exam["id"]) log.info(msg) update_exam(exam_id=exam["id"], is_proctored=False, is_active=False)
def register_special_exams(course_key): """ This is typically called on a course published signal. The course is examined for sequences that are marked as timed exams. Then these are registered with the edx-proctoring subsystem. Likewise, if formerly registered exams are unmarked, then those registered exams are marked as inactive """ if not settings.FEATURES.get('ENABLE_SPECIAL_EXAMS'): # if feature is not enabled then do a quick exit return course = modulestore().get_course(course_key) if course is None: raise ItemNotFoundError(u"Course {} does not exist", six.text_type(course_key)) # lint-amnesty, pylint: disable=raising-format-tuple if not course.enable_proctored_exams and not course.enable_timed_exams: # likewise if course does not have these features turned on # then quickly exit return # get all sequences, since they can be marked as timed/proctored exams _timed_exams = modulestore().get_items(course_key, qualifiers={ 'category': 'sequential', }, settings={ 'is_time_limited': True, }) # filter out any potential dangling sequences timed_exams = [ timed_exam for timed_exam in _timed_exams if is_item_in_course_tree(timed_exam) ] # enumerate over list of sequences which are time-limited and # add/update any exam entries in edx-proctoring for timed_exam in timed_exams: msg = ( u'Found {location} as a timed-exam in course structure. Inspecting...' .format(location=six.text_type(timed_exam.location))) log.info(msg) exam_metadata = { 'exam_name': timed_exam.display_name, 'time_limit_mins': timed_exam.default_time_limit_minutes, 'due_date': timed_exam.due if not course.self_paced else None, 'is_proctored': timed_exam.is_proctored_exam, # backends that support onboarding exams will treat onboarding exams as practice 'is_practice_exam': timed_exam.is_practice_exam or timed_exam.is_onboarding_exam, 'is_active': True, 'hide_after_due': timed_exam.hide_after_due, 'backend': course.proctoring_provider, } try: exam = get_exam_by_content_id(six.text_type(course_key), six.text_type(timed_exam.location)) # update case, make sure everything is synced exam_metadata['exam_id'] = exam['id'] exam_id = update_exam(**exam_metadata) msg = u'Updated timed exam {exam_id}'.format(exam_id=exam['id']) log.info(msg) except ProctoredExamNotFoundException: exam_metadata['course_id'] = six.text_type(course_key) exam_metadata['content_id'] = six.text_type(timed_exam.location) exam_id = create_exam(**exam_metadata) msg = u'Created new timed exam {exam_id}'.format(exam_id=exam_id) log.info(msg) exam_review_policy_metadata = { 'exam_id': exam_id, 'set_by_user_id': timed_exam.edited_by, 'review_policy': timed_exam.exam_review_rules, } # only create/update exam policy for the proctored exams if timed_exam.is_proctored_exam and not timed_exam.is_practice_exam and not timed_exam.is_onboarding_exam: try: update_review_policy(**exam_review_policy_metadata) except ProctoredExamReviewPolicyNotFoundException: if timed_exam.exam_review_rules: # won't save an empty rule. create_exam_review_policy(**exam_review_policy_metadata) msg = u'Created new exam review policy with exam_id {exam_id}'.format( exam_id=exam_id) log.info(msg) else: try: # remove any associated review policy remove_review_policy(exam_id=exam_id) except ProctoredExamReviewPolicyNotFoundException: pass # then see which exams we have in edx-proctoring that are not in # our current list. That means the the user has disabled it exams = get_all_exams_for_course(course_key) for exam in exams: if exam['is_active']: # try to look up the content_id in the sequences location search = [ timed_exam for timed_exam in timed_exams if six.text_type(timed_exam.location) == exam['content_id'] ] if not search: # This means it was turned off in Studio, we need to mark # the exam as inactive (we don't delete!) msg = u'Disabling timed exam {exam_id}'.format( exam_id=exam['id']) log.info(msg) update_exam( exam_id=exam['id'], is_proctored=False, is_active=False, )
def get_user_proctored_exams(username, request): enrollments = CourseEnrollment.objects.filter(is_active=True, user__username=username) system = request.data.get('system') result = {} for enrollment in enrollments: course = enrollment.course try: course_id = str(course.id) except AttributeError: continue cohorts = CourseUserGroup.objects.filter( course_id=enrollment.course_id, users__username=username, group_type=CourseUserGroup.COHORT, name=VERIFIED) if course_id not in result and cohorts.exists(): proctoring_service = modulestore().get_course( CourseKey.from_string(course_id)).proctoring_service if system and system != proctoring_service: continue result[course_id] = { "id": course_id, "name": course.display_name, "uri": request.build_absolute_uri( reverse('course_structure_api:v0:detail', kwargs={'course_id': course_id})), "image_url": course.course_image_url, "start": course.start, "end": course.end, "system": proctoring_service, 'exams': [] } exams = get_all_exams_for_course(course_id=course.id) for exam in exams: if exam['is_proctored']: item_id = UsageKey.from_string(exam['content_id']) item = modulestore().get_item(item_id) exam['visible_to_staff_only'] = item.visible_to_staff_only oldest = None due_dates = [] for vertical in item.get_children(): if vertical.due: due_dates.append(vertical.due) if due_dates: oldest = min(due_dates) exam['deadline'] = oldest exam['start'] = item.start result[course_id]['exams'].append(exam) result = { key: value for key, value in result.items() if len(value['exams']) > 0 } return result
def get_user_proctored_exams(username, request): enrollments = CourseEnrollment.objects.filter(is_active=True, user__username=username, mode=VERIFIED) system = request.data.get('system') if not system: system = request.GET.get('system') if system: system = system.strip() if 'ITMOproctor' in system: system = 'ITMOproctor' result = {} course_ids = [] for enrollment in enrollments: course = enrollment.course if course and course.end and course.end < timezone.now(): continue try: course_id = str(course.id) except AttributeError: continue cohorts = CourseUserGroup.objects.filter( course_id=enrollment.course_id, users__username=username, group_type=CourseUserGroup.COHORT, name__startswith=VERIFIED) #if course_id not in course_ids and cohorts.exists(): if course_id not in course_ids: course_ids.append(course_id) courses = [] if course_ids: courses = ProctoredCourse.fetch_by_course_ids(course_ids) for course in courses: course_id = course.edx_id proctoring_service = [ c.strip() for c in course.available_proctoring_services.split(',') ] if system and system not in proctoring_service: continue result[course_id] = { 'id': course_id, 'name': course.display_name, 'uri': request.build_absolute_uri( reverse('course-detail', kwargs={'course_key_string': course_id})), 'image_url': course.image_url, 'start': course.start, 'end': course.end, 'system': system, 'exams': [] } exams = get_all_exams_for_course(course_id=course.id, detailed=True) for exam in exams: if exam['is_proctored']: exam_data = exam['extended_params'] if exam['extended_params'] and exam['extended_params']['updated'] \ else get_xblock_exam_params(exam['content_id']) exam_proctoring_system = exam_data['service'] if len(proctoring_service) > 1 and not exam_proctoring_system: logging.warning( 'For course {} and exam {} proctoring service not specified. Available are {}' .format(course_id, exam, proctoring_service)) continue if len( proctoring_service ) > 1 and exam_proctoring_system and exam_proctoring_system != system: logging.warning( 'For course {} and exam {} proctoring service is {}, but system is {}' .format(course_id, exam, exam_proctoring_system, system)) continue exam_review_checkbox = exam_data['exam_review_checkbox'] if 'voice' in exam_review_checkbox: exam_review_checkbox['voices'] = exam_review_checkbox.pop( 'voice') if 'aid' in exam_review_checkbox: exam_review_checkbox[ 'human_assistant'] = exam_review_checkbox.pop('aid') exam['exam_review_checkbox'] = exam_review_checkbox exam['visible_to_staff_only'] = exam_data[ 'visible_to_staff_only'] exam['start'] = exam_data['start'] exam['deadline'] = exam_data['deadline'] result[course_id]['exams'].append(exam) result = { key: value for key, value in result.items() if len(value['exams']) > 0 } return result
def register_special_exams(course_key): """ This is typically called on a course published signal. The course is examined for sequences that are marked as timed exams. Then these are registered with the edx-proctoring subsystem. Likewise, if formerly registered exams are unmarked, then those registered exams are marked as inactive """ if not settings.FEATURES.get('ENABLE_SPECIAL_EXAMS'): # if feature is not enabled then do a quick exit return course = modulestore().get_course(course_key) if not course.enable_proctored_exams and not course.enable_timed_exams: # likewise if course does not have these features turned on # then quickly exit return # get all sequences, since they can be marked as timed/proctored exams _timed_exams = modulestore().get_items(course_key, qualifiers={ 'category': 'sequential', }, settings={ 'is_time_limited': True, }) # filter out any potential dangling sequences timed_exams = [ timed_exam for timed_exam in _timed_exams if is_item_in_course_tree(timed_exam) ] # enumerate over list of sequences which are time-limited and # add/update any exam entries in edx-proctoring for timed_exam in timed_exams: msg = ( 'Found {location} as a timed-exam in course structure. Inspecting...' .format(location=unicode(timed_exam.location))) log.info(msg) try: exam = get_exam_by_content_id(unicode(course_key), unicode(timed_exam.location)) # update case, make sure everything is synced exam_id = update_exam( exam_id=exam['id'], exam_name=timed_exam.display_name, time_limit_mins=timed_exam.default_time_limit_minutes, due_date=timed_exam.due, is_proctored=timed_exam.is_proctored_exam, is_practice_exam=timed_exam.is_practice_exam, is_active=True, hide_after_due=timed_exam.hide_after_due, ) msg = 'Updated timed exam {exam_id}'.format(exam_id=exam['id']) log.info(msg) except ProctoredExamNotFoundException: exam_id = create_exam( course_id=unicode(course_key), content_id=unicode(timed_exam.location), exam_name=timed_exam.display_name, time_limit_mins=timed_exam.default_time_limit_minutes, due_date=timed_exam.due, is_proctored=timed_exam.is_proctored_exam, is_practice_exam=timed_exam.is_practice_exam, is_active=True, hide_after_due=timed_exam.hide_after_due, ) msg = 'Created new timed exam {exam_id}'.format(exam_id=exam_id) log.info(msg) # only create/update exam policy for the proctored exams if timed_exam.is_proctored_exam and not timed_exam.is_practice_exam: try: update_review_policy( exam_id=exam_id, set_by_user_id=timed_exam.edited_by, review_policy=timed_exam.exam_review_rules) except ProctoredExamReviewPolicyNotFoundException: if timed_exam.exam_review_rules: # won't save an empty rule. create_exam_review_policy( exam_id=exam_id, set_by_user_id=timed_exam.edited_by, review_policy=timed_exam.exam_review_rules) msg = 'Created new exam review policy with exam_id {exam_id}'.format( exam_id=exam_id) log.info(msg) else: try: # remove any associated review policy remove_review_policy(exam_id=exam_id) except ProctoredExamReviewPolicyNotFoundException: pass # then see which exams we have in edx-proctoring that are not in # our current list. That means the the user has disabled it exams = get_all_exams_for_course(course_key) for exam in exams: if exam['is_active']: # try to look up the content_id in the sequences location search = [ timed_exam for timed_exam in timed_exams if unicode(timed_exam.location) == exam['content_id'] ] if not search: # This means it was turned off in Studio, we need to mark # the exam as inactive (we don't delete!) msg = 'Disabling timed exam {exam_id}'.format( exam_id=exam['id']) log.info(msg) update_exam( exam_id=exam['id'], is_proctored=False, is_active=False, )
try: return Response(data=get_exam_by_content_id( course_id, content_id), status=status.HTTP_200_OK) except ProctoredExamNotFoundException, ex: LOG.exception(ex) return Response( status=status.HTTP_400_BAD_REQUEST, data={ "detail": "The exam with course_id, content_id does not exist." }) else: timed_exams_only = not request.user.is_staff result_set = get_all_exams_for_course( course_id=course_id, timed_exams_only=timed_exams_only, active_only=True) return Response(result_set) class StudentProctoredExamAttempt(AuthenticatedAPIView): """ Endpoint for the StudentProctoredExamAttempt /edx_proctoring/v1/proctored_exam/attempt Supports: HTTP POST: Starts an exam attempt. HTTP PUT: Stops an exam attempt. HTTP GET: Returns the status of an exam attempt.
if content_id is not None: # get by course_id & content_id try: return Response( data=get_exam_by_content_id(course_id, content_id), status=status.HTTP_200_OK ) except ProctoredExamNotFoundException, ex: LOG.exception(ex) return Response( status=status.HTTP_400_BAD_REQUEST, data={"detail": "The exam with course_id, content_id does not exist."} ) else: result_set = get_all_exams_for_course( course_id=course_id ) return Response(result_set) class StudentProctoredExamAttempt(AuthenticatedAPIView): """ Endpoint for the StudentProctoredExamAttempt /edx_proctoring/v1/proctored_exam/attempt Supports: HTTP POST: Starts an exam attempt. HTTP PUT: Stops an exam attempt. HTTP GET: Returns the status of an exam attempt.
if content_id is not None: # get by course_id & content_id try: return Response(data=get_exam_by_content_id( course_id, content_id), status=status.HTTP_200_OK) except ProctoredExamNotFoundException, ex: LOG.exception(ex) return Response( status=status.HTTP_400_BAD_REQUEST, data={ "detail": "The exam with course_id, content_id does not exist." }) else: result_set = get_all_exams_for_course(course_id=course_id) return Response(result_set) class StudentProctoredExamAttempt(AuthenticatedAPIView): """ Endpoint for the StudentProctoredExamAttempt /edx_proctoring/v1/proctored_exam/attempt Supports: HTTP POST: Starts an exam attempt. HTTP PUT: Stops an exam attempt. HTTP GET: Returns the status of an exam attempt. HTTP PUT
def get_user_proctored_exams(username, request): enrollments = CourseEnrollment.objects.filter(is_active=True, user__username=username, mode=VERIFIED) system = request.data.get('system') if not system: system = request.GET.get('system') if system: system = system.strip() if 'ITMO' in system: system = 'ITMO' result = {} course_ids = [] for enrollment in enrollments: course = enrollment.course if course and course.end and course.end < timezone.now(): continue try: course_id = str(course.id) except AttributeError: continue cohorts = CourseUserGroup.objects.filter( course_id=enrollment.course_id, users__username=username, group_type=CourseUserGroup.COHORT, name__startswith=VERIFIED ) if course_id not in course_ids and cohorts.exists(): course_ids.append(course_id) courses = [] if course_ids: courses = ProctoredCourse.fetch_by_course_ids(course_ids) for course in courses: course_id = course.edx_id proctoring_service = [c.strip() for c in course.available_proctoring_services.split(',')] if system and system not in proctoring_service: continue result[course_id] = { 'id': course_id, 'name': course.display_name, 'uri': request.build_absolute_uri( reverse('course_structure_api:v0:detail', kwargs={'course_id': course_id})), 'image_url': course.image_url, 'start': course.start, 'end': course.end, 'system': system, 'exams': [] } exams = get_all_exams_for_course(course_id=course.id, detailed=True) for exam in exams: if exam['is_proctored']: exam_data = exam['extended_params'] if exam['extended_params'] and exam['extended_params']['updated'] \ else get_xblock_exam_params(exam['content_id']) exam_proctoring_system = exam_data['service'] if len(proctoring_service) > 1 and not exam_proctoring_system: logging.warning('For course {} and exam {} proctoring service not specified. Available are {}' .format(course_id, exam, proctoring_service)) continue if len(proctoring_service) > 1 and exam_proctoring_system and exam_proctoring_system != system: logging.warning('For course {} and exam {} proctoring service is {}, but system is {}' .format(course_id, exam, exam_proctoring_system, system)) continue exam_review_checkbox = exam_data['exam_review_checkbox'] if 'voice' in exam_review_checkbox: exam_review_checkbox['voices'] = exam_review_checkbox.pop('voice') if 'aid' in exam_review_checkbox: exam_review_checkbox['human_assistant'] = exam_review_checkbox.pop('aid') exam['exam_review_checkbox'] = exam_review_checkbox exam['visible_to_staff_only'] = exam_data['visible_to_staff_only'] exam['start'] = exam_data['start'] exam['deadline'] = exam_data['deadline'] result[course_id]['exams'].append(exam) result = {key: value for key, value in result.items() if len(value['exams']) > 0} return result
def get(self, request, course_id, exam_id=None): """ Redirect to dashboard for a given course and optional exam_id """ exam = None backend = None ext_exam_id = None attempt_id = None show_configuration_dashboard = False if exam_id: exam = get_exam_by_id(exam_id) backend = get_backend_provider(exam=exam) # the exam_id in the url is our database id (for ease of lookups) # but the backend needs its external id for the instructor dashboard ext_exam_id = exam['external_id'] attempt_id = request.GET.get('attempt', None) # only show the configuration dashboard if an exam_id is passed in show_configuration_dashboard = request.GET.get('config', '').lower() == 'true' else: existing_backend_name = None for exam in get_all_exams_for_course(course_id, True): if not exam.get('is_proctored'): # We should only get backends of exams which are configured to be proctored continue exam_backend_name = exam.get('backend') backend = get_backend_provider(name=exam_backend_name) if existing_backend_name and exam_backend_name != existing_backend_name: # In this case, what are we supposed to do?! # It should not be possible to get in this state, because # course teams will be prevented from updating the backend after the course start date error_message = u"Multiple backends for course %r %r != %r" % ( course_id, existing_backend_name, exam_backend_name ) return Response(data=error_message, status=400) else: existing_backend_name = exam_backend_name if not exam: return Response( data=_(u'No exams in course {course_id}.').format(course_id=course_id), status=404, headers={'X-Frame-Options': 'sameorigin'} ) if not backend: return Response( data=_(u'No proctored exams in course {course_id}').format(course_id=course_id), status=404, headers={'X-Frame-Options': 'sameorigin'} ) user = { 'id': obscured_user_id(request.user.id, exam['backend']), 'full_name': request.user.profile.name, 'email': request.user.email } url = backend.get_instructor_url( exam['course_id'], user, exam_id=ext_exam_id, attempt_id=attempt_id, show_configuration_dashboard=show_configuration_dashboard ) if not url: return Response( data=_(u'No instructor dashboard for {proctor_service}').format( proctor_service=backend.verbose_name ), status=404, headers={'X-Frame-Options': 'sameorigin'} ) return redirect(url)
def register_special_exams(course_key): """ This is typically called on a course published signal. The course is examined for sequences that are marked as timed exams. Then these are registered with the edx-proctoring subsystem. Likewise, if formerly registered exams are unmarked, then those registered exams are marked as inactive """ if not settings.FEATURES.get('ENABLE_SPECIAL_EXAMS'): # if feature is not enabled then do a quick exit return course = modulestore().get_course(course_key) if course is None: raise ItemNotFoundError(u"Course {} does not exist", unicode(course_key)) if not course.enable_proctored_exams and not course.enable_timed_exams: # likewise if course does not have these features turned on # then quickly exit return # get all sequences, since they can be marked as timed/proctored exams _timed_exams = modulestore().get_items( course_key, qualifiers={ 'category': 'sequential', }, settings={ 'is_time_limited': True, } ) # filter out any potential dangling sequences timed_exams = [ timed_exam for timed_exam in _timed_exams if is_item_in_course_tree(timed_exam) ] # enumerate over list of sequences which are time-limited and # add/update any exam entries in edx-proctoring for timed_exam in timed_exams: msg = ( u'Found {location} as a timed-exam in course structure. Inspecting...'.format( location=unicode(timed_exam.location) ) ) log.info(msg) exam_metadata = { 'exam_name': timed_exam.display_name, 'time_limit_mins': timed_exam.default_time_limit_minutes, 'due_date': timed_exam.due, 'is_proctored': timed_exam.is_proctored_exam, # backends that support onboarding exams will treat onboarding exams as practice 'is_practice_exam': timed_exam.is_practice_exam or timed_exam.is_onboarding_exam, 'is_active': True, 'hide_after_due': timed_exam.hide_after_due, 'backend': course.proctoring_provider, } try: exam = get_exam_by_content_id(unicode(course_key), unicode(timed_exam.location)) # update case, make sure everything is synced exam_metadata['exam_id'] = exam['id'] exam_id = update_exam(**exam_metadata) msg = u'Updated timed exam {exam_id}'.format(exam_id=exam['id']) log.info(msg) except ProctoredExamNotFoundException: exam_metadata['course_id'] = unicode(course_key) exam_metadata['content_id'] = unicode(timed_exam.location) exam_id = create_exam(**exam_metadata) msg = u'Created new timed exam {exam_id}'.format(exam_id=exam_id) log.info(msg) exam_review_policy_metadata = { 'exam_id': exam_id, 'set_by_user_id': timed_exam.edited_by, 'review_policy': timed_exam.exam_review_rules, } # only create/update exam policy for the proctored exams if timed_exam.is_proctored_exam and not timed_exam.is_practice_exam and not timed_exam.is_onboarding_exam: try: update_review_policy(**exam_review_policy_metadata) except ProctoredExamReviewPolicyNotFoundException: if timed_exam.exam_review_rules: # won't save an empty rule. create_exam_review_policy(**exam_review_policy_metadata) msg = u'Created new exam review policy with exam_id {exam_id}'.format(exam_id=exam_id) log.info(msg) else: try: # remove any associated review policy remove_review_policy(exam_id=exam_id) except ProctoredExamReviewPolicyNotFoundException: pass # then see which exams we have in edx-proctoring that are not in # our current list. That means the the user has disabled it exams = get_all_exams_for_course(course_key) for exam in exams: if exam['is_active']: # try to look up the content_id in the sequences location search = [ timed_exam for timed_exam in timed_exams if unicode(timed_exam.location) == exam['content_id'] ] if not search: # This means it was turned off in Studio, we need to mark # the exam as inactive (we don't delete!) msg = u'Disabling timed exam {exam_id}'.format(exam_id=exam['id']) log.info(msg) update_exam( exam_id=exam['id'], is_proctored=False, is_active=False, )