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