STUD-1911, STUD-1932: Publish sections, subsections and units from course outline.
This commit is contained in:
379
cms/static/js/views/modals/course_outline_modals.js
Normal file
379
cms/static/js/views/modals/course_outline_modals.js
Normal file
@@ -0,0 +1,379 @@
|
||||
/**
|
||||
* The CourseOutlineXBlockModal is a Backbone view that shows an editor in a modal window.
|
||||
* It has nested views: for release date, due date and grading format.
|
||||
* It is invoked using the editXBlock method and uses xblock_info as a model,
|
||||
* and upon save parent invokes refresh function that fetches updated model and
|
||||
* re-renders edited course outline.
|
||||
*/
|
||||
define(['jquery', 'backbone', 'underscore', 'gettext', 'js/views/baseview',
|
||||
'js/views/modals/base_modal', 'date', 'js/views/utils/xblock_utils',
|
||||
'js/utils/date_utils'
|
||||
], function(
|
||||
$, Backbone, _, gettext, BaseView, BaseModal, date, XBlockViewUtils, DateUtils
|
||||
) {
|
||||
'use strict';
|
||||
var CourseOutlineXBlockModal, SettingsXBlockModal, PublishXBlockModal, AbstractEditor, BaseDateEditor,
|
||||
ReleaseDateEditor, DueDateEditor, GradingEditor, PublishEditor, StaffLockEditor;
|
||||
|
||||
CourseOutlineXBlockModal = BaseModal.extend({
|
||||
events : {
|
||||
'click .action-save': 'save'
|
||||
},
|
||||
|
||||
options: $.extend({}, BaseModal.prototype.options, {
|
||||
modalName: 'course-outline',
|
||||
modalType: 'edit-settings',
|
||||
addSaveButton: true,
|
||||
modalSize: 'med',
|
||||
viewSpecificClasses: 'confirm',
|
||||
editors: []
|
||||
}),
|
||||
|
||||
initialize: function() {
|
||||
BaseModal.prototype.initialize.call(this);
|
||||
this.events = $.extend({}, BaseModal.prototype.events, this.events);
|
||||
this.template = this.loadTemplate('course-outline-modal');
|
||||
this.options.title = this.getTitle();
|
||||
},
|
||||
|
||||
afterRender: function () {
|
||||
BaseModal.prototype.afterRender.call(this);
|
||||
this.initializeEditors();
|
||||
},
|
||||
|
||||
initializeEditors: function () {
|
||||
this.options.editors = _.map(this.options.editors, function (Editor) {
|
||||
return new Editor({
|
||||
parentElement: this.$('.modal-section'),
|
||||
model: this.model,
|
||||
xblockType: this.options.xblockType
|
||||
});
|
||||
}, this);
|
||||
},
|
||||
|
||||
getTitle: function () {
|
||||
return '';
|
||||
},
|
||||
|
||||
getIntroductionMessage: function () {
|
||||
return '';
|
||||
},
|
||||
|
||||
getContentHtml: function() {
|
||||
return this.template(this.getContext());
|
||||
},
|
||||
|
||||
save: function(event) {
|
||||
event.preventDefault();
|
||||
var requestData = this.getRequestData();
|
||||
if (!_.isEqual(requestData, { metadata: {} })) {
|
||||
XBlockViewUtils.updateXBlockFields(this.model, requestData, {
|
||||
success: this.options.onSave
|
||||
});
|
||||
}
|
||||
this.hide();
|
||||
},
|
||||
|
||||
/**
|
||||
* Return context for the modal.
|
||||
* @return {Object}
|
||||
*/
|
||||
getContext: function () {
|
||||
return $.extend({
|
||||
xblockInfo: this.model,
|
||||
introductionMessage: this.getIntroductionMessage()
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Return request data.
|
||||
* @return {Object}
|
||||
*/
|
||||
getRequestData: function () {
|
||||
var requestData = _.map(this.options.editors, function (editor) {
|
||||
return editor.getRequestData();
|
||||
});
|
||||
|
||||
return $.extend.apply(this, [true, {}].concat(requestData));
|
||||
}
|
||||
});
|
||||
|
||||
SettingsXBlockModal = CourseOutlineXBlockModal.extend({
|
||||
getTitle: function () {
|
||||
return interpolate(
|
||||
gettext('%(display_name)s Settings'),
|
||||
{ display_name: this.model.get('display_name') }, true
|
||||
);
|
||||
},
|
||||
|
||||
getIntroductionMessage: function () {
|
||||
return interpolate(
|
||||
gettext('Change the settings for %(display_name)s'),
|
||||
{ display_name: this.model.get('display_name') }, true
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
PublishXBlockModal = CourseOutlineXBlockModal.extend({
|
||||
events : {
|
||||
'click .action-publish': 'save'
|
||||
},
|
||||
|
||||
initialize: function() {
|
||||
CourseOutlineXBlockModal.prototype.initialize.call(this);
|
||||
if (this.options.xblockType) {
|
||||
this.options.modalName = 'bulkpublish-' + this.options.xblockType;
|
||||
}
|
||||
},
|
||||
|
||||
getTitle: function () {
|
||||
return interpolate(
|
||||
gettext('Publish %(display_name)s'),
|
||||
{ display_name: this.model.get('display_name') }, true
|
||||
);
|
||||
},
|
||||
|
||||
getIntroductionMessage: function () {
|
||||
return interpolate(
|
||||
gettext('Publish all unpublished changes for this %(item)s?'),
|
||||
{ item: this.options.xblockType }, true
|
||||
);
|
||||
},
|
||||
|
||||
addActionButtons: function() {
|
||||
this.addActionButton('publish', gettext('Publish'), true);
|
||||
this.addActionButton('cancel', gettext('Cancel'));
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
AbstractEditor = BaseView.extend({
|
||||
tagName: 'section',
|
||||
templateName: null,
|
||||
initialize: function() {
|
||||
this.template = this.loadTemplate(this.templateName);
|
||||
this.parentElement = this.options.parentElement;
|
||||
this.render();
|
||||
},
|
||||
|
||||
render: function () {
|
||||
var html = this.template($.extend({}, {
|
||||
xblockInfo: this.model,
|
||||
xblockType: this.options.xblockType
|
||||
}, this.getContext()));
|
||||
|
||||
this.$el.html(html);
|
||||
this.parentElement.append(this.$el);
|
||||
},
|
||||
|
||||
getContext: function () {
|
||||
return {};
|
||||
},
|
||||
|
||||
getRequestData: function () {
|
||||
return {};
|
||||
}
|
||||
});
|
||||
|
||||
BaseDateEditor = AbstractEditor.extend({
|
||||
// Attribute name in the model, should be defined in children classes.
|
||||
fieldName: null,
|
||||
|
||||
events : {
|
||||
'click .clear-date': 'clearValue'
|
||||
},
|
||||
|
||||
afterRender: function () {
|
||||
AbstractEditor.prototype.afterRender.call(this);
|
||||
this.$('input.date').datepicker({'dateFormat': 'm/d/yy'});
|
||||
this.$('input.time').timepicker({
|
||||
'timeFormat' : 'H:i',
|
||||
'forceRoundTime': true
|
||||
});
|
||||
if (this.model.get(this.fieldName)) {
|
||||
DateUtils.setDate(
|
||||
this.$('input.date'), this.$('input.time'),
|
||||
this.model.get(this.fieldName)
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
DueDateEditor = BaseDateEditor.extend({
|
||||
fieldName: 'due',
|
||||
templateName: 'due-date-editor',
|
||||
className: 'modal-section-content has-actions due-date-input grading-due-date',
|
||||
|
||||
getValue: function () {
|
||||
return DateUtils.getDate(this.$('#due_date'), this.$('#due_time'));
|
||||
},
|
||||
|
||||
clearValue: function (event) {
|
||||
event.preventDefault();
|
||||
this.$('#due_time, #due_date').val('');
|
||||
},
|
||||
|
||||
getRequestData: function () {
|
||||
return {
|
||||
metadata: {
|
||||
'due': this.getValue()
|
||||
}
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
ReleaseDateEditor = BaseDateEditor.extend({
|
||||
fieldName: 'start',
|
||||
templateName: 'release-date-editor',
|
||||
className: 'edit-settings-release scheduled-date-input',
|
||||
startingReleaseDate: null,
|
||||
|
||||
afterRender: function () {
|
||||
BaseDateEditor.prototype.afterRender.call(this);
|
||||
// Store the starting date and time so that we can determine if the user
|
||||
// actually changed it when "Save" is pressed.
|
||||
this.startingReleaseDate = this.getValue();
|
||||
},
|
||||
|
||||
getValue: function () {
|
||||
return DateUtils.getDate(this.$('#start_date'), this.$('#start_time'));
|
||||
},
|
||||
|
||||
clearValue: function (event) {
|
||||
event.preventDefault();
|
||||
this.$('#start_time, #start_date').val('');
|
||||
},
|
||||
|
||||
getRequestData: function () {
|
||||
var newReleaseDate = this.getValue();
|
||||
if (JSON.stringify(newReleaseDate) === JSON.stringify(this.startingReleaseDate)) {
|
||||
return {};
|
||||
}
|
||||
return {
|
||||
metadata: {
|
||||
'start': newReleaseDate
|
||||
}
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
GradingEditor = AbstractEditor.extend({
|
||||
templateName: 'grading-editor',
|
||||
className: 'edit-settings-grading',
|
||||
|
||||
afterRender: function () {
|
||||
AbstractEditor.prototype.afterRender.call(this);
|
||||
this.setValue(this.model.get('format'));
|
||||
},
|
||||
|
||||
setValue: function (value) {
|
||||
this.$('#grading_type').val(value);
|
||||
},
|
||||
|
||||
getValue: function () {
|
||||
return this.$('#grading_type').val();
|
||||
},
|
||||
|
||||
getRequestData: function () {
|
||||
return {
|
||||
'graderType': this.getValue()
|
||||
};
|
||||
},
|
||||
|
||||
getContext: function () {
|
||||
return {
|
||||
graderTypes: JSON.parse(this.model.get('course_graders'))
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
PublishEditor = AbstractEditor.extend({
|
||||
templateName: 'publish-editor',
|
||||
className: 'edit-settings-publish',
|
||||
getRequestData: function () {
|
||||
return {
|
||||
publish: 'make_public'
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
StaffLockEditor = AbstractEditor.extend({
|
||||
templateName: 'staff-lock-editor',
|
||||
className: 'edit-staff-lock',
|
||||
isModelLocked: function() {
|
||||
return this.model.get('has_explicit_staff_lock');
|
||||
},
|
||||
|
||||
isAncestorLocked: function() {
|
||||
return this.model.get('ancestor_has_staff_lock');
|
||||
},
|
||||
|
||||
afterRender: function () {
|
||||
AbstractEditor.prototype.afterRender.call(this);
|
||||
this.setLock(this.isModelLocked());
|
||||
},
|
||||
|
||||
setLock: function(value) {
|
||||
this.$('#staff_lock').prop('checked', value);
|
||||
},
|
||||
|
||||
isLocked: function() {
|
||||
return this.$('#staff_lock').is(':checked');
|
||||
},
|
||||
|
||||
hasChanges: function() {
|
||||
return this.isModelLocked() != this.isLocked();
|
||||
},
|
||||
|
||||
getRequestData: function() {
|
||||
return this.hasChanges() ? {
|
||||
publish: 'republish',
|
||||
metadata: {
|
||||
visible_to_staff_only: this.isLocked() ? true : null
|
||||
}
|
||||
} : {};
|
||||
},
|
||||
|
||||
getContext: function () {
|
||||
return {
|
||||
hasExplicitStaffLock: this.isModelLocked(),
|
||||
ancestorLocked: this.isAncestorLocked()
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
getModal: function (type, xblockInfo, options) {
|
||||
if (type === 'edit') {
|
||||
return this.getEditModal(xblockInfo, options);
|
||||
} else if (type === 'publish') {
|
||||
return this.getPublishModal(xblockInfo, options);
|
||||
}
|
||||
},
|
||||
|
||||
getEditModal: function (xblockInfo, options) {
|
||||
var editors = [];
|
||||
|
||||
if (xblockInfo.isChapter()) {
|
||||
editors = [ReleaseDateEditor, StaffLockEditor];
|
||||
} else if (xblockInfo.isSequential()) {
|
||||
editors = [ReleaseDateEditor, GradingEditor, DueDateEditor, StaffLockEditor];
|
||||
} else if (xblockInfo.isVertical()) {
|
||||
editors = [StaffLockEditor];
|
||||
}
|
||||
|
||||
return new SettingsXBlockModal($.extend({
|
||||
editors: editors,
|
||||
model: xblockInfo
|
||||
}, options));
|
||||
},
|
||||
|
||||
getPublishModal: function (xblockInfo, options) {
|
||||
return new PublishXBlockModal($.extend({
|
||||
editors: [PublishEditor],
|
||||
model: xblockInfo
|
||||
}, options));
|
||||
}
|
||||
};
|
||||
});
|
||||
@@ -1,299 +0,0 @@
|
||||
/**
|
||||
* The EditSectionXBlockModal is a Backbone view that shows an editor in a modal window.
|
||||
* It has nested views: for release date, due date, grading format, and staff lock.
|
||||
* It is invoked using the editXBlock method and uses xblock_info as a model,
|
||||
* and upon save parent invokes refresh function that fetches updated model and
|
||||
* re-renders edited course outline.
|
||||
*/
|
||||
define(['jquery', 'backbone', 'underscore', 'gettext', 'js/views/modals/base_modal',
|
||||
'date', 'js/views/utils/xblock_utils', 'js/utils/date_utils', 'js/views/utils/view_utils'
|
||||
],
|
||||
function(
|
||||
$, Backbone, _, gettext, BaseModal, date, XBlockViewUtils, DateUtils, ViewUtils
|
||||
) {
|
||||
'use strict';
|
||||
var EditSectionXBlockModal, BaseDateView, ReleaseDateView, DueDateView,
|
||||
GradingView, StaffLockView;
|
||||
|
||||
EditSectionXBlockModal = BaseModal.extend({
|
||||
events : {
|
||||
'click .action-save': 'save',
|
||||
'click .action-modes a': 'changeMode'
|
||||
},
|
||||
|
||||
options: $.extend({}, BaseModal.prototype.options, {
|
||||
modalName: 'edit-outline-item',
|
||||
modalType: 'edit-settings',
|
||||
addSaveButton: true,
|
||||
modalSize: 'med',
|
||||
viewSpecificClasses: 'confirm'
|
||||
}),
|
||||
|
||||
initialize: function() {
|
||||
BaseModal.prototype.initialize.call(this);
|
||||
this.events = _.extend({}, BaseModal.prototype.events, this.events);
|
||||
this.template = this.loadTemplate('edit-outline-item-modal');
|
||||
this.options.title = this.getTitle();
|
||||
this.initializeComponents();
|
||||
},
|
||||
|
||||
getTitle: function () {
|
||||
return _.template(
|
||||
gettext('<%= sectionName %> Settings'),
|
||||
{ sectionName: this.model.get('display_name') }
|
||||
);
|
||||
},
|
||||
|
||||
getContentHtml: function() {
|
||||
return this.template(this.getContext());
|
||||
},
|
||||
|
||||
afterRender: function() {
|
||||
BaseModal.prototype.render.apply(this, arguments);
|
||||
this.invokeComponentMethod('afterRender');
|
||||
},
|
||||
|
||||
save: function(event) {
|
||||
event.preventDefault();
|
||||
var requestData = _.extend({}, this.getRequestData(), {
|
||||
metadata: this.getMetadata()
|
||||
});
|
||||
// Only update if something changed to prevent items from erroneously entering draft state
|
||||
if (!_.isEqual(requestData, { metadata: {} })) {
|
||||
XBlockViewUtils.updateXBlockFields(this.model, requestData, {
|
||||
success: this.options.onSave
|
||||
});
|
||||
}
|
||||
this.hide();
|
||||
},
|
||||
|
||||
/**
|
||||
* Call the method on each value in the list. If the element of the
|
||||
* list doesn't have such a method it will be skipped.
|
||||
* @param {String} methodName The method name needs to be called.
|
||||
* @return {Object}
|
||||
*/
|
||||
invokeComponentMethod: function (methodName) {
|
||||
var values = _.map(this.components, function (component) {
|
||||
if (_.isFunction(component[methodName])) {
|
||||
return component[methodName].call(component);
|
||||
}
|
||||
});
|
||||
|
||||
return _.extend.apply(this, [{}].concat(values));
|
||||
},
|
||||
|
||||
/**
|
||||
* Return context for the modal.
|
||||
* @return {Object}
|
||||
*/
|
||||
getContext: function () {
|
||||
return _.extend({
|
||||
xblockInfo: this.model,
|
||||
xblockType: XBlockViewUtils.getXBlockType(this.model.get('category'), this.parentInfo, true)
|
||||
}, this.invokeComponentMethod('getContext'));
|
||||
},
|
||||
|
||||
/**
|
||||
* Return request data.
|
||||
* @return {Object}
|
||||
*/
|
||||
getRequestData: function () {
|
||||
return this.invokeComponentMethod('getRequestData');
|
||||
},
|
||||
|
||||
/**
|
||||
* Return metadata for the XBlock.
|
||||
* @return {Object}
|
||||
*/
|
||||
getMetadata: function () {
|
||||
return this.invokeComponentMethod('getMetadata');
|
||||
},
|
||||
|
||||
/**
|
||||
* Initialize internal components.
|
||||
*/
|
||||
initializeComponents: function () {
|
||||
this.components = [];
|
||||
this.components.push(
|
||||
new StaffLockView({
|
||||
selector: '.edit-staff-lock',
|
||||
parentView: this,
|
||||
model: this.model
|
||||
})
|
||||
);
|
||||
|
||||
if (this.model.isChapter() || this.model.isSequential()) {
|
||||
this.components.push(
|
||||
new ReleaseDateView({
|
||||
selector: '.scheduled-date-input',
|
||||
parentView: this,
|
||||
model: this.model
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
if (this.model.isSequential()) {
|
||||
this.components.push(
|
||||
new DueDateView({
|
||||
selector: '.due-date-input',
|
||||
parentView: this,
|
||||
model: this.model
|
||||
}),
|
||||
new GradingView({
|
||||
selector: '.edit-settings-grading',
|
||||
parentView: this,
|
||||
model: this.model
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
BaseDateView = Backbone.View.extend({
|
||||
// Attribute name in the model, should be defined in children classes.
|
||||
fieldName: null,
|
||||
|
||||
events : {
|
||||
'click .clear-date': 'clearValue'
|
||||
},
|
||||
|
||||
afterRender: function () {
|
||||
this.setElement(this.options.parentView.$(this.options.selector).get(0));
|
||||
this.$('input.date').datepicker({'dateFormat': 'm/d/yy'});
|
||||
this.$('input.time').timepicker({
|
||||
'timeFormat' : 'H:i',
|
||||
'forceRoundTime': true
|
||||
});
|
||||
if (this.model.get(this.fieldName)) {
|
||||
DateUtils.setDate(
|
||||
this.$('input.date'), this.$('input.time'),
|
||||
this.model.get(this.fieldName)
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
DueDateView = BaseDateView.extend({
|
||||
fieldName: 'due',
|
||||
|
||||
getValue: function () {
|
||||
return DateUtils.getDate(this.$('#due_date'), this.$('#due_time'));
|
||||
},
|
||||
|
||||
clearValue: function (event) {
|
||||
event.preventDefault();
|
||||
this.$('#due_time, #due_date').val('');
|
||||
},
|
||||
|
||||
getMetadata: function () {
|
||||
return {
|
||||
'due': this.getValue()
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
ReleaseDateView = BaseDateView.extend({
|
||||
fieldName: 'start',
|
||||
startingReleaseDate: null,
|
||||
|
||||
afterRender: function () {
|
||||
BaseDateView.prototype.afterRender.call(this);
|
||||
// Store the starting date and time so that we can determine if the user
|
||||
// actually changed it when "Save" is pressed.
|
||||
this.startingReleaseDate = this.getValue();
|
||||
},
|
||||
|
||||
getValue: function () {
|
||||
return DateUtils.getDate(this.$('#start_date'), this.$('#start_time'));
|
||||
},
|
||||
|
||||
clearValue: function (event) {
|
||||
event.preventDefault();
|
||||
this.$('#start_time, #start_date').val('');
|
||||
},
|
||||
|
||||
getMetadata: function () {
|
||||
var newReleaseDate = this.getValue();
|
||||
if (JSON.stringify(newReleaseDate) === JSON.stringify(this.startingReleaseDate)) {
|
||||
return {};
|
||||
}
|
||||
return {
|
||||
'start': newReleaseDate
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
GradingView = Backbone.View.extend({
|
||||
afterRender: function () {
|
||||
this.setElement(this.options.parentView.$(this.options.selector).get(0));
|
||||
this.setValue(this.model.get('format'));
|
||||
},
|
||||
|
||||
setValue: function (value) {
|
||||
this.$('#grading_type').val(value);
|
||||
},
|
||||
|
||||
getValue: function () {
|
||||
return this.$('#grading_type').val();
|
||||
},
|
||||
|
||||
getRequestData: function () {
|
||||
return {
|
||||
'graderType': this.getValue()
|
||||
};
|
||||
},
|
||||
|
||||
getContext: function () {
|
||||
return {
|
||||
graderTypes: JSON.parse(this.model.get('course_graders'))
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
StaffLockView = Backbone.View.extend({
|
||||
isModelLocked: function() {
|
||||
return this.model.get('has_explicit_staff_lock');
|
||||
},
|
||||
|
||||
isAncestorLocked: function() {
|
||||
return this.model.get('ancestor_has_staff_lock');
|
||||
},
|
||||
|
||||
afterRender: function () {
|
||||
this.setElement(this.options.parentView.$(this.options.selector).get(0));
|
||||
this.setLock(this.isModelLocked());
|
||||
},
|
||||
|
||||
setLock: function(value) {
|
||||
this.$('#staff_lock').prop('checked', value);
|
||||
},
|
||||
|
||||
isLocked: function() {
|
||||
return this.$('#staff_lock').is(':checked');
|
||||
},
|
||||
|
||||
hasChanges: function() {
|
||||
return this.isModelLocked() != this.isLocked();
|
||||
},
|
||||
|
||||
getRequestData: function() {
|
||||
return this.hasChanges() ? { publish: 'republish' } : {};
|
||||
},
|
||||
|
||||
getMetadata: function() {
|
||||
// Setting visible_to_staff_only to null when disabled will delete the field from this
|
||||
// xblock, allowing it to inherit the value of its ancestors.
|
||||
return this.hasChanges() ? { visible_to_staff_only: this.isLocked() ? true : null } : {};
|
||||
},
|
||||
|
||||
getContext: function () {
|
||||
return {
|
||||
hasExplicitStaffLock: this.isModelLocked(),
|
||||
ancestorLocked: this.isAncestorLocked()
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return EditSectionXBlockModal;
|
||||
});
|
||||
Reference in New Issue
Block a user