% endif
- <%
- section_class = "level-nesting" if xblock.has_children else "level-element"
- collapsible_class = "is-collapsible" if xblock.has_children else ""
- %>
diff --git a/cms/templates/widgets/sequence-edit.html b/cms/templates/widgets/sequence-edit.html
index 546633ee6e..4838e8a538 100644
--- a/cms/templates/widgets/sequence-edit.html
+++ b/cms/templates/widgets/sequence-edit.html
@@ -1,54 +1,3 @@
-
-
-
<%include file="metadata-edit.html" />
-
diff --git a/common/lib/xmodule/xmodule/js/src/sequence/edit.coffee b/common/lib/xmodule/xmodule/js/src/sequence/edit.coffee
index 33942bc97d..1856266697 100644
--- a/common/lib/xmodule/xmodule/js/src/sequence/edit.coffee
+++ b/common/lib/xmodule/xmodule/js/src/sequence/edit.coffee
@@ -1,9 +1,2 @@
class @SequenceDescriptor extends XModule.Descriptor
- constructor: (@element) ->
- @$tabs = $(@element).find("#sequence-list")
- @$tabs.sortable(
- update: (event, ui) => @update()
- )
- save: ->
- children: $('#sequence-list li a', @element).map((idx, el) -> $(el).data('id')).toArray()
diff --git a/common/lib/xmodule/xmodule/js/src/vertical/edit.coffee b/common/lib/xmodule/xmodule/js/src/vertical/edit.coffee
index 8dbf8e2550..09bec3bef5 100644
--- a/common/lib/xmodule/xmodule/js/src/vertical/edit.coffee
+++ b/common/lib/xmodule/xmodule/js/src/vertical/edit.coffee
@@ -1,9 +1,2 @@
class @VerticalDescriptor extends XModule.Descriptor
- constructor: (@element) ->
- @$items = $(@element).find(".vert-mod")
- @$items.sortable(
- update: (event, ui) => @update()
- )
- save: ->
- children: $('.vert-mod div', @element).map((idx, el) -> $(el).data('id')).toArray()
diff --git a/common/lib/xmodule/xmodule/js/src/wrapper/edit.coffee b/common/lib/xmodule/xmodule/js/src/wrapper/edit.coffee
deleted file mode 100644
index 801b09d168..0000000000
--- a/common/lib/xmodule/xmodule/js/src/wrapper/edit.coffee
+++ /dev/null
@@ -1,10 +0,0 @@
-class @WrapperDescriptor extends XModule.Descriptor
- constructor: (@element) ->
- console.log 'WrapperDescriptor'
- @$items = $(@element).find(".vert-mod")
- @$items.sortable(
- update: (event, ui) => @update()
- )
-
- save: ->
- children: $('.vert-mod div', @element).map((idx, el) -> $(el).data('id')).toArray()
diff --git a/common/lib/xmodule/xmodule/split_test_module.py b/common/lib/xmodule/xmodule/split_test_module.py
index 40a772bcf2..f0c7d67240 100644
--- a/common/lib/xmodule/xmodule/split_test_module.py
+++ b/common/lib/xmodule/xmodule/split_test_module.py
@@ -8,12 +8,13 @@ from webob import Response
from xmodule.progress import Progress
from xmodule.seq_module import SequenceDescriptor
+from xmodule.studio_editable import StudioEditableModule
from xmodule.x_module import XModule, module_attr
from lxml import etree
from xblock.core import XBlock
-from xblock.fields import Scope, Integer, ReferenceValueDict
+from xblock.fields import Scope, Integer, String, ReferenceValueDict
from xblock.fragment import Fragment
log = logging.getLogger('edx.' + __name__)
@@ -23,6 +24,13 @@ class SplitTestFields(object):
"""Fields needed for split test module"""
has_children = True
+ display_name = String(
+ display_name="Display Name",
+ help="This name appears in the horizontal navigation at the top of the page.",
+ scope=Scope.settings,
+ default="Experiment Block"
+ )
+
user_partition_id = Integer(
help="Which user partition is used for this test",
scope=Scope.content
@@ -45,7 +53,7 @@ class SplitTestFields(object):
@XBlock.needs('user_tags') # pylint: disable=abstract-method
@XBlock.wants('partitions')
-class SplitTestModule(SplitTestFields, XModule):
+class SplitTestModule(SplitTestFields, XModule, StudioEditableModule):
"""
Show the user the appropriate child. Uses the ExperimentState
API to figure out which child to show.
@@ -177,21 +185,10 @@ class SplitTestModule(SplitTestFields, XModule):
Renders the Studio preview by rendering each child so that they can all be seen and edited.
"""
fragment = Fragment()
- contents = []
-
- for child in self.descriptor.get_children():
- rendered_child = self.runtime.get_module(child).render('student_view', context)
- fragment.add_frag_resources(rendered_child)
-
- contents.append({
- 'id': child.location.to_deprecated_string(),
- 'content': rendered_child.content
- })
-
- fragment.add_content(self.system.render_template('vert_module.html', {
- 'items': contents
- }))
-
+ # Only render the children when this block is being shown as the container
+ root_xblock = context.get('root_xblock')
+ if root_xblock and root_xblock.location == self.location:
+ self.render_children(context, fragment, can_reorder=False)
return fragment
def student_view(self, context):
@@ -296,3 +293,11 @@ class SplitTestDescriptor(SplitTestFields, SequenceDescriptor):
makes it use module.get_child_descriptors().
"""
return True
+
+ @property
+ def non_editable_metadata_fields(self):
+ non_editable_fields = super(SplitTestDescriptor, self).non_editable_metadata_fields
+ non_editable_fields.extend([
+ SplitTestDescriptor.due,
+ ])
+ return non_editable_fields
diff --git a/common/lib/xmodule/xmodule/studio_editable.py b/common/lib/xmodule/xmodule/studio_editable.py
index 799dbf1103..81284cd6e1 100644
--- a/common/lib/xmodule/xmodule/studio_editable.py
+++ b/common/lib/xmodule/xmodule/studio_editable.py
@@ -5,25 +5,34 @@ Mixin to support editing in Studio.
class StudioEditableModule(object):
"""
- Helper methods for supporting Studio editing of xblocks.
+ Helper methods for supporting Studio editing of xblocks/xmodules.
+
+ This class is only intended to be used with an XModule, as it assumes the existence of
+ self.descriptor and self.system.
"""
- def render_reorderable_children(self, context, fragment):
+ def render_children(self, context, fragment, can_reorder=False, can_add=False, view_name='student_view'):
"""
- Renders children with the appropriate HTML structure for drag and drop.
+ Renders the children of the module with HTML appropriate for Studio. If can_reorder is True,
+ then the children will be rendered to support drag and drop.
"""
contents = []
- for child in self.get_display_items():
- context['reorderable_items'].add(child.location)
- rendered_child = child.render('student_view', context)
+ for child in self.descriptor.get_children(): # pylint: disable=E1101
+ if can_reorder:
+ context['reorderable_items'].add(child.location)
+ child_module = self.system.get_module(child) # pylint: disable=E1101
+ rendered_child = child_module.render(view_name, context)
fragment.add_frag_resources(rendered_child)
contents.append({
+ 'id': child.location.to_deprecated_string(),
'content': rendered_child.content
})
- fragment.add_content(self.system.render_template("studio_render_children_view.html", {
+ fragment.add_content(self.system.render_template("studio_render_children_view.html", { # pylint: disable=E1101
'items': contents,
'xblock_context': context,
+ 'can_add': can_add,
+ 'can_reorder': can_reorder,
}))
diff --git a/common/lib/xmodule/xmodule/tests/test_split_module.py b/common/lib/xmodule/xmodule/tests/test_split_module.py
index 05223800f1..35a7f7b595 100644
--- a/common/lib/xmodule/xmodule/tests/test_split_module.py
+++ b/common/lib/xmodule/xmodule/tests/test_split_module.py
@@ -43,7 +43,7 @@ class SplitTestModuleTest(XModuleXmlImportTest):
xml.HtmlFactory(parent=split_test, url_name='split_test_cond1', text='HTML FOR GROUP 1')
self.course = self.process_xml(course)
- course_seq = self.course.get_children()[0]
+ self.course_sequence = self.course.get_children()[0]
self.module_system = get_test_system()
def get_module(descriptor):
@@ -71,7 +71,7 @@ class SplitTestModuleTest(XModuleXmlImportTest):
)
self.module_system._services['partitions'] = self.partitions_service # pylint: disable=protected-access
- self.split_test_module = course_seq.get_children()[0]
+ self.split_test_module = self.course_sequence.get_children()[0]
self.split_test_module.bind_for_student(self.module_system, self.split_test_module._field_data) # pylint: disable=protected-access
@ddt.data(('0', 'split_test_cond0'), ('1', 'split_test_cond1'))
@@ -147,3 +147,40 @@ class SplitTestModuleTest(XModuleXmlImportTest):
self.assertEquals(fields.get('user_partition_id'), '0')
self.assertIsNotNone(fields.get('group_id_to_child'))
self.assertEquals(len(children), 2)
+
+ def test_render_studio_view(self):
+ """
+ Test the rendering of the Studio view.
+ """
+
+ # The split_test module should render both its groups when it is the root
+ reorderable_items = set()
+ context = {
+ 'runtime_type': 'studio',
+ 'container_view': True,
+ 'reorderable_items': reorderable_items,
+ 'root_xblock': self.split_test_module,
+ }
+ html = self.module_system.render(self.split_test_module, 'student_view', context).content
+ self.assertIn('HTML FOR GROUP 0', html)
+ self.assertIn('HTML FOR GROUP 1', html)
+
+ # When rendering as a child, it shouldn't render either of its groups
+ reorderable_items = set()
+ context = {
+ 'runtime_type': 'studio',
+ 'container_view': True,
+ 'reorderable_items': reorderable_items,
+ 'root_xblock': self.course_sequence,
+ }
+ html = self.module_system.render(self.split_test_module, 'student_view', context).content
+ self.assertNotIn('HTML FOR GROUP 0', html)
+ self.assertNotIn('HTML FOR GROUP 1', html)
+
+ def test_settings(self):
+ """
+ Test the settings configuration.
+ """
+ non_editable_metadata_fields = self.split_test_module.non_editable_metadata_fields
+ self.assertIn(SplitTestDescriptor.due, non_editable_metadata_fields)
+ self.assertNotIn(SplitTestDescriptor.display_name, non_editable_metadata_fields)
diff --git a/common/lib/xmodule/xmodule/tests/test_vertical.py b/common/lib/xmodule/xmodule/tests/test_vertical.py
index 9d1e792734..600a37d224 100644
--- a/common/lib/xmodule/xmodule/tests/test_vertical.py
+++ b/common/lib/xmodule/xmodule/tests/test_vertical.py
@@ -54,9 +54,20 @@ class VerticalModuleTestCase(BaseVerticalModuleTest):
"""
Test the rendering of the Studio view
"""
+ # Vertical shouldn't render children on the unit page
+ context = {
+ 'runtime_type': 'studio',
+ 'container_view': False,
+ }
+ html = self.module_system.render(self.vertical, 'student_view', context).content
+ self.assertNotIn(self.test_html_1, html)
+ self.assertNotIn(self.test_html_2, html)
+
+ # Vertical should render reorderable children on the container page
reorderable_items = set()
context = {
'runtime_type': 'studio',
+ 'container_view': True,
'reorderable_items': reorderable_items,
}
html = self.module_system.render(self.vertical, 'student_view', context).content
diff --git a/common/lib/xmodule/xmodule/vertical_module.py b/common/lib/xmodule/xmodule/vertical_module.py
index c75d76a283..cfb8081bea 100644
--- a/common/lib/xmodule/xmodule/vertical_module.py
+++ b/common/lib/xmodule/xmodule/vertical_module.py
@@ -30,7 +30,10 @@ class VerticalModule(VerticalFields, XModule, StudioEditableModule):
Renders the Studio preview view, which supports drag and drop.
"""
fragment = Fragment()
- self.render_reorderable_children(context, fragment)
+ # For the container page we want the full drag-and-drop, but for unit pages we want
+ # a more concise version that appears alongside the "View =>" link.
+ if context.get('container_view'):
+ self.render_children(context, fragment, can_reorder=True, can_add=True)
return fragment
def render_view(self, context, template_name):
@@ -82,3 +85,11 @@ class VerticalDescriptor(VerticalFields, SequenceDescriptor):
# TODO (victor): Does this need its own definition_to_xml method? Otherwise it looks
# like verticals will get exported as sequentials...
+
+ @property
+ def non_editable_metadata_fields(self):
+ non_editable_fields = super(VerticalDescriptor, self).non_editable_metadata_fields
+ non_editable_fields.extend([
+ VerticalDescriptor.due,
+ ])
+ return non_editable_fields
diff --git a/common/lib/xmodule/xmodule/wrapper_module.py b/common/lib/xmodule/xmodule/wrapper_module.py
index d675f102bf..96546c3628 100644
--- a/common/lib/xmodule/xmodule/wrapper_module.py
+++ b/common/lib/xmodule/xmodule/wrapper_module.py
@@ -20,7 +20,3 @@ class WrapperDescriptor(VerticalDescriptor):
module_class = WrapperModule
has_children = True
-
- js = {'coffee': [resource_string(__name__, 'js/src/vertical/edit.coffee')]}
- js_module_name = "VerticalDescriptor"
-
diff --git a/common/static/sass/_mixins.scss b/common/static/sass/_mixins.scss
index 69e8cc200a..c96ff8657a 100644
--- a/common/static/sass/_mixins.scss
+++ b/common/static/sass/_mixins.scss
@@ -54,19 +54,19 @@
// extends - justify-content right for display:flex alignment in older browsers
%ui-justify-right-flex {
- -webkit-box-pack: end;
- -moz-box-pack: end;
- -ms-flex-pack: end;
- -webkit-justify-content: end;
+ -webkit-box-pack: flex-end;
+ -moz-box-pack: flex-end;
+ -ms-flex-pack: flex-end;
+ -webkit-justify-content: flex-end;
justify-content: flex-end;
}
// extends - justify-content left for display:flex alignment in older browsers
%ui-justify-left-flex {
- -webkit-box-pack: start;
- -moz-box-pack: start;
- -ms-flex-pack: start;
- -webkit-justify-content: start;
+ -webkit-box-pack: flex-start;
+ -moz-box-pack: flex-start;
+ -ms-flex-pack: flex-start;
+ -webkit-justify-content: flex-start;
justify-content: flex-start;
}
diff --git a/common/test/acceptance/pages/studio/component_editor.py b/common/test/acceptance/pages/studio/component_editor.py
new file mode 100644
index 0000000000..57990b93b1
--- /dev/null
+++ b/common/test/acceptance/pages/studio/component_editor.py
@@ -0,0 +1,66 @@
+from bok_choy.page_object import PageObject
+from selenium.webdriver.common.keys import Keys
+from selenium.webdriver.common.action_chains import ActionChains
+from utils import click_css
+
+
+class ComponentEditorView(PageObject):
+ """
+ A :class:`.PageObject` representing the rendered view of a component editor.
+
+ This class assumes that the editor is our default editor as displayed for xmodules.
+ """
+ BODY_SELECTOR = '.xblock-editor'
+
+ def __init__(self, browser, locator):
+ """
+ Args:
+ browser (selenium.webdriver): The Selenium-controlled browser that this page is loaded in.
+ locator (str): The locator that identifies which xblock this :class:`.xblock-editor` relates to.
+ """
+ super(ComponentEditorView, self).__init__(browser)
+ self.locator = locator
+
+ def is_browser_on_page(self):
+ return self.q(css='{}[data-locator="{}"]'.format(self.BODY_SELECTOR, self.locator)).present
+
+ def _bounded_selector(self, selector):
+ """
+ Return `selector`, but limited to this particular `ComponentEditorView` context
+ """
+ return '{}[data-locator="{}"] {}'.format(
+ self.BODY_SELECTOR,
+ self.locator,
+ selector
+ )
+
+ def url(self):
+ """
+ Returns None because this is not directly accessible via URL.
+ """
+ return None
+
+ def get_setting_entry_index(self, label):
+ """
+ Returns the index of the setting entry with given label (display name) within the Settings modal.
+ """
+ # TODO: will need to handle tabbed "Settings" in future (current usage is in vertical, only shows Settings.
+ setting_labels = self.q(css=self._bounded_selector('.metadata_edit .wrapper-comp-setting .setting-label'))
+ for index, setting in enumerate(setting_labels):
+ if setting.text == label:
+ return index
+ return None
+
+ def set_field_value_and_save(self, label, value):
+ """
+ Set the field with given label (display name) to the specified value, and presses Save.
+ """
+ index = self.get_setting_entry_index(label)
+ elem = self.q(css=self._bounded_selector('.metadata_edit div.wrapper-comp-setting input.setting-input'))[index]
+ # Click in the field, delete the value there.
+ action = ActionChains(self.browser).click(elem)
+ for _x in range(0, len(elem.get_attribute('value'))):
+ action = action.send_keys(Keys.BACKSPACE)
+ # Send the new text, then Tab to move to the next field (so change event is triggered).
+ action.send_keys(value).send_keys(Keys.TAB).perform()
+ click_css(self, 'a.action-save')
diff --git a/common/test/acceptance/pages/studio/container.py b/common/test/acceptance/pages/studio/container.py
index 70d7f8db56..82baa3b673 100644
--- a/common/test/acceptance/pages/studio/container.py
+++ b/common/test/acceptance/pages/studio/container.py
@@ -3,7 +3,7 @@ Container page in Studio
"""
from bok_choy.page_object import PageObject
-from bok_choy.promise import Promise
+from bok_choy.promise import Promise, EmptyPromise
from . import BASE_URL
from selenium.webdriver.common.action_chains import ActionChains
@@ -15,15 +15,24 @@ class ContainerPage(PageObject):
"""
Container page in Studio
"""
+ NAME_SELECTOR = 'a.navigation-current'
- def __init__(self, browser, unit_locator):
+ def __init__(self, browser, locator):
super(ContainerPage, self).__init__(browser)
- self.unit_locator = unit_locator
+ self.locator = locator
@property
def url(self):
"""URL to the container page for an xblock."""
- return "{}/container/{}".format(BASE_URL, self.unit_locator)
+ return "{}/container/{}".format(BASE_URL, self.locator)
+
+ @property
+ def name(self):
+ titles = self.q(css=self.NAME_SELECTOR).text
+ if titles:
+ return titles[0]
+ else:
+ return None
def is_browser_on_page(self):
@@ -91,6 +100,14 @@ class ContainerPage(PageObject):
# Click the confirmation dialog button
click_css(self, 'a.button.action-primary', 0)
+ def edit(self):
+ self.q(css='.edit-button').first.click()
+ EmptyPromise(
+ lambda: self.q(css='.xblock-studio_view').present,
+ 'Wait for the Studio editor to be present'
+ ).fulfill()
+
+ return self
class XBlockWrapper(PageObject):
diff --git a/common/test/acceptance/pages/studio/utils.py b/common/test/acceptance/pages/studio/utils.py
index 5ccce44e48..33bac1e8c1 100644
--- a/common/test/acceptance/pages/studio/utils.py
+++ b/common/test/acceptance/pages/studio/utils.py
@@ -5,7 +5,7 @@ from bok_choy.promise import Promise
from selenium.webdriver.common.action_chains import ActionChains
-def click_css(page, css, source_index, require_notification=True):
+def click_css(page, css, source_index=0, require_notification=True):
"""
Click the button/link with the given css and index on the specified page (subclass of PageObject).
diff --git a/common/test/acceptance/tests/test_studio_container.py b/common/test/acceptance/tests/test_studio_container.py
index b74c85826c..1300a6274d 100644
--- a/common/test/acceptance/tests/test_studio_container.py
+++ b/common/test/acceptance/tests/test_studio_container.py
@@ -1,11 +1,15 @@
"""
Acceptance tests for Studio related to the container page.
"""
+
from ..pages.studio.auto_auth import AutoAuthPage
from ..pages.studio.overview import CourseOutlinePage
from ..fixtures.course import CourseFixture, XBlockFixtureDesc
from .helpers import UniqueCourseTest
+from ..pages.studio.component_editor import ComponentEditorView
+
+from unittest import skip
class ContainerBase(UniqueCourseTest):
@@ -85,13 +89,17 @@ class ContainerBase(UniqueCourseTest):
).install()
def go_to_container_page(self, make_draft=False):
+ unit = self.go_to_unit_page(make_draft)
+ container = unit.components[0].go_to_container()
+ return container
+
+ def go_to_unit_page(self, make_draft=False):
self.outline.visit()
subsection = self.outline.section('Test Section').subsection('Test Subsection')
unit = subsection.toggle_expand().unit('Test Unit').go_to()
if make_draft:
unit.edit_draft()
- container = unit.components[0].go_to_container()
- return container
+ return unit
def verify_ordering(self, container, expected_orderings):
xblocks = container.xblocks
@@ -131,6 +139,7 @@ class DragAndDropTest(ContainerBase):
expected_ordering
)
+ @skip("Sporadically drags outside of the Group.")
def test_reorder_in_group(self):
"""
Drag Group A Item 2 before Group A Item 1.
@@ -303,3 +312,36 @@ class DeleteComponentTest(ContainerBase):
{self.group_b: [self.group_b_item_1, self.group_b_item_2]},
{self.group_empty: []}]
self.delete_and_verify(self.group_a_item_1_action_index, expected_ordering)
+
+
+class EditContainerTest(ContainerBase):
+ """
+ Tests of editing a container.
+ """
+ __test__ = True
+
+ def modify_display_name_and_verify(self, component):
+ """
+ Helper method for changing a display name.
+ """
+ modified_name = 'modified'
+ self.assertNotEqual(component.name, modified_name)
+ component.edit()
+ component_editor = ComponentEditorView(self.browser, component.locator)
+ component_editor.set_field_value_and_save('Display Name', modified_name)
+ self.assertEqual(component.name, modified_name)
+
+ def test_edit_container_on_unit_page(self):
+ """
+ Test the "edit" button on a container appearing on the unit page.
+ """
+ unit = self.go_to_unit_page(make_draft=True)
+ component = unit.components[0]
+ self.modify_display_name_and_verify(component)
+
+ def test_edit_container_on_container_page(self):
+ """
+ Test the "edit" button on a container appearing on the container page.
+ """
+ container = self.go_to_container_page(make_draft=True)
+ self.modify_display_name_and_verify(container)
diff --git a/lms/templates/studio_render_children_view.html b/lms/templates/studio_render_children_view.html
index 0e220ce117..34be5f3ff6 100644
--- a/lms/templates/studio_render_children_view.html
+++ b/lms/templates/studio_render_children_view.html
@@ -1,8 +1,12 @@
-
+% if can_reorder:
+
+% endif
% for item in items:
${item['content']}
% endfor
-
-% if not xblock_context['read_only']:
+% if can_reorder:
+
+% endif
+% if can_add and not xblock_context['read_only']:
% endif