Commit a31a728a by Mushtaq Ali

Add duplicate functionality for unit, sub-section and section - TNL-5797, TNL-5798

parent dc8d020e
...@@ -635,7 +635,7 @@ def _create_item(request): ...@@ -635,7 +635,7 @@ def _create_item(request):
) )
def _duplicate_item(parent_usage_key, duplicate_source_usage_key, user, display_name=None): def _duplicate_item(parent_usage_key, duplicate_source_usage_key, user, display_name=None, is_child=False):
""" """
Duplicate an existing xblock as a child of the supplied parent_usage_key. Duplicate an existing xblock as a child of the supplied parent_usage_key.
""" """
...@@ -653,6 +653,10 @@ def _duplicate_item(parent_usage_key, duplicate_source_usage_key, user, display_ ...@@ -653,6 +653,10 @@ def _duplicate_item(parent_usage_key, duplicate_source_usage_key, user, display_
for field in source_item.fields.values(): for field in source_item.fields.values():
if field.scope == Scope.settings and field.is_set_on(source_item): if field.scope == Scope.settings and field.is_set_on(source_item):
duplicate_metadata[field.name] = field.read_from(source_item) duplicate_metadata[field.name] = field.read_from(source_item)
if is_child:
display_name = display_name or source_item.display_name or source_item.category
if display_name is not None: if display_name is not None:
duplicate_metadata['display_name'] = display_name duplicate_metadata['display_name'] = display_name
else: else:
...@@ -698,7 +702,7 @@ def _duplicate_item(parent_usage_key, duplicate_source_usage_key, user, display_ ...@@ -698,7 +702,7 @@ def _duplicate_item(parent_usage_key, duplicate_source_usage_key, user, display_
if source_item.has_children and not children_handled: if source_item.has_children and not children_handled:
dest_module.children = dest_module.children or [] dest_module.children = dest_module.children or []
for child in source_item.children: for child in source_item.children:
dupe = _duplicate_item(dest_module.location, child, user=user) dupe = _duplicate_item(dest_module.location, child, user=user, is_child=True)
if dupe not in dest_module.children: # _duplicate_item may add the child for us. if dupe not in dest_module.children: # _duplicate_item may add the child for us.
dest_module.children.append(dupe) dest_module.children.append(dupe)
store.update_item(dest_module, user.id) store.update_item(dest_module, user.id)
...@@ -944,8 +948,9 @@ def create_xblock_info(xblock, data=None, metadata=None, include_ancestor_info=F ...@@ -944,8 +948,9 @@ def create_xblock_info(xblock, data=None, metadata=None, include_ancestor_info=F
visibility_state = None visibility_state = None
published = modulestore().has_published_version(xblock) if not is_library_block else None published = modulestore().has_published_version(xblock) if not is_library_block else None
# defining the default value 'True' for delete, drag and add new child actions in xblock_actions for each xblock. # defining the default value 'True' for delete, duplicate, drag and add new child actions
xblock_actions = {'deletable': True, 'draggable': True, 'childAddable': True} # in xblock_actions for each xblock.
xblock_actions = {'deletable': True, 'draggable': True, 'childAddable': True, 'duplicable': True}
explanatory_message = None explanatory_message = None
# is_entrance_exam is inherited metadata. # is_entrance_exam is inherited metadata.
......
...@@ -484,7 +484,8 @@ class DuplicateHelper(object): ...@@ -484,7 +484,8 @@ class DuplicateHelper(object):
"Duplicated item differs from original" "Duplicated item differs from original"
) )
def _check_equality(self, source_usage_key, duplicate_usage_key, parent_usage_key=None, check_asides=False): def _check_equality(self, source_usage_key, duplicate_usage_key, parent_usage_key=None, check_asides=False,
is_child=False):
""" """
Gets source and duplicated items from the modulestore using supplied usage keys. Gets source and duplicated items from the modulestore using supplied usage keys.
Then verifies that they represent equivalent items (modulo parents and other Then verifies that they represent equivalent items (modulo parents and other
...@@ -523,10 +524,9 @@ class DuplicateHelper(object): ...@@ -523,10 +524,9 @@ class DuplicateHelper(object):
"Parent duplicate should be different from source" "Parent duplicate should be different from source"
) )
# Set the location, display name, and parent to be the same so we can make sure the rest of the # Set the location and parent to be the same so we can make sure the rest of the
# duplicate is equal. # duplicate is equal.
duplicated_item.location = original_item.location duplicated_item.location = original_item.location
duplicated_item.display_name = original_item.display_name
duplicated_item.parent = original_item.parent duplicated_item.parent = original_item.parent
# Children will also be duplicated, so for the purposes of testing equality, we will set # Children will also be duplicated, so for the purposes of testing equality, we will set
...@@ -538,11 +538,26 @@ class DuplicateHelper(object): ...@@ -538,11 +538,26 @@ class DuplicateHelper(object):
"Duplicated item differs in number of children" "Duplicated item differs in number of children"
) )
for i in xrange(len(original_item.children)): for i in xrange(len(original_item.children)):
if not self._check_equality(original_item.children[i], duplicated_item.children[i]): if not self._check_equality(original_item.children[i], duplicated_item.children[i], is_child=True):
return False return False
duplicated_item.children = original_item.children duplicated_item.children = original_item.children
return self._verify_duplicate_display_name(original_item, duplicated_item, is_child)
return original_item == duplicated_item def _verify_duplicate_display_name(self, original_item, duplicated_item, is_child=False):
"""
Verifies display name of duplicated item.
"""
if is_child:
if original_item.display_name is None:
return duplicated_item.display_name == original_item.category
return duplicated_item.display_name == original_item.display_name
if original_item.display_name is not None:
return duplicated_item.display_name == "Duplicate of '{display_name}'".format(
display_name=original_item.display_name
)
return duplicated_item.display_name == "Duplicate of {display_name}".format(
display_name=original_item.category
)
def _duplicate_item(self, parent_usage_key, source_usage_key, display_name=None): def _duplicate_item(self, parent_usage_key, source_usage_key, display_name=None):
""" """
...@@ -571,16 +586,20 @@ class TestDuplicateItem(ItemTest, DuplicateHelper): ...@@ -571,16 +586,20 @@ class TestDuplicateItem(ItemTest, DuplicateHelper):
resp = self.create_xblock(parent_usage_key=self.usage_key, category='chapter') resp = self.create_xblock(parent_usage_key=self.usage_key, category='chapter')
self.chapter_usage_key = self.response_usage_key(resp) self.chapter_usage_key = self.response_usage_key(resp)
# create a sequential containing a problem and an html component # create a sequential
resp = self.create_xblock(parent_usage_key=self.chapter_usage_key, category='sequential') resp = self.create_xblock(parent_usage_key=self.chapter_usage_key, category='sequential')
self.seq_usage_key = self.response_usage_key(resp) self.seq_usage_key = self.response_usage_key(resp)
# create a vertical containing a problem and an html component
resp = self.create_xblock(parent_usage_key=self.seq_usage_key, category='vertical')
self.vert_usage_key = self.response_usage_key(resp)
# create problem and an html component # create problem and an html component
resp = self.create_xblock(parent_usage_key=self.seq_usage_key, category='problem', resp = self.create_xblock(parent_usage_key=self.vert_usage_key, category='problem',
boilerplate='multiplechoice.yaml') boilerplate='multiplechoice.yaml')
self.problem_usage_key = self.response_usage_key(resp) self.problem_usage_key = self.response_usage_key(resp)
resp = self.create_xblock(parent_usage_key=self.seq_usage_key, category='html') resp = self.create_xblock(parent_usage_key=self.vert_usage_key, category='html')
self.html_usage_key = self.response_usage_key(resp) self.html_usage_key = self.response_usage_key(resp)
# Create a second sequential just (testing children of children) # Create a second sequential just (testing children of children)
...@@ -591,8 +610,9 @@ class TestDuplicateItem(ItemTest, DuplicateHelper): ...@@ -591,8 +610,9 @@ class TestDuplicateItem(ItemTest, DuplicateHelper):
Tests that a duplicated xblock is identical to the original, Tests that a duplicated xblock is identical to the original,
except for location and display name. except for location and display name.
""" """
self._duplicate_and_verify(self.problem_usage_key, self.seq_usage_key) self._duplicate_and_verify(self.problem_usage_key, self.vert_usage_key)
self._duplicate_and_verify(self.html_usage_key, self.seq_usage_key) self._duplicate_and_verify(self.html_usage_key, self.vert_usage_key)
self._duplicate_and_verify(self.vert_usage_key, self.seq_usage_key)
self._duplicate_and_verify(self.seq_usage_key, self.chapter_usage_key) self._duplicate_and_verify(self.seq_usage_key, self.chapter_usage_key)
self._duplicate_and_verify(self.chapter_usage_key, self.usage_key) self._duplicate_and_verify(self.chapter_usage_key, self.usage_key)
...@@ -625,9 +645,10 @@ class TestDuplicateItem(ItemTest, DuplicateHelper): ...@@ -625,9 +645,10 @@ class TestDuplicateItem(ItemTest, DuplicateHelper):
"duplicated item not ordered after source item" "duplicated item not ordered after source item"
) )
verify_order(self.problem_usage_key, self.seq_usage_key, 0) verify_order(self.problem_usage_key, self.vert_usage_key, 0)
# 2 because duplicate of problem should be located before. # 2 because duplicate of problem should be located before.
verify_order(self.html_usage_key, self.seq_usage_key, 2) verify_order(self.html_usage_key, self.vert_usage_key, 2)
verify_order(self.vert_usage_key, self.seq_usage_key, 0)
verify_order(self.seq_usage_key, self.chapter_usage_key, 0) verify_order(self.seq_usage_key, self.chapter_usage_key, 0)
# Test duplicating something into a location that is not the parent of the original item. # Test duplicating something into a location that is not the parent of the original item.
...@@ -645,12 +666,12 @@ class TestDuplicateItem(ItemTest, DuplicateHelper): ...@@ -645,12 +666,12 @@ class TestDuplicateItem(ItemTest, DuplicateHelper):
return usage_key return usage_key
# Display name comes from template. # Display name comes from template.
dupe_usage_key = verify_name(self.problem_usage_key, self.seq_usage_key, "Duplicate of 'Multiple Choice'") dupe_usage_key = verify_name(self.problem_usage_key, self.vert_usage_key, "Duplicate of 'Multiple Choice'")
# Test dupe of dupe. # Test dupe of dupe.
verify_name(dupe_usage_key, self.seq_usage_key, "Duplicate of 'Duplicate of 'Multiple Choice''") verify_name(dupe_usage_key, self.vert_usage_key, "Duplicate of 'Duplicate of 'Multiple Choice''")
# Uses default display_name of 'Text' from HTML component. # Uses default display_name of 'Text' from HTML component.
verify_name(self.html_usage_key, self.seq_usage_key, "Duplicate of 'Text'") verify_name(self.html_usage_key, self.vert_usage_key, "Duplicate of 'Text'")
# The sequence does not have a display_name set, so category is shown. # The sequence does not have a display_name set, so category is shown.
verify_name(self.seq_usage_key, self.chapter_usage_key, "Duplicate of sequential") verify_name(self.seq_usage_key, self.chapter_usage_key, "Duplicate of sequential")
......
...@@ -196,6 +196,10 @@ function(Backbone, _, str, ModuleUtils) { ...@@ -196,6 +196,10 @@ function(Backbone, _, str, ModuleUtils) {
return this.isActionRequired('deletable'); return this.isActionRequired('deletable');
}, },
isDuplicable: function() {
return this.isActionRequired('duplicable');
},
isDraggable: function() { isDraggable: function() {
return this.isActionRequired('draggable'); return this.isActionRequired('draggable');
}, },
......
...@@ -400,6 +400,70 @@ define(['jquery', 'edx-ui-toolkit/js/utils/spec-helpers/ajax-helpers', 'common/j ...@@ -400,6 +400,70 @@ define(['jquery', 'edx-ui-toolkit/js/utils/spec-helpers/ajax-helpers', 'common/j
}); });
}); });
describe('Duplicate an xblock', function() {
var duplicateXBlockWithSuccess;
duplicateXBlockWithSuccess = function(xblockLocator, parentLocator, xblockType, xblockIndex) {
getItemHeaders(xblockType).find('.duplicate-button')[xblockIndex].click();
// verify content of request
AjaxHelpers.expectJsonRequest(requests, 'POST', '/xblock/', {
duplicate_source_locator: xblockLocator,
parent_locator: parentLocator
});
// send the response
AjaxHelpers.respondWithJson(requests, {
locator: 'locator-duplicated-xblock'
});
};
it('section can be duplicated', function() {
createCourseOutlinePage(this, mockCourseJSON);
expect(outlinePage.$('.list-sections li.outline-section').length).toEqual(1);
expect(getItemsOfType('section').length, 1);
duplicateXBlockWithSuccess('mock-section', 'mock-course', 'section', 0);
expect(getItemHeaders('section').length, 2);
});
it('subsection can be duplicated', function() {
createCourseOutlinePage(this, mockCourseJSON);
expect(getItemsOfType('subsection').length, 1);
duplicateXBlockWithSuccess('mock-subsection', 'mock-section', 'subsection', 0);
expect(getItemHeaders('subsection').length, 2);
});
it('unit can be duplicated', function() {
createCourseOutlinePage(this, mockCourseJSON);
expandItemsAndVerifyState('subsection');
expect(getItemsOfType('unit').length, 1);
duplicateXBlockWithSuccess('mock-unit', 'mock-subsection', 'unit', 0);
expect(getItemHeaders('unit').length, 2);
});
it('shows a notification when duplicating', function() {
var notificationSpy = EditHelpers.createNotificationSpy();
createCourseOutlinePage(this, mockCourseJSON);
getItemHeaders('section').find('.duplicate-button').first()
.click();
EditHelpers.verifyNotificationShowing(notificationSpy, /Duplicating/);
AjaxHelpers.respondWithJson(requests, {locator: 'locator-duplicated-xblock'});
EditHelpers.verifyNotificationHidden(notificationSpy);
});
it('does not duplicate an xblock upon failure', function() {
var notificationSpy = EditHelpers.createNotificationSpy();
createCourseOutlinePage(this, mockCourseJSON);
expect(getItemHeaders('section').length, 1);
getItemHeaders('section').find('.duplicate-button').first()
.click();
EditHelpers.verifyNotificationShowing(notificationSpy, /Duplicating/);
AjaxHelpers.respondWithError(requests);
expect(getItemHeaders('section').length, 2);
EditHelpers.verifyNotificationShowing(notificationSpy, /Duplicating/);
});
});
describe('Empty course', function() { describe('Empty course', function() {
it('shows an empty course message initially', function() { it('shows an empty course message initially', function() {
createCourseOutlinePage(this, mockEmptyCourseJSON); createCourseOutlinePage(this, mockEmptyCourseJSON);
......
...@@ -91,9 +91,28 @@ define(['jquery', 'underscore', 'js/views/xblock_outline', 'common/js/components ...@@ -91,9 +91,28 @@ define(['jquery', 'underscore', 'js/views/xblock_outline', 'common/js/components
} }
}, },
onSectionAdded: function(locator) { /**
* Perform specific actions for duplicated xblock.
* @param {String} locator The locator of the new duplicated xblock.
* @param {String} xblockType The front-end terminology of the xblock category.
* @param {jquery Element} xblockElement The xblock element to be duplicated.
*/
onChildDuplicated: function(locator, xblockType, xblockElement) {
var scrollOffset = ViewUtils.getScrollOffset(xblockElement);
if (xblockType === 'section') {
this.onSectionAdded(locator, xblockElement, scrollOffset);
} else {
// For all other block types, refresh the view and do the following:
// - show the new block expanded
// - ensure it is scrolled into view
// - make its name editable
this.refresh(this.createNewItemViewState(locator, scrollOffset));
}
},
onSectionAdded: function(locator, xblockElement, scrollOffset) {
var self = this, var self = this,
initialState = self.createNewItemViewState(locator), initialState = self.createNewItemViewState(locator, scrollOffset),
sectionInfo, sectionView; sectionInfo, sectionView;
// For new chapters in a non-empty view, add a new child view and render it // For new chapters in a non-empty view, add a new child view and render it
// to avoid the expense of refreshing the entire page. // to avoid the expense of refreshing the entire page.
...@@ -108,7 +127,7 @@ define(['jquery', 'underscore', 'js/views/xblock_outline', 'common/js/components ...@@ -108,7 +127,7 @@ define(['jquery', 'underscore', 'js/views/xblock_outline', 'common/js/components
sectionView.initialState = initialState; sectionView.initialState = initialState;
sectionView.expandedLocators = self.expandedLocators; sectionView.expandedLocators = self.expandedLocators;
sectionView.render(); sectionView.render();
self.addChildView(sectionView); self.addChildView(sectionView, xblockElement);
sectionView.setViewState(initialState); sectionView.setViewState(initialState);
}); });
} else { } else {
......
...@@ -209,10 +209,12 @@ define(['jquery', 'underscore', 'gettext', 'js/views/pages/base_page', 'common/j ...@@ -209,10 +209,12 @@ define(['jquery', 'underscore', 'gettext', 'js/views/pages/base_page', 'common/j
buttonPanel = target.closest('.add-xblock-component'), buttonPanel = target.closest('.add-xblock-component'),
listPanel = buttonPanel.prev(), listPanel = buttonPanel.prev(),
scrollOffset = ViewUtils.getScrollOffset(buttonPanel), scrollOffset = ViewUtils.getScrollOffset(buttonPanel),
placeholderElement = this.createPlaceholderElement().appendTo(listPanel), $placeholderEl = $(this.createPlaceholderElement()),
requestData = _.extend(template, { requestData = _.extend(template, {
parent_locator: parentLocator parent_locator: parentLocator
}); }),
placeholderElement;
placeholderElement = $placeholderEl.appendTo(listPanel);
return $.postJSON(this.getURLRoot() + '/', requestData, return $.postJSON(this.getURLRoot() + '/', requestData,
_.bind(this.onNewXBlock, this, placeholderElement, scrollOffset, false)) _.bind(this.onNewXBlock, this, placeholderElement, scrollOffset, false))
.fail(function() { .fail(function() {
...@@ -226,22 +228,19 @@ define(['jquery', 'underscore', 'gettext', 'js/views/pages/base_page', 'common/j ...@@ -226,22 +228,19 @@ define(['jquery', 'underscore', 'gettext', 'js/views/pages/base_page', 'common/j
// and then onNewXBlock will replace it with a rendering of the xblock. Note that // and then onNewXBlock will replace it with a rendering of the xblock. Note that
// for xblocks that can't be replaced inline, the entire parent will be refreshed. // for xblocks that can't be replaced inline, the entire parent will be refreshed.
var self = this, var self = this,
parent = xblockElement.parent(); parentElement = self.findXBlockElement(xblockElement.parent()),
ViewUtils.runOperationShowingMessage(gettext('Duplicating'), scrollOffset = ViewUtils.getScrollOffset(xblockElement),
function() { $placeholderEl = $(self.createPlaceholderElement()),
var scrollOffset = ViewUtils.getScrollOffset(xblockElement), placeholderElement;
placeholderElement = self.createPlaceholderElement().insertAfter(xblockElement),
parentElement = self.findXBlockElement(parent), placeholderElement = $placeholderEl.insertAfter(xblockElement);
requestData = { XBlockUtils.duplicateXBlock(xblockElement, parentElement)
duplicate_source_locator: xblockElement.data('locator'), .done(function(data) {
parent_locator: parentElement.data('locator') self.onNewXBlock(placeholderElement, scrollOffset, true, data);
}; })
return $.postJSON(self.getURLRoot() + '/', requestData, .fail(function() {
_.bind(self.onNewXBlock, self, placeholderElement, scrollOffset, true)) // Remove the placeholder if the update failed
.fail(function() { placeholderElement.remove();
// Remove the placeholder if the update failed
placeholderElement.remove();
});
}); });
}, },
...@@ -319,7 +318,7 @@ define(['jquery', 'underscore', 'gettext', 'js/views/pages/base_page', 'common/j ...@@ -319,7 +318,7 @@ define(['jquery', 'underscore', 'gettext', 'js/views/pages/base_page', 'common/j
updateHtml: function(element, html) { updateHtml: function(element, html) {
// Replace the element with the new HTML content, rather than adding // Replace the element with the new HTML content, rather than adding
// it as child elements. // it as child elements.
this.$el = $(html).replaceAll(element); this.$el = $(html).replaceAll(element); // safe-lint: disable=javascript-jquery-insertion
} }
}); });
temporaryView = new TemporaryXBlockView({ temporaryView = new TemporaryXBlockView({
......
/** /**
* Provides utilities for views to work with xblocks. * Provides utilities for views to work with xblocks.
*/ */
define(['jquery', 'underscore', 'gettext', 'common/js/components/utils/view_utils', 'js/utils/module'], define(['jquery', 'underscore', 'gettext', 'common/js/components/utils/view_utils', 'js/utils/module',
function($, _, gettext, ViewUtils, ModuleUtils) { 'edx-ui-toolkit/js/utils/string-utils'],
var addXBlock, deleteXBlock, createUpdateRequestData, updateXBlockField, VisibilityState, function($, _, gettext, ViewUtils, ModuleUtils, StringUtils) {
getXBlockVisibilityClass, getXBlockListTypeClass, updateXBlockFields; 'use strict';
var addXBlock, duplicateXBlock, deleteXBlock, createUpdateRequestData, updateXBlockField, VisibilityState,
getXBlockVisibilityClass, getXBlockListTypeClass, updateXBlockFields, getXBlockType;
/** /**
* Represents the possible visibility states for an xblock: * Represents the possible visibility states for an xblock:
...@@ -66,6 +68,30 @@ define(['jquery', 'underscore', 'gettext', 'common/js/components/utils/view_util ...@@ -66,6 +68,30 @@ define(['jquery', 'underscore', 'gettext', 'common/js/components/utils/view_util
}; };
/** /**
* Duplicates the specified xblock element in its parent xblock.
* @param {jquery Element} xblockElement The xblock element to be duplicated.
* @param {jquery Element} parentElement Parent element of the xblock element to be duplicated,
* new duplicated xblock would be placed under this xblock.
* @returns {jQuery promise} A promise representing the duplication of the xblock.
*/
duplicateXBlock = function(xblockElement, parentElement) {
return ViewUtils.runOperationShowingMessage(gettext('Duplicating'),
function() {
var duplicationOperation = $.Deferred();
$.postJSON(ModuleUtils.getUpdateUrl(), {
duplicate_source_locator: xblockElement.data('locator'),
parent_locator: parentElement.data('locator')
}, function(data) {
duplicationOperation.resolve(data);
})
.fail(function() {
duplicationOperation.reject();
});
return duplicationOperation.promise();
});
};
/**
* Deletes the specified xblock. * Deletes the specified xblock.
* @param xblockInfo The model for the xblock to be deleted. * @param xblockInfo The model for the xblock to be deleted.
* @param xblockType A string representing the type of the xblock to be deleted. * @param xblockType A string representing the type of the xblock to be deleted.
...@@ -87,9 +113,9 @@ define(['jquery', 'underscore', 'gettext', 'common/js/components/utils/view_util ...@@ -87,9 +113,9 @@ define(['jquery', 'underscore', 'gettext', 'common/js/components/utils/view_util
); );
}, },
messageBody; messageBody;
xblockType = xblockType || 'component'; xblockType = xblockType || 'component'; // eslint-disable-line no-param-reassign
messageBody = interpolate( messageBody = StringUtils.interpolate(
gettext('Deleting this %(xblock_type)s is permanent and cannot be undone.'), gettext('Deleting this {xblock_type} is permanent and cannot be undone.'),
{xblock_type: xblockType}, {xblock_type: xblockType},
true true
); );
...@@ -97,14 +123,14 @@ define(['jquery', 'underscore', 'gettext', 'common/js/components/utils/view_util ...@@ -97,14 +123,14 @@ define(['jquery', 'underscore', 'gettext', 'common/js/components/utils/view_util
if (xblockInfo.get('is_prereq')) { if (xblockInfo.get('is_prereq')) {
messageBody += ' ' + gettext('Any content that has listed this content as a prerequisite will also have access limitations removed.'); // eslint-disable-line max-len messageBody += ' ' + gettext('Any content that has listed this content as a prerequisite will also have access limitations removed.'); // eslint-disable-line max-len
ViewUtils.confirmThenRunOperation( ViewUtils.confirmThenRunOperation(
interpolate( StringUtils.interpolate(
gettext('Delete this %(xblock_type)s (and prerequisite)?'), gettext('Delete this {xblock_type} (and prerequisite)?'),
{xblock_type: xblockType}, {xblock_type: xblockType},
true true
), ),
messageBody, messageBody,
interpolate( StringUtils.interpolate(
gettext('Yes, delete this %(xblock_type)s'), gettext('Yes, delete this {xblock_type}'),
{xblock_type: xblockType}, {xblock_type: xblockType},
true true
), ),
...@@ -112,14 +138,14 @@ define(['jquery', 'underscore', 'gettext', 'common/js/components/utils/view_util ...@@ -112,14 +138,14 @@ define(['jquery', 'underscore', 'gettext', 'common/js/components/utils/view_util
); );
} else { } else {
ViewUtils.confirmThenRunOperation( ViewUtils.confirmThenRunOperation(
interpolate( StringUtils.interpolate(
gettext('Delete this %(xblock_type)s?'), gettext('Delete this {xblock_type}?'),
{xblock_type: xblockType}, {xblock_type: xblockType},
true true
), ),
messageBody, messageBody,
interpolate( StringUtils.interpolate(
gettext('Yes, delete this %(xblock_type)s'), gettext('Yes, delete this {xblock_type}'),
{xblock_type: xblockType}, {xblock_type: xblockType},
true true
), ),
...@@ -217,6 +243,7 @@ define(['jquery', 'underscore', 'gettext', 'common/js/components/utils/view_util ...@@ -217,6 +243,7 @@ define(['jquery', 'underscore', 'gettext', 'common/js/components/utils/view_util
return { return {
'VisibilityState': VisibilityState, 'VisibilityState': VisibilityState,
'addXBlock': addXBlock, 'addXBlock': addXBlock,
duplicateXBlock: duplicateXBlock,
'deleteXBlock': deleteXBlock, 'deleteXBlock': deleteXBlock,
'updateXBlockField': updateXBlockField, 'updateXBlockField': updateXBlockField,
'getXBlockVisibilityClass': getXBlockVisibilityClass, 'getXBlockVisibilityClass': getXBlockVisibilityClass,
......
...@@ -14,8 +14,10 @@ ...@@ -14,8 +14,10 @@
* - edit_display_name - true if the shown xblock's display name should be in inline edit mode * - edit_display_name - true if the shown xblock's display name should be in inline edit mode
*/ */
define(['jquery', 'underscore', 'gettext', 'js/views/baseview', 'common/js/components/utils/view_utils', define(['jquery', 'underscore', 'gettext', 'js/views/baseview', 'common/js/components/utils/view_utils',
'js/views/utils/xblock_utils', 'js/views/xblock_string_field_editor'], 'js/views/utils/xblock_utils', 'js/views/xblock_string_field_editor',
function($, _, gettext, BaseView, ViewUtils, XBlockViewUtils, XBlockStringFieldEditor) { 'edx-ui-toolkit/js/utils/string-utils', 'edx-ui-toolkit/js/utils/html-utils'],
function($, _, gettext, BaseView, ViewUtils, XBlockViewUtils, XBlockStringFieldEditor, StringUtils, HtmlUtils) {
'use strict';
var XBlockOutlineView = BaseView.extend({ var XBlockOutlineView = BaseView.extend({
// takes XBlockInfo as a model // takes XBlockInfo as a model
...@@ -68,7 +70,10 @@ define(['jquery', 'underscore', 'gettext', 'js/views/baseview', 'common/js/compo ...@@ -68,7 +70,10 @@ define(['jquery', 'underscore', 'gettext', 'js/views/baseview', 'common/js/compo
if (this.parentInfo) { if (this.parentInfo) {
this.setElement($(html)); this.setElement($(html));
} else { } else {
this.$el.html(html); HtmlUtils.setHtml(
this.$el,
HtmlUtils.HTML(html)
);
} }
}, },
...@@ -83,7 +88,7 @@ define(['jquery', 'underscore', 'gettext', 'js/views/baseview', 'common/js/compo ...@@ -83,7 +88,7 @@ define(['jquery', 'underscore', 'gettext', 'js/views/baseview', 'common/js/compo
defaultNewChildName = null, defaultNewChildName = null,
isCollapsed = this.shouldRenderChildren() && !this.shouldExpandChildren(); isCollapsed = this.shouldRenderChildren() && !this.shouldExpandChildren();
if (childInfo) { if (childInfo) {
addChildName = interpolate(gettext('New %(component_type)s'), { addChildName = StringUtils.interpolate(gettext('New {component_type}'), {
component_type: childInfo.display_name component_type: childInfo.display_name
}, true); }, true);
defaultNewChildName = childInfo.display_name; defaultNewChildName = childInfo.display_name;
...@@ -126,8 +131,12 @@ define(['jquery', 'underscore', 'gettext', 'js/views/baseview', 'common/js/compo ...@@ -126,8 +131,12 @@ define(['jquery', 'underscore', 'gettext', 'js/views/baseview', 'common/js/compo
return this.$('> .outline-content > ol'); return this.$('> .outline-content > ol');
}, },
addChildView: function(childView) { addChildView: function(childView, xblockElement) {
this.getListElement().append(childView.$el); if (xblockElement) {
childView.$el.insertAfter(xblockElement);
} else {
this.getListElement().append(childView.$el);
}
}, },
addNameEditor: function() { addNameEditor: function() {
...@@ -186,6 +195,7 @@ define(['jquery', 'underscore', 'gettext', 'js/views/baseview', 'common/js/compo ...@@ -186,6 +195,7 @@ define(['jquery', 'underscore', 'gettext', 'js/views/baseview', 'common/js/compo
addButtonActions: function(element) { addButtonActions: function(element) {
var self = this; var self = this;
element.find('.delete-button').click(_.bind(this.handleDeleteEvent, this)); element.find('.delete-button').click(_.bind(this.handleDeleteEvent, this));
element.find('.duplicate-button').click(_.bind(this.handleDuplicateEvent, this));
element.find('.button-new').click(_.bind(this.handleAddEvent, this)); element.find('.button-new').click(_.bind(this.handleAddEvent, this));
}, },
...@@ -281,9 +291,9 @@ define(['jquery', 'underscore', 'gettext', 'js/views/baseview', 'common/js/compo ...@@ -281,9 +291,9 @@ define(['jquery', 'underscore', 'gettext', 'js/views/baseview', 'common/js/compo
handleDeleteEvent: function(event) { handleDeleteEvent: function(event) {
var self = this, var self = this,
parentView = this.parentView; parentView = this.parentView,
xblockType = XBlockViewUtils.getXBlockType(this.model.get('category'), parentView.model, true);
event.preventDefault(); event.preventDefault();
var xblockType = XBlockViewUtils.getXBlockType(this.model.get('category'), parentView.model, true);
XBlockViewUtils.deleteXBlock(this.model, xblockType).done(function() { XBlockViewUtils.deleteXBlock(this.model, xblockType).done(function() {
if (parentView) { if (parentView) {
parentView.onChildDeleted(self, event); parentView.onChildDeleted(self, event);
...@@ -291,12 +301,50 @@ define(['jquery', 'underscore', 'gettext', 'js/views/baseview', 'common/js/compo ...@@ -291,12 +301,50 @@ define(['jquery', 'underscore', 'gettext', 'js/views/baseview', 'common/js/compo
}); });
}, },
/**
* Finds appropriate parent element for an xblock element.
* @param {jquery Element} xblockElement The xblock element to be duplicated.
* @param {String} xblockType The front-end terminology of the xblock category.
* @returns {jquery Element} Appropriate parent element of xblock element.
*/
getParentElement: function(xblockElement, xblockType) {
var xblockMap = {
unit: 'subsection',
subsection: 'section',
section: 'course'
},
parentXblockType = xblockMap[xblockType];
return xblockElement.closest('.outline-' + parentXblockType);
},
/**
* Duplicate event handler.
*/
handleDuplicateEvent: function(event) {
var self = this,
xblockType = XBlockViewUtils.getXBlockType(self.model.get('category'), self.parentView.model, true),
xblockElement = $(event.currentTarget).closest('.outline-item'),
parentElement = self.getParentElement(xblockElement, xblockType);
event.preventDefault();
XBlockViewUtils.duplicateXBlock(xblockElement, parentElement)
.done(function(data) {
if (self.parentView) {
self.parentView.onChildDuplicated(
data.locator,
xblockType,
xblockElement
);
}
});
},
handleAddEvent: function(event) { handleAddEvent: function(event) {
var self = this, var self = this,
target = $(event.currentTarget), $target = $(event.currentTarget),
category = target.data('category'); category = $target.data('category');
event.preventDefault(); event.preventDefault();
XBlockViewUtils.addXBlock(target).done(function(locator) { XBlockViewUtils.addXBlock($target).done(function(locator) {
self.onChildAdded(locator, category, event); self.onChildAdded(locator, category, event);
}); });
} }
......
...@@ -112,6 +112,14 @@ if (is_proctored_exam) { ...@@ -112,6 +112,14 @@ if (is_proctored_exam) {
</a> </a>
</li> </li>
<% } %> <% } %>
<% if (xblockInfo.isDuplicable()) { %>
<li class="action-item action-duplicate">
<a href="#" data-tooltip="<%- gettext('Duplicate') %>" class="duplicate-button action-button">
<span class="icon fa fa-copy" aria-hidden="true"></span>
<span class="sr action-button-text"><%- gettext('Duplicate') %></span>
</a>
</li>
<% } %>
<% if (xblockInfo.isDeletable()) { %> <% if (xblockInfo.isDeletable()) { %>
<li class="action-item action-delete"> <li class="action-item action-delete">
<a href="#" data-tooltip="<%- gettext('Delete') %>" class="delete-button action-button"> <a href="#" data-tooltip="<%- gettext('Delete') %>" class="delete-button action-button">
......
...@@ -38,7 +38,7 @@ ...@@ -38,7 +38,7 @@
<section class="content"> <section class="content">
<article class="content-primary" role="main"> <article class="content-primary" role="main">
<div class="wrapper-dnd"> <div class="wrapper-dnd">
<article class="outline" data-locator="mock-course" data-course-key="slashes:MockCourse"> <article class="outline outline-course" data-locator="mock-course" data-course-key="slashes:MockCourse">
<div class="no-content add-xblock-component"> <div class="no-content add-xblock-component">
<p>You haven't added any content to this course yet. <p>You haven't added any content to this course yet.
<a href="#" class="button button-new" data-category="chapter" data-parent="mock-course" data-default-name="Section" title="Click to add a new section"> <a href="#" class="button button-new" data-category="chapter" data-parent="mock-course" data-default-name="Section" title="Click to add a new section">
......
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