Activate Next and Previous Buttons across sections

MA-2152
MA-2153
This commit is contained in:
Nimisha Asthagiri
2016-02-25 11:05:41 -05:00
parent a77000a89c
commit 8cbf99aca4
17 changed files with 680 additions and 159 deletions

View File

@@ -37,6 +37,7 @@ from course_modes.tests.factories import CourseModeFactory
from courseware.model_data import set_score
from courseware.testutils import RenderXBlockTestMixin
from courseware.tests.factories import StudentModuleFactory
from courseware.url_helpers import get_redirect_url
from courseware.user_state_client import DjangoXBlockUserStateClient
from edxmako.tests import mako_middleware_process_request
from lms.djangoapps.commerce.utils import EcommerceService # pylint: disable=import-error
@@ -192,9 +193,25 @@ class ViewsTestCase(ModuleStoreTestCase):
super(ViewsTestCase, self).setUp()
self.course = CourseFactory.create(display_name=u'teᴛ course')
self.chapter = ItemFactory.create(category='chapter', parent_location=self.course.location)
self.section = ItemFactory.create(category='sequential', parent_location=self.chapter.location, due=datetime(2013, 9, 18, 11, 30, 00))
self.section = ItemFactory.create(
category='sequential',
parent_location=self.chapter.location,
due=datetime(2013, 9, 18, 11, 30, 00),
)
self.vertical = ItemFactory.create(category='vertical', parent_location=self.section.location)
self.component = ItemFactory.create(category='problem', parent_location=self.vertical.location)
self.component = ItemFactory.create(
category='problem',
parent_location=self.vertical.location,
display_name='Problem 1',
)
self.section2 = ItemFactory.create(category='sequential', parent_location=self.chapter.location)
self.vertical2 = ItemFactory.create(category='vertical', parent_location=self.section2.location)
ItemFactory.create(
category='problem',
parent_location=self.vertical2.location,
display_name='Problem 2',
)
self.course_key = self.course.id
self.password = '123456'
@@ -210,6 +227,74 @@ class ViewsTestCase(ModuleStoreTestCase):
self.org = u"ꜱᴛᴀʀᴋ ɪɴᴅᴜꜱᴛʀɪᴇꜱ"
self.org_html = "<p>'+Stark/Industries+'</p>"
def test_index_success(self):
response = self._verify_index_response()
self.assertIn('Problem 2', response.content)
# re-access to the main course page redirects to last accessed view.
url = reverse('courseware', kwargs={'course_id': unicode(self.course_key)})
response = self.client.get(url)
self.assertEqual(response.status_code, 302)
response = self.client.get(response.url) # pylint: disable=no-member
self.assertNotIn('Problem 1', response.content)
self.assertIn('Problem 2', response.content)
def test_index_nonexistent_chapter(self):
self._verify_index_response(expected_response_code=404, chapter_name='non-existent')
def test_index_nonexistent_chapter_masquerade(self):
with patch('courseware.views.setup_masquerade') as patch_masquerade:
masquerade = MagicMock(role='student')
patch_masquerade.return_value = (masquerade, self.user)
self._verify_index_response(expected_response_code=302, chapter_name='non-existent')
def test_index_nonexistent_section(self):
self._verify_index_response(expected_response_code=404, section_name='non-existent')
def test_index_nonexistent_section_masquerade(self):
with patch('courseware.views.setup_masquerade') as patch_masquerade:
masquerade = MagicMock(role='student')
patch_masquerade.return_value = (masquerade, self.user)
self._verify_index_response(expected_response_code=302, section_name='non-existent')
def _verify_index_response(self, expected_response_code=200, chapter_name=None, section_name=None):
"""
Verifies the response when the courseware index page is accessed with
the given chapter and section names.
"""
self.client.login(username=self.user.username, password=self.password)
url = reverse(
'courseware_section',
kwargs={
'course_id': unicode(self.course_key),
'chapter': unicode(self.chapter.location.name) if chapter_name is None else chapter_name,
'section': unicode(self.section2.location.name) if section_name is None else section_name,
}
)
response = self.client.get(url)
self.assertEqual(response.status_code, expected_response_code)
return response
def test_index_no_visible_section_in_chapter(self):
self.client.login(username=self.user.username, password=self.password)
# reload the chapter from the store so its children information is updated
self.chapter = self.store.get_item(self.chapter.location)
# disable the visibility of the sections in the chapter
for section in self.chapter.get_children():
section.visible_to_staff_only = True
self.store.update_item(section, ModuleStoreEnum.UserID.test)
url = reverse(
'courseware_chapter',
kwargs={'course_id': unicode(self.course.id), 'chapter': unicode(self.chapter.location.name)},
)
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
self.assertNotIn('Problem 1', response.content)
self.assertNotIn('Problem 2', response.content)
@unittest.skipUnless(settings.FEATURES.get('ENABLE_SHOPPING_CART'), "Shopping Cart not enabled in settings")
@patch.dict(settings.FEATURES, {'ENABLE_PAID_COURSE_REGISTRATION': True})
def test_course_about_in_cart(self):
@@ -299,15 +384,37 @@ class ViewsTestCase(ModuleStoreTestCase):
self.assertEqual(views.user_groups(mock_user), [])
def test_get_current_child(self):
self.assertIsNone(views.get_current_child(MagicMock()))
mock_xmodule = MagicMock()
self.assertIsNone(views.get_current_child(mock_xmodule))
mock_xmodule.position = -1
mock_xmodule.get_display_items.return_value = ['one', 'two']
mock_xmodule.get_display_items.return_value = ['one', 'two', 'three']
self.assertEqual(views.get_current_child(mock_xmodule), 'one')
mock_xmodule_2 = MagicMock()
mock_xmodule_2.position = 3
mock_xmodule_2.get_display_items.return_value = []
self.assertIsNone(views.get_current_child(mock_xmodule_2))
mock_xmodule.position = 2
self.assertEqual(views.get_current_child(mock_xmodule), 'two')
self.assertEqual(views.get_current_child(mock_xmodule, requested_child='first'), 'one')
self.assertEqual(views.get_current_child(mock_xmodule, requested_child='last'), 'three')
mock_xmodule.position = 3
mock_xmodule.get_display_items.return_value = []
self.assertIsNone(views.get_current_child(mock_xmodule))
def test_get_redirect_url(self):
self.assertIn(
'activate_block_id',
get_redirect_url(self.course_key, self.section.location),
)
self.assertIn(
'child=first',
get_redirect_url(self.course_key, self.section.location, child='first'),
)
self.assertIn(
'child=last',
get_redirect_url(self.course_key, self.section.location, child='last'),
)
def test_redirect_to_course_position(self):
mock_module = MagicMock()

