Commit cbaf0af4 by Bessie Steinberg

Subsections with prereqs now visible in outline

WL-1317

Previously, the course outline would only show subsections that had
prerequisites if the prerequisite was already met. It would give no
indication that the subsection existed before the prequisite was met.
The course outline will now show subsections with indications that that
section (a) has prerequisites and (b) whether the prerequisite has been
met.

fix tests
parent 93c3f5f9
......@@ -367,6 +367,9 @@ def get_course_content_milestones(course_id, content_id, relationship, user_id=N
user={"id": user_id}
)
if content_id is None:
return request_cache_dict[user_id][relationship]
return [m for m in request_cache_dict[user_id][relationship] if m['content_id'] == unicode(content_id)]
......
......@@ -146,7 +146,7 @@ class TestGetBlocksQueryCounts(SharedModuleStoreTestCase):
self._get_blocks(
course,
expected_mongo_queries=0,
expected_sql_queries=6 if with_storage_backing else 5,
expected_sql_queries=5 if with_storage_backing else 4,
)
@ddt.data(
......@@ -164,5 +164,5 @@ class TestGetBlocksQueryCounts(SharedModuleStoreTestCase):
self._get_blocks(
course,
expected_mongo_queries,
expected_sql_queries=14 if with_storage_backing else 6,
expected_sql_queries=13 if with_storage_backing else 5,
)
......@@ -66,8 +66,6 @@ class MilestonesAndSpecialExamsTransformer(BlockStructureTransformer):
if usage_info.has_staff_access:
return False
elif self.has_pending_milestones_for_user(block_key, usage_info):
return True
elif self.gated_by_required_content(block_key, block_structure, required_content):
return True
elif (settings.FEATURES.get('ENABLE_SPECIAL_EXAMS', False) and
......
......@@ -137,12 +137,12 @@ class MilestonesTransformerTestCase(CourseStructureTestCase, MilestonesTestCaseM
(
'H',
'A',
('course', 'A', 'B', 'C',)
('course', 'A', 'B', 'C', 'H', 'I')
),
(
'H',
'ProctoredExam',
('course', 'A', 'B', 'C'),
('course', 'A', 'B', 'C', 'H', 'I'),
),
)
@ddt.unpack
......@@ -160,7 +160,7 @@ class MilestonesTransformerTestCase(CourseStructureTestCase, MilestonesTestCaseM
self.course.enable_subsection_gating = True
self.setup_gated_section(self.blocks[gated_block_ref], self.blocks[gating_block_ref])
with self.assertNumQueries(8):
with self.assertNumQueries(6):
self.get_blocks_and_check_against_expected(self.user, expected_blocks_before_completion)
# clear the request cache to simulate a new request
......@@ -174,7 +174,7 @@ class MilestonesTransformerTestCase(CourseStructureTestCase, MilestonesTestCaseM
self.user,
)
with self.assertNumQueries(8):
with self.assertNumQueries(6):
self.get_blocks_and_check_against_expected(self.user, self.ALL_BLOCKS_EXCEPT_SPECIAL)
def test_staff_access(self):
......@@ -195,7 +195,7 @@ class MilestonesTransformerTestCase(CourseStructureTestCase, MilestonesTestCaseM
self.course.enable_subsection_gating = True
self.setup_gated_section(self.blocks['H'], self.blocks['A'])
expected_blocks = (
'course', 'A', 'B', 'C', 'ProctoredExam', 'D', 'E', 'PracticeExam', 'F', 'G', 'TimedExam', 'J', 'K'
'course', 'A', 'B', 'C', 'ProctoredExam', 'D', 'E', 'PracticeExam', 'F', 'G', 'TimedExam', 'J', 'K', 'H', 'I'
)
self.get_blocks_and_check_against_expected(self.user, expected_blocks)
# clear the request cache to simulate a new request
......
......@@ -35,9 +35,32 @@ from openedx.core.djangolib.markup import HTML, Text
>
<div class="subsection-text">
## Subsection title
<span class="subsection-title">${ subsection['display_name'] }</span>
<span class="subsection-title">
% if subsection['id'] in milestones:
% if milestones[subsection['id']]['completed_prereqs']:
<span class="menu-icon icon fa fa-unlock"
aria-hidden="true">
</span>
% else:
<span class="menu-icon icon fa fa-lock"
aria-hidden="true">
</span>
% endif
% endif
<span class="subsection-title-name">
${ subsection['display_name'] }
</span>
% if subsection['id'] in milestones:
% if milestones[subsection['id']]['completed_prereqs']:
<span class="sr">&nbsp;${_("Unlocked")}</span>
% else:
<span class="details">
${ _("(Prerequisite required)") }
</span>
% endif
% endif
<div class="details">
## There are behavior differences between rendering of subsections which have
## exams (timed, graded, etc) and those that do not.
##
......@@ -91,11 +114,12 @@ from openedx.core.djangolib.markup import HTML, Text
class="menu-icon icon fa fa-pencil-square-o"
aria-hidden="true"
></span>
<span class="sr">${_("This content is graded")}</span>
<span class="sr">&nbsp;${_("This content is graded")}</span>
% endif
% endif
</span>
% endif
</div> <!-- /details -->
</div> <!-- /subsection-text -->
<div class="subsection-actions">
......
......@@ -6,17 +6,23 @@ import json
from django.core.urlresolvers import reverse
from pyquery import PyQuery as pq
from gating import api as lms_gating_api
from mock import patch, Mock
from courseware.tests.factories import StaffFactory
from openedx.core.lib.gating import api as gating_api
from student.models import CourseEnrollment
from student.tests.factories import UserFactory
from xmodule.modulestore import ModuleStoreEnum
from xmodule.modulestore.tests.django_utils import SharedModuleStoreTestCase
from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory
from milestones.tests.utils import MilestonesTestCaseMixin
from lms.djangoapps.course_api.blocks.transformers.milestones import MilestonesAndSpecialExamsTransformer
from .test_course_home import course_home_url
TEST_PASSWORD = 'test'
GATING_NAMESPACE_QUALIFIER = '.gating'
class TestCourseOutlinePage(SharedModuleStoreTestCase):
......@@ -107,6 +113,132 @@ class TestCourseOutlinePage(SharedModuleStoreTestCase):
self.assertNotIn(vertical.display_name, response_content)
class TestCourseOutlinePageWithPrerequisites(SharedModuleStoreTestCase, MilestonesTestCaseMixin):
"""
Test the course outline view with prerequisites.
"""
TRANSFORMER_CLASS_TO_TEST = MilestonesAndSpecialExamsTransformer
@classmethod
def setUpClass(cls):
"""
Creates a test course that can be used for non-destructive tests
"""
cls.PREREQ_REQUIRED = '(Prerequisite required)'
cls.UNLOCKED = 'Unlocked'
with super(TestCourseOutlinePageWithPrerequisites, cls).setUpClassAndTestData():
cls.course, cls.course_blocks = cls.create_test_course()
@classmethod
def setUpTestData(cls):
"""Set up and enroll our fake user in the course."""
cls.user = UserFactory(password=TEST_PASSWORD)
CourseEnrollment.enroll(cls.user, cls.course.id)
@classmethod
def create_test_course(cls):
"""Creates a test course."""
course = CourseFactory.create()
course.enable_subsection_gating = True
course_blocks = {}
with cls.store.bulk_operations(course.id):
course_blocks['chapter'] = ItemFactory.create(category='chapter', parent_location=course.location)
course_blocks['prerequisite'] = ItemFactory.create(category='sequential', parent_location=course_blocks['chapter'].location, display_name='Prerequisite Exam')
course_blocks['gated_content'] = ItemFactory.create(category='sequential', parent_location=course_blocks['chapter'].location, display_name='Gated Content')
course_blocks['prerequisite_vertical'] = ItemFactory.create(category='vertical', parent_location=course_blocks['prerequisite'].location)
course_blocks['gated_content_vertical'] = ItemFactory.create(category='vertical', parent_location=course_blocks['gated_content'].location)
course.children = [course_blocks['chapter']]
course_blocks['chapter'].children = [course_blocks['prerequisite'], course_blocks['gated_content']]
course_blocks['prerequisite'].children = [course_blocks['prerequisite_vertical']]
course_blocks['gated_content'].children = [course_blocks['gated_content_vertical']]
if hasattr(cls, 'user'):
CourseEnrollment.enroll(cls.user, course.id)
return course, course_blocks
def setUp(self):
"""
Set up for the tests.
"""
super(TestCourseOutlinePageWithPrerequisites, self).setUp()
self.client.login(username=self.user.username, password=TEST_PASSWORD)
def setup_gated_section(self, gated_block, gating_block):
"""
Test helper to create a gating requirement
Args:
gated_block: The block the that learner will not have access to until they complete the gating block
gating_block: (The prerequisite) The block that must be completed to get access to the gated block
"""
gating_api.add_prerequisite(self.course.id, unicode(gating_block.location))
gating_api.set_required_content(self.course.id, gated_block.location, gating_block.location, 100)
def test_content_locked(self):
"""
Test that a sequntial/subsection with unmet prereqs correctly indicated that its content is locked
"""
course = self.course
self.setup_gated_section(self.course_blocks['gated_content'], self.course_blocks['prerequisite'])
response = self.client.get(course_home_url(course))
self.assertEqual(response.status_code, 200)
response_content = pq(response.content)
# check the subsection is present and has the title 'Gated Content (Prerequisite required)'
gated_subsection_title = '{} {}'.format(self.course_blocks['gated_content'].display_name, self.PREREQ_REQUIRED)
gated_subsections = [subsection for subsection in response_content.items('.subsection-title') if gated_subsection_title in subsection.text()]
self.assertTrue(gated_subsections)
# check that there is only one subtitle with that name
self.assertTrue(len(gated_subsections))
# check the lock icon is there
self.assertTrue(gated_subsections[0].children('.fa-lock'))
def test_content_unlocked(self):
"""
Test that a sequntial/subsection with unmet prereqs correctly indicated that its content is locked
"""
course = self.course
self.setup_gated_section(self.course_blocks['gated_content'], self.course_blocks['prerequisite'])
# complete the prerequiste to unlock the gated content
# this call triggers reevaluation of prerequisites fulfilled by the gating block.
with patch('gating.api._get_subsection_percentage', Mock(return_value=100)):
lms_gating_api.evaluate_prerequisite(
self.course,
Mock(location=self.course_blocks['prerequisite'].location),
self.user,
)
response = self.client.get(course_home_url(course))
self.assertEqual(response.status_code, 200)
response_content = pq(response.content)
# check the subsection is present and has the title 'Gated Content'
gated_subsection_title = self.course_blocks['gated_content'].display_name
gated_subsections = [subsection for subsection in response_content.items('.subsection-title') if gated_subsection_title in subsection.text()]
self.assertTrue(gated_subsections)
# check that there is only one subtitle with that name
self.assertTrue(len(gated_subsections))
# check that the subtitle DOES NOT say '(Prerequisite required)'
self.assertFalse(self.PREREQ_REQUIRED in gated_subsections[0].text())
# check the unlock icon is there
self.assertTrue(gated_subsections[0].children('.fa-unlock'))
# check that screen reader text is there, should say "Unlocked"
self.assertTrue(gated_subsections[0].children('.sr'))
self.assertTrue(self.UNLOCKED in gated_subsections[0].children('.sr').text())
class TestCourseOutlineResumeCourse(SharedModuleStoreTestCase):
"""
Test start course and resume course for the course outline view.
......
......@@ -9,7 +9,10 @@ from web_fragments.fragment import Fragment
from courseware.courses import get_course_overview_with_access
from openedx.core.djangoapps.plugin_api.views import EdxFragmentView
from lms.djangoapps.course_api.blocks.api import get_blocks
from ..utils import get_course_outline_block_tree
from util.milestones_helpers import get_course_content_milestones
from xmodule.modulestore.django import modulestore
class CourseOutlineFragmentView(EdxFragmentView):
......@@ -23,15 +26,44 @@ class CourseOutlineFragmentView(EdxFragmentView):
"""
course_key = CourseKey.from_string(course_id)
course_overview = get_course_overview_with_access(request.user, 'load', course_key, check_if_enrolled=True)
course_block_tree = get_course_outline_block_tree(request, course_id)
if not course_block_tree:
return None
content_milestones = self.get_content_milestones(request, course_key)
context = {
'csrf': csrf(request)['csrf_token'],
'course': course_overview,
'blocks': course_block_tree,
'milestones': content_milestones
}
html = render_to_string('course_experience/course-outline-fragment.html', context)
return Fragment(html)
def get_content_milestones(self, request, course_key):
"""
Returns dict of subsections with prerequisites and whether the prerequisite has been completed or not
"""
course_content_milestones = {}
all_course_prereqs = get_course_content_milestones(
course_id=course_key,
content_id=None,
relationship='requires',
user_id=None)
for milestone in all_course_prereqs:
course_content_milestones[milestone['content_id']] = {'completed_prereqs': True}
unfulfilled_prereqs = get_course_content_milestones(
course_id=course_key,
content_id=None,
relationship='requires',
user_id=request.user.id)
for milestone in unfulfilled_prereqs:
course_content_milestones[milestone['content_id']]['completed_prereqs'] = False
return course_content_milestones
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment