Esempio n. 1
0
    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)
Esempio n. 2
0
    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
Esempio n. 4
0
    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)
Esempio n. 5
0
    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)
Esempio n. 6
0
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
Esempio n. 7
0
    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)
Esempio n. 8
0
    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
Esempio n. 9
0
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
Esempio n. 10
0
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
Esempio n. 11
0
    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)
Esempio n. 12
0
    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)
Esempio n. 13
0
    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
Esempio n. 15
0
    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)
Esempio n. 16
0
    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'})
Esempio n. 17
0
    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'})
Esempio n. 18
0
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
Esempio n. 19
0
    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
Esempio n. 20
0
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
Esempio n. 21
0
    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)
Esempio n. 22
0
    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)
Esempio n. 23
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)
Esempio n. 24
0
    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)
Esempio n. 25
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)
Esempio n. 26
0
    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)
Esempio n. 27
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)
Esempio n. 28
0
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
Esempio n. 29
0
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
Esempio n. 30
0
    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)
Esempio n. 31
0
                    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.
Esempio n. 32
0
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
Esempio n. 33
0
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)
Esempio n. 34
0
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,
                )
Esempio n. 35
0
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
Esempio n. 36
0
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
Esempio n. 37
0
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,
                )
Esempio n. 38
0
                    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.

Esempio n. 39
0
                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.

Esempio n. 40
0
                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
Esempio n. 41
0
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
Esempio n. 42
0
    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)
Esempio n. 43
0
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,
                )