Add duplicate functionality for unit, sub-section and section - TNL-5797, TNL-5798
This commit is contained in:
@@ -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.
|
||||
"""
|
||||
@@ -653,6 +653,10 @@ def _duplicate_item(parent_usage_key, duplicate_source_usage_key, user, display_
|
||||
for field in source_item.fields.values():
|
||||
if field.scope == Scope.settings and field.is_set_on(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:
|
||||
duplicate_metadata['display_name'] = display_name
|
||||
else:
|
||||
@@ -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:
|
||||
dest_module.children = dest_module.children or []
|
||||
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.
|
||||
dest_module.children.append(dupe)
|
||||
store.update_item(dest_module, user.id)
|
||||
@@ -944,8 +948,9 @@ def create_xblock_info(xblock, data=None, metadata=None, include_ancestor_info=F
|
||||
visibility_state = 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.
|
||||
xblock_actions = {'deletable': True, 'draggable': True, 'childAddable': True}
|
||||
# defining the default value 'True' for delete, duplicate, drag and add new child actions
|
||||
# in xblock_actions for each xblock.
|
||||
xblock_actions = {'deletable': True, 'draggable': True, 'childAddable': True, 'duplicable': True}
|
||||
explanatory_message = None
|
||||
|
||||
# is_entrance_exam is inherited metadata.
|
||||
|
||||
@@ -484,7 +484,8 @@ class DuplicateHelper(object):
|
||||
"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.
|
||||
Then verifies that they represent equivalent items (modulo parents and other
|
||||
@@ -523,10 +524,9 @@ class DuplicateHelper(object):
|
||||
"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.
|
||||
duplicated_item.location = original_item.location
|
||||
duplicated_item.display_name = original_item.display_name
|
||||
duplicated_item.parent = original_item.parent
|
||||
|
||||
# Children will also be duplicated, so for the purposes of testing equality, we will set
|
||||
@@ -538,11 +538,26 @@ class DuplicateHelper(object):
|
||||
"Duplicated item differs in number of 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
|
||||
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):
|
||||
"""
|
||||
@@ -571,16 +586,20 @@ class TestDuplicateItem(ItemTest, DuplicateHelper):
|
||||
resp = self.create_xblock(parent_usage_key=self.usage_key, category='chapter')
|
||||
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')
|
||||
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
|
||||
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')
|
||||
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)
|
||||
|
||||
# Create a second sequential just (testing children of children)
|
||||
@@ -591,8 +610,9 @@ class TestDuplicateItem(ItemTest, DuplicateHelper):
|
||||
Tests that a duplicated xblock is identical to the original,
|
||||
except for location and display name.
|
||||
"""
|
||||
self._duplicate_and_verify(self.problem_usage_key, self.seq_usage_key)
|
||||
self._duplicate_and_verify(self.html_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.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.chapter_usage_key, self.usage_key)
|
||||
|
||||
@@ -625,9 +645,10 @@ class TestDuplicateItem(ItemTest, DuplicateHelper):
|
||||
"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.
|
||||
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)
|
||||
|
||||
# Test duplicating something into a location that is not the parent of the original item.
|
||||
@@ -645,12 +666,12 @@ class TestDuplicateItem(ItemTest, DuplicateHelper):
|
||||
return usage_key
|
||||
|
||||
# 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.
|
||||
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.
|
||||
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.
|
||||
verify_name(self.seq_usage_key, self.chapter_usage_key, "Duplicate of sequential")
|
||||
|
||||
@@ -196,6 +196,10 @@ function(Backbone, _, str, ModuleUtils) {
|
||||
return this.isActionRequired('deletable');
|
||||
},
|
||||
|
||||
isDuplicable: function() {
|
||||
return this.isActionRequired('duplicable');
|
||||
},
|
||||
|
||||
isDraggable: function() {
|
||||
return this.isActionRequired('draggable');
|
||||
},
|
||||
|
||||
@@ -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() {
|
||||
it('shows an empty course message initially', function() {
|
||||
createCourseOutlinePage(this, mockEmptyCourseJSON);
|
||||
|
||||
@@ -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,
|
||||
initialState = self.createNewItemViewState(locator),
|
||||
initialState = self.createNewItemViewState(locator, scrollOffset),
|
||||
sectionInfo, sectionView;
|
||||
// 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.
|
||||
@@ -108,7 +127,7 @@ define(['jquery', 'underscore', 'js/views/xblock_outline', 'common/js/components
|
||||
sectionView.initialState = initialState;
|
||||
sectionView.expandedLocators = self.expandedLocators;
|
||||
sectionView.render();
|
||||
self.addChildView(sectionView);
|
||||
self.addChildView(sectionView, xblockElement);
|
||||
sectionView.setViewState(initialState);
|
||||
});
|
||||
} else {
|
||||
|
||||
@@ -209,10 +209,12 @@ define(['jquery', 'underscore', 'gettext', 'js/views/pages/base_page', 'common/j
|
||||
buttonPanel = target.closest('.add-xblock-component'),
|
||||
listPanel = buttonPanel.prev(),
|
||||
scrollOffset = ViewUtils.getScrollOffset(buttonPanel),
|
||||
placeholderElement = this.createPlaceholderElement().appendTo(listPanel),
|
||||
$placeholderEl = $(this.createPlaceholderElement()),
|
||||
requestData = _.extend(template, {
|
||||
parent_locator: parentLocator
|
||||
});
|
||||
}),
|
||||
placeholderElement;
|
||||
placeholderElement = $placeholderEl.appendTo(listPanel);
|
||||
return $.postJSON(this.getURLRoot() + '/', requestData,
|
||||
_.bind(this.onNewXBlock, this, placeholderElement, scrollOffset, false))
|
||||
.fail(function() {
|
||||
@@ -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
|
||||
// for xblocks that can't be replaced inline, the entire parent will be refreshed.
|
||||
var self = this,
|
||||
parent = xblockElement.parent();
|
||||
ViewUtils.runOperationShowingMessage(gettext('Duplicating'),
|
||||
function() {
|
||||
var scrollOffset = ViewUtils.getScrollOffset(xblockElement),
|
||||
placeholderElement = self.createPlaceholderElement().insertAfter(xblockElement),
|
||||
parentElement = self.findXBlockElement(parent),
|
||||
requestData = {
|
||||
duplicate_source_locator: xblockElement.data('locator'),
|
||||
parent_locator: parentElement.data('locator')
|
||||
};
|
||||
return $.postJSON(self.getURLRoot() + '/', requestData,
|
||||
_.bind(self.onNewXBlock, self, placeholderElement, scrollOffset, true))
|
||||
.fail(function() {
|
||||
// Remove the placeholder if the update failed
|
||||
placeholderElement.remove();
|
||||
});
|
||||
parentElement = self.findXBlockElement(xblockElement.parent()),
|
||||
scrollOffset = ViewUtils.getScrollOffset(xblockElement),
|
||||
$placeholderEl = $(self.createPlaceholderElement()),
|
||||
placeholderElement;
|
||||
|
||||
placeholderElement = $placeholderEl.insertAfter(xblockElement);
|
||||
XBlockUtils.duplicateXBlock(xblockElement, parentElement)
|
||||
.done(function(data) {
|
||||
self.onNewXBlock(placeholderElement, scrollOffset, true, data);
|
||||
})
|
||||
.fail(function() {
|
||||
// Remove the placeholder if the update failed
|
||||
placeholderElement.remove();
|
||||
});
|
||||
},
|
||||
|
||||
@@ -319,7 +318,7 @@ define(['jquery', 'underscore', 'gettext', 'js/views/pages/base_page', 'common/j
|
||||
updateHtml: function(element, html) {
|
||||
// Replace the element with the new HTML content, rather than adding
|
||||
// it as child elements.
|
||||
this.$el = $(html).replaceAll(element);
|
||||
this.$el = $(html).replaceAll(element); // safe-lint: disable=javascript-jquery-insertion
|
||||
}
|
||||
});
|
||||
temporaryView = new TemporaryXBlockView({
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
/**
|
||||
* Provides utilities for views to work with xblocks.
|
||||
*/
|
||||
define(['jquery', 'underscore', 'gettext', 'common/js/components/utils/view_utils', 'js/utils/module'],
|
||||
function($, _, gettext, ViewUtils, ModuleUtils) {
|
||||
var addXBlock, deleteXBlock, createUpdateRequestData, updateXBlockField, VisibilityState,
|
||||
getXBlockVisibilityClass, getXBlockListTypeClass, updateXBlockFields;
|
||||
define(['jquery', 'underscore', 'gettext', 'common/js/components/utils/view_utils', 'js/utils/module',
|
||||
'edx-ui-toolkit/js/utils/string-utils'],
|
||||
function($, _, gettext, ViewUtils, ModuleUtils, StringUtils) {
|
||||
'use strict';
|
||||
var addXBlock, duplicateXBlock, deleteXBlock, createUpdateRequestData, updateXBlockField, VisibilityState,
|
||||
getXBlockVisibilityClass, getXBlockListTypeClass, updateXBlockFields, getXBlockType;
|
||||
|
||||
/**
|
||||
* Represents the possible visibility states for an xblock:
|
||||
@@ -65,6 +67,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.
|
||||
* @param xblockInfo The model for the xblock to be deleted.
|
||||
@@ -87,9 +113,9 @@ define(['jquery', 'underscore', 'gettext', 'common/js/components/utils/view_util
|
||||
);
|
||||
},
|
||||
messageBody;
|
||||
xblockType = xblockType || 'component';
|
||||
messageBody = interpolate(
|
||||
gettext('Deleting this %(xblock_type)s is permanent and cannot be undone.'),
|
||||
xblockType = xblockType || 'component'; // eslint-disable-line no-param-reassign
|
||||
messageBody = StringUtils.interpolate(
|
||||
gettext('Deleting this {xblock_type} is permanent and cannot be undone.'),
|
||||
{xblock_type: xblockType},
|
||||
true
|
||||
);
|
||||
@@ -97,14 +123,14 @@ define(['jquery', 'underscore', 'gettext', 'common/js/components/utils/view_util
|
||||
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
|
||||
ViewUtils.confirmThenRunOperation(
|
||||
interpolate(
|
||||
gettext('Delete this %(xblock_type)s (and prerequisite)?'),
|
||||
StringUtils.interpolate(
|
||||
gettext('Delete this {xblock_type} (and prerequisite)?'),
|
||||
{xblock_type: xblockType},
|
||||
true
|
||||
),
|
||||
messageBody,
|
||||
interpolate(
|
||||
gettext('Yes, delete this %(xblock_type)s'),
|
||||
StringUtils.interpolate(
|
||||
gettext('Yes, delete this {xblock_type}'),
|
||||
{xblock_type: xblockType},
|
||||
true
|
||||
),
|
||||
@@ -112,14 +138,14 @@ define(['jquery', 'underscore', 'gettext', 'common/js/components/utils/view_util
|
||||
);
|
||||
} else {
|
||||
ViewUtils.confirmThenRunOperation(
|
||||
interpolate(
|
||||
gettext('Delete this %(xblock_type)s?'),
|
||||
StringUtils.interpolate(
|
||||
gettext('Delete this {xblock_type}?'),
|
||||
{xblock_type: xblockType},
|
||||
true
|
||||
),
|
||||
messageBody,
|
||||
interpolate(
|
||||
gettext('Yes, delete this %(xblock_type)s'),
|
||||
StringUtils.interpolate(
|
||||
gettext('Yes, delete this {xblock_type}'),
|
||||
{xblock_type: xblockType},
|
||||
true
|
||||
),
|
||||
@@ -217,6 +243,7 @@ define(['jquery', 'underscore', 'gettext', 'common/js/components/utils/view_util
|
||||
return {
|
||||
'VisibilityState': VisibilityState,
|
||||
'addXBlock': addXBlock,
|
||||
duplicateXBlock: duplicateXBlock,
|
||||
'deleteXBlock': deleteXBlock,
|
||||
'updateXBlockField': updateXBlockField,
|
||||
'getXBlockVisibilityClass': getXBlockVisibilityClass,
|
||||
|
||||
@@ -14,8 +14,10 @@
|
||||
* - 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',
|
||||
'js/views/utils/xblock_utils', 'js/views/xblock_string_field_editor'],
|
||||
function($, _, gettext, BaseView, ViewUtils, XBlockViewUtils, XBlockStringFieldEditor) {
|
||||
'js/views/utils/xblock_utils', 'js/views/xblock_string_field_editor',
|
||||
'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({
|
||||
// takes XBlockInfo as a model
|
||||
|
||||
@@ -68,7 +70,10 @@ define(['jquery', 'underscore', 'gettext', 'js/views/baseview', 'common/js/compo
|
||||
if (this.parentInfo) {
|
||||
this.setElement($(html));
|
||||
} 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
|
||||
defaultNewChildName = null,
|
||||
isCollapsed = this.shouldRenderChildren() && !this.shouldExpandChildren();
|
||||
if (childInfo) {
|
||||
addChildName = interpolate(gettext('New %(component_type)s'), {
|
||||
addChildName = StringUtils.interpolate(gettext('New {component_type}'), {
|
||||
component_type: childInfo.display_name
|
||||
}, true);
|
||||
defaultNewChildName = childInfo.display_name;
|
||||
@@ -126,8 +131,12 @@ define(['jquery', 'underscore', 'gettext', 'js/views/baseview', 'common/js/compo
|
||||
return this.$('> .outline-content > ol');
|
||||
},
|
||||
|
||||
addChildView: function(childView) {
|
||||
this.getListElement().append(childView.$el);
|
||||
addChildView: function(childView, xblockElement) {
|
||||
if (xblockElement) {
|
||||
childView.$el.insertAfter(xblockElement);
|
||||
} else {
|
||||
this.getListElement().append(childView.$el);
|
||||
}
|
||||
},
|
||||
|
||||
addNameEditor: function() {
|
||||
@@ -186,6 +195,7 @@ define(['jquery', 'underscore', 'gettext', 'js/views/baseview', 'common/js/compo
|
||||
addButtonActions: function(element) {
|
||||
var self = 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));
|
||||
},
|
||||
|
||||
@@ -281,9 +291,9 @@ define(['jquery', 'underscore', 'gettext', 'js/views/baseview', 'common/js/compo
|
||||
|
||||
handleDeleteEvent: function(event) {
|
||||
var self = this,
|
||||
parentView = this.parentView;
|
||||
parentView = this.parentView,
|
||||
xblockType = XBlockViewUtils.getXBlockType(this.model.get('category'), parentView.model, true);
|
||||
event.preventDefault();
|
||||
var xblockType = XBlockViewUtils.getXBlockType(this.model.get('category'), parentView.model, true);
|
||||
XBlockViewUtils.deleteXBlock(this.model, xblockType).done(function() {
|
||||
if (parentView) {
|
||||
parentView.onChildDeleted(self, event);
|
||||
@@ -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) {
|
||||
var self = this,
|
||||
target = $(event.currentTarget),
|
||||
category = target.data('category');
|
||||
$target = $(event.currentTarget),
|
||||
category = $target.data('category');
|
||||
event.preventDefault();
|
||||
XBlockViewUtils.addXBlock(target).done(function(locator) {
|
||||
XBlockViewUtils.addXBlock($target).done(function(locator) {
|
||||
self.onChildAdded(locator, category, event);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -112,6 +112,14 @@ if (is_proctored_exam) {
|
||||
</a>
|
||||
</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()) { %>
|
||||
<li class="action-item action-delete">
|
||||
<a href="#" data-tooltip="<%- gettext('Delete') %>" class="delete-button action-button">
|
||||
|
||||
@@ -38,7 +38,7 @@
|
||||
<section class="content">
|
||||
<article class="content-primary" role="main">
|
||||
<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">
|
||||
<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">
|
||||
|
||||
Reference in New Issue
Block a user