コード例 #1
0
class CoursePerformancePresenterTests(TestCase):
    def setUp(self):
        cache.clear()
        self.course_id = 'edX/DemoX/Demo_Course'
        self.problem_id = 'i4x://edX/DemoX.1/problem/05d289c5ad3d47d48a77622c4a81ec36'
        self.presenter = CoursePerformancePresenter(None, self.course_id)
        self.factory = CoursePerformanceDataFactory()

    @mock.patch('analyticsclient.module.Module.answer_distribution')
    def test_multiple_answer_distribution(self, mock_answer_distribution):

        mock_data = utils.get_mock_api_answer_distribution_multiple_questions_data(self.course_id)
        mock_answer_distribution.return_value = mock_data

        problem_parts = [
            {
                'part_id': 'i4x-edX-DemoX_1-problem-5e3c6d6934494d87b3a025676c7517c1_2_1',
                'expected': {
                    'active_question': 'Submissions for Part 1: Is this a text problem?',
                    'problem_part_description': 'Part 1: Is this a text problem?',
                    'is_random': False,
                    'answer_type': 'text'
                }
            },
            {
                'part_id': 'i4x-edX-DemoX_1-problem-5e3c6d6934494d87b3a025676c7517c1_3_1',
                'expected': {
                    'active_question': 'Submissions for Part 2: Is this a numeric problem?',
                    'problem_part_description': 'Part 2: Is this a numeric problem?',
                    'is_random': False,
                    'answer_type': 'numeric'
                }
            },
            {
                'part_id': 'i4x-edX-DemoX_1-problem-5e3c6d6934494d87b3a025676c7517c1_4_1',
                'expected': {
                    'active_question': 'Submissions for Part 3: Is this a randomized problem?',
                    'problem_part_description': 'Part 3: Is this a '
                                                'randomized problem?',
                    'is_random': True,
                    'answer_type': 'numeric'
                }
            }
        ]
        questions = utils.get_presenter_performance_answer_distribution_multiple_questions()
        self.assertAnswerDistribution(problem_parts, questions)

    @mock.patch('analyticsclient.module.Module.answer_distribution')
    def test_single_answer_distribution(self, mock_answer_distribution):

        mock_data = utils.get_mock_api_answer_distribution_single_question_data(self.course_id)
        mock_answer_distribution.return_value = mock_data

        problem_parts = [
            {
                'part_id': 'i4x-edX-DemoX_1-problem-5e3c6d6934494d87b3a025676c7517c1_2_1',
                'expected': {
                    'active_question': 'Submissions: Is this a text problem?',
                    'problem_part_description': 'Is this a text problem?',
                    'is_random': False,
                    'answer_type': 'text'
                }
            }
        ]
        questions = utils.get_presenter_performance_answer_distribution_single_question()
        self.assertAnswerDistribution(problem_parts, questions)

    def assertAnswerDistribution(self, expected_problem_parts, expected_questions):
        for part in expected_problem_parts:
            expected = part['expected']
            answer_distribution_entry = self.presenter.get_answer_distribution(self.problem_id, part['part_id'])
            self.assertEqual(answer_distribution_entry.last_updated, utils.CREATED_DATETIME)
            self.assertListEqual(answer_distribution_entry.questions, expected_questions)
            self.assertEqual(answer_distribution_entry.problem_part_description, expected['problem_part_description'])
            self.assertEqual(answer_distribution_entry.active_question, expected['active_question'])
            self.assertEqual(answer_distribution_entry.answer_type, expected['answer_type'])
            self.assertEqual(answer_distribution_entry.is_random, expected['is_random'])

            expected_answer_distribution = utils.get_filtered_answer_distribution(self.course_id, part['part_id'])
            self.assertListEqual(answer_distribution_entry.answer_distribution, expected_answer_distribution)
            if answer_distribution_entry.is_random:
                self.assertIsNone(answer_distribution_entry.answer_distribution_limited)
            else:
                self.assertListEqual(answer_distribution_entry.answer_distribution_limited,
                                     expected_answer_distribution[:12])

    @mock.patch('slumber.Resource.get', mock.Mock(return_value=CoursePerformanceDataFactory.grading_policy))
    def test_grading_policy(self):
        """ Verify the presenter returns the correct grading policy. """
        grading_policy = self.presenter.grading_policy()
        self.assertListEqual(grading_policy, CoursePerformanceDataFactory.grading_policy)

        percent = self.presenter.get_max_policy_display_percent(grading_policy)
        self.assertEqual(100, percent)

        percent = self.presenter.get_max_policy_display_percent([{'weight': 0.0}, {'weight': 1.0}, {'weight': 0.04}])
        self.assertEqual(90, percent)

    @mock.patch('courses.presenters.performance.CoursePerformancePresenter.grading_policy',
                mock.Mock(return_value=CoursePerformanceDataFactory.grading_policy))
    def test_assignment_types(self):
        """ Verify the presenter returns the correct assignment types. """
        self.assertListEqual(self.presenter.assignment_types(), CoursePerformanceDataFactory.assignment_types)

    def test_assignments(self):
        """ Verify the presenter returns the correct assignments and sets the last updated date. """

        self.assertIsNone(self.presenter.last_updated)

        with mock.patch('slumber.Resource.get', mock.Mock(return_value=self.factory.structure)):
            with mock.patch('analyticsclient.course.Course.problems', self.factory.problems):
                # With no assignment type set, the method should return all assignment types.
                assignments = self.presenter.assignments()
                expected_assignments = self.factory.present_assignments()

                self.assertListEqual(assignments, expected_assignments)
                self.assertEqual(self.presenter.last_updated, utils.CREATED_DATETIME)

                # With an assignment type set, the presenter should return only the assignments of the specified type.
                self.maxDiff = None
                for assignment_type in self.factory.assignment_types:
                    cache.clear()
                    expected = [assignment for assignment in expected_assignments if
                                assignment[u'assignment_type'] == assignment_type]

                    for index, assignment in enumerate(expected):
                        assignment[u'index'] = index + 1

                    self.assertListEqual(self.presenter.assignments(assignment_type), expected)

    def test_assignment(self):
        """ Verify the presenter returns a specific assignment. """
        with mock.patch('courses.presenters.performance.CoursePerformancePresenter.assignments',
                        mock.Mock(return_value=self.factory.present_assignments())):
            # The method should return None if the assignment does not exist.
            self.assertIsNone(self.presenter.assignment(None))
            self.assertIsNone(self.presenter.assignment('non-existent-id'))

            # The method should return an individual assignment if the ID exists.
            assignment = self.factory.present_assignments()[0]
            self.assertDictEqual(self.presenter.assignment(assignment[u'id']), assignment)
