From a199c6bfd9be464187a58880d25cd05a69a8bfca Mon Sep 17 00:00:00 2001 From: Michael Terry Date: Mon, 16 Mar 2020 15:27:45 -0400 Subject: [PATCH] Show number of problems in outline Specifically, the number of 'problem' blocks that are graded, in a given subsection. Also shows an icon next to the subsection if so. AA-45 --- .../course-outline-fragment.html | 12 +++++++- .../tests/views/test_course_outline.py | 28 +++++++++++++++++++ openedx/features/course_experience/utils.py | 16 +++++++++++ 3 files changed, 55 insertions(+), 1 deletion(-) diff --git a/openedx/features/course_experience/templates/course_experience/course-outline-fragment.html b/openedx/features/course_experience/templates/course_experience/course-outline-fragment.html index 75c6f871ae..649731f25e 100644 --- a/openedx/features/course_experience/templates/course_experience/course-outline-fragment.html +++ b/openedx/features/course_experience/templates/course_experience/course-outline-fragment.html @@ -10,7 +10,8 @@ import pytz from datetime import date, datetime, timedelta from django.utils import timezone -from django.utils.translation import ugettext as _ +from django.utils.translation import gettext as _ +from django.utils.translation import ngettext from openedx.core.djangolib.markup import HTML, Text %> @@ -58,6 +59,7 @@ reset_deadlines_banner_displayed = False needs_prereqs = not gated_content[subsection['id']]['completed_prereqs'] if gated_subsection else False scored = 'scored' if subsection.get('scored', False) else '' graded = 'graded' if subsection.get('graded') else '' + num_graded_problems = subsection.get('num_graded_problems', 0) due_date = subsection.get('due') overdue = due_date is not None and due_date < timezone.now() and not subsection.get('complete', True) %> @@ -77,8 +79,16 @@ reset_deadlines_banner_displayed = False class="subsection-text outline-button" id="${ subsection['id'] }" > + % if num_graded_problems: + + % endif

${ subsection['display_name'] } + % if num_graded_problems: + ${ngettext("({number} Question)", + "({number} Questions)", + num_graded_problems).format(number=num_graded_problems)} + % endif

% if subsection.get('complete'): diff --git a/openedx/features/course_experience/tests/views/test_course_outline.py b/openedx/features/course_experience/tests/views/test_course_outline.py index 7d6216ae35..9114682918 100644 --- a/openedx/features/course_experience/tests/views/test_course_outline.py +++ b/openedx/features/course_experience/tests/views/test_course_outline.py @@ -135,6 +135,34 @@ class TestCourseOutlinePage(SharedModuleStoreTestCase): self.assertContains(response, sequential.format) self.assertTrue(sequential.children) + def test_num_graded_problems(self): + course = CourseFactory.create() + with self.store.bulk_operations(course.id): + chapter = ItemFactory.create(category='chapter', parent_location=course.location) + sequential = ItemFactory.create(category='sequential', parent_location=chapter.location) + problem = ItemFactory.create(category='problem', parent_location=sequential.location) + sequential2 = ItemFactory.create(category='sequential', parent_location=chapter.location) + problem2 = ItemFactory.create(category='problem', graded=True, has_score=True, + parent_location=sequential2.location) + sequential3 = ItemFactory.create(category='sequential', parent_location=chapter.location) + problem3_1 = ItemFactory.create(category='problem', graded=True, has_score=True, + parent_location=sequential3.location) + problem3_2 = ItemFactory.create(category='problem', graded=True, has_score=True, + parent_location=sequential3.location) + course.children = [chapter] + chapter.children = [sequential, sequential2, sequential3] + sequential.children = [problem] + sequential2.children = [problem2] + sequential3.children = [problem3_1, problem3_2] + CourseEnrollment.enroll(self.user, course.id) + + url = course_home_url(course) + response = self.client.get(url) + content = response.content.decode('utf8') + self.assertRegex(content, sequential.display_name + r'\s*') + self.assertRegex(content, sequential2.display_name + r'\s*\(1 Question\)\s*') + self.assertRegex(content, sequential3.display_name + r'\s*\(2 Questions\)\s*') + def test_reset_course_deadlines(self): course = self.courses[0] enrollment = CourseEnrollment.objects.get(course_id=course.id) diff --git a/openedx/features/course_experience/utils.py b/openedx/features/course_experience/utils.py index 30ecb3cf69..a31e74ed3c 100644 --- a/openedx/features/course_experience/utils.py +++ b/openedx/features/course_experience/utils.py @@ -136,6 +136,21 @@ def get_course_outline_block_tree(request, course_id, user=None): block['scored'] = False return False + def recurse_num_graded_problems(block): + """ + Marks each block with the number of graded and scored leaf blocks below it as 'num_graded_problems' + + Must be run after recurse_mark_scored. + """ + children = block.get('children', []) + if children: + num_graded_problems = sum(recurse_num_graded_problems(child) for child in children) + else: + num_graded_problems = 1 if block.get('scored') and block.get('graded') else 0 + + block['num_graded_problems'] = num_graded_problems + return num_graded_problems + def recurse_mark_auth_denial(block): """ Mark this block as 'scored' if any of its descendents are 'scored' (that is, 'has_score' and 'weight' > 0). @@ -192,6 +207,7 @@ def get_course_outline_block_tree(request, course_id, user=None): if course_outline_root_block: populate_children(course_outline_root_block, all_blocks['blocks']) recurse_mark_scored(course_outline_root_block) + recurse_num_graded_problems(course_outline_root_block) recurse_mark_auth_denial(course_outline_root_block) if user: set_last_accessed_default(course_outline_root_block)