Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions cms/djangoapps/contentstore/views/item.py
Original file line number Diff line number Diff line change
Expand Up @@ -739,6 +739,8 @@ def _move_item(source_usage_key, target_parent_usage_key, user, target_index=Non
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 target_parent_type == 'split_test':
error = 'You can not move an item directly into content experiment.'
elif source_index is None:
error = '{source_usage_key} not found in {parent_usage_key}.'.format(
source_usage_key=unicode(source_usage_key),
Expand Down Expand Up @@ -1119,15 +1121,15 @@ def create_xblock_info(xblock, data=None, metadata=None, include_ancestor_info=F
xblock_info = {
'id': unicode(xblock.location),
'display_name': xblock.display_name_with_default,
'category': xblock.category
'category': xblock.category,
'has_children': xblock.has_children
}
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,
Expand Down
24 changes: 18 additions & 6 deletions cms/djangoapps/contentstore/views/tests/test_item.py
Original file line number Diff line number Diff line change
Expand Up @@ -948,17 +948,17 @@ def test_invalid_move(self):
"""
Test invalid move.
"""
parent_loc = self.store.get_parent_location(self.chapter_usage_key)
response = self._move_component(self.chapter_usage_key, self.usage_key)
parent_loc = self.store.get_parent_location(self.html_usage_key)
response = self._move_component(self.html_usage_key, self.seq_usage_key)
self.assertEqual(response.status_code, 400)
response = json.loads(response.content)

expected_error = 'You can not move {source_type} into {target_type}.'.format(
source_type=self.chapter_usage_key.block_type,
target_type=self.usage_key.block_type
source_type=self.html_usage_key.block_type,
target_type=self.seq_usage_key.block_type
)
self.assertEqual(expected_error, response['error'])
new_parent_loc = self.store.get_parent_location(self.chapter_usage_key)
new_parent_loc = self.store.get_parent_location(self.html_usage_key)
self.assertEqual(new_parent_loc, parent_loc)

def test_move_current_parent(self):
Expand Down Expand Up @@ -1039,11 +1039,23 @@ def test_move_content_experiment_components(self):

def test_move_into_content_experiment_groups(self):
"""
Test that a component can be moved to content experiment.
Test that a component can be moved to content experiment groups.
"""
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_into_content_experiment_level(self):
"""
Test that a component can not be moved directly to content experiment level.
"""
self.setup_and_verify_content_experiment(0)
response = self._move_component(self.html_usage_key, self.split_test_usage_key)
self.assertEqual(response.status_code, 400)
response = json.loads(response.content)

self.assertEqual(response['error'], 'You can not move an item directly into content experiment.')
self.assertEqual(self.store.get_parent_location(self.html_usage_key), self.vert_usage_key)

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.
Expand Down
4 changes: 4 additions & 0 deletions cms/static/js/models/xblock_info.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,10 @@ function(Backbone, _, str, ModuleUtils) {
* publishing info was explicitly requested.
*/
'published_by': null,
/**
* True if the xblock is a parentable xblock.
*/
has_children: null,
/**
* True if the xblock has changes.
* Note: this is not always provided as a performance optimization. It is only provided for
Expand Down
130 changes: 84 additions & 46 deletions cms/static/js/spec/views/move_xblock_spec.js
Original file line number Diff line number Diff line change
@@ -1,17 +1,19 @@
define(['jquery', 'underscore', 'edx-ui-toolkit/js/utils/spec-helpers/ajax-helpers',
define(['jquery', 'underscore', 'edx-ui-toolkit/js/utils/spec-helpers/ajax-helpers', 'js/spec_helpers/edit_helpers',
'common/js/spec_helpers/template_helpers', 'common/js/spec_helpers/view_helpers',
'js/views/modals/move_xblock_modal', 'edx-ui-toolkit/js/utils/html-utils',
'js/views/modals/move_xblock_modal', 'js/views/pages/container', 'edx-ui-toolkit/js/utils/html-utils',
'edx-ui-toolkit/js/utils/string-utils', 'js/models/xblock_info'],
function($, _, AjaxHelpers, TemplateHelpers, ViewHelpers, MoveXBlockModal, HtmlUtils, StringUtils, XBlockInfo) {
function($, _, AjaxHelpers, EditHelpers, TemplateHelpers, ViewHelpers, MoveXBlockModal, ContainerPage, HtmlUtils,
StringUtils, XBlockInfo) {
'use strict';
describe('MoveXBlock', function() {
var modal, showModal, renderViews, createXBlockInfo, createCourseOutline, courseOutlineOptions,
parentChildMap, categoryMap, createChildXBlockInfo, xblockAncestorInfo, courseOutline,
verifyBreadcrumbViewInfo, verifyListViewInfo, getDisplayedInfo, clickForwardButton,
clickBreadcrumbButton, verifyXBlockInfo, nextCategory, verifyMoveEnabled, getSentRequests,
verifyNotificationStatus, sendMoveXBlockRequest, moveXBlockWithSuccess,
verifyConfirmationFeedbackTitleHtml, verifyConfirmationFeedbackRedirectLinkHtml,
verifyUndoConfirmationFeedbackTitleHtml, verifyConfirmationFeedbackUndoMoveActionHtml,
verifyNotificationStatus, sendMoveXBlockRequest, moveXBlockWithSuccess, getMovedAlertNotification,
verifyConfirmationFeedbackTitleText, verifyConfirmationFeedbackRedirectLinkText,
verifyUndoConfirmationFeedbackTitleText, verifyConfirmationFeedbackUndoMoveActionText,
sourceParentXBlockInfo, mockContainerPage, createContainerPage, containerPage,
sourceDisplayName = 'component_display_name_0',
sourceLocator = 'component_ID_0',
sourceParentLocator = 'unit_ID_0';
Expand Down Expand Up @@ -62,20 +64,39 @@ define(['jquery', 'underscore', 'edx-ui-toolkit/js/utils/spec-helpers/ajax-helpe
]
};

sourceParentXBlockInfo = new XBlockInfo({
id: sourceParentLocator,
display_name: 'unit_display_name_0',
category: 'vertical'
});

createContainerPage = function() {
containerPage = new ContainerPage({
model: sourceParentXBlockInfo,
templates: EditHelpers.mockComponentTemplates,
el: $('#content'),
isUnitPage: true
});
};

beforeEach(function() {
setFixtures("<div id='page-alert'></div>");
mockContainerPage = readFixtures('mock/mock-container-page.underscore');
TemplateHelpers.installTemplates([
'basic-modal',
'modal-button',
'move-xblock-modal'
]);
appendSetFixtures(mockContainerPage);
createContainerPage();
courseOutline = createCourseOutline(courseOutlineOptions);
showModal();
});

afterEach(function() {
modal.hide();
courseOutline = null;
containerPage.remove();
});

showModal = function() {
Expand All @@ -85,11 +106,7 @@ define(['jquery', 'underscore', 'edx-ui-toolkit/js/utils/spec-helpers/ajax-helpe
display_name: sourceDisplayName,
category: 'component'
}),
sourceParentXBlockInfo: new XBlockInfo({
id: sourceParentLocator,
display_name: 'unit_display_name_0',
category: 'vertical'
}),
sourceParentXBlockInfo: sourceParentXBlockInfo,
XBlockUrlRoot: '/xblock'
});
modal.show();
Expand Down Expand Up @@ -338,6 +355,13 @@ define(['jquery', 'underscore', 'edx-ui-toolkit/js/utils/spec-helpers/ajax-helpe
ViewHelpers.verifyNotificationHidden(notificationSpy);
};

/**
* Get move alert confirmation message HTML
*/
getMovedAlertNotification = function() {
return $('#page-alert');
};

/**
* Send move xblock request.
*
Expand Down Expand Up @@ -384,33 +408,36 @@ define(['jquery', 'underscore', 'edx-ui-toolkit/js/utils/spec-helpers/ajax-helpe
});
modal.$el.find('.modal-actions .action-move').click();
sendMoveXBlockRequest(requests, sourceLocator);
expect(modal.movedAlertView).toBeDefined();
verifyConfirmationFeedbackTitleHtml(sourceDisplayName);
verifyConfirmationFeedbackRedirectLinkHtml();
verifyConfirmationFeedbackUndoMoveActionHtml();
AjaxHelpers.expectJsonRequest(requests, 'GET', '/xblock/' + sourceParentLocator);
AjaxHelpers.respondWithJson(requests, sourceParentXBlockInfo);
expect(getMovedAlertNotification().html().length).not.toEqual(0);
verifyConfirmationFeedbackTitleText(sourceDisplayName);
verifyConfirmationFeedbackRedirectLinkText();
verifyConfirmationFeedbackUndoMoveActionText();
};

/**
* Verify success banner message html has correct title html.
* Verify success banner message html has correct title text.
*
* @param {String} displayName XBlock display name
*/
verifyConfirmationFeedbackTitleHtml = function(displayName) {
expect(modal.movedAlertView.$el.find('.title').html().trim())
.toEqual(StringUtils.interpolate('Success! "{displayName}" has been moved.',
{
displayName: displayName
})
);
verifyConfirmationFeedbackTitleText = function(displayName) {
expect(getMovedAlertNotification().find('.title').html()
.trim())
.toEqual(StringUtils.interpolate('Success! "{displayName}" has been moved.',
{
displayName: displayName
})
);
};

/**
* Verify undo success banner message html has correct title html.
* Verify undo success banner message html has correct title text.
*
* @param {String} displayName XBlock display name
*/
verifyUndoConfirmationFeedbackTitleHtml = function(displayName) {
expect(modal.movedAlertView.$el.find('.title').html()).toEqual(
verifyUndoConfirmationFeedbackTitleText = function(displayName) {
expect(getMovedAlertNotification().find('.title').html()).toEqual(
StringUtils.interpolate(
'Move cancelled. "{sourceDisplayName}" has been moved back to its original location.',
{
Expand All @@ -421,25 +448,18 @@ define(['jquery', 'underscore', 'edx-ui-toolkit/js/utils/spec-helpers/ajax-helpe
};

/**
* Verify success banner message html has correct redirect link html.
* Verify success banner message html has correct redirect link text.
*/
verifyConfirmationFeedbackRedirectLinkHtml = function() {
expect(modal.movedAlertView.$el.find('.copy').html().indexOf(
HtmlUtils.HTML(
'<button class="action-secondary action-cancel">Take me to the new location</button>'
) !== -1
)).toBeTruthy();
verifyConfirmationFeedbackRedirectLinkText = function() {
expect(getMovedAlertNotification().find('.nav-actions .action-secondary').html())
.toEqual('Take me to the new location');
};

/**
* Verify success banner message html has correct undo move button html.
* Verify success banner message html has correct undo move text.
*/
verifyConfirmationFeedbackUndoMoveActionHtml = function() {
expect(modal.movedAlertView.$el.find('.copy').html().indexOf(
HtmlUtils.HTML(
'<button class="action-primary action-save">Undo Move</button>'
) !== -1
)).toBeTruthy();
verifyConfirmationFeedbackUndoMoveActionText = function() {
expect(getMovedAlertNotification().find('.nav-actions .action-primary').html()).toEqual('Undo move');
};

/**
Expand Down Expand Up @@ -633,6 +653,24 @@ define(['jquery', 'underscore', 'edx-ui-toolkit/js/utils/spec-helpers/ajax-helpe
expect(modal.$el.find('.modal-actions .action-move').hasClass('is-disabled')).toBeFalsy();
});

it('is enabled when moving a component inside a parentable component', function() {
// create a source parent with has_childern set true
modal.sourceParentXBlockInfo = new XBlockInfo({
category: 'conditional',
display_name: 'Parentable Component',
has_children: true,
id: 'PARENTABLE_ID'
});
// navigate and verify move button is enabled
renderViews(courseOutline);
_.each(_.range(3), function() {
clickForwardButton(0);
});

// move is enabled when moving a component.
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',
Expand All @@ -651,7 +689,7 @@ define(['jquery', 'underscore', 'edx-ui-toolkit/js/utils/spec-helpers/ajax-helpe
it('can not move in a disabled state', function() {
verifyMoveEnabled(false);
modal.$el.find('.modal-actions .action-move').click();
expect(modal.movedAlertView).toBeNull();
expect(getMovedAlertNotification().html().length).toEqual(0);
expect(getSentRequests().length).toEqual(0);
});

Expand All @@ -662,21 +700,21 @@ define(['jquery', 'underscore', 'edx-ui-toolkit/js/utils/spec-helpers/ajax-helpe

it('do not move an xblock when cancel button is clicked', function() {
modal.$el.find('.modal-actions .action-cancel').click();
expect(modal.movedAlertView).toBeNull();
expect(getMovedAlertNotification().html().length).toEqual(0);
expect(getSentRequests().length).toEqual(0);
});

it('undo move an xblock when undo move link is clicked', function() {
var sourceIndex = 0,
requests = AjaxHelpers.requests(this);
moveXBlockWithSuccess(requests);
modal.movedAlertView.$el.find('.action-save').click();
getMovedAlertNotification().find('.action-save').click();
AjaxHelpers.respondWithJson(requests, {
move_source_locator: sourceLocator,
parent_locator: sourceParentLocator,
target_index: sourceIndex
});
verifyUndoConfirmationFeedbackTitleHtml(sourceDisplayName);
verifyUndoConfirmationFeedbackTitleText(sourceDisplayName);
});
});

Expand All @@ -698,7 +736,7 @@ define(['jquery', 'underscore', 'edx-ui-toolkit/js/utils/spec-helpers/ajax-helpe
requests = AjaxHelpers.requests(this);
moveXBlockWithSuccess(requests);
notificationSpy = ViewHelpers.createNotificationSpy();
modal.movedAlertView.$el.find('.action-save').click();
getMovedAlertNotification().find('.action-save').click();
verifyNotificationStatus(requests, notificationSpy, 'Undo moving');
});

Expand All @@ -719,7 +757,7 @@ define(['jquery', 'underscore', 'edx-ui-toolkit/js/utils/spec-helpers/ajax-helpe
var requests = AjaxHelpers.requests(this),
notificationSpy = ViewHelpers.createNotificationSpy('Error');
moveXBlockWithSuccess(requests);
modal.movedAlertView.$el.find('.action-save').click();
getMovedAlertNotification().find('.action-save').click();
AjaxHelpers.respondWithError(requests);
ViewHelpers.verifyNotificationShowing(notificationSpy, "Studio's having trouble saving your work");
});
Expand Down
3 changes: 3 additions & 0 deletions cms/static/js/spec/views/pages/container_spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,9 @@ define(['jquery', 'underscore', 'underscore.string', 'edx-ui-toolkit/js/utils/sp

afterEach(function() {
EditHelpers.uninstallMockXBlock();
if (containerPage !== undefined) {
containerPage.remove();
}
});

respondWithHtml = function(html) {
Expand Down
3 changes: 3 additions & 0 deletions cms/static/js/spec/views/pages/container_subviews_spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@ define(['jquery', 'underscore', 'underscore.string', 'edx-ui-toolkit/js/utils/sp

afterEach(function() {
delete window.course;
if (containerPage !== undefined) {
containerPage.remove();
}
});

defaultXBlockInfo = {
Expand Down
Loading