コード例 #2
0
class CoursePerformancePresenterTests(TestCase):

    def setUp(self):
        cache.clear()
        self.course_id = PERFORMER_PRESENTER_COURSE_ID
        self.problem_id = 'i4x://edX/DemoX.1/problem/05d289c5ad3d47d48a77622c4a81ec36'
        self.presenter = CoursePerformancePresenter(None, self.course_id)
        self.factory = CoursePerformanceDataFactory()

    # First and last response counts were added, insights can handle both types of API responses at the moment.
    @data(
        annotated(
            utils.get_mock_api_answer_distribution_multiple_questions_data(PERFORMER_PRESENTER_COURSE_ID),
            'count'
        ),
        annotated(
            utils.get_mock_api_answer_distribution_multiple_questions_first_last_data(PERFORMER_PRESENTER_COURSE_ID),
            'first_last'
        ),
    )
    @mock.patch('analyticsclient.module.Module.answer_distribution')
    def test_multiple_answer_distribution(self, mock_data, mock_answer_distribution):
        mock_answer_distribution.reset_mock()
        mock_answer_distribution.return_value = mock_data

        problem_parts = [
            {
                'part_id': 'i4x-edX-DemoX_1-problem-5e3c6d6934494d87b3a025676c7517c1_2_1',
                'expected': {
                    'active_question': 'Submissions for Part 1: Is this a text problem?',
                    'problem_part_description': 'Part 1: Is this a text problem?',
                    'is_random': False,
                    'answer_type': 'text'
                }
            },
            {
                'part_id': 'i4x-edX-DemoX_1-problem-5e3c6d6934494d87b3a025676c7517c1_3_1',
                'expected': {
                    'active_question': 'Submissions for Part 2: Is this a numeric problem?',
                    'problem_part_description': 'Part 2: Is this a numeric problem?',
                    'is_random': False,
                    'answer_type': 'numeric'
                }
            },
            {
                'part_id': 'i4x-edX-DemoX_1-problem-5e3c6d6934494d87b3a025676c7517c1_4_1',
                'expected': {
                    'active_question': 'Submissions for Part 3: Is this a randomized problem?',
                    'problem_part_description': 'Part 3: Is this a '
                                                'randomized problem?',
                    'is_random': True,
                    'answer_type': 'numeric'
                }
            }
        ]
        questions = utils.get_presenter_performance_answer_distribution_multiple_questions()
        self.assertAnswerDistribution(problem_parts, questions, mock_data)

    @mock.patch('analyticsclient.module.Module.answer_distribution')
    def test_single_answer_distribution(self, mock_answer_distribution):

        mock_data = utils.get_mock_api_answer_distribution_single_question_data(self.course_id)
        mock_answer_distribution.return_value = mock_data

        problem_parts = [
            {
                'part_id': 'i4x-edX-DemoX_1-problem-5e3c6d6934494d87b3a025676c7517c1_2_1',
                'expected': {
                    'active_question': 'Submissions: Is this a text problem?',
                    'problem_part_description': 'Is this a text problem?',
                    'is_random': False,
                    'answer_type': 'text'
                }
            }
        ]
        questions = utils.get_presenter_performance_answer_distribution_single_question()
        self.assertAnswerDistribution(problem_parts, questions, mock_data)

    def assertAnswerDistribution(self, expected_problem_parts, expected_questions, answer_distribution_data):
        for part in expected_problem_parts:
            expected = part['expected']
            answer_distribution_entry = self.presenter.get_answer_distribution(self.problem_id, part['part_id'])
            self.assertEqual(answer_distribution_entry.last_updated, utils.CREATED_DATETIME)
            self.assertListEqual(answer_distribution_entry.questions, expected_questions)
            self.assertEqual(answer_distribution_entry.problem_part_description, expected['problem_part_description'])
            self.assertEqual(answer_distribution_entry.active_question, expected['active_question'])
            self.assertEqual(answer_distribution_entry.answer_type, expected['answer_type'])
            self.assertEqual(answer_distribution_entry.is_random, expected['is_random'])

            expected_answer_distribution = [d for d in answer_distribution_data if d['part_id'] == part['part_id']]
            self.assertListEqual(answer_distribution_entry.answer_distribution, expected_answer_distribution)
            if answer_distribution_entry.is_random:
                self.assertIsNone(answer_distribution_entry.answer_distribution_limited)
            else:
                self.assertListEqual(answer_distribution_entry.answer_distribution_limited,
                                     expected_answer_distribution[:12])

    @mock.patch('slumber.Resource.get', mock.Mock(return_value=CoursePerformanceDataFactory.grading_policy))
    def test_grading_policy(self):
        """
        Verify the presenter returns the correct grading policy.

        Empty (non-truthy) assignment types should be dropped.
        """

        grading_policy = self.presenter.grading_policy()
        self.assertListEqual(grading_policy, self.factory.presented_grading_policy)

        percent = self.presenter.get_max_policy_display_percent(grading_policy)
        self.assertEqual(100, percent)

        percent = self.presenter.get_max_policy_display_percent([{'weight': 0.0}, {'weight': 1.0}, {'weight': 0.04}])
        self.assertEqual(90, percent)

    def test_assignment_types(self):
        """ Verify the presenter returns the correct assignment types. """
        with mock.patch('courses.presenters.performance.CoursePerformancePresenter.grading_policy',
                        mock.Mock(return_value=self.factory.presented_grading_policy)):
            self.assertListEqual(self.presenter.assignment_types(), self.factory.presented_assignment_types)

    def test_assignments(self):
        """ Verify the presenter returns the correct assignments and sets the last updated date. """

        self.assertIsNone(self.presenter.last_updated)

        with mock.patch('slumber.Resource.get', mock.Mock(return_value=self.factory.structure)):
            with mock.patch('analyticsclient.course.Course.problems', self.factory.problems):
                # With no assignment type set, the method should return all assignment types.
                assignments = self.presenter.assignments()
                expected_assignments = self.factory.presented_assignments

                self.assertListEqual(assignments, expected_assignments)
                self.assertEqual(self.presenter.last_updated, utils.CREATED_DATETIME)

                # With an assignment type set, the presenter should return only the assignments of the specified type.
                for assignment_type in self.factory.presented_assignment_types:
                    cache.clear()
                    expected = [assignment for assignment in expected_assignments if
                                assignment[u'assignment_type'] == assignment_type['name']]

                    for index, assignment in enumerate(expected):
                        assignment[u'index'] = index + 1

                    self.assertListEqual(self.presenter.assignments(assignment_type), expected)

    def test_assignment(self):
        """ Verify the presenter returns a specific assignment. """
        with mock.patch('courses.presenters.performance.CoursePerformancePresenter.assignments',
                        mock.Mock(return_value=self.factory.presented_assignments)):
            # The method should return None if the assignment does not exist.
            self.assertIsNone(self.presenter.assignment(None))
            self.assertIsNone(self.presenter.assignment('non-existent-id'))

            # The method should return an individual assignment if the ID exists.
            assignment = self.factory.presented_assignments[0]
            self.assertDictEqual(self.presenter.assignment(assignment[u'id']), assignment)

    def test_problem(self):
        """ Verify the presenter returns a specific problem. """
        problem = self.factory.presented_assignments[0]['children'][0]
        _id = problem['id']

        with mock.patch('slumber.Resource.get', mock.Mock(return_value=self.factory.structure)):
            actual = self.presenter.block(_id)
            expected = {
                'id': _id,
                'name': problem['name'],
                'children': []
            }
            self.assertDictContainsSubset(expected, actual)

    def test_sections(self):
        """ Verify the presenter returns a specific assignment. """
        ungraded_problems = self.factory.problems(False)
        with mock.patch('slumber.Resource.get', mock.Mock(return_value=self.factory.structure)):
            with mock.patch('analyticsclient.course.Course.problems', mock.Mock(return_value=ungraded_problems)):
                expected = self.factory.presented_sections
                self.assertListEqual(self.presenter.sections(), expected)

    def test_section(self):
        """ Verify the presenter returns a specific assignment. """
        ungraded_problems = self.factory.problems(False)
        with mock.patch('slumber.Resource.get', mock.Mock(return_value=self.factory.structure)):
            with mock.patch('analyticsclient.course.Course.problems', mock.Mock(return_value=ungraded_problems)):
                # The method should return None if the assignment does not exist.
                self.assertIsNone(self.presenter.section(None))
                self.assertIsNone(self.presenter.section('non-existent-id'))
                expected = self.factory.presented_sections[0]
                self.assertEqual(self.presenter.section(expected['id']), expected)

    def test_subsections(self):
        """ Verify the presenter returns a specific assignment. """
        ungraded_problems = self.factory.problems(False)
        with mock.patch('slumber.Resource.get', mock.Mock(return_value=self.factory.structure)):
            with mock.patch('analyticsclient.course.Course.problems', mock.Mock(return_value=ungraded_problems)):
                # The method should return None if the assignment does not exist.
                self.assertIsNone(self.presenter.subsections(None))
                self.assertIsNone(self.presenter.subsections('non-existent-id'))
                section = self.factory.presented_sections[0]
                expected = section['children']
                self.assertListEqual(self.presenter.subsections(section['id']), expected)

    def test_subsection(self):
        """ Verify the presenter returns a specific assignment. """
        ungraded_problems = self.factory.problems(False)
        with mock.patch('slumber.Resource.get', mock.Mock(return_value=self.factory.structure)):
            with mock.patch('analyticsclient.course.Course.problems', mock.Mock(return_value=ungraded_problems)):
                # The method should return None if the assignment does not exist.
                self.assertIsNone(self.presenter.subsection(None, None))
                self.assertIsNone(self.presenter.subsection('non-existent-id', 'nope'))
                section = self.factory.presented_sections[0]
                expected_subsection = section['children'][0]
                self.assertEqual(self.presenter.subsection(section['id'], expected_subsection['id']),
                                 expected_subsection)

    def test_subsection_problems(self):
        """ Verify the presenter returns a specific assignment. """
        ungraded_problems = self.factory.problems(False)
        with mock.patch('slumber.Resource.get', mock.Mock(return_value=self.factory.structure)):
            with mock.patch('analyticsclient.course.Course.problems', mock.Mock(return_value=ungraded_problems)):
                # The method should return None if the assignment does not exist.
                self.assertIsNone(self.presenter.subsection_children(None, None))
                self.assertIsNone(self.presenter.subsection_children('non-existent-id', 'nope'))
                section = self.factory.presented_sections[0]
                subsection = section['children'][0]
                expected_problems = subsection['children']
                self.assertListEqual(
                    self.presenter.subsection_children(section['id'], subsection['id']), expected_problems)
