Conditionally display gated content in courseware
Display gated sections in course outline, navigation and in the course when user has met prerequiste condition. WL-1273, WL-1317
This commit is contained in:
@@ -344,11 +344,14 @@ def add_course_content_milestone(course_id, content_id, relationship, milestone)
|
||||
return milestones_api.add_course_content_milestone(course_id, content_id, relationship, milestone)
|
||||
|
||||
|
||||
def get_course_content_milestones(course_id, content_id, relationship, user_id=None):
|
||||
def get_course_content_milestones(course_id, content_id=None, relationship='requires', user_id=None):
|
||||
"""
|
||||
Client API operation adapter/wrapper
|
||||
Uses the request cache to store all of a user's
|
||||
milestones
|
||||
|
||||
Returns all content blocks in a course if content_id is None, otherwise it just returns that
|
||||
specific content block.
|
||||
"""
|
||||
if not settings.FEATURES.get('MILESTONES_APP'):
|
||||
return []
|
||||
@@ -367,6 +370,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)]
|
||||
|
||||
|
||||
|
||||
@@ -159,7 +159,7 @@ class ProctoringFields(object):
|
||||
|
||||
@XBlock.wants('proctoring')
|
||||
@XBlock.wants('verification')
|
||||
@XBlock.wants('milestones')
|
||||
@XBlock.wants('gating')
|
||||
@XBlock.wants('credit')
|
||||
@XBlock.needs('user')
|
||||
@XBlock.needs('bookmarks')
|
||||
@@ -231,8 +231,6 @@ class SequenceModule(SequenceFields, ProctoringFields, XModule):
|
||||
banner_text, special_html = special_html_view
|
||||
if special_html and not masquerading_as_specific_student:
|
||||
return Fragment(special_html)
|
||||
else:
|
||||
banner_text = self._gated_content_staff_banner()
|
||||
return self._student_view(context, banner_text)
|
||||
|
||||
def _special_exam_student_view(self):
|
||||
@@ -270,20 +268,6 @@ class SequenceModule(SequenceFields, ProctoringFields, XModule):
|
||||
|
||||
return banner_text, hidden_content_html
|
||||
|
||||
def _gated_content_staff_banner(self):
|
||||
"""
|
||||
Checks whether the content is gated for learners. If so,
|
||||
returns a banner_text depending on whether user is staff.
|
||||
"""
|
||||
milestones_service = self.runtime.service(self, 'milestones')
|
||||
if milestones_service:
|
||||
content_milestones = milestones_service.get_course_content_milestones(
|
||||
self.course_id, self.location, 'requires'
|
||||
)
|
||||
banner_text = _('This subsection is unlocked for learners when they meet the prerequisite requirements.')
|
||||
if content_milestones and self.runtime.user_is_staff:
|
||||
return banner_text
|
||||
|
||||
def _can_user_view_content(self, course):
|
||||
"""
|
||||
Returns whether the runtime user can view the content
|
||||
@@ -307,10 +291,21 @@ class SequenceModule(SequenceFields, ProctoringFields, XModule):
|
||||
"""
|
||||
display_items = self.get_display_items()
|
||||
self._update_position(context, len(display_items))
|
||||
prereq_met = True
|
||||
prereq_meta_info = {}
|
||||
|
||||
if self._required_prereq():
|
||||
if self.runtime.user_is_staff:
|
||||
banner_text = _('This subsection is unlocked for learners when they meet the prerequisite requirements.')
|
||||
else:
|
||||
# check if prerequisite has been met
|
||||
prereq_met, prereq_meta_info = self._compute_is_prereq_met(True)
|
||||
if prereq_met and not self._is_gate_fulfilled():
|
||||
banner_text = _('This section is a prerequisite. You must complete this section in order to unlock additional content.')
|
||||
|
||||
fragment = Fragment()
|
||||
params = {
|
||||
'items': self._render_student_view_for_items(context, display_items, fragment),
|
||||
'items': self._render_student_view_for_items(context, display_items, fragment) if prereq_met else [],
|
||||
'element_id': self.location.html_id(),
|
||||
'item_id': text_type(self.location),
|
||||
'position': self.position,
|
||||
@@ -320,6 +315,7 @@ class SequenceModule(SequenceFields, ProctoringFields, XModule):
|
||||
'prev_url': context.get('prev_url'),
|
||||
'banner_text': banner_text,
|
||||
'disable_navigation': not self.is_user_authenticated(context),
|
||||
'gated_content': self._get_gated_content_info(prereq_met, prereq_meta_info)
|
||||
}
|
||||
fragment.add_content(self.system.render_template("seq_module.html", params))
|
||||
|
||||
@@ -328,6 +324,68 @@ class SequenceModule(SequenceFields, ProctoringFields, XModule):
|
||||
|
||||
return fragment
|
||||
|
||||
def _get_gated_content_info(self, prereq_met, prereq_meta_info):
|
||||
"""
|
||||
Returns a dict of information about gated_content context
|
||||
"""
|
||||
gated_content = {}
|
||||
gated_content['gated'] = not prereq_met
|
||||
gated_content['prereq_url'] = prereq_meta_info['url'] if not prereq_met else None
|
||||
gated_content['prereq_section_name'] = prereq_meta_info['display_name'] if not prereq_met else None
|
||||
gated_content['gated_section_name'] = self.display_name
|
||||
|
||||
return gated_content
|
||||
|
||||
def _is_gate_fulfilled(self):
|
||||
"""
|
||||
Determines if this section is a prereq and has any unfulfilled milestones.
|
||||
|
||||
Returns:
|
||||
True if section has no unfufilled milestones or is not a prerequisite.
|
||||
False otherwise
|
||||
"""
|
||||
gating_service = self.runtime.service(self, 'gating')
|
||||
if gating_service:
|
||||
fulfilled = gating_service.is_gate_fulfilled(
|
||||
self.course_id, self.location, self.runtime.user_id
|
||||
)
|
||||
return fulfilled
|
||||
|
||||
return True
|
||||
|
||||
def _required_prereq(self):
|
||||
"""
|
||||
Checks whether a prerequisite is required for this Section
|
||||
|
||||
Returns:
|
||||
milestone if a prereq is required, None otherwise
|
||||
"""
|
||||
gating_service = self.runtime.service(self, 'gating')
|
||||
if gating_service:
|
||||
milestone = gating_service.required_prereq(
|
||||
self.course_id, self.location, 'requires'
|
||||
)
|
||||
return milestone
|
||||
|
||||
return None
|
||||
|
||||
def _compute_is_prereq_met(self, recalc_on_unmet):
|
||||
"""
|
||||
Evaluate if the user has completed the prerequisite
|
||||
|
||||
Arguments:
|
||||
recalc_on_unmet: Recalculate the subsection grade if prereq has not yet been met
|
||||
|
||||
Returns:
|
||||
tuple: True|False,
|
||||
prereq_meta_info = { 'url': prereq_url, 'display_name': prereq_name}
|
||||
"""
|
||||
gating_service = self.runtime.service(self, 'gating')
|
||||
if gating_service:
|
||||
return gating_service.compute_is_prereq_met(self.location, self.runtime.user_id, recalc_on_unmet)
|
||||
|
||||
return True, {}
|
||||
|
||||
def _update_position(self, context, number_of_display_items):
|
||||
"""
|
||||
Update the user's sequential position given the context and the
|
||||
|
||||
@@ -118,6 +118,7 @@ class SequenceBlockTestCase(XModuleXmlImportTest):
|
||||
)
|
||||
self._assert_view_at_position(html, expected_position=1)
|
||||
self.assertIn(unicode(self.sequence_3_1.location), html)
|
||||
self.assertIn("'gated': False", html)
|
||||
self.assertIn("'next_url': 'NextSequential'", html)
|
||||
self.assertIn("'prev_url': 'PrevSequential'", html)
|
||||
|
||||
@@ -178,3 +179,97 @@ class SequenceBlockTestCase(XModuleXmlImportTest):
|
||||
)
|
||||
self.assertIn("hidden_content.html", html)
|
||||
self.assertIn(progress_url, html)
|
||||
|
||||
def _assert_gated(self, html, sequence):
|
||||
"""
|
||||
Assert sequence content is gated
|
||||
"""
|
||||
self.assertIn("seq_module.html", html)
|
||||
self.assertIn("'banner_text': None", html)
|
||||
self.assertIn("'items': []", html)
|
||||
self.assertIn("'gated': True", html)
|
||||
self.assertIn("'prereq_url': 'PrereqUrl'", html)
|
||||
self.assertIn("'prereq_section_name': 'PrereqSectionName'", html)
|
||||
self.assertIn("'gated_section_name': u'{}'".format(unicode(sequence.display_name)), html)
|
||||
self.assertIn("'next_url': 'NextSequential'", html)
|
||||
self.assertIn("'prev_url': 'PrevSequential'", html)
|
||||
|
||||
def _assert_prereq(self, html, sequence):
|
||||
"""
|
||||
Assert sequence is a prerequisite with unfulfilled gates
|
||||
"""
|
||||
self.assertIn("seq_module.html", html)
|
||||
self.assertIn(
|
||||
"'banner_text': 'This section is a prerequisite. "
|
||||
"You must complete this section in order to unlock additional content.'",
|
||||
html
|
||||
)
|
||||
self.assertIn("'gated': False", html)
|
||||
self.assertIn(unicode(sequence.location), html)
|
||||
self.assertIn("'prereq_url': None", html)
|
||||
self.assertIn("'prereq_section_name': None", html)
|
||||
self.assertIn("'next_url': 'NextSequential'", html)
|
||||
self.assertIn("'prev_url': 'PrevSequential'", html)
|
||||
|
||||
def _assert_ungated(self, html, sequence):
|
||||
"""
|
||||
Assert sequence is not gated
|
||||
"""
|
||||
self.assertIn("seq_module.html", html)
|
||||
self.assertIn("'banner_text': None", html)
|
||||
self.assertIn("'gated': False", html)
|
||||
self.assertIn(unicode(sequence.location), html)
|
||||
self.assertIn("'prereq_url': None", html)
|
||||
self.assertIn("'prereq_section_name': None", html)
|
||||
self.assertIn("'next_url': 'NextSequential'", html)
|
||||
self.assertIn("'prev_url': 'PrevSequential'", html)
|
||||
|
||||
def test_gated_content(self):
|
||||
"""
|
||||
Test when sequence is both a prerequisite for a sequence
|
||||
and gated on another prerequisite sequence
|
||||
"""
|
||||
# setup seq_1_2 as a gate and gated
|
||||
gating_mock_1_2 = Mock()
|
||||
gating_mock_1_2.return_value.is_gate_fulfilled.return_value = False
|
||||
gating_mock_1_2.return_value.required_prereq.return_value = True
|
||||
gating_mock_1_2.return_value.compute_is_prereq_met.return_value = [
|
||||
False,
|
||||
{'url': 'PrereqUrl', 'display_name': 'PrereqSectionName'}
|
||||
]
|
||||
self.sequence_1_2.xmodule_runtime._services['gating'] = gating_mock_1_2 # pylint: disable=protected-access
|
||||
self.sequence_1_2.display_name = 'sequence_1_2'
|
||||
|
||||
html = self._get_rendered_student_view(
|
||||
self.sequence_1_2,
|
||||
extra_context=dict(next_url='NextSequential', prev_url='PrevSequential'),
|
||||
)
|
||||
|
||||
# expect content to be gated, with no banner
|
||||
self._assert_gated(html, self.sequence_1_2)
|
||||
|
||||
# change seq_1_2 to be ungated, but still a gate (prequiste)
|
||||
gating_mock_1_2.return_value.is_gate_fulfilled.return_value = False
|
||||
gating_mock_1_2.return_value.required_prereq.return_value = True
|
||||
gating_mock_1_2.return_value.compute_is_prereq_met.return_value = [True, {}]
|
||||
|
||||
html = self._get_rendered_student_view(
|
||||
self.sequence_1_2,
|
||||
extra_context=dict(next_url='NextSequential', prev_url='PrevSequential'),
|
||||
)
|
||||
|
||||
# assert that content and preq banner is shown
|
||||
self._assert_prereq(html, self.sequence_1_2)
|
||||
|
||||
# change seq_1_2 to have no unfulfilled gates
|
||||
gating_mock_1_2.return_value.is_gate_fulfilled.return_value = True
|
||||
gating_mock_1_2.return_value.required_prereq.return_value = True
|
||||
gating_mock_1_2.return_value.compute_is_prereq_met.return_value = [True, {}]
|
||||
|
||||
html = self._get_rendered_student_view(
|
||||
self.sequence_1_2,
|
||||
extra_context=dict(next_url='NextSequential', prev_url='PrevSequential'),
|
||||
)
|
||||
|
||||
# assert content shown as normal
|
||||
self._assert_ungated(html, self.sequence_1_2)
|
||||
|
||||
@@ -83,7 +83,7 @@ class CourseOutlinePage(PageObject):
|
||||
SECTION_SELECTOR = '.outline-item.section:nth-of-type({0})'
|
||||
SECTION_TITLES_SELECTOR = '.section-name h3'
|
||||
SUBSECTION_SELECTOR = SECTION_SELECTOR + ' .subsection:nth-of-type({1}) .outline-item'
|
||||
SUBSECTION_TITLES_SELECTOR = SECTION_SELECTOR + ' .subsection .subsection-title'
|
||||
SUBSECTION_TITLES_SELECTOR = SECTION_SELECTOR + ' .subsection .subsection-title .subsection-title-name'
|
||||
OUTLINE_RESUME_COURSE_SELECTOR = '.outline-item .resume-right'
|
||||
|
||||
def __init__(self, browser, parent_page):
|
||||
|
||||
@@ -150,9 +150,13 @@ class GatingTest(UniqueCourseTest):
|
||||
"""
|
||||
Given that I am a student
|
||||
When I visit the LMS Courseware
|
||||
Then I cannot see a gated subsection
|
||||
Then I can see a gated subsection
|
||||
The gated subsection should have a lock icon
|
||||
and be in the format: "<Subsection Title> (Prerequisite Required)"
|
||||
When I fulfill the gating Prerequisite
|
||||
Then I can see the gated subsection
|
||||
Now the gated subsection should have an unlock icon
|
||||
and screen readers should read the section as: "<Subsection Title> Unlocked"
|
||||
"""
|
||||
self._setup_prereq()
|
||||
self._setup_gated_subsection()
|
||||
@@ -160,7 +164,7 @@ class GatingTest(UniqueCourseTest):
|
||||
self._auto_auth(self.STUDENT_USERNAME, self.STUDENT_EMAIL, False)
|
||||
|
||||
self.course_home_page.visit()
|
||||
self.assertEqual(self.course_home_page.outline.num_subsections, 1)
|
||||
self.assertEqual(self.course_home_page.outline.num_subsections, 2)
|
||||
|
||||
# Fulfill prerequisite and verify that gated subsection is shown
|
||||
self.courseware_page.visit()
|
||||
@@ -175,7 +179,9 @@ class GatingTest(UniqueCourseTest):
|
||||
Then I can see all gated subsections
|
||||
Displayed along with notification banners
|
||||
Then if I masquerade as a student
|
||||
Then I cannot see a gated subsection
|
||||
Then I can see a gated subsection
|
||||
The gated subsection should have a lock icon
|
||||
and be in the format: "<Subsection Title> (Prerequisite Required)"
|
||||
When I fufill the gating prerequisite
|
||||
Then I can see the gated subsection (without a banner)
|
||||
"""
|
||||
@@ -204,10 +210,11 @@ class GatingTest(UniqueCourseTest):
|
||||
|
||||
self.course_home_page.visit()
|
||||
self.course_home_page.preview.set_staff_view_mode('Learner')
|
||||
self.assertEqual(self.course_home_page.outline.num_subsections, 1)
|
||||
self.assertEqual(self.course_home_page.outline.num_subsections, 2)
|
||||
self.course_home_page.outline.go_to_section('Test Section 1', 'Test Subsection 1')
|
||||
self.courseware_page.wait_for_page()
|
||||
self.assertFalse(self.courseware_page.has_banner())
|
||||
# banner displayed informing section is a prereq
|
||||
self.assertTrue(self.courseware_page.has_banner())
|
||||
|
||||
self.course_home_page.visit()
|
||||
self.course_home_page.preview.set_staff_view_mode_specific_student(self.STUDENT_USERNAME)
|
||||
|
||||
Reference in New Issue
Block a user