restrict move action
This commit is contained in:
@@ -283,6 +283,23 @@ def reverse_usage_url(handler_name, usage_key, kwargs=None):
|
||||
return reverse_url(handler_name, 'usage_key_string', usage_key, kwargs)
|
||||
|
||||
|
||||
def get_group_display_name(user_partitions, xblock_display_name):
|
||||
"""
|
||||
Get the group name if matching group xblock is found.
|
||||
|
||||
Arguments:
|
||||
user_partitions (Dict): Locator of source item.
|
||||
xblock_display_name (String): Display name of group xblock.
|
||||
|
||||
Returns:
|
||||
group name (String): Group name of the matching group.
|
||||
"""
|
||||
for user_partition in user_partitions:
|
||||
for group in user_partition['groups']:
|
||||
if str(group['id']) in xblock_display_name:
|
||||
return group['name']
|
||||
|
||||
|
||||
def get_user_partition_info(xblock, schemes=None, course=None):
|
||||
"""
|
||||
Retrieve user partition information for an XBlock for display in editors.
|
||||
|
||||
@@ -29,7 +29,7 @@ from cms.lib.xblock.authoring_mixin import VISIBILITY_VIEW
|
||||
from contentstore.utils import (
|
||||
find_release_date_source, find_staff_lock_source, is_currently_visible_to_students,
|
||||
ancestor_has_staff_lock, has_children_visible_to_specific_content_groups,
|
||||
get_user_partition_info,
|
||||
get_user_partition_info, get_group_display_name,
|
||||
)
|
||||
from contentstore.views.helpers import is_unit, xblock_studio_url, xblock_primary_child_category, \
|
||||
xblock_type_display_name, get_parent_xblock, create_xblock, usage_key_with_run
|
||||
@@ -675,6 +675,21 @@ def _get_source_index(source_usage_key, source_parent):
|
||||
return None
|
||||
|
||||
|
||||
def is_source_item_in_target_parents(source_item, target_parent):
|
||||
"""
|
||||
Returns True if source item is found in target parents otherwise False.
|
||||
|
||||
Arguments:
|
||||
source_item (XBlock): Source Xblock.
|
||||
target_parent (XBlock): Target XBlock.
|
||||
"""
|
||||
target_ancestors = _create_xblock_ancestor_info(target_parent, is_concise=True)['ancestors']
|
||||
for target_ancestor in target_ancestors:
|
||||
if unicode(source_item.location) == target_ancestor['id']:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def _move_item(source_usage_key, target_parent_usage_key, user, target_index=None):
|
||||
"""
|
||||
Move an existing xblock as a child of the supplied target_parent_usage_key.
|
||||
@@ -688,8 +703,11 @@ def _move_item(source_usage_key, target_parent_usage_key, user, target_index=Non
|
||||
JsonResponse: Information regarding move operation. It may contains error info if an invalid move operation
|
||||
is performed.
|
||||
"""
|
||||
# Get the list of all component type XBlocks
|
||||
component_types = sorted(set(name for name, class_ in XBlock.load_classes()) - set(DIRECT_ONLY_CATEGORIES))
|
||||
# Get the list of all parentable component type XBlocks.
|
||||
parent_component_types = list(
|
||||
set(name for name, class_ in XBlock.load_classes() if getattr(class_, 'has_children', False)) -
|
||||
set(DIRECT_ONLY_CATEGORIES)
|
||||
)
|
||||
|
||||
store = modulestore()
|
||||
with store.bulk_operations(source_usage_key.course_key):
|
||||
@@ -705,18 +723,22 @@ def _move_item(source_usage_key, target_parent_usage_key, user, target_index=Non
|
||||
source_index = _get_source_index(source_usage_key, source_parent)
|
||||
|
||||
valid_move_type = {
|
||||
'vertical': source_type if source_type in component_types else 'component',
|
||||
'sequential': 'vertical',
|
||||
'chapter': 'sequential',
|
||||
}
|
||||
|
||||
if valid_move_type.get(target_parent_type, '') != source_type:
|
||||
if (valid_move_type.get(target_parent_type, '') != source_type and
|
||||
target_parent_type not in parent_component_types):
|
||||
error = 'You can not move {source_type} into {target_parent_type}.'.format(
|
||||
source_type=source_type,
|
||||
target_parent_type=target_parent_type,
|
||||
)
|
||||
elif source_parent.location == target_parent.location:
|
||||
error = 'You can not move an item into the same parent.'
|
||||
elif source_item.location == target_parent.location:
|
||||
error = 'You can not move an item into itself.'
|
||||
elif is_source_item_in_target_parents(source_item, target_parent):
|
||||
error = 'You can not move an item into it\'s child.'
|
||||
elif source_index is None:
|
||||
error = '{source_usage_key} not found in {parent_usage_key}.'.format(
|
||||
source_usage_key=unicode(source_usage_key),
|
||||
@@ -1093,6 +1115,7 @@ def create_xblock_info(xblock, data=None, metadata=None, include_ancestor_info=F
|
||||
# a percent value out of 100, e.g. "58%" means "58/100".
|
||||
pct_sign=_('%'))
|
||||
|
||||
user_partitions = get_user_partition_info(xblock, course=course)
|
||||
xblock_info = {
|
||||
'id': unicode(xblock.location),
|
||||
'display_name': xblock.display_name_with_default,
|
||||
@@ -1101,6 +1124,10 @@ def create_xblock_info(xblock, data=None, metadata=None, include_ancestor_info=F
|
||||
if is_concise:
|
||||
if child_info and len(child_info.get('children', [])) > 0:
|
||||
xblock_info['child_info'] = child_info
|
||||
# Groups are labelled with their internal ids, rather than with the group name. Replace id with display name.
|
||||
group_display_name = get_group_display_name(user_partitions, xblock_info['display_name'])
|
||||
xblock_info['display_name'] = group_display_name if group_display_name else xblock_info['display_name']
|
||||
xblock_info['has_children'] = xblock.has_children
|
||||
else:
|
||||
xblock_info.update({
|
||||
'edited_on': get_default_time_display(xblock.subtree_edited_on) if xblock.subtree_edited_on else None,
|
||||
@@ -1121,7 +1148,7 @@ def create_xblock_info(xblock, data=None, metadata=None, include_ancestor_info=F
|
||||
'actions': xblock_actions,
|
||||
'explanatory_message': explanatory_message,
|
||||
'group_access': xblock.group_access,
|
||||
'user_partitions': get_user_partition_info(xblock, course=course),
|
||||
'user_partitions': user_partitions,
|
||||
})
|
||||
|
||||
if xblock.category == 'sequential':
|
||||
|
||||
@@ -274,6 +274,7 @@ def _studio_wrap_xblock(xblock, view, frag, context, display_name_only=False):
|
||||
'can_edit': context.get('can_edit', True),
|
||||
'can_edit_visibility': context.get('can_edit_visibility', True),
|
||||
'can_add': context.get('can_add', True),
|
||||
'can_move': context.get('can_move', True)
|
||||
}
|
||||
html = render_to_string('studio_xblock_wrapper.html', template_context)
|
||||
frag = wrap_fragment(frag, html)
|
||||
|
||||
@@ -12,11 +12,13 @@ from django.utils import http
|
||||
|
||||
import contentstore.views.component as views
|
||||
from contentstore.views.tests.utils import StudioPageTestCase
|
||||
from contentstore.tests.test_libraries import LibraryTestCase
|
||||
from xmodule.modulestore import ModuleStoreEnum
|
||||
from xmodule.modulestore.django import modulestore
|
||||
from xmodule.modulestore.tests.factories import ItemFactory
|
||||
from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory
|
||||
|
||||
|
||||
class ContainerPageTestCase(StudioPageTestCase):
|
||||
class ContainerPageTestCase(StudioPageTestCase, LibraryTestCase):
|
||||
"""
|
||||
Unit tests for the container page.
|
||||
"""
|
||||
@@ -128,6 +130,44 @@ class ContainerPageTestCase(StudioPageTestCase):
|
||||
self.validate_preview_html(published_child_container, self.container_view)
|
||||
self.validate_preview_html(published_child_vertical, self.reorderable_child_view)
|
||||
|
||||
def test_library_page_preview_html(self):
|
||||
"""
|
||||
Verify that a library xblock's container (library page) preview returns the expected HTML.
|
||||
"""
|
||||
# Add some content to library.
|
||||
self._add_simple_content_block()
|
||||
self.validate_preview_html(self.library, self.container_view, can_reorder=False, can_move=False)
|
||||
|
||||
def test_library_content_preview_html(self):
|
||||
"""
|
||||
Verify that a library content block container page preview returns the expected HTML.
|
||||
"""
|
||||
# Library content block is only supported in split courses.
|
||||
with modulestore().default_store(ModuleStoreEnum.Type.split):
|
||||
course = CourseFactory.create()
|
||||
|
||||
# Add some content to library
|
||||
self._add_simple_content_block()
|
||||
|
||||
# Create a library content block
|
||||
lc_block = self._add_library_content_block(course, self.lib_key)
|
||||
self.assertEqual(len(lc_block.children), 0)
|
||||
|
||||
# Refresh children to be reflected in lc_block
|
||||
lc_block = self._refresh_children(lc_block)
|
||||
self.assertEqual(len(lc_block.children), 1)
|
||||
|
||||
self.validate_preview_html(
|
||||
lc_block,
|
||||
self.container_view,
|
||||
can_add=False,
|
||||
can_reorder=False,
|
||||
can_move=False,
|
||||
can_edit=True,
|
||||
can_duplicate=False,
|
||||
can_delete=False
|
||||
)
|
||||
|
||||
def test_draft_container_preview_html(self):
|
||||
"""
|
||||
Verify that a draft xblock's container preview returns the expected HTML.
|
||||
|
||||
@@ -755,6 +755,13 @@ class TestMoveItem(ItemTest):
|
||||
default_store = self.store.default_modulestore.get_modulestore_type()
|
||||
|
||||
self.course = CourseFactory.create(default_store=default_store)
|
||||
|
||||
# Create group configurations
|
||||
self.course.user_partitions = [
|
||||
UserPartition(0, 'first_partition', 'Test Partition', [Group("0", 'alpha'), Group("1", 'beta')])
|
||||
]
|
||||
self.store.update_item(self.course, self.user.id)
|
||||
|
||||
# Create a parent chapter
|
||||
chap1 = self.create_xblock(parent_usage_key=self.course.location, display_name='chapter1', category='chapter')
|
||||
self.chapter_usage_key = self.response_usage_key(chap1)
|
||||
@@ -762,27 +769,54 @@ class TestMoveItem(ItemTest):
|
||||
chap2 = self.create_xblock(parent_usage_key=self.course.location, display_name='chapter2', category='chapter')
|
||||
self.chapter2_usage_key = self.response_usage_key(chap2)
|
||||
|
||||
# create a sequential
|
||||
# Create a sequential
|
||||
seq1 = self.create_xblock(parent_usage_key=self.chapter_usage_key, display_name='seq1', category='sequential')
|
||||
self.seq_usage_key = self.response_usage_key(seq1)
|
||||
|
||||
seq2 = self.create_xblock(parent_usage_key=self.chapter_usage_key, display_name='seq2', category='sequential')
|
||||
self.seq2_usage_key = self.response_usage_key(seq2)
|
||||
|
||||
# create a vertical
|
||||
# Create a vertical
|
||||
vert1 = self.create_xblock(parent_usage_key=self.seq_usage_key, display_name='vertical1', category='vertical')
|
||||
self.vert_usage_key = self.response_usage_key(vert1)
|
||||
|
||||
vert2 = self.create_xblock(parent_usage_key=self.seq_usage_key, display_name='vertical2', category='vertical')
|
||||
self.vert2_usage_key = self.response_usage_key(vert2)
|
||||
|
||||
# create problem and an html component
|
||||
# Create problem and an html component
|
||||
problem1 = self.create_xblock(parent_usage_key=self.vert_usage_key, display_name='problem1', category='problem')
|
||||
self.problem_usage_key = self.response_usage_key(problem1)
|
||||
|
||||
html1 = self.create_xblock(parent_usage_key=self.vert_usage_key, display_name='html1', category='html')
|
||||
self.html_usage_key = self.response_usage_key(html1)
|
||||
|
||||
# Create a content experiment
|
||||
resp = self.create_xblock(category='split_test', parent_usage_key=self.vert_usage_key)
|
||||
self.split_test_usage_key = self.response_usage_key(resp)
|
||||
|
||||
def setup_and_verify_content_experiment(self, partition_id):
|
||||
"""
|
||||
Helper method to set up group configurations to content experiment.
|
||||
|
||||
Arguments:
|
||||
partition_id (int): User partition id.
|
||||
"""
|
||||
split_test = self.get_item_from_modulestore(self.split_test_usage_key, verify_is_draft=True)
|
||||
|
||||
# Initially, no user_partition_id is set, and the split_test has no children.
|
||||
self.assertEqual(split_test.user_partition_id, -1)
|
||||
self.assertEqual(len(split_test.children), 0)
|
||||
|
||||
# Set group configuration
|
||||
self.client.ajax_post(
|
||||
reverse_usage_url("xblock_handler", self.split_test_usage_key),
|
||||
data={'metadata': {'user_partition_id': str(partition_id)}}
|
||||
)
|
||||
split_test = self.get_item_from_modulestore(self.split_test_usage_key, verify_is_draft=True)
|
||||
self.assertEqual(split_test.user_partition_id, partition_id)
|
||||
self.assertEqual(len(split_test.children), len(self.course.user_partitions[partition_id].groups))
|
||||
return split_test
|
||||
|
||||
def _move_component(self, source_usage_key, target_usage_key, target_index=None):
|
||||
"""
|
||||
Helper method to send move request and returns the response.
|
||||
@@ -853,7 +887,7 @@ class TestMoveItem(ItemTest):
|
||||
"""
|
||||
parent = self.get_item_from_modulestore(self.vert_usage_key)
|
||||
children = parent.get_children()
|
||||
self.assertEqual(len(children), 2)
|
||||
self.assertEqual(len(children), 3)
|
||||
|
||||
# Create a component within vert2.
|
||||
resp = self.create_xblock(parent_usage_key=self.vert2_usage_key, display_name='html2', category='html')
|
||||
@@ -863,7 +897,7 @@ class TestMoveItem(ItemTest):
|
||||
self.assert_move_item(html2_usage_key, self.vert_usage_key, 1)
|
||||
parent = self.get_item_from_modulestore(self.vert_usage_key)
|
||||
children = parent.get_children()
|
||||
self.assertEqual(len(children), 3)
|
||||
self.assertEqual(len(children), 4)
|
||||
self.assertEqual(children[1].location, html2_usage_key)
|
||||
|
||||
def test_move_undo(self):
|
||||
@@ -940,6 +974,108 @@ class TestMoveItem(ItemTest):
|
||||
self.assertEqual(response['error'], 'You can not move an item into the same parent.')
|
||||
self.assertEqual(self.store.get_parent_location(self.html_usage_key), parent_loc)
|
||||
|
||||
def test_can_not_move_into_itself(self):
|
||||
"""
|
||||
Test that a component can not be moved to itself.
|
||||
"""
|
||||
library_content = self.create_xblock(
|
||||
parent_usage_key=self.vert_usage_key, display_name='library content block', category='library_content'
|
||||
)
|
||||
library_content_usage_key = self.response_usage_key(library_content)
|
||||
parent_loc = self.store.get_parent_location(library_content_usage_key)
|
||||
self.assertEqual(parent_loc, self.vert_usage_key)
|
||||
response = self._move_component(library_content_usage_key, library_content_usage_key)
|
||||
self.assertEqual(response.status_code, 400)
|
||||
response = json.loads(response.content)
|
||||
|
||||
self.assertEqual(response['error'], 'You can not move an item into itself.')
|
||||
self.assertEqual(self.store.get_parent_location(self.html_usage_key), parent_loc)
|
||||
|
||||
def test_move_library_content(self):
|
||||
"""
|
||||
Test that library content can be moved to any other valid location.
|
||||
"""
|
||||
library_content = self.create_xblock(
|
||||
parent_usage_key=self.vert_usage_key, display_name='library content block', category='library_content'
|
||||
)
|
||||
library_content_usage_key = self.response_usage_key(library_content)
|
||||
parent_loc = self.store.get_parent_location(library_content_usage_key)
|
||||
self.assertEqual(parent_loc, self.vert_usage_key)
|
||||
self.assert_move_item(library_content_usage_key, self.vert2_usage_key)
|
||||
|
||||
def test_move_into_library_content(self):
|
||||
"""
|
||||
Test that a component can be moved into library content.
|
||||
"""
|
||||
library_content = self.create_xblock(
|
||||
parent_usage_key=self.vert_usage_key, display_name='library content block', category='library_content'
|
||||
)
|
||||
library_content_usage_key = self.response_usage_key(library_content)
|
||||
self.assert_move_item(self.html_usage_key, library_content_usage_key)
|
||||
|
||||
def test_move_content_experiment(self):
|
||||
"""
|
||||
Test that a content experiment can be moved.
|
||||
"""
|
||||
self.setup_and_verify_content_experiment(0)
|
||||
|
||||
# Move content experiment
|
||||
self.assert_move_item(self.split_test_usage_key, self.vert2_usage_key)
|
||||
|
||||
def test_move_content_experiment_components(self):
|
||||
"""
|
||||
Test that component inside content experiment can be moved to any other valid location.
|
||||
"""
|
||||
split_test = self.setup_and_verify_content_experiment(0)
|
||||
|
||||
# Add html component to Group A.
|
||||
html1 = self.create_xblock(
|
||||
parent_usage_key=split_test.children[0], display_name='html1', category='html'
|
||||
)
|
||||
html_usage_key = self.response_usage_key(html1)
|
||||
|
||||
# Move content experiment
|
||||
self.assert_move_item(html_usage_key, self.vert2_usage_key)
|
||||
|
||||
def test_move_into_content_experiment_groups(self):
|
||||
"""
|
||||
Test that a component can be moved to content experiment.
|
||||
"""
|
||||
split_test = self.setup_and_verify_content_experiment(0)
|
||||
self.assert_move_item(self.html_usage_key, split_test.children[0])
|
||||
|
||||
def test_can_not_move_content_experiment_into_its_children(self):
|
||||
"""
|
||||
Test that a content experiment can not be moved inside any of it's children.
|
||||
"""
|
||||
split_test = self.setup_and_verify_content_experiment(0)
|
||||
|
||||
# Try to move content experiment inside it's child groups.
|
||||
for child_vert_usage_key in split_test.children:
|
||||
response = self._move_component(self.split_test_usage_key, child_vert_usage_key)
|
||||
self.assertEqual(response.status_code, 400)
|
||||
response = json.loads(response.content)
|
||||
|
||||
self.assertEqual(response['error'], 'You can not move an item into it\'s child.')
|
||||
self.assertEqual(self.store.get_parent_location(self.split_test_usage_key), self.vert_usage_key)
|
||||
|
||||
# Create content experiment inside group A and set it's group configuration.
|
||||
resp = self.create_xblock(category='split_test', parent_usage_key=split_test.children[0])
|
||||
child_split_test_usage_key = self.response_usage_key(resp)
|
||||
self.client.ajax_post(
|
||||
reverse_usage_url("xblock_handler", child_split_test_usage_key),
|
||||
data={'metadata': {'user_partition_id': str(0)}}
|
||||
)
|
||||
child_split_test = self.get_item_from_modulestore(self.split_test_usage_key, verify_is_draft=True)
|
||||
|
||||
# Try to move content experiment further down the level to a child group A nested inside main group A.
|
||||
response = self._move_component(self.split_test_usage_key, child_split_test.children[0])
|
||||
self.assertEqual(response.status_code, 400)
|
||||
response = json.loads(response.content)
|
||||
|
||||
self.assertEqual(response['error'], 'You can not move an item into it\'s child.')
|
||||
self.assertEqual(self.store.get_parent_location(self.split_test_usage_key), self.vert_usage_key)
|
||||
|
||||
def test_move_invalid_source_index(self):
|
||||
"""
|
||||
Test moving an item to an invalid index.
|
||||
@@ -1611,6 +1747,31 @@ class TestEditSplitModule(ItemTest):
|
||||
self.assertEqual(vertical_0.location, split_test.group_id_to_child['0'])
|
||||
self.assertEqual(vertical_1.location, split_test.group_id_to_child['1'])
|
||||
|
||||
def test_split_xblock_info_group_name(self):
|
||||
"""
|
||||
Test that concise outline for split test component gives display name as group name.
|
||||
"""
|
||||
split_test = self.get_item_from_modulestore(self.split_test_usage_key, verify_is_draft=True)
|
||||
# Initially, no user_partition_id is set, and the split_test has no children.
|
||||
self.assertEqual(split_test.user_partition_id, -1)
|
||||
self.assertEqual(len(split_test.children), 0)
|
||||
# Set the user_partition_id to 0.
|
||||
split_test = self._update_partition_id(0)
|
||||
# Verify that child verticals have been set to match the groups
|
||||
self.assertEqual(len(split_test.children), 2)
|
||||
|
||||
# Get xblock outline
|
||||
xblock_info = create_xblock_info(
|
||||
split_test,
|
||||
is_concise=True,
|
||||
include_child_info=True,
|
||||
include_children_predicate=lambda xblock: xblock.has_children,
|
||||
course=self.course,
|
||||
user=self.request.user
|
||||
)
|
||||
self.assertEqual(xblock_info['child_info']['children'][0]['display_name'], 'alpha')
|
||||
self.assertEqual(xblock_info['child_info']['children'][1]['display_name'], 'beta')
|
||||
|
||||
def test_change_user_partition_id(self):
|
||||
"""
|
||||
Test what happens when the user_partition_id is changed to a different groups
|
||||
|
||||
@@ -41,34 +41,48 @@ class StudioPageTestCase(CourseTestCase):
|
||||
resp_content = json.loads(resp.content)
|
||||
return resp_content['html']
|
||||
|
||||
def validate_preview_html(self, xblock, view_name, can_add=True):
|
||||
def validate_preview_html(self, xblock, view_name, can_add=True, can_reorder=True, can_move=True,
|
||||
can_edit=True, can_duplicate=True, can_delete=True):
|
||||
"""
|
||||
Verify that the specified xblock's preview has the expected HTML elements.
|
||||
"""
|
||||
html = self.get_preview_html(xblock, view_name)
|
||||
self.validate_html_for_add_buttons(html, can_add)
|
||||
|
||||
# Verify drag handles always appear.
|
||||
drag_handle_html = '<span data-tooltip="Drag to reorder" class="drag-handle action"></span>'
|
||||
self.assertIn(drag_handle_html, html)
|
||||
|
||||
# Verify that there are no action buttons for public blocks
|
||||
expected_button_html = [
|
||||
'<button class="btn-default edit-button action-button">',
|
||||
self.validate_html_for_action_button(
|
||||
html,
|
||||
'<div class="add-xblock-component new-component-item adding"></div>',
|
||||
can_add
|
||||
)
|
||||
self.validate_html_for_action_button(
|
||||
html,
|
||||
'<span data-tooltip="Drag to reorder" class="drag-handle action"></span>',
|
||||
can_reorder
|
||||
)
|
||||
self.validate_html_for_action_button(
|
||||
html,
|
||||
'<button data-tooltip="Move" class="btn-default move-button action-button">',
|
||||
can_move
|
||||
)
|
||||
self.validate_html_for_action_button(
|
||||
html,
|
||||
'button class="btn-default edit-button action-button">',
|
||||
can_edit
|
||||
)
|
||||
self.validate_html_for_action_button(
|
||||
html,
|
||||
'<button data-tooltip="Delete" class="btn-default delete-button action-button">',
|
||||
can_duplicate
|
||||
)
|
||||
self.validate_html_for_action_button(
|
||||
html,
|
||||
'<button data-tooltip="Duplicate" class="btn-default duplicate-button action-button">',
|
||||
'<button data-tooltip="Move" class="btn-default move-button action-button">'
|
||||
]
|
||||
for button_html in expected_button_html:
|
||||
self.assertIn(button_html, html)
|
||||
can_delete
|
||||
)
|
||||
|
||||
def validate_html_for_add_buttons(self, html, can_add=True):
|
||||
def validate_html_for_action_button(self, html, expected_html, can_action=True):
|
||||
"""
|
||||
Validate that the specified HTML has the appropriate add actions for the current publish state.
|
||||
Validate that the specified HTML has specific action..
|
||||
"""
|
||||
# Verify that there are no add buttons for public blocks
|
||||
add_button_html = '<div class="add-xblock-component new-component-item adding"></div>'
|
||||
if can_add:
|
||||
self.assertIn(add_button_html, html)
|
||||
if can_action:
|
||||
self.assertIn(expected_html, html)
|
||||
else:
|
||||
self.assertNotIn(add_button_html, html)
|
||||
self.assertNotIn(expected_html, html)
|
||||
|
||||
@@ -205,13 +205,17 @@ define(['jquery', 'underscore', 'edx-ui-toolkit/js/utils/spec-helpers/ajax-helpe
|
||||
return category + '_display_name_' + xblockIndex;
|
||||
})
|
||||
);
|
||||
if (category !== 'component') {
|
||||
if (category === 'component') {
|
||||
if (hasCurrentLocation) {
|
||||
expect(displayedInfo.currentLocationText).toEqual('(Currently selected)');
|
||||
}
|
||||
} else {
|
||||
if (hasCurrentLocation) {
|
||||
expect(displayedInfo.currentLocationText).toEqual('(Current location)');
|
||||
}
|
||||
expect(displayedInfo.forwardButtonSRTexts).toEqual(
|
||||
_.map(_.range(expectedXBlocksCount), function() {
|
||||
return 'Click for children';
|
||||
return 'View child items';
|
||||
})
|
||||
);
|
||||
expect(displayedInfo.forwardButtonCount).toEqual(expectedXBlocksCount);
|
||||
@@ -519,15 +523,8 @@ define(['jquery', 'underscore', 'edx-ui-toolkit/js/utils/spec-helpers/ajax-helpe
|
||||
});
|
||||
});
|
||||
|
||||
describe('Move an xblock', function() {
|
||||
it('can not move in a disabled state', function() {
|
||||
verifyMoveEnabled(false);
|
||||
modal.$el.find('.modal-actions .action-move').click();
|
||||
expect(modal.movedAlertView).toBeNull();
|
||||
expect(getSentRequests().length).toEqual(0);
|
||||
});
|
||||
|
||||
it('move button is disabled when navigating to same parent', function() {
|
||||
describe('Move button', function() {
|
||||
it('is disabled when navigating to same parent', function() {
|
||||
// select a target parent as the same as source parent and click
|
||||
renderViews(courseOutline);
|
||||
_.each(_.range(3), function() {
|
||||
@@ -536,7 +533,7 @@ define(['jquery', 'underscore', 'edx-ui-toolkit/js/utils/spec-helpers/ajax-helpe
|
||||
verifyMoveEnabled('component', true);
|
||||
});
|
||||
|
||||
it('move button is enabled when navigating to different parent', function() {
|
||||
it('is enabled when navigating to different parent', function() {
|
||||
// select a target parent as the different as source parent and click
|
||||
renderViews(courseOutline);
|
||||
_.each(_.range(3), function() {
|
||||
@@ -553,6 +550,111 @@ define(['jquery', 'underscore', 'edx-ui-toolkit/js/utils/spec-helpers/ajax-helpe
|
||||
verifyXBlockInfo(courseOutlineOptions, 'section', 1, 'forward', false);
|
||||
});
|
||||
|
||||
it('is disbabled when navigating to same source xblock', function() {
|
||||
var outline,
|
||||
libraryContentXBlockInfo = {
|
||||
category: 'library_content',
|
||||
display_name: 'Library Content',
|
||||
has_children: true,
|
||||
id: 'LIBRARY_CONTENT_ID'
|
||||
},
|
||||
outlineOptions = {library_content: 1, component: 1};
|
||||
|
||||
// make above xblock source xblock.
|
||||
modal.sourceXBlockInfo = libraryContentXBlockInfo;
|
||||
outline = createXBlockInfo('component', outlineOptions, libraryContentXBlockInfo);
|
||||
renderViews(outline);
|
||||
expect(modal.$el.find('.modal-actions .action-move').hasClass('is-disabled')).toBeTruthy();
|
||||
|
||||
// select a target parent
|
||||
clickForwardButton(0);
|
||||
expect(modal.$el.find('.modal-actions .action-move').hasClass('is-disabled')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('is disabled when navigating inside source content experiment', function() {
|
||||
var outline,
|
||||
splitTestXBlockInfo = {
|
||||
category: 'split_test',
|
||||
display_name: 'Content Experiment',
|
||||
has_children: true,
|
||||
id: 'SPLIT_TEST_ID'
|
||||
},
|
||||
outlineOptions = {split_test: 1, unit: 2, component: 1};
|
||||
|
||||
// make above xblock source xblock.
|
||||
modal.sourceXBlockInfo = splitTestXBlockInfo;
|
||||
outline = createXBlockInfo('unit', outlineOptions, splitTestXBlockInfo);
|
||||
renderViews(outline);
|
||||
expect(modal.$el.find('.modal-actions .action-move').hasClass('is-disabled')).toBeTruthy();
|
||||
|
||||
// navigate to groups level
|
||||
clickForwardButton(0);
|
||||
expect(modal.$el.find('.modal-actions .action-move').hasClass('is-disabled')).toBeTruthy();
|
||||
|
||||
// navigate to component level inside a group
|
||||
clickForwardButton(0);
|
||||
|
||||
// move should be disabled because we are navigating inside source xblock
|
||||
expect(modal.$el.find('.modal-actions .action-move').hasClass('is-disabled')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('is disabled when navigating to any content experiment groups', function() {
|
||||
var outline,
|
||||
splitTestXBlockInfo = {
|
||||
category: 'split_test',
|
||||
display_name: 'Content Experiment',
|
||||
has_children: true,
|
||||
id: 'SPLIT_TEST_ID'
|
||||
},
|
||||
outlineOptions = {split_test: 1, unit: 2, component: 1};
|
||||
|
||||
// group level should be disabled but component level inside groups should be movable
|
||||
outline = createXBlockInfo('unit', outlineOptions, splitTestXBlockInfo);
|
||||
renderViews(outline);
|
||||
|
||||
// move is disabled on groups level
|
||||
expect(modal.$el.find('.modal-actions .action-move').hasClass('is-disabled')).toBeTruthy();
|
||||
|
||||
// navigate to component level inside a group
|
||||
clickForwardButton(1);
|
||||
expect(modal.$el.find('.modal-actions .action-move').hasClass('is-disabled')).toBeFalsy();
|
||||
});
|
||||
|
||||
it('is enabled when navigating to any parentable component', function() {
|
||||
var parentableXBlockInfo = {
|
||||
category: 'vertical',
|
||||
display_name: 'Parentable Component',
|
||||
has_children: true,
|
||||
id: 'PARENTABLE_ID'
|
||||
};
|
||||
renderViews(parentableXBlockInfo);
|
||||
|
||||
// move is enabled on parentable xblocks.
|
||||
expect(modal.$el.find('.modal-actions .action-move').hasClass('is-disabled')).toBeFalsy();
|
||||
});
|
||||
|
||||
it('is disabled when navigating to any non-parentable component', function() {
|
||||
var nonParentableXBlockInfo = {
|
||||
category: 'html',
|
||||
display_name: 'Non Parentable Component',
|
||||
has_children: false,
|
||||
id: 'NON_PARENTABLE_ID'
|
||||
};
|
||||
renderViews(nonParentableXBlockInfo);
|
||||
|
||||
// move is disabled on non-parent xblocks.
|
||||
expect(modal.$el.find('.modal-actions .action-move').hasClass('is-disabled')).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Move an xblock', function() {
|
||||
it('can not move in a disabled state', function() {
|
||||
verifyMoveEnabled(false);
|
||||
modal.$el.find('.modal-actions .action-move').click();
|
||||
expect(modal.movedAlertView).toBeNull();
|
||||
expect(getSentRequests().length).toEqual(0);
|
||||
});
|
||||
|
||||
it('move an xblock when move button is clicked', function() {
|
||||
var requests = AjaxHelpers.requests(this);
|
||||
moveXBlockWithSuccess(requests);
|
||||
|
||||
@@ -122,6 +122,7 @@ function($, Backbone, _, gettext, BaseView, XBlockViewUtils, MoveXBlockUtils, Ht
|
||||
this.moveXBlockListView = new MoveXBlockListView(
|
||||
{
|
||||
model: new XBlockInfoModel(courseOutlineInfo, {parse: true}),
|
||||
sourceXBlockInfo: this.sourceXBlockInfo,
|
||||
ancestorInfo: ancestorInfo
|
||||
}
|
||||
);
|
||||
@@ -136,12 +137,30 @@ function($, Backbone, _, gettext, BaseView, XBlockViewUtils, MoveXBlockUtils, Ht
|
||||
}
|
||||
},
|
||||
|
||||
isValidCategory: function(sourceParentType, targetParentType, targetHasChildren) {
|
||||
var basicBlockTypes = ['course', 'chapter', 'sequential', 'vertical'];
|
||||
// Treat source parent component as vertical to support move child components under content experiment
|
||||
// and other similar xblocks.
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
sourceParentType = sourceParentType === 'split_test' ? 'vertical' : sourceParentType;
|
||||
// Treat target parent component as a vertical to support move to parentable target parent components.
|
||||
// Also, moving a component directly to content experiment is not allowed, we need to visit to group level.
|
||||
if (targetHasChildren && !_.contains(basicBlockTypes, targetParentType) &&
|
||||
targetParentType !== 'split_test') {
|
||||
targetParentType = 'vertical'; // eslint-disable-line no-param-reassign
|
||||
}
|
||||
return targetParentType === sourceParentType;
|
||||
},
|
||||
|
||||
enableMoveOperation: function(targetParentXBlockInfo) {
|
||||
var isValidMove = false,
|
||||
sourceParentType = this.sourceParentXBlockInfo.get('category'),
|
||||
targetParentType = targetParentXBlockInfo.get('category');
|
||||
targetParentType = targetParentXBlockInfo.get('category'),
|
||||
targetHasChildren = targetParentXBlockInfo.get('has_children');
|
||||
|
||||
if (targetParentType === sourceParentType && this.sourceParentXBlockInfo.id !== targetParentXBlockInfo.id) {
|
||||
if (this.isValidCategory(sourceParentType, targetParentType, targetHasChildren) &&
|
||||
this.sourceParentXBlockInfo.id !== targetParentXBlockInfo.id && // same parent case
|
||||
this.sourceXBlockInfo.id !== targetParentXBlockInfo.id) { // same source item case
|
||||
isValidMove = true;
|
||||
this.targetParentXBlockInfo = targetParentXBlockInfo;
|
||||
}
|
||||
|
||||
@@ -33,7 +33,8 @@ function($, Backbone, _, gettext, HtmlUtils, StringUtils, XBlockUtils, MoveXBloc
|
||||
section: gettext('Sections'),
|
||||
subsection: gettext('Subsections'),
|
||||
unit: gettext('Units'),
|
||||
component: gettext('Components')
|
||||
component: gettext('Components'),
|
||||
group: gettext('Groups')
|
||||
},
|
||||
|
||||
events: {
|
||||
@@ -43,6 +44,7 @@ function($, Backbone, _, gettext, HtmlUtils, StringUtils, XBlockUtils, MoveXBloc
|
||||
initialize: function(options) {
|
||||
this.visitedAncestors = [];
|
||||
this.template = HtmlUtils.template(MoveXBlockListViewTemplate);
|
||||
this.sourceXBlockInfo = options.sourceXBlockInfo;
|
||||
this.ancestorInfo = options.ancestorInfo;
|
||||
this.listenTo(Backbone, 'move:breadcrumbButtonPressed', this.handleBreadcrumbButtonPress);
|
||||
this.renderXBlockInfo();
|
||||
@@ -53,6 +55,7 @@ function($, Backbone, _, gettext, HtmlUtils, StringUtils, XBlockUtils, MoveXBloc
|
||||
this.$el,
|
||||
this.template(
|
||||
{
|
||||
sourceXBlockId: this.sourceXBlockInfo.id,
|
||||
xblocks: this.childrenInfo.children,
|
||||
noChildText: this.getNoChildText(),
|
||||
categoryText: this.getCategoryText(),
|
||||
@@ -123,10 +126,14 @@ function($, Backbone, _, gettext, HtmlUtils, StringUtils, XBlockUtils, MoveXBloc
|
||||
* Set parent and child XBlock categories.
|
||||
*/
|
||||
setDisplayedXBlocksCategories: function() {
|
||||
this.parentInfo.category = XBlockUtils.getXBlockType(
|
||||
this.parentInfo.parent.get('category'),
|
||||
this.visitedAncestors[this.visitedAncestors.length - 2]
|
||||
);
|
||||
var childCategory = 'component';
|
||||
this.parentInfo.category = XBlockUtils.getXBlockType(this.parentInfo.parent.get('category'));
|
||||
if (!_.contains(_.keys(this.categoryRelationMap), this.parentInfo.category)) {
|
||||
if (this.parentInfo.category === 'split_test') {
|
||||
childCategory = 'group'; // This is just to show groups text on group listing.
|
||||
}
|
||||
this.categoryRelationMap[this.parentInfo.category] = childCategory;
|
||||
}
|
||||
this.childrenInfo.category = this.categoryRelationMap[this.parentInfo.category];
|
||||
},
|
||||
|
||||
@@ -136,25 +143,19 @@ function($, Backbone, _, gettext, HtmlUtils, StringUtils, XBlockUtils, MoveXBloc
|
||||
* @returns {any} Integer or undefined
|
||||
*/
|
||||
getCurrentLocationIndex: function() {
|
||||
var category, ancestorXBlock, currentLocationIndex;
|
||||
|
||||
if (this.childrenInfo.category === 'component' || this.childrenInfo.children.length === 0) {
|
||||
return currentLocationIndex;
|
||||
}
|
||||
|
||||
category = this.childrenInfo.children[0].get('category');
|
||||
ancestorXBlock = _.find(
|
||||
this.ancestorInfo.ancestors, function(ancestor) { return ancestor.category === category; }
|
||||
);
|
||||
|
||||
if (ancestorXBlock) {
|
||||
_.each(this.childrenInfo.children, function(xblock, index) {
|
||||
if (ancestorXBlock.display_name === xblock.get('display_name') &&
|
||||
ancestorXBlock.id === xblock.get('id')) {
|
||||
currentLocationIndex = index;
|
||||
}
|
||||
});
|
||||
}
|
||||
var self = this,
|
||||
currentLocationIndex;
|
||||
_.each(self.childrenInfo.children, function(xblock, index) {
|
||||
if (xblock.get('id') === self.sourceXBlockInfo.id) {
|
||||
currentLocationIndex = index;
|
||||
} else {
|
||||
_.each(self.ancestorInfo.ancestors, function(ancestor) {
|
||||
if (ancestor.display_name === xblock.get('display_name') && ancestor.id === xblock.get('id')) {
|
||||
currentLocationIndex = index;
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return currentLocationIndex;
|
||||
},
|
||||
|
||||
@@ -395,20 +395,21 @@
|
||||
}
|
||||
|
||||
.component {
|
||||
display: block;
|
||||
display: inline-block;
|
||||
color: $black;
|
||||
padding: ($baseline/4) ($baseline/2);
|
||||
}
|
||||
|
||||
.xblock-displayname {
|
||||
@include float(left);
|
||||
}
|
||||
|
||||
.button-forward, .component {
|
||||
border: none;
|
||||
padding: ($baseline/2);
|
||||
}
|
||||
|
||||
.button-forward {
|
||||
.xblock-displayname {
|
||||
@include float(left);
|
||||
}
|
||||
|
||||
padding: ($baseline/2);
|
||||
.forward-sr-icon {
|
||||
@include float(right);
|
||||
|
||||
|
||||
@@ -18,11 +18,7 @@
|
||||
var xblock = xblocks[i];
|
||||
%>
|
||||
<li class="xblock-item" data-item-index="<%- i %>">
|
||||
<% if (XBlocksCategory === 'component') { %>
|
||||
<span class="xblock-displayname component truncate">
|
||||
<%- xblock.get('display_name') %>
|
||||
</span>
|
||||
<% } else { %>
|
||||
<% if (sourceXBlockId !== xblock.id && (xblock.get('child_info') || XBlocksCategory !== 'component')) { %>
|
||||
<button class="button-forward" >
|
||||
<span class="xblock-displayname truncate">
|
||||
<%- xblock.get('display_name') %>
|
||||
@@ -33,8 +29,19 @@
|
||||
</span>
|
||||
<% } %>
|
||||
<span class="icon fa fa-arrow-right forward-sr-icon" aria-hidden="true"></span>
|
||||
<span class="sr forward-sr-text"><%- gettext("Click for children") %></span>
|
||||
<span class="sr forward-sr-text"><%- gettext("View child items") %></span>
|
||||
</button>
|
||||
<% } else { %>
|
||||
<span class="component">
|
||||
<span class="xblock-displayname truncate">
|
||||
<%- xblock.get('display_name') %>
|
||||
</span>
|
||||
<% if(currentLocationIndex === i) { %>
|
||||
<span class="current-location">
|
||||
(<%- gettext('Currently selected') %>)
|
||||
</span>
|
||||
<% } %>
|
||||
</span>
|
||||
<% } %>
|
||||
</li>
|
||||
<% } %>
|
||||
|
||||
@@ -89,7 +89,8 @@ messages = xblock.validate().to_json()
|
||||
<span class="sr">${_("Duplicate")}</span>
|
||||
</button>
|
||||
</li>
|
||||
|
||||
% endif
|
||||
% if can_move:
|
||||
<li class="action-item action-move">
|
||||
<button data-tooltip="${_("Move")}" class="btn-default move-button action-button">
|
||||
<span class="stack-move-icon fa-stack fa-lg ">
|
||||
|
||||
@@ -338,6 +338,7 @@ class LibraryContentModule(LibraryContentFields, XModule, StudioEditableModule):
|
||||
'display_name': self.display_name or self.url_name,
|
||||
}))
|
||||
context['can_edit_visibility'] = False
|
||||
context['can_move'] = False
|
||||
self.render_children(context, fragment, can_reorder=False, can_add=False)
|
||||
# else: When shown on a unit page, don't show any sort of preview -
|
||||
# just the status of this block in the validation area.
|
||||
|
||||
@@ -80,6 +80,7 @@ class LibraryRoot(XBlock):
|
||||
children_to_show = self.children[item_start:item_end] # pylint: disable=no-member
|
||||
|
||||
force_render = context.get('force_render', None)
|
||||
context['can_move'] = False
|
||||
|
||||
for child_key in children_to_show:
|
||||
# Children must have a separate context from the library itself. Make a copy.
|
||||
|
||||
Reference in New Issue
Block a user