View File

@@ -7,12 +7,13 @@ from xmodule.modulestore.django import modulestore
from django.core.urlresolvers import reverse
def get_redirect_url(course_key, usage_key):
def get_redirect_url(course_key, usage_key, child=None):
""" Returns the redirect url back to courseware
Args:
course_id(str): Course Id string
location(str): The location id of course component
child(str): Optional child parameter to pass to the URL
Raises:
ItemNotFoundError if no data at the location or NoPathToItem if location not in any class
@@ -50,4 +51,7 @@ def get_redirect_url(course_key, usage_key):
redirect_url += "?{}".format(urlencode({'activate_block_id': unicode(final_target_id)}))
if child:
redirect_url += "&child={}".format(child)
return redirect_url

View File

@@ -181,7 +181,7 @@ def render_accordion(user, request, course, chapter, section, field_data_cache):
return render_to_string('courseware/accordion.html', context)
def get_current_child(xmodule, min_depth=None):
def get_current_child(xmodule, min_depth=None, requested_child=None):
"""
Get the xmodule.position's display item of an xmodule that has a position and
children. If xmodule has no position or is out of bounds, return the first
@@ -193,23 +193,36 @@ def get_current_child(xmodule, min_depth=None):
Returns None only if there are no children at all.
"""
def _get_child(children):
"""
Returns either the first or last child based on the value of
the requested_child parameter. If requested_child is None,
returns the first child.
"""
if requested_child == 'first':
return children[0]
elif requested_child == 'last':
return children[-1]
else:
return children[0]
def _get_default_child_module(child_modules):
"""Returns the first child of xmodule, subject to min_depth."""
if not child_modules:
default_child = None
elif not min_depth > 0:
default_child = child_modules[0]
default_child = _get_child(child_modules)
else:
content_children = [child for child in child_modules if
child.has_children_at_depth(min_depth - 1) and child.get_display_items()]
default_child = content_children[0] if content_children else None
default_child = _get_child(content_children) if content_children else None
return default_child
if not hasattr(xmodule, 'position'):
return None
if xmodule.position is None:
if xmodule.position is None or requested_child:
return _get_default_child_module(xmodule.get_display_items())
else:
# position is 1-indexed.
@@ -421,14 +434,6 @@ def _index_bulk_op(request, course_key, chapter, section, position):
field_data_cache = FieldDataCache.cache_for_descriptor_descendents(
course_key, user, course, depth=2)
course_module = get_module_for_descriptor(
user, request, course, field_data_cache, course_key, course=course
)
if course_module is None:
log.warning(u'If you see this, something went wrong: if we got this'
u' far, should have gotten a course module for this user')
return redirect(reverse('about_course', args=[course_key.to_deprecated_string()]))
studio_url = get_studio_url(course, 'course')
language_preference = get_user_preference(request.user, LANGUAGE_KEY)
@@ -478,89 +483,89 @@ def _index_bulk_op(request, course_key, chapter, section, position):
# passing CONTENT_DEPTH avoids returning 404 for a course with an
# empty first section and a second section with content
return redirect_to_course_position(course_module, CONTENT_DEPTH)
return redirect_to_course_position(course, CONTENT_DEPTH)
chapter_descriptor = course.get_child_by(lambda m: m.location.name == chapter)
if chapter_descriptor is not None:
save_child_position(course_module, chapter)
save_child_position(course, chapter)
else:
raise Http404('No chapter descriptor found with name {}'.format(chapter))
chapter_module = course_module.get_child_by(lambda m: m.location.name == chapter)
if chapter_module is None:
# User may be trying to access a chapter that isn't live yet
if masquerade and masquerade.role == 'student': # if staff is masquerading as student be kinder, don't 404
log.debug('staff masquerading as student: no chapter %s', chapter)
return redirect(reverse('courseware', args=[course.id.to_deprecated_string()]))
raise Http404
raise Http404('No chapter descriptor found with name {}'.format(chapter))
if course_has_entrance_exam(course):
# Message should not appear outside the context of entrance exam subsection.
# if section is none then we don't need to show message on welcome back screen also.
if getattr(chapter_module, 'is_entrance_exam', False) and section is not None:
if getattr(chapter_descriptor, 'is_entrance_exam', False) and section is not None:
context['entrance_exam_current_score'] = get_entrance_exam_score(request, course)
context['entrance_exam_passed'] = user_has_passed_entrance_exam(request, course)
if section is not None:
section_descriptor = chapter_descriptor.get_child_by(lambda m: m.location.name == section)
if section_descriptor is None:
# Specifically asked-for section doesn't exist
if masquerade and masquerade.role == 'student': # don't 404 if staff is masquerading as student
log.debug('staff masquerading as student: no section %s', section)
return redirect(reverse('courseware', args=[course.id.to_deprecated_string()]))
raise Http404
# Allow chromeless operation
if section_descriptor.chrome:
chrome = [s.strip() for s in section_descriptor.chrome.lower().split(",")]
if 'accordion' not in chrome:
context['disable_accordion'] = True
if 'tabs' not in chrome:
context['disable_tabs'] = True
if section_descriptor.default_tab:
context['default_tab'] = section_descriptor.default_tab
# cdodge: this looks silly, but let's refetch the section_descriptor with depth=None
# which will prefetch the children more efficiently than doing a recursive load
section_descriptor = modulestore().get_item(section_descriptor.location, depth=None)
# Load all descendants of the section, because we're going to display its
# html, which in general will need all of its children
field_data_cache.add_descriptor_descendents(
section_descriptor, depth=None
)
section_module = get_module_for_descriptor(
user,
request,
section_descriptor,
field_data_cache,
course_key,
position,
course=course
)
if section_module is None:
# User may be trying to be clever and access something
# they don't have access to.
raise Http404
# Save where we are in the chapter.
save_child_position(chapter_module, section)
section_render_context = {'activate_block_id': request.GET.get('activate_block_id')}
context['fragment'] = section_module.render(STUDENT_VIEW, section_render_context)
context['section_title'] = section_descriptor.display_name_with_default_escaped
else:
prev_section = get_current_child(chapter_module)
if prev_section is None:
if section is None:
section_descriptor = get_current_child(chapter_descriptor, requested_child=request.GET.get("child"))
if section_descriptor:
section = section_descriptor.url_name
else:
# Something went wrong -- perhaps this chapter has no sections visible to the user.
# Clearing out the last-visited state and showing "first-time" view by redirecting
# to courseware.
course_module.position = None
course_module.save()
course.position = None
course.save()
return redirect(reverse('courseware', args=[course.id.to_deprecated_string()]))
else:
section_descriptor = chapter_descriptor.get_child_by(lambda m: m.location.name == section)
if section_descriptor is None:
# Specifically asked-for section doesn't exist
if masquerade and masquerade.role == 'student': # don't 404 if staff is masquerading as student
log.debug('staff masquerading as student: no section %s', section)
return redirect(reverse('courseware', args=[course.id.to_deprecated_string()]))
raise Http404
# Allow chromeless operation
if section_descriptor.chrome:
chrome = [s.strip() for s in section_descriptor.chrome.lower().split(",")]
if 'accordion' not in chrome:
context['disable_accordion'] = True
if 'tabs' not in chrome:
context['disable_tabs'] = True
if section_descriptor.default_tab:
context['default_tab'] = section_descriptor.default_tab
# cdodge: this looks silly, but let's refetch the section_descriptor with depth=None
# which will prefetch the children more efficiently than doing a recursive load
section_descriptor = modulestore().get_item(section_descriptor.location, depth=None)
# Load all descendants of the section, because we're going to display its
# html, which in general will need all of its children
field_data_cache.add_descriptor_descendents(
section_descriptor, depth=None
)
section_module = get_module_for_descriptor(
user,
request,
section_descriptor,
field_data_cache,
course_key,
position,
course=course
)
# Save where we are in the chapter.
save_child_position(chapter_descriptor, section)
section_render_context = {
'activate_block_id': request.GET.get('activate_block_id'),
'redirect_url_func': (
get_redirect_url if settings.FEATURES.get('ENABLE_NEXT_BUTTON_ACROSS_SECTIONS') else None
),
'requested_child': request.GET.get("child"),
}
context['accordion'] = render_accordion(user, request, course, chapter, section, field_data_cache)
context['fragment'] = section_module.render(STUDENT_VIEW, section_render_context)
context['section_title'] = section_descriptor.display_name_with_default_escaped
result = render_to_response('courseware/courseware.html', context)
except Exception as e:

View File

@@ -162,6 +162,9 @@ FEATURES['ENABLE_COURSEWARE_SEARCH'] = True
# Enable dashboard search for tests
FEATURES['ENABLE_DASHBOARD_SEARCH'] = True
# Enable cross-section Next button for tests
FEATURES['ENABLE_NEXT_BUTTON_ACROSS_SECTIONS'] = True
# Use MockSearchEngine as the search engine for test scenario
SEARCH_ENGINE = "search.tests.mock_search_engine.MockSearchEngine"
# Path at which to store the mock index

View File

@@ -371,7 +371,10 @@ FEATURES = {
# This is the default, but can be disabled if all history
# lives in the Extended table, saving the frontend from
# making multiple queries.
'ENABLE_READING_FROM_MULTIPLE_HISTORY_TABLES': True
'ENABLE_READING_FROM_MULTIPLE_HISTORY_TABLES': True,
# Enable Next Button to jump sequences in Sequence Navigation bar.
'ENABLE_NEXT_BUTTON_ACROSS_SECTIONS': False,
}
# Ignore static asset files on import which match this pattern

View File

@@ -1,6 +1,6 @@
<%! from django.utils.translation import ugettext as _ %>
<div id="sequence_${element_id}" class="sequence" data-id="${item_id}" data-position="${position}" data-ajax-url="${ajax_url}" >
<div id="sequence_${element_id}" class="sequence" data-id="${item_id}" data-position="${position}" data-ajax-url="${ajax_url}" data-next-url="${next_url}" data-prev-url="${prev_url}">
<div class="path"></div>
<div class="sequence-nav">
<button class="sequence-nav-button button-previous">