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)