Commit 8cbf99ac by Nimisha Asthagiri

Activate Next and Previous Buttons across sections

MA-2152
MA-2153
parent a77000a8
...@@ -8,6 +8,8 @@ class @Sequence ...@@ -8,6 +8,8 @@ class @Sequence
@num_contents = @contents.length @num_contents = @contents.length
@id = @el.data('id') @id = @el.data('id')
@ajaxUrl = @el.data('ajax-url') @ajaxUrl = @el.data('ajax-url')
@nextUrl = @el.data('next-url')
@prevUrl = @el.data('prev-url')
@base_page_title = " | " + document.title @base_page_title = " | " + document.title
@initProgress() @initProgress()
@bind() @bind()
...@@ -73,23 +75,35 @@ class @Sequence ...@@ -73,23 +75,35 @@ class @Sequence
when 'in_progress' then element.addClass('progress-some') when 'in_progress' then element.addClass('progress-some')
when 'done' then element.addClass('progress-done') when 'done' then element.addClass('progress-done')
toggleArrows: => enableButton: (button_class, button_action) ->
@$('.sequence-nav-button').unbind('click') @$(button_class).removeClass('disabled').removeAttr('disabled').click(button_action)
if @contents.length == 0 ## There are no modules to display, and therefore no nav to build. disableButton: (button_class) ->
@$('.sequence-nav-button.button-previous').addClass('disabled').attr('disabled', true) @$(button_class).addClass('disabled').attr('disabled', true)
@$('.sequence-nav-button.button-next').addClass('disabled').attr('disabled', true)
return
if @position == 1 ## 1 != 0 here. 1 is the first item in the sequence nav. setButtonLabel: (button_class, button_label) ->
@$('.sequence-nav-button.button-previous').addClass('disabled').attr('disabled', true) @$(button_class + ' .sr').html(button_label)
else
@$('.sequence-nav-button.button-previous').removeClass('disabled').removeAttr('disabled').click(@previous)
if @position == @contents.length ## If the final position on the nav matches the total contents. updateButtonState: (button_class, button_action, action_label_prefix, is_at_boundary, boundary_url) ->
@$('.sequence-nav-button.button-next').addClass('disabled').attr('disabled', true) if is_at_boundary and boundary_url == 'None'
@disableButton(button_class)
else else
@$('.sequence-nav-button.button-next').removeClass('disabled').removeAttr('disabled').click(@next) button_label = action_label_prefix + (if is_at_boundary then ' Section' else ' Unit')
@setButtonLabel(button_class, button_label)
@enableButton(button_class, button_action)
toggleArrows: =>
@$('.sequence-nav-button').unbind('click')
# previous button
first_tab = @position == 1
previous_button_class = '.sequence-nav-button.button-previous'
@updateButtonState(previous_button_class, @previous, 'Previous', first_tab, @prevUrl)
# next button
last_tab = @position >= @contents.length # use inequality in case contents.length is 0 and position is 1.
next_button_class = '.sequence-nav-button.button-next'
@updateButtonState(next_button_class, @next, 'Next', last_tab, @nextUrl)
render: (new_position) -> render: (new_position) ->
if @position != new_position if @position != new_position
...@@ -164,10 +178,15 @@ class @Sequence ...@@ -164,10 +178,15 @@ class @Sequence
new: new_position new: new_position
id: @id id: @id
# If the bottom nav is used, scroll to the top of the page on change. if (direction == "seq_next") and (@position == @contents.length)
if $(event.target).closest('nav[class="sequence-bottom"]').length > 0 window.location.href = @nextUrl
$.scrollTo 0, 150 else if (direction == "seq_prev") and (@position == 1)
@render new_position window.location.href = @prevUrl
else
# If the bottom nav is used, scroll to the top of the page on change.
if $(event.target).closest('nav[class="sequence-bottom"]').length > 0
$.scrollTo 0, 150
@render new_position
link_for: (position) -> link_for: (position) ->
@$("#sequence-list a[data-element=#{position}]") @$("#sequence-list a[data-element=#{position}]")
......
...@@ -141,16 +141,8 @@ class SequenceModule(SequenceFields, ProctoringFields, XModule): ...@@ -141,16 +141,8 @@ class SequenceModule(SequenceFields, ProctoringFields, XModule):
# If position is specified in system, then use that instead. # If position is specified in system, then use that instead.
position = getattr(self.system, 'position', None) position = getattr(self.system, 'position', None)
if position is not None: if position is not None:
try: assert isinstance(position, int)
self.position = int(self.system.position) self.position = self.system.position
except (ValueError, TypeError):
# Check for https://openedx.atlassian.net/browse/LMS-6496
warnings.warn(
"Sequential position cannot be converted to an integer: {pos!r}".format(
pos=self.system.position,
),
RuntimeWarning,
)
def get_progress(self): def get_progress(self):
''' Return the total progress, adding total done and total available. ''' Return the total progress, adding total done and total available.
...@@ -177,9 +169,16 @@ class SequenceModule(SequenceFields, ProctoringFields, XModule): ...@@ -177,9 +169,16 @@ class SequenceModule(SequenceFields, ProctoringFields, XModule):
raise NotFoundError('Unexpected dispatch type') raise NotFoundError('Unexpected dispatch type')
def student_view(self, context): def student_view(self, context):
display_items = self.get_display_items()
# If we're rendering this sequence, but no position is set yet, # If we're rendering this sequence, but no position is set yet,
# or exceeds the length of the displayable items,
# default the position to the first element # default the position to the first element
if self.position is None: if context.get('requested_child') == 'first':
self.position = 1
elif context.get('requested_child') == 'last':
self.position = len(display_items) or None
elif self.position is None or self.position > len(display_items):
self.position = 1 self.position = 1
## Returns a set of all types of all sub-children ## Returns a set of all types of all sub-children
...@@ -211,7 +210,6 @@ class SequenceModule(SequenceFields, ProctoringFields, XModule): ...@@ -211,7 +210,6 @@ class SequenceModule(SequenceFields, ProctoringFields, XModule):
fragment.add_content(view_html) fragment.add_content(view_html)
return fragment return fragment
display_items = self.get_display_items()
for child in display_items: for child in display_items:
is_bookmarked = bookmarks_service.is_bookmarked(usage_key=child.scope_ids.usage_id) is_bookmarked = bookmarks_service.is_bookmarked(usage_key=child.scope_ids.usage_id)
context["bookmarked"] = is_bookmarked context["bookmarked"] = is_bookmarked
...@@ -245,6 +243,16 @@ class SequenceModule(SequenceFields, ProctoringFields, XModule): ...@@ -245,6 +243,16 @@ class SequenceModule(SequenceFields, ProctoringFields, XModule):
'position': self.position, 'position': self.position,
'tag': self.location.category, 'tag': self.location.category,
'ajax_url': self.system.ajax_url, 'ajax_url': self.system.ajax_url,
'next_url': _compute_next_url(
self.location,
parent_module,
context.get('redirect_url_func'),
),
'prev_url': _compute_previous_url(
self.location,
parent_module,
context.get('redirect_url_func'),
),
} }
fragment.add_content(self.system.render_template("seq_module.html", params)) fragment.add_content(self.system.render_template("seq_module.html", params))
...@@ -453,3 +461,88 @@ class SequenceDescriptor(SequenceFields, ProctoringFields, MakoModuleDescriptor, ...@@ -453,3 +461,88 @@ class SequenceDescriptor(SequenceFields, ProctoringFields, MakoModuleDescriptor,
xblock_body["content_type"] = "Sequence" xblock_body["content_type"] = "Sequence"
return xblock_body return xblock_body
def _compute_next_url(block_location, parent_block, redirect_url_func):
"""
Returns the url for the next block after the given block.
"""
def get_next_block_location(parent_block, index_in_parent):
"""
Returns the next block in the parent_block after the block with the given
index_in_parent.
"""
if index_in_parent + 1 < len(parent_block.children):
return parent_block.children[index_in_parent + 1]
else:
return None
return _compute_next_or_prev_url(
block_location,
parent_block,
redirect_url_func,
get_next_block_location,
'first',
)
def _compute_previous_url(block_location, parent_block, redirect_url_func):
"""
Returns the url for the previous block after the given block.
"""
def get_previous_block_location(parent_block, index_in_parent):
"""
Returns the previous block in the parent_block before the block with the given
index_in_parent.
"""
return parent_block.children[index_in_parent - 1] if index_in_parent else None
return _compute_next_or_prev_url(
block_location,
parent_block,
redirect_url_func,
get_previous_block_location,
'last',
)
def _compute_next_or_prev_url(
block_location,
parent_block,
redirect_url_func,
get_next_or_prev_block,
redirect_url_child_param,
):
"""
Returns the url for the next or previous block from the given block.
Arguments:
block_location: Location of the block that is being navigated.
parent_block: Parent block of the given block.
redirect_url_func: Function that computes a redirect URL directly to
a block, given the block's location.
get_next_or_prev_block: Function that returns the next or previous
block in the parent, or None if doesn't exist.
redirect_url_child_param: Value to pass for the child parameter to the
redirect_url_func.
"""
if redirect_url_func:
index_in_parent = parent_block.children.index(block_location)
next_or_prev_block_location = get_next_or_prev_block(parent_block, index_in_parent)
if next_or_prev_block_location:
return redirect_url_func(
block_location.course_key,
next_or_prev_block_location,
child=redirect_url_child_param,
)
else:
grandparent = parent_block.get_parent()
if grandparent:
return _compute_next_or_prev_url(
parent_block.location,
grandparent,
redirect_url_func,
get_next_or_prev_block,
redirect_url_child_param,
)
return None
"""
Tests for sequence module.
"""
# pylint: disable=no-member
from mock import Mock
from xblock.reference.user_service import XBlockUser, UserService
from xmodule.tests import get_test_system
from xmodule.tests.xml import XModuleXmlImportTest
from xmodule.tests.xml import factories as xml
from xmodule.x_module import STUDENT_VIEW
from xmodule.seq_module import _compute_next_url, _compute_previous_url, SequenceModule
class StubUserService(UserService):
"""
Stub UserService for testing the sequence module.
"""
def get_current_user(self):
"""
Implements abstract method for getting the current user.
"""
user = XBlockUser()
user.opt_attrs['edx-platform.username'] = 'test user'
return user
class SequenceBlockTestCase(XModuleXmlImportTest):
"""
Tests for the Sequence Module.
"""
@classmethod
def setUpClass(cls):
super(SequenceBlockTestCase, cls).setUpClass()
course_xml = cls._set_up_course_xml()
cls.course = cls.process_xml(course_xml)
cls._set_up_module_system(cls.course)
for chapter_index in range(len(cls.course.get_children())):
chapter = cls._set_up_block(cls.course, chapter_index)
setattr(cls, 'chapter_{}'.format(chapter_index + 1), chapter)
for sequence_index in range(len(chapter.get_children())):
sequence = cls._set_up_block(chapter, sequence_index)
setattr(cls, 'sequence_{}_{}'.format(chapter_index + 1, sequence_index + 1), sequence)
@classmethod
def _set_up_course_xml(cls):
"""
Sets up and returns XML course structure.
"""
course = xml.CourseFactory.build()
chapter_1 = xml.ChapterFactory.build(parent=course) # has 2 child sequences
xml.ChapterFactory.build(parent=course) # has 0 child sequences
chapter_3 = xml.ChapterFactory.build(parent=course) # has 1 child sequence
chapter_4 = xml.ChapterFactory.build(parent=course) # has 2 child sequences
xml.SequenceFactory.build(parent=chapter_1)
xml.SequenceFactory.build(parent=chapter_1)
sequence_3_1 = xml.SequenceFactory.build(parent=chapter_3) # has 3 verticals
xml.SequenceFactory.build(parent=chapter_4)
xml.SequenceFactory.build(parent=chapter_4)
for _ in range(3):
xml.VerticalFactory.build(parent=sequence_3_1)
return course
@classmethod
def _set_up_block(cls, parent, index_in_parent):
"""
Sets up the stub sequence module for testing.
"""
block = parent.get_children()[index_in_parent]
cls._set_up_module_system(block)
block.xmodule_runtime._services['bookmarks'] = Mock() # pylint: disable=protected-access
block.xmodule_runtime._services['user'] = StubUserService() # pylint: disable=protected-access
block.xmodule_runtime.xmodule_instance = getattr(block, '_xmodule', None) # pylint: disable=protected-access
block.parent = parent.location
return block
@classmethod
def _set_up_module_system(cls, block):
"""
Sets up the test module system for the given block.
"""
module_system = get_test_system()
module_system.descriptor_runtime = block._runtime # pylint: disable=protected-access
block.xmodule_runtime = module_system
def test_student_view_init(self):
seq_module = SequenceModule(runtime=Mock(position=2), descriptor=Mock(), scope_ids=Mock())
self.assertEquals(seq_module.position, 2) # matches position set in the runtime
def test_render_student_view(self):
html = self._get_rendered_student_view(self.sequence_3_1, requested_child=None)
self._assert_view_at_position(html, expected_position=1)
self.assertIn(unicode(self.sequence_3_1.location), html)
self.assertIn("'next_url': u'{}'".format(unicode(self.chapter_4.location)), html)
self.assertIn("'prev_url': u'{}'".format(unicode(self.chapter_2.location)), html)
def test_student_view_first_child(self):
html = self._get_rendered_student_view(self.sequence_3_1, requested_child='first')
self._assert_view_at_position(html, expected_position=1)
def test_student_view_last_child(self):
html = self._get_rendered_student_view(self.sequence_3_1, requested_child='last')
self._assert_view_at_position(html, expected_position=3)
def _get_rendered_student_view(self, sequence, requested_child):
"""
Returns the rendered student view for the given sequence and the
requested_child parameter.
"""
return sequence.xmodule_runtime.render(
sequence,
STUDENT_VIEW,
{
'redirect_url_func': lambda course_key, block_location, child: unicode(block_location),
'requested_child': requested_child,
},
).content
def _assert_view_at_position(self, rendered_html, expected_position):
"""
Verifies that the rendered view contains the expected position.
"""
self.assertIn("'position': {}".format(expected_position), rendered_html)
def test_compute_next_url(self):
for sequence, parent, expected_next_sequence_location in [
(self.sequence_1_1, self.chapter_1, self.sequence_1_2.location),
(self.sequence_1_2, self.chapter_1, self.chapter_2.location),
(self.sequence_3_1, self.chapter_3, self.chapter_4.location),
(self.sequence_4_1, self.chapter_4, self.sequence_4_2.location),
(self.sequence_4_2, self.chapter_4, None),
]:
actual_next_sequence_location = _compute_next_url(
sequence.location,
parent,
lambda course_key, block_location, child: block_location,
)
self.assertEquals(actual_next_sequence_location, expected_next_sequence_location)
def test_compute_previous_url(self):
for sequence, parent, expected_prev_sequence_location in [
(self.sequence_1_1, self.chapter_1, None),
(self.sequence_1_2, self.chapter_1, self.sequence_1_1.location),
(self.sequence_3_1, self.chapter_3, self.chapter_2.location),
(self.sequence_4_1, self.chapter_4, self.chapter_3.location),
(self.sequence_4_2, self.chapter_4, self.sequence_4_1.location),
]:
actual_next_sequence_location = _compute_previous_url(
sequence.location,
parent,
lambda course_key, block_location, child: block_location,
)
self.assertEquals(actual_next_sequence_location, expected_prev_sequence_location)
...@@ -57,7 +57,8 @@ class InMemorySystem(XMLParsingSystem, MakoDescriptorSystem): # pylint: disable ...@@ -57,7 +57,8 @@ class InMemorySystem(XMLParsingSystem, MakoDescriptorSystem): # pylint: disable
class XModuleXmlImportTest(TestCase): class XModuleXmlImportTest(TestCase):
"""Base class for tests that use basic XML parsing""" """Base class for tests that use basic XML parsing"""
def process_xml(self, xml_import_data): @classmethod
def process_xml(cls, xml_import_data):
"""Use the `xml_import_data` to import an :class:`XBlock` from XML.""" """Use the `xml_import_data` to import an :class:`XBlock` from XML."""
system = InMemorySystem(xml_import_data) system = InMemorySystem(xml_import_data)
return system.process_xml(xml_import_data.xml_string) return system.process_xml(xml_import_data.xml_string)
...@@ -8,6 +8,7 @@ from fs.memoryfs import MemoryFS ...@@ -8,6 +8,7 @@ from fs.memoryfs import MemoryFS
from factory import Factory, lazy_attribute, post_generation, Sequence from factory import Factory, lazy_attribute, post_generation, Sequence
from lxml import etree from lxml import etree
from xblock.mixins import HierarchyMixin
from xmodule.modulestore.inheritance import InheritanceMixin from xmodule.modulestore.inheritance import InheritanceMixin
from xmodule.x_module import XModuleMixin from xmodule.x_module import XModuleMixin
from xmodule.modulestore import only_xmodules from xmodule.modulestore import only_xmodules
...@@ -68,7 +69,7 @@ class XmlImportFactory(Factory): ...@@ -68,7 +69,7 @@ class XmlImportFactory(Factory):
model = XmlImportData model = XmlImportData
filesystem = MemoryFS() filesystem = MemoryFS()
xblock_mixins = (InheritanceMixin, XModuleMixin) xblock_mixins = (InheritanceMixin, XModuleMixin, HierarchyMixin)
xblock_select = only_xmodules xblock_select = only_xmodules
url_name = Sequence(str) url_name = Sequence(str)
attribs = {} attribs = {}
...@@ -142,6 +143,11 @@ class CourseFactory(XmlImportFactory): ...@@ -142,6 +143,11 @@ class CourseFactory(XmlImportFactory):
static_asset_path = 'xml_test_course' static_asset_path = 'xml_test_course'
class ChapterFactory(XmlImportFactory):
"""Factory for <chapter> nodes"""
tag = 'chapter'
class SequenceFactory(XmlImportFactory): class SequenceFactory(XmlImportFactory):
"""Factory for <sequential> nodes""" """Factory for <sequential> nodes"""
tag = 'sequential' tag = 'sequential'
......
...@@ -3,7 +3,7 @@ Course navigation page object ...@@ -3,7 +3,7 @@ Course navigation page object
""" """
import re import re
from bok_choy.page_object import PageObject from bok_choy.page_object import PageObject, unguarded
from bok_choy.promise import EmptyPromise from bok_choy.promise import EmptyPromise
...@@ -165,10 +165,11 @@ class CourseNavPage(PageObject): ...@@ -165,10 +165,11 @@ class CourseNavPage(PageObject):
""" """
desc = "currently at section '{0}' and subsection '{1}'".format(section_title, subsection_title) desc = "currently at section '{0}' and subsection '{1}'".format(section_title, subsection_title)
return EmptyPromise( return EmptyPromise(
lambda: self._is_on_section(section_title, subsection_title), desc lambda: self.is_on_section(section_title, subsection_title), desc
) )
def _is_on_section(self, section_title, subsection_title): @unguarded
def is_on_section(self, section_title, subsection_title):
""" """
Return a boolean indicating whether the user is on the section and subsection Return a boolean indicating whether the user is on the section and subsection
with the specified titles. with the specified titles.
...@@ -203,13 +204,9 @@ class CourseNavPage(PageObject): ...@@ -203,13 +204,9 @@ class CourseNavPage(PageObject):
""" """
return self.REMOVE_SPAN_TAG_RE.search(element.get_attribute('innerHTML')).groups()[0].strip() return self.REMOVE_SPAN_TAG_RE.search(element.get_attribute('innerHTML')).groups()[0].strip()
def go_to_sequential_position(self, sequential_position): @property
def active_subsection_url(self):
""" """
Within a section/subsection navigate to the sequential position specified by `sequential_position`. return the url of the active subsection in the left nav
Arguments:
sequential_position (int): position in sequential bar
""" """
sequential_position_css = '#tab_{0}'.format(sequential_position - 1) return self.q(css='.chapter-content-container .menu-item.active a').attrs('href')[0]
self.q(css=sequential_position_css).first.click()
...@@ -21,6 +21,13 @@ class CoursewarePage(CoursePage): ...@@ -21,6 +21,13 @@ class CoursewarePage(CoursePage):
return self.q(css='body.courseware').present return self.q(css='body.courseware').present
@property @property
def chapter_count_in_navigation(self):
"""
Returns count of chapters available on LHS navigation.
"""
return len(self.q(css='nav.course-navigation a.chapter'))
@property
def num_sections(self): def num_sections(self):
""" """
Return the number of sections in the sidebar on the page Return the number of sections in the sidebar on the page
...@@ -101,11 +108,66 @@ class CoursewarePage(CoursePage): ...@@ -101,11 +108,66 @@ class CoursewarePage(CoursePage):
return element.text[0] return element.text[0]
return None return None
def get_active_subsection_url(self): def go_to_sequential_position(self, sequential_position):
"""
Within a section/subsection navigate to the sequential position specified by `sequential_position`.
Arguments:
sequential_position (int): position in sequential bar
"""
sequential_position_css = '#sequence-list #tab_{0}'.format(sequential_position - 1)
self.q(css=sequential_position_css).first.click()
@property
def sequential_position(self):
"""
Returns the position of the active tab in the sequence.
"""
tab_id = self._active_sequence_tab.attrs('id')[0]
return int(tab_id.split('_')[1])
@property
def _active_sequence_tab(self): # pylint: disable=missing-docstring
return self.q(css='#sequence-list .nav-item.active')
@property
def is_next_button_enabled(self): # pylint: disable=missing-docstring
return not self.q(css='.sequence-nav > .sequence-nav-button.button-next.disabled').is_present()
@property
def is_previous_button_enabled(self): # pylint: disable=missing-docstring
return not self.q(css='.sequence-nav > .sequence-nav-button.button-previous.disabled').is_present()
def click_next_button_on_top(self): # pylint: disable=missing-docstring
self._click_navigation_button('sequence-nav', 'button-next')
def click_next_button_on_bottom(self): # pylint: disable=missing-docstring
self._click_navigation_button('sequence-bottom', 'button-next')
def click_previous_button_on_top(self): # pylint: disable=missing-docstring
self._click_navigation_button('sequence-nav', 'button-previous')
def click_previous_button_on_bottom(self): # pylint: disable=missing-docstring
self._click_navigation_button('sequence-bottom', 'button-previous')
def _click_navigation_button(self, top_or_bottom_class, next_or_previous_class):
""" """
return the url of the active subsection in the left nav Clicks the navigation button, given the respective CSS classes.
""" """
return self.q(css='.chapter-content-container .menu-item.active a').attrs('href')[0] previous_tab_id = self._active_sequence_tab.attrs('data-id')[0]
def is_at_new_tab_id():
"""
Returns whether the active tab has changed. It is defensive
against the case where the page is still being loaded.
"""
active_tab = self._active_sequence_tab
return active_tab and previous_tab_id != active_tab.attrs('data-id')[0]
self.q(
css='.{} > .sequence-nav-button.{}'.format(top_or_bottom_class, next_or_previous_class)
).first.click()
EmptyPromise(is_at_new_tab_id, "Button navigation fulfilled").fulfill()
@property @property
def can_start_proctored_exam(self): def can_start_proctored_exam(self):
...@@ -161,13 +223,6 @@ class CoursewarePage(CoursePage): ...@@ -161,13 +223,6 @@ class CoursewarePage(CoursePage):
and "You have passed the entrance exam" in self.entrance_exam_message_selector.text[0] and "You have passed the entrance exam" in self.entrance_exam_message_selector.text[0]
@property @property
def chapter_count_in_navigation(self):
"""
Returns count of chapters available on LHS navigation.
"""
return len(self.q(css='nav.course-navigation a.chapter'))
@property
def is_timer_bar_present(self): def is_timer_bar_present(self):
""" """
Returns True if the timed/proctored exam timer bar is visible on the courseware. Returns True if the timed/proctored exam timer bar is visible on the courseware.
......
...@@ -2,7 +2,8 @@ ...@@ -2,7 +2,8 @@
""" """
End-to-end tests for the LMS. End-to-end tests for the LMS.
""" """
import time from nose.plugins.attrib import attr
from unittest import skip
from ..helpers import UniqueCourseTest from ..helpers import UniqueCourseTest
from ...pages.studio.auto_auth import AutoAuthPage from ...pages.studio.auto_auth import AutoAuthPage
...@@ -420,13 +421,20 @@ class CoursewareMultipleVerticalsTest(UniqueCourseTest): ...@@ -420,13 +421,20 @@ class CoursewareMultipleVerticalsTest(UniqueCourseTest):
course_fix.add_children( course_fix.add_children(
XBlockFixtureDesc('chapter', 'Test Section 1').add_children( XBlockFixtureDesc('chapter', 'Test Section 1').add_children(
XBlockFixtureDesc('sequential', 'Test Subsection 1').add_children( XBlockFixtureDesc('sequential', 'Test Subsection 1,1').add_children(
XBlockFixtureDesc('problem', 'Test Problem 1', data='<problem>problem 1 dummy body</problem>'), XBlockFixtureDesc('problem', 'Test Problem 1', data='<problem>problem 1 dummy body</problem>'),
XBlockFixtureDesc('html', 'html 1', data="<html>html 1 dummy body</html>"), XBlockFixtureDesc('html', 'html 1', data="<html>html 1 dummy body</html>"),
XBlockFixtureDesc('problem', 'Test Problem 2', data="<problem>problem 2 dummy body</problem>"), XBlockFixtureDesc('problem', 'Test Problem 2', data="<problem>problem 2 dummy body</problem>"),
XBlockFixtureDesc('html', 'html 2', data="<html>html 2 dummy body</html>"), XBlockFixtureDesc('html', 'html 2', data="<html>html 2 dummy body</html>"),
), ),
XBlockFixtureDesc('sequential', 'Test Subsection 2'), XBlockFixtureDesc('sequential', 'Test Subsection 1,2').add_children(
XBlockFixtureDesc('problem', 'Test Problem 3', data='<problem>problem 3 dummy body</problem>'),
),
),
XBlockFixtureDesc('chapter', 'Test Section 2').add_children(
XBlockFixtureDesc('sequential', 'Test Subsection 2,1').add_children(
XBlockFixtureDesc('problem', 'Test Problem 4', data='<problem>problem 4 dummy body</problem>'),
),
), ),
).install() ).install()
...@@ -436,10 +444,53 @@ class CoursewareMultipleVerticalsTest(UniqueCourseTest): ...@@ -436,10 +444,53 @@ class CoursewareMultipleVerticalsTest(UniqueCourseTest):
self.courseware_page.visit() self.courseware_page.visit()
self.course_nav = CourseNavPage(self.browser) self.course_nav = CourseNavPage(self.browser)
def test_navigation_buttons(self):
# start in first section
self.assert_navigation_state('Test Section 1', 'Test Subsection 1,1', 0, next_enabled=True, prev_enabled=False)
# next takes us to next tab in sequential
self.courseware_page.click_next_button_on_top()
self.assert_navigation_state('Test Section 1', 'Test Subsection 1,1', 1, next_enabled=True, prev_enabled=True)
# go to last sequential position
self.courseware_page.go_to_sequential_position(4)
self.assert_navigation_state('Test Section 1', 'Test Subsection 1,1', 3, next_enabled=True, prev_enabled=True)
# next takes us to next sequential
self.courseware_page.click_next_button_on_bottom()
self.assert_navigation_state('Test Section 1', 'Test Subsection 1,2', 0, next_enabled=True, prev_enabled=True)
# next takes us to next chapter
self.courseware_page.click_next_button_on_top()
self.assert_navigation_state('Test Section 2', 'Test Subsection 2,1', 0, next_enabled=False, prev_enabled=True)
# previous takes us to previous chapter
self.courseware_page.click_previous_button_on_top()
self.assert_navigation_state('Test Section 1', 'Test Subsection 1,2', 0, next_enabled=True, prev_enabled=True)
# previous takes us to last tab in previous sequential
self.courseware_page.click_previous_button_on_bottom()
self.assert_navigation_state('Test Section 1', 'Test Subsection 1,1', 3, next_enabled=True, prev_enabled=True)
# previous takes us to previous tab in sequential
self.courseware_page.click_previous_button_on_bottom()
self.assert_navigation_state('Test Section 1', 'Test Subsection 1,1', 2, next_enabled=True, prev_enabled=True)
def assert_navigation_state(
self, section_title, subsection_title, subsection_position, next_enabled, prev_enabled
):
"""
Verifies that the navigation state is as expected.
"""
self.assertTrue(self.course_nav.is_on_section(section_title, subsection_title))
self.assertEquals(self.courseware_page.sequential_position, subsection_position)
self.assertEquals(self.courseware_page.is_next_button_enabled, next_enabled)
self.assertEquals(self.courseware_page.is_previous_button_enabled, prev_enabled)
def test_tab_position(self): def test_tab_position(self):
# test that using the position in the url direct to correct tab in courseware # test that using the position in the url direct to correct tab in courseware
self.course_nav.go_to_section('Test Section 1', 'Test Subsection 1') self.course_nav.go_to_section('Test Section 1', 'Test Subsection 1,1')
subsection_url = self.courseware_page.get_active_subsection_url() subsection_url = self.course_nav.active_subsection_url
url_part_list = subsection_url.split('/') url_part_list = subsection_url.split('/')
self.assertEqual(len(url_part_list), 9) self.assertEqual(len(url_part_list), 9)
...@@ -481,3 +532,15 @@ class CoursewareMultipleVerticalsTest(UniqueCourseTest): ...@@ -481,3 +532,15 @@ class CoursewareMultipleVerticalsTest(UniqueCourseTest):
position=4 position=4
).visit() ).visit()
self.assertIn('html 2 dummy body', html2_page.get_selected_tab_content()) self.assertIn('html 2 dummy body', html2_page.get_selected_tab_content())
@skip('Fix a11y test errors.')
@attr('a11y')
def test_courseware_a11y(self):
"""
Run accessibility audit for the problem type.
"""
self.course_nav.go_to_section('Test Section 1', 'Test Subsection 1,1')
# Set the scope to the sequence navigation
self.courseware_page.a11y_audit.config.set_scope(
include=['div.sequence-nav'])
self.courseware_page.a11y_audit.check_for_accessibility_errors()
...@@ -194,14 +194,14 @@ class EdxNotesDefaultInteractionsTest(EdxNotesTestMixin): ...@@ -194,14 +194,14 @@ class EdxNotesDefaultInteractionsTest(EdxNotesTestMixin):
self.create_notes(components) self.create_notes(components)
self.assert_text_in_notes(self.note_unit_page.notes) self.assert_text_in_notes(self.note_unit_page.notes)
self.course_nav.go_to_sequential_position(2) self.courseware_page.go_to_sequential_position(2)
components = self.note_unit_page.components components = self.note_unit_page.components
self.create_notes(components) self.create_notes(components)
components = self.note_unit_page.refresh() components = self.note_unit_page.refresh()
self.assert_text_in_notes(self.note_unit_page.notes) self.assert_text_in_notes(self.note_unit_page.notes)
self.course_nav.go_to_sequential_position(1) self.courseware_page.go_to_sequential_position(1)
components = self.note_unit_page.components components = self.note_unit_page.components
self.assert_text_in_notes(self.note_unit_page.notes) self.assert_text_in_notes(self.note_unit_page.notes)
...@@ -227,7 +227,7 @@ class EdxNotesDefaultInteractionsTest(EdxNotesTestMixin): ...@@ -227,7 +227,7 @@ class EdxNotesDefaultInteractionsTest(EdxNotesTestMixin):
self.edit_notes(components) self.edit_notes(components)
self.assert_text_in_notes(self.note_unit_page.notes) self.assert_text_in_notes(self.note_unit_page.notes)
self.course_nav.go_to_sequential_position(2) self.courseware_page.go_to_sequential_position(2)
components = self.note_unit_page.components components = self.note_unit_page.components
self.edit_notes(components) self.edit_notes(components)
self.assert_text_in_notes(self.note_unit_page.notes) self.assert_text_in_notes(self.note_unit_page.notes)
...@@ -235,7 +235,7 @@ class EdxNotesDefaultInteractionsTest(EdxNotesTestMixin): ...@@ -235,7 +235,7 @@ class EdxNotesDefaultInteractionsTest(EdxNotesTestMixin):
components = self.note_unit_page.refresh() components = self.note_unit_page.refresh()
self.assert_text_in_notes(self.note_unit_page.notes) self.assert_text_in_notes(self.note_unit_page.notes)
self.course_nav.go_to_sequential_position(1) self.courseware_page.go_to_sequential_position(1)
components = self.note_unit_page.components components = self.note_unit_page.components
self.assert_text_in_notes(self.note_unit_page.notes) self.assert_text_in_notes(self.note_unit_page.notes)
...@@ -261,7 +261,7 @@ class EdxNotesDefaultInteractionsTest(EdxNotesTestMixin): ...@@ -261,7 +261,7 @@ class EdxNotesDefaultInteractionsTest(EdxNotesTestMixin):
self.remove_notes(components) self.remove_notes(components)
self.assert_notes_are_removed(components) self.assert_notes_are_removed(components)
self.course_nav.go_to_sequential_position(2) self.courseware_page.go_to_sequential_position(2)
components = self.note_unit_page.components components = self.note_unit_page.components
self.remove_notes(components) self.remove_notes(components)
self.assert_notes_are_removed(components) self.assert_notes_are_removed(components)
...@@ -269,7 +269,7 @@ class EdxNotesDefaultInteractionsTest(EdxNotesTestMixin): ...@@ -269,7 +269,7 @@ class EdxNotesDefaultInteractionsTest(EdxNotesTestMixin):
components = self.note_unit_page.refresh() components = self.note_unit_page.refresh()
self.assert_notes_are_removed(components) self.assert_notes_are_removed(components)
self.course_nav.go_to_sequential_position(1) self.courseware_page.go_to_sequential_position(1)
components = self.note_unit_page.components components = self.note_unit_page.components
self.assert_notes_are_removed(components) self.assert_notes_are_removed(components)
...@@ -1106,10 +1106,10 @@ class EdxNotesPageTest(EventsTestMixin, EdxNotesTestMixin): ...@@ -1106,10 +1106,10 @@ class EdxNotesPageTest(EventsTestMixin, EdxNotesTestMixin):
self.assertTrue(note.is_visible) self.assertTrue(note.is_visible)
note = self.note_unit_page.notes[1] note = self.note_unit_page.notes[1]
self.assertFalse(note.is_visible) self.assertFalse(note.is_visible)
self.course_nav.go_to_sequential_position(2) self.courseware_page.go_to_sequential_position(2)
note = self.note_unit_page.notes[0] note = self.note_unit_page.notes[0]
self.assertFalse(note.is_visible) self.assertFalse(note.is_visible)
self.course_nav.go_to_sequential_position(1) self.courseware_page.go_to_sequential_position(1)
note = self.note_unit_page.notes[0] note = self.note_unit_page.notes[0]
self.assertFalse(note.is_visible) self.assertFalse(note.is_visible)
...@@ -1494,7 +1494,7 @@ class EdxNotesToggleNotesTest(EdxNotesTestMixin): ...@@ -1494,7 +1494,7 @@ class EdxNotesToggleNotesTest(EdxNotesTestMixin):
# Disable all notes # Disable all notes
self.note_unit_page.toggle_visibility() self.note_unit_page.toggle_visibility()
self.assertEqual(len(self.note_unit_page.notes), 0) self.assertEqual(len(self.note_unit_page.notes), 0)
self.course_nav.go_to_sequential_position(2) self.courseware_page.go_to_sequential_position(2)
self.assertEqual(len(self.note_unit_page.notes), 0) self.assertEqual(len(self.note_unit_page.notes), 0)
self.course_nav.go_to_section(u"Test Section 1", u"Test Subsection 2") self.course_nav.go_to_section(u"Test Section 1", u"Test Subsection 2")
self.assertEqual(len(self.note_unit_page.notes), 0) self.assertEqual(len(self.note_unit_page.notes), 0)
...@@ -1520,7 +1520,7 @@ class EdxNotesToggleNotesTest(EdxNotesTestMixin): ...@@ -1520,7 +1520,7 @@ class EdxNotesToggleNotesTest(EdxNotesTestMixin):
# the page. # the page.
self.note_unit_page.toggle_visibility() self.note_unit_page.toggle_visibility()
self.assertGreater(len(self.note_unit_page.notes), 0) self.assertGreater(len(self.note_unit_page.notes), 0)
self.course_nav.go_to_sequential_position(2) self.courseware_page.go_to_sequential_position(2)
self.assertGreater(len(self.note_unit_page.notes), 0) self.assertGreater(len(self.note_unit_page.notes), 0)
self.course_nav.go_to_section(u"Test Section 1", u"Test Subsection 2") self.course_nav.go_to_section(u"Test Section 1", u"Test Subsection 2")
self.assertGreater(len(self.note_unit_page.notes), 0) self.assertGreater(len(self.note_unit_page.notes), 0)
...@@ -1546,7 +1546,7 @@ class PublishSectionTest(CourseOutlineTest): ...@@ -1546,7 +1546,7 @@ class PublishSectionTest(CourseOutlineTest):
self.assertTrue(section.publish_action) self.assertTrue(section.publish_action)
self.courseware.visit() self.courseware.visit()
self.assertEqual(1, self.courseware.num_xblock_components) self.assertEqual(1, self.courseware.num_xblock_components)
self.course_nav.go_to_sequential_position(2) self.courseware.go_to_sequential_position(2)
self.assertEqual(1, self.courseware.num_xblock_components) self.assertEqual(1, self.courseware.num_xblock_components)
def test_section_publishing(self): def test_section_publishing(self):
...@@ -1571,7 +1571,7 @@ class PublishSectionTest(CourseOutlineTest): ...@@ -1571,7 +1571,7 @@ class PublishSectionTest(CourseOutlineTest):
self.assertFalse(unit.publish_action) self.assertFalse(unit.publish_action)
self.courseware.visit() self.courseware.visit()
self.assertEqual(1, self.courseware.num_xblock_components) self.assertEqual(1, self.courseware.num_xblock_components)
self.course_nav.go_to_sequential_position(2) self.courseware.go_to_sequential_position(2)
self.assertEqual(1, self.courseware.num_xblock_components) self.assertEqual(1, self.courseware.num_xblock_components)
self.course_nav.go_to_section(SECTION_NAME, 'Test Subsection 2') self.course_nav.go_to_section(SECTION_NAME, 'Test Subsection 2')
self.assertEqual(1, self.courseware.num_xblock_components) self.assertEqual(1, self.courseware.num_xblock_components)
......
...@@ -11,6 +11,7 @@ from unittest import skipIf, skip ...@@ -11,6 +11,7 @@ from unittest import skipIf, skip
from ..helpers import UniqueCourseTest, is_youtube_available, YouTubeStubConfig from ..helpers import UniqueCourseTest, is_youtube_available, YouTubeStubConfig
from ...pages.lms.video.video import VideoPage from ...pages.lms.video.video import VideoPage
from ...pages.lms.tab_nav import TabNavPage from ...pages.lms.tab_nav import TabNavPage
from ...pages.lms.courseware import CoursewarePage
from ...pages.lms.course_nav import CourseNavPage from ...pages.lms.course_nav import CourseNavPage
from ...pages.lms.auto_auth import AutoAuthPage from ...pages.lms.auto_auth import AutoAuthPage
from ...pages.lms.course_info import CourseInfoPage from ...pages.lms.course_info import CourseInfoPage
...@@ -49,6 +50,7 @@ class VideoBaseTest(UniqueCourseTest): ...@@ -49,6 +50,7 @@ class VideoBaseTest(UniqueCourseTest):
self.video = VideoPage(self.browser) self.video = VideoPage(self.browser)
self.tab_nav = TabNavPage(self.browser) self.tab_nav = TabNavPage(self.browser)
self.course_nav = CourseNavPage(self.browser) self.course_nav = CourseNavPage(self.browser)
self.courseware = CoursewarePage(self.browser, self.course_id)
self.course_info_page = CourseInfoPage(self.browser, self.course_id) self.course_info_page = CourseInfoPage(self.browser, self.course_id)
self.auth_page = AutoAuthPage(self.browser, course_id=self.course_id) self.auth_page = AutoAuthPage(self.browser, course_id=self.course_id)
...@@ -190,7 +192,7 @@ class VideoBaseTest(UniqueCourseTest): ...@@ -190,7 +192,7 @@ class VideoBaseTest(UniqueCourseTest):
""" """
Navigate to sequential specified by `video_display_name` Navigate to sequential specified by `video_display_name`
""" """
self.course_nav.go_to_sequential_position(position) self.courseware.go_to_sequential_position(position)
self.video.wait_for_video_player_render() self.video.wait_for_video_player_render()
...@@ -916,13 +918,13 @@ class YouTubeVideoTest(VideoBaseTest): ...@@ -916,13 +918,13 @@ class YouTubeVideoTest(VideoBaseTest):
execute_video_steps(['A']) execute_video_steps(['A'])
# go to second sequential position # go to second sequential position
self.course_nav.go_to_sequential_position(2) self.courseware.go_to_sequential_position(2)
execute_video_steps(tab2_video_names) execute_video_steps(tab2_video_names)
# go back to first sequential position # go back to first sequential position
# we are again playing tab 1 videos to ensure that switching didn't broke some video functionality. # we are again playing tab 1 videos to ensure that switching didn't broke some video functionality.
self.course_nav.go_to_sequential_position(1) self.courseware.go_to_sequential_position(1)
execute_video_steps(tab1_video_names) execute_video_steps(tab1_video_names)
self.video.browser.refresh() self.video.browser.refresh()
......
...@@ -37,6 +37,7 @@ from course_modes.tests.factories import CourseModeFactory ...@@ -37,6 +37,7 @@ from course_modes.tests.factories import CourseModeFactory
from courseware.model_data import set_score from courseware.model_data import set_score
from courseware.testutils import RenderXBlockTestMixin from courseware.testutils import RenderXBlockTestMixin
from courseware.tests.factories import StudentModuleFactory from courseware.tests.factories import StudentModuleFactory
from courseware.url_helpers import get_redirect_url
from courseware.user_state_client import DjangoXBlockUserStateClient from courseware.user_state_client import DjangoXBlockUserStateClient
from edxmako.tests import mako_middleware_process_request from edxmako.tests import mako_middleware_process_request
from lms.djangoapps.commerce.utils import EcommerceService # pylint: disable=import-error from lms.djangoapps.commerce.utils import EcommerceService # pylint: disable=import-error
...@@ -192,9 +193,25 @@ class ViewsTestCase(ModuleStoreTestCase): ...@@ -192,9 +193,25 @@ class ViewsTestCase(ModuleStoreTestCase):
super(ViewsTestCase, self).setUp() super(ViewsTestCase, self).setUp()
self.course = CourseFactory.create(display_name=u'teꜱᴛ course') self.course = CourseFactory.create(display_name=u'teꜱᴛ course')
self.chapter = ItemFactory.create(category='chapter', parent_location=self.course.location) 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.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.course_key = self.course.id
self.password = '123456' self.password = '123456'
...@@ -210,6 +227,74 @@ class ViewsTestCase(ModuleStoreTestCase): ...@@ -210,6 +227,74 @@ class ViewsTestCase(ModuleStoreTestCase):
self.org = u"ꜱᴛᴀʀᴋ ɪɴᴅᴜꜱᴛʀɪᴇꜱ" self.org = u"ꜱᴛᴀʀᴋ ɪɴᴅᴜꜱᴛʀɪᴇꜱ"
self.org_html = "<p>'+Stark/Industries+'</p>" 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") @unittest.skipUnless(settings.FEATURES.get('ENABLE_SHOPPING_CART'), "Shopping Cart not enabled in settings")
@patch.dict(settings.FEATURES, {'ENABLE_PAID_COURSE_REGISTRATION': True}) @patch.dict(settings.FEATURES, {'ENABLE_PAID_COURSE_REGISTRATION': True})
def test_course_about_in_cart(self): def test_course_about_in_cart(self):
...@@ -299,15 +384,37 @@ class ViewsTestCase(ModuleStoreTestCase): ...@@ -299,15 +384,37 @@ class ViewsTestCase(ModuleStoreTestCase):
self.assertEqual(views.user_groups(mock_user), []) self.assertEqual(views.user_groups(mock_user), [])
def test_get_current_child(self): def test_get_current_child(self):
self.assertIsNone(views.get_current_child(MagicMock()))
mock_xmodule = MagicMock() mock_xmodule = MagicMock()
self.assertIsNone(views.get_current_child(mock_xmodule))
mock_xmodule.position = -1 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') self.assertEqual(views.get_current_child(mock_xmodule), 'one')
mock_xmodule_2 = MagicMock()
mock_xmodule_2.position = 3 mock_xmodule.position = 2
mock_xmodule_2.get_display_items.return_value = [] self.assertEqual(views.get_current_child(mock_xmodule), 'two')
self.assertIsNone(views.get_current_child(mock_xmodule_2)) 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): def test_redirect_to_course_position(self):
mock_module = MagicMock() mock_module = MagicMock()
......
...@@ -7,12 +7,13 @@ from xmodule.modulestore.django import modulestore ...@@ -7,12 +7,13 @@ from xmodule.modulestore.django import modulestore
from django.core.urlresolvers import reverse 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 """ Returns the redirect url back to courseware
Args: Args:
course_id(str): Course Id string course_id(str): Course Id string
location(str): The location id of course component location(str): The location id of course component
child(str): Optional child parameter to pass to the URL
Raises: Raises:
ItemNotFoundError if no data at the location or NoPathToItem if location not in any class 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): ...@@ -50,4 +51,7 @@ def get_redirect_url(course_key, usage_key):
redirect_url += "?{}".format(urlencode({'activate_block_id': unicode(final_target_id)})) redirect_url += "?{}".format(urlencode({'activate_block_id': unicode(final_target_id)}))
if child:
redirect_url += "&child={}".format(child)
return redirect_url return redirect_url
...@@ -162,6 +162,9 @@ FEATURES['ENABLE_COURSEWARE_SEARCH'] = True ...@@ -162,6 +162,9 @@ FEATURES['ENABLE_COURSEWARE_SEARCH'] = True
# Enable dashboard search for tests # Enable dashboard search for tests
FEATURES['ENABLE_DASHBOARD_SEARCH'] = True 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 # Use MockSearchEngine as the search engine for test scenario
SEARCH_ENGINE = "search.tests.mock_search_engine.MockSearchEngine" SEARCH_ENGINE = "search.tests.mock_search_engine.MockSearchEngine"
# Path at which to store the mock index # Path at which to store the mock index
......
...@@ -371,7 +371,10 @@ FEATURES = { ...@@ -371,7 +371,10 @@ FEATURES = {
# This is the default, but can be disabled if all history # This is the default, but can be disabled if all history
# lives in the Extended table, saving the frontend from # lives in the Extended table, saving the frontend from
# making multiple queries. # 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 # Ignore static asset files on import which match this pattern
......
<%! from django.utils.translation import ugettext as _ %> <%! 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="path"></div>
<div class="sequence-nav"> <div class="sequence-nav">
<button class="sequence-nav-button button-previous"> <button class="sequence-nav-button button-previous">
......
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