コード例 #3
0
class PerformanceTemplateView(CourseTemplateWithNavView, CourseAPIMixin):
    """
    Base view for course performance pages.
    """
    assignment_type = None
    assignment_id = None
    assignment = None
    presenter = None

    # Translators: Do not translate UTC.
    update_message = _(
        'Problem submission data was last updated %(update_date)s at %(update_time)s UTC.'
    )

    secondary_nav_items = [
        {
            'name': 'graded_content',
            'label': _('Graded Content'),
            'view': 'courses:performance_graded_content'
        },
    ]
    active_primary_nav_item = 'performance'
    page_title = _('Graded Content')
    active_secondary_nav_item = 'graded_content'

    def dispatch(self, request, *args, **kwargs):
        self.assignment_id = kwargs.get('assignment_id')

        try:
            return super(PerformanceTemplateView,
                         self).dispatch(request, *args, **kwargs)
        except SlumberBaseException as e:
            # Return the appropriate response if a 404 occurred.
            response = getattr(e, 'response')
            if response is not None and response.status_code == 404:
                logger.info('Course API data not found for %s: %s',
                            self.course_id, e)
                raise Http404

            # Not a 404. Continue raising the error.
            logger.error(
                'An error occurred while using Slumber to communicate with an API: %s',
                e)
            raise

    def _deslugify_assignment_type(self):
        """
        Assignment type is slugified in the templates to avoid issues with our URL regex failing to match unknown
        assignment types. This method changes the assignment type to the human-friendly version. If no match is found,
        the assignment type is unchanged.
        """
        for assignment_type in self.presenter.assignment_types():
            if self.assignment_type == slugify(assignment_type):
                self.assignment_type = assignment_type
                break

    def get_context_data(self, **kwargs):
        context = super(PerformanceTemplateView,
                        self).get_context_data(**kwargs)
        self.presenter = CoursePerformancePresenter(self.access_token,
                                                    self.course_id)

        context['assignment_types'] = self.presenter.assignment_types()

        if self.assignment_id:
            assignment = self.presenter.assignment(self.assignment_id)
            if assignment:
                context['assignment'] = assignment
                context['assignment_name'] = assignment['name']
                self.assignment = assignment
                self.assignment_type = assignment['assignment_type']
            else:
                logger.info('Assignment %s not found.', self.assignment_id)
                raise Http404

        if self.assignment_type:
            self._deslugify_assignment_type()
            assignments = self.presenter.assignments(self.assignment_type)

            context['js_data']['course']['assignments'] = assignments
            context['js_data']['course'][
                'assignmentsHaveSubmissions'] = self.presenter.has_submissions(
                    assignments)
            context['js_data']['course'][
                'assignmentType'] = self.assignment_type

            context.update({
                'assignment_type':
                self.assignment_type,
                'assignments':
                assignments,
                'update_message':
                self.get_last_updated_message(self.presenter.last_updated)
            })

        return context