Commit 0b4305a4 by Bill Filler

Merge branch 'bessiesteinberg/WL-1317-course-ouline-gated-content' into…

Merge branch 'bessiesteinberg/WL-1317-course-ouline-gated-content' into bfiller/gated-content-combined
parents 64b9dd86 2e3616d5
......@@ -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,
)
......@@ -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">
......
......@@ -176,7 +176,7 @@ class TestCourseHomePage(CourseHomePageTestCase):
course_home_url(self.course)
# Fetch the view and verify the query counts
with self.assertNumQueries(49, table_blacklist=QUERY_COUNT_TABLE_BLACKLIST):
with self.assertNumQueries(50, table_blacklist=QUERY_COUNT_TABLE_BLACKLIST):
with check_mongo_calls(4):
url = course_home_url(self.course)
self.client.get(url)
......
......@@ -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