STUD-1911, STUD-1932: Publish sections, subsections and units from course outline.

This commit is contained in:
polesye
2014-08-05 16:37:02 +03:00
committed by Tim Babych
parent 06842be865
commit 732bcbdeb6
26 changed files with 1942 additions and 1034 deletions

View File

@@ -59,7 +59,7 @@ function(Backbone, _, str, ModuleUtils) {
*/
"visibility_state": null,
/**
* True iff the release date of the xblock is in the past.
* True if the release date of the xblock is in the past.
*/
'released_to_students': null,
/**
@@ -153,6 +153,10 @@ function(Backbone, _, str, ModuleUtils) {
return childInfo && childInfo.children.length > 0;
},
isPublishable: function(){
return !this.get('published') || this.get('has_changes');
},
/**
* Return a list of convenience methods to check affiliation to the category.
* @return {Array}

View File

@@ -5,14 +5,14 @@ define(["jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/view_helpers"
describe("CourseOutlinePage", function() {
var createCourseOutlinePage, displayNameInput, model, outlinePage, requests,
getItemsOfType, getItemHeaders, verifyItemsExpanded, expandItemsAndVerifyState, collapseItemsAndVerifyState,
createMockCourseJSON, createMockSectionJSON, createMockSubsectionJSON,
mockCourseJSON, mockEmptyCourseJSON, mockSingleSectionCourseJSON,
createMockCourseJSON, createMockSectionJSON, createMockSubsectionJSON, verifyTypePublishable,
mockCourseJSON, mockEmptyCourseJSON, mockSingleSectionCourseJSON, createMockVerticalJSON,
mockOutlinePage = readFixtures('mock/mock-course-outline-page.underscore');
createMockCourseJSON = function(id, displayName, children) {
return {
id: id,
display_name: displayName,
createMockCourseJSON = function(options, children) {
return $.extend(true, {}, {
id: 'mock-course',
display_name: 'Mock Course',
category: 'course',
studio_url: '/course/slashes:MockCourse',
is_container: true,
@@ -22,17 +22,18 @@ define(["jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/view_helpers"
edited_by: 'MockUser',
has_explicit_staff_lock: false,
child_info: {
display_name: 'Section',
category: 'chapter',
children: children
display_name: 'Section',
children: []
}
};
}, options, {child_info: {children: children}});
};
createMockSectionJSON = function(id, displayName, children) {
return {
id: id,
createMockSectionJSON = function(options, children) {
return $.extend(true, {}, {
id: 'mock-section',
display_name: 'Mock Section',
category: 'chapter',
display_name: displayName,
studio_url: '/course/slashes:MockCourse',
is_container: true,
has_changes: false,
@@ -43,14 +44,15 @@ define(["jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/view_helpers"
child_info: {
category: 'sequential',
display_name: 'Subsection',
children: children
children: []
}
};
}, options, {child_info: {children: children}});
};
createMockSubsectionJSON = function(id, displayName, children) {
return {
id: id,
display_name: displayName,
createMockSubsectionJSON = function(options, children) {
return $.extend(true, {}, {
id: 'mock-subsection',
display_name: 'Mock Subsection',
category: 'sequential',
studio_url: '/course/slashes:MockCourse',
is_container: true,
@@ -63,9 +65,24 @@ define(["jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/view_helpers"
child_info: {
category: 'vertical',
display_name: 'Unit',
children: children
children: []
}
};
}, options, {child_info: {children: children}});
};
createMockVerticalJSON = function(options) {
return $.extend(true, {}, {
id: 'mock-unit',
display_name: 'Mock Unit',
category: 'vertical',
studio_url: '/container/mock-unit',
is_container: true,
has_changes: false,
published: true,
visibility_state: 'unscheduled',
edited_on: 'Jul 02, 2014 at 20:56 UTC',
edited_by: 'MockUser'
}, options);
};
getItemsOfType = function(type) {
@@ -108,40 +125,100 @@ define(["jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/view_helpers"
return outlinePage;
};
verifyTypePublishable = function (type, getMockCourseJSON) {
var createCourseOutlinePageAndShowUnit, verifyPublishButton;
createCourseOutlinePageAndShowUnit = function (test, courseJSON, createOnly) {
outlinePage = createCourseOutlinePage.apply(this, arguments);
if (type === 'unit') {
expandItemsAndVerifyState('subsection');
}
};
verifyPublishButton = function (test, courseJSON, createOnly) {
createCourseOutlinePageAndShowUnit.apply(this, arguments);
expect(getItemHeaders(type).find('.publish-button')).toExist();
};
it('can be published', function() {
var mockCourseJSON = getMockCourseJSON({
has_changes: true
});
createCourseOutlinePageAndShowUnit(this, mockCourseJSON);
getItemHeaders(type).find('.publish-button').click();
$(".wrapper-modal-window .action-publish").click();
create_sinon.expectJsonRequest(requests, 'POST', '/xblock/mock-' + type, {
publish : 'make_public'
});
expect(requests[0].requestHeaders['X-HTTP-Method-Override']).toBe('PATCH');
create_sinon.respondWithJson(requests, {});
create_sinon.expectJsonRequest(requests, 'GET', '/xblock/outline/mock-section');
});
it('should show publish button if it is not published and not changed', function() {
var mockCourseJSON = getMockCourseJSON({
has_changes: false,
published: false
});
verifyPublishButton(this, mockCourseJSON);
});
it('should show publish button if it is published and changed', function() {
var mockCourseJSON = getMockCourseJSON({
has_changes: true,
published: true
});
verifyPublishButton(this, mockCourseJSON);
});
it('should show publish button if it is not published, but changed', function() {
var mockCourseJSON = getMockCourseJSON({
has_changes: true,
published: false
});
verifyPublishButton(this, mockCourseJSON);
});
it('should hide publish button if it is not changed, but published', function() {
var mockCourseJSON = getMockCourseJSON({
has_changes: false,
published: true
});
createCourseOutlinePageAndShowUnit(this, mockCourseJSON);
expect(getItemHeaders(type).find('.publish-button')).not.toExist();
});
};
beforeEach(function () {
view_helpers.installMockAnalytics();
view_helpers.installViewTemplates();
view_helpers.installTemplate('course-outline');
view_helpers.installTemplate('xblock-string-field-editor');
view_helpers.installTemplate('modal-button');
view_helpers.installTemplate('basic-modal');
view_helpers.installTemplate('edit-outline-item-modal');
view_helpers.installTemplates([
'course-outline', 'xblock-string-field-editor', 'modal-button',
'basic-modal', 'course-outline-modal', 'release-date-editor',
'due-date-editor', 'grading-editor', 'publish-editor',
'staff-lock-editor'
]);
appendSetFixtures(mockOutlinePage);
mockCourseJSON = createMockCourseJSON('mock-course', 'Mock Course', [
createMockSectionJSON('mock-section', 'Mock Section', [
createMockSubsectionJSON('mock-subsection', 'Mock Subsection', [{
id: 'mock-unit',
display_name: 'Mock Unit',
category: 'vertical',
studio_url: '/container/mock-unit',
is_container: true,
has_changes: false,
published: true,
visibility_state: 'unscheduled',
edited_on: 'Jul 02, 2014 at 20:56 UTC',
edited_by: 'MockUser'
}])
mockCourseJSON = createMockCourseJSON({}, [
createMockSectionJSON({}, [
createMockSubsectionJSON({}, [
createMockVerticalJSON()
])
])
]);
mockEmptyCourseJSON = createMockCourseJSON('mock-course', 'Mock Course', []);
mockSingleSectionCourseJSON = createMockCourseJSON('mock-course', 'Mock Course', [
createMockSectionJSON('mock-section', 'Mock Section', [])
mockEmptyCourseJSON = createMockCourseJSON();
mockSingleSectionCourseJSON = createMockCourseJSON({}, [
createMockSectionJSON()
]);
});
afterEach(function () {
view_helpers.removeMockAnalytics();
edit_helpers.cancelModalIfShowing();
// Clean up after the $.datepicker
$("#start_date").datepicker( "destroy" );
$("#due_date").datepicker( "destroy" );
$('.ui-datepicker').remove();
});
describe('Initial display', function() {
@@ -201,7 +278,7 @@ define(["jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/view_helpers"
// Expect the UI to just fetch the new section and repaint it
create_sinon.expectJsonRequest(requests, 'GET', '/xblock/outline/mock-section-2');
create_sinon.respondWithJson(requests,
createMockSectionJSON('mock-section-2', 'Mock Section 2', []));
createMockSectionJSON({id: 'mock-section-2', display_name: 'Mock Section 2'}));
sectionElements = getItemsOfType('section');
expect(sectionElements.length).toBe(2);
expect($(sectionElements[0]).data('locator')).toEqual('mock-section');
@@ -269,9 +346,9 @@ define(["jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/view_helpers"
it('can be deleted', function() {
var promptSpy = view_helpers.createPromptSpy(), requestCount;
createCourseOutlinePage(this, createMockCourseJSON('mock-course', 'Mock Course', [
createMockSectionJSON('mock-section', 'Mock Section', []),
createMockSectionJSON('mock-section-2', 'Mock Section 2', [])
createCourseOutlinePage(this, createMockCourseJSON({}, [
createMockSectionJSON(),
createMockSectionJSON({id: 'mock-section-2', display_name: 'Mock Section 2'})
]));
getItemHeaders('section').find('.delete-button').first().click();
view_helpers.confirmPrompt(promptSpy);
@@ -356,7 +433,7 @@ define(["jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/view_helpers"
outlinePage.$('.section-header-actions .configure-button').click();
$("#start_date").val("1/2/2015");
// Section release date can't be cleared.
expect($(".edit-outline-item-modal .action-clear")).not.toExist();
expect($(".wrapper-modal-window .action-clear")).not.toExist();
// Section does not contain due_date or grading type selector
expect($("due_date")).not.toExist();
@@ -364,9 +441,7 @@ define(["jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/view_helpers"
// Staff lock controls are always visible
expect($("#staff_lock")).toExist();
$(".edit-outline-item-modal .action-save").click();
$(".wrapper-modal-window .action-save").click();
create_sinon.expectJsonRequest(requests, 'POST', '/xblock/mock-section', {
"metadata":{
"start":"2015-01-02T00:00:00.000Z"
@@ -376,25 +451,16 @@ define(["jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/view_helpers"
// This is the response for the change operation.
create_sinon.respondWithJson(requests, {});
var mockResponseSectionJSON = $.extend(true, {},
createMockSectionJSON('mock-section', 'Mock Section', [
createMockSubsectionJSON('mock-subsection', 'Mock Subsection', [{
id: 'mock-unit',
display_name: 'Mock Unit',
category: 'vertical',
studio_url: '/container/mock-unit',
is_container: true,
has_changes: true,
published: false,
edited_on: 'Jul 02, 2014 at 20:56 UTC',
edited_by: 'MockUser'
}
var mockResponseSectionJSON = createMockSectionJSON({
release_date: 'Jan 02, 2015 at 00:00 UTC'
}, [
createMockSubsectionJSON({}, [
createMockVerticalJSON({
has_changes: true,
published: false
})
])
]),
{
release_date: 'Jan 02, 2015 at 00:00 UTC',
}
);
]);
create_sinon.expectJsonRequest(requests, 'GET', '/xblock/outline/mock-section')
expect(requests.length).toBe(2);
// This is the response for the subsequent fetch operation for the section.
@@ -402,6 +468,46 @@ define(["jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/view_helpers"
expect($(".outline-section .status-release-value")).toContainText("Jan 02, 2015 at 00:00 UTC");
});
verifyTypePublishable('section', function (options) {
return createMockCourseJSON({}, [
createMockSectionJSON(options, [
createMockSubsectionJSON({}, [
createMockVerticalJSON()
])
])
]);
});
it('can display a publish modal with a list of unpublished subsections and units', function () {
var mockCourseJSON = createMockCourseJSON({}, [
createMockSectionJSON({has_changes: true}, [
createMockSubsectionJSON({has_changes: true}, [
createMockVerticalJSON(),
createMockVerticalJSON({has_changes: true, display_name: 'Unit 100'}),
createMockVerticalJSON({published: false, display_name: 'Unit 50'})
]),
createMockSubsectionJSON({has_changes: true}, [
createMockVerticalJSON({has_changes: true, display_name: 'Unit 1'})
]),
createMockSubsectionJSON({}, [createMockVerticalJSON])
]),
createMockSectionJSON({has_changes: true}, [
createMockSubsectionJSON({has_changes: true}, [
createMockVerticalJSON({has_changes: true}),
])
])
]), modalWindow;
createCourseOutlinePage(this, mockCourseJSON, false);
getItemHeaders('section').first().find('.publish-button').click();
modalWindow = $('.wrapper-modal-window');
expect(modalWindow.find('.outline-unit').length).toBe(3);
expect(_.compact(_.map(modalWindow.find('.outline-unit').text().split("\n"), $.trim))).toEqual(
['Unit 100', 'Unit 50', 'Unit 1']
)
expect(modalWindow.find('.outline-subsection').length).toBe(2);
});
});
describe("Subsection", function() {
@@ -419,37 +525,25 @@ define(["jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/view_helpers"
};
// Contains hard-coded dates because dates are presented in different formats.
mockServerValuesJson = $.extend(true, {},
createMockSectionJSON('mock-section', 'Mock Section', [
createMockSubsectionJSON('mock-subsection', 'Mock Subsection', [{
id: 'mock-unit',
display_name: 'Mock Unit',
category: 'vertical',
studio_url: '/container/mock-unit',
is_container: true,
has_changes: true,
published: false,
edited_on: 'Jul 02, 2014 at 20:56 UTC',
edited_by: 'MockUser'
}
var mockServerValuesJson = createMockSectionJSON({
release_date: 'Jan 01, 2970 at 05:00 UTC'
}, [
createMockSubsectionJSON({
graded: true,
due_date: 'Jul 10, 2014 at 00:00 UTC',
release_date: 'Jul 09, 2014 at 00:00 UTC',
start: "2014-07-09T00:00:00Z",
format: "Lab",
due: "2014-07-10T00:00:00Z",
has_explicit_staff_lock: true,
staff_only_message: true
}, [
createMockVerticalJSON({
has_changes: true,
published: false
})
])
]),
{
release_date: 'Jan 01, 2970 at 05:00 UTC',
child_info: { //Section child_info
children: [{ // Section children
graded: true,
due_date: 'Jul 10, 2014 at 00:00 UTC',
release_date: 'Jul 09, 2014 at 00:00 UTC',
start: "2014-07-09T00:00:00Z",
format: "Lab",
due: "2014-07-10T00:00:00Z",
has_explicit_staff_lock: true,
staff_only_message: true
}]
}
}
);
]);
it('can be deleted', function() {
var promptSpy = view_helpers.createPromptSpy();
@@ -492,9 +586,12 @@ define(["jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/view_helpers"
create_sinon.respondWithJson(requests, { });
// This is the response for the subsequent fetch operation for the section.
create_sinon.respondWithJson(requests,
createMockSectionJSON('mock-section', 'Mock Section', [
createMockSubsectionJSON('mock-subsection', updatedDisplayName, [])
]));
createMockSectionJSON({}, [
createMockSubsectionJSON({
display_name: updatedDisplayName
})
])
);
// Find the display name again in the refreshed DOM and verify it
displayNameWrapper = getItemHeaders('subsection').find('.wrapper-xblock-field');
view_helpers.verifyInlineEditChange(displayNameWrapper, updatedDisplayName);
@@ -514,7 +611,7 @@ define(["jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/view_helpers"
createCourseOutlinePage(this, mockCourseJSON, false);
outlinePage.$('.outline-subsection .configure-button').click();
setEditModalValues("7/9/2014", "7/10/2014", "Lab", true);
$(".edit-outline-item-modal .action-save").click();
$(".wrapper-modal-window .action-save").click();
create_sinon.expectJsonRequest(requests, 'POST', '/xblock/mock-subsection', {
"graderType":"Lab",
"publish": "republish",
@@ -550,7 +647,7 @@ define(["jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/view_helpers"
createCourseOutlinePage(this, mockCourseJSON, false);
outlinePage.$('.outline-item .outline-subsection .configure-button').click();
setEditModalValues("7/9/2014", "7/10/2014", "Lab", true);
$(".edit-outline-item-modal .action-save").click();
$(".wrapper-modal-window .action-save").click();
// This is the response for the change operation.
create_sinon.respondWithJson(requests, {});
@@ -568,29 +665,62 @@ define(["jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/view_helpers"
expect($("#grading_type").val()).toBe('Lab');
expect($("#staff_lock").is(":checked")).toBe(true);
$(".edit-outline-item-modal .scheduled-date-input .action-clear").click();
$(".edit-outline-item-modal .due-date-input .action-clear").click();
$(".wrapper-modal-window .scheduled-date-input .action-clear").click();
$(".wrapper-modal-window .due-date-input .action-clear").click();
expect($("#start_date").val()).toBe('');
expect($("#due_date").val()).toBe('');
$("#grading_type").val('notgraded');
$("#staff_lock").prop('checked', false);
$(".edit-outline-item-modal .action-save").click();
$(".wrapper-modal-window .action-save").click();
// This is the response for the change operation.
create_sinon.respondWithJson(requests, {});
// This is the response for the subsequent fetch operation.
create_sinon.respondWithJson(requests,
createMockSectionJSON('mock-section', 'Mock Section', [
createMockSubsectionJSON('mock-subsection', 'Mock Subsection', [])
])
createMockSectionJSON({}, [createMockSubsectionJSON()])
);
expect($(".outline-subsection .status-release-value")).not.toContainText("Jul 09, 2014 at 00:00 UTC");
expect($(".outline-subsection .status-grading-date")).not.toExist();
expect($(".outline-subsection .status-grading-value")).not.toExist();
expect($(".outline-subsection .status-message-copy")).not.toContainText("Contains staff only content");
});
verifyTypePublishable('subsection', function (options) {
return createMockCourseJSON({}, [
createMockSectionJSON({}, [
createMockSubsectionJSON(options, [
createMockVerticalJSON()
])
])
]);
});
it('can display a publish modal with a list of unpublished units', function () {
var mockCourseJSON = createMockCourseJSON({}, [
createMockSectionJSON({has_changes: true}, [
createMockSubsectionJSON({has_changes: true}, [
createMockVerticalJSON(),
createMockVerticalJSON({has_changes: true, display_name: "Unit 100"}),
createMockVerticalJSON({published: false, display_name: "Unit 50"})
]),
createMockSubsectionJSON({has_changes: true}, [
createMockVerticalJSON({has_changes: true})
]),
createMockSubsectionJSON({}, [createMockVerticalJSON])
])
]), modalWindow;
createCourseOutlinePage(this, mockCourseJSON, false);
getItemHeaders('subsection').first().find('.publish-button').click();
modalWindow = $('.wrapper-modal-window');
expect(modalWindow.find('.outline-unit').length).toBe(2);
expect(_.compact(_.map(modalWindow.find('.outline-unit').text().split("\n"), $.trim))).toEqual(
['Unit 100', 'Unit 50']
)
expect(modalWindow.find('.outline-subsection')).not.toExist();
});
});
// Note: most tests for units can be found in Bok Choy
@@ -615,6 +745,16 @@ define(["jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/view_helpers"
unitAnchor = getItemsOfType('unit').find('.unit-title a');
expect(unitAnchor.attr('href')).toBe('/container/mock-unit');
});
verifyTypePublishable('unit', function (options) {
return createMockCourseJSON({}, [
createMockSectionJSON({}, [
createMockSubsectionJSON({}, [
createMockVerticalJSON(options)
])
])
]);
});
});
describe("Date and Time picker", function() {

View File

@@ -8,9 +8,12 @@
* - changes cause a refresh of the entire section rather than just the view for the changed xblock
* - adding units will automatically redirect to the unit page rather than showing them inline
*/
define(["jquery", "underscore", "js/views/xblock_outline", "js/views/utils/view_utils",
"js/models/xblock_outline_info", "js/views/modals/edit_outline_item", "js/utils/drag_and_drop"],
function($, _, XBlockOutlineView, ViewUtils, XBlockOutlineInfo, EditSectionXBlockModal, ContentDragger) {
define(["jquery", "underscore", "js/views/xblock_outline", "js/views/utils/view_utils", "js/views/utils/xblock_utils",
"js/models/xblock_outline_info", "js/views/modals/course_outline_modals", "js/utils/drag_and_drop"],
function(
$, _, XBlockOutlineView, ViewUtils, XBlockViewUtils,
XBlockOutlineInfo, CourseOutlineModalsFactory, ContentDragger
) {
var CourseOutlineView = XBlockOutlineView.extend({
// takes XBlockOutlineInfo as a model
@@ -144,13 +147,30 @@ define(["jquery", "underscore", "js/views/xblock_outline", "js/views/utils/view_
},
editXBlock: function() {
var modal = new EditSectionXBlockModal({
model: this.model,
var modal = CourseOutlineModalsFactory.getModal('edit', this.model, {
onSave: this.refresh.bind(this),
parentInfo: this.parentInfo
parentInfo: this.parentInfo,
xblockType: XBlockViewUtils.getXBlockType(
this.model.get('category'), this.parentView.model, true
)
});
modal.show();
if (modal) {
modal.show();
}
},
publishXBlock: function() {
var modal = CourseOutlineModalsFactory.getModal('publish', this.model, {
onSave: this.refresh.bind(this),
xblockType: XBlockViewUtils.getXBlockType(
this.model.get('category'), this.parentView.model, true
)
});
if (modal) {
modal.show();
}
},
addButtonActions: function(element) {
@@ -159,6 +179,10 @@ define(["jquery", "underscore", "js/views/xblock_outline", "js/views/utils/view_
event.preventDefault();
this.editXBlock();
}.bind(this));
element.find('.publish-button').click(function(event) {
event.preventDefault();
this.publishXBlock();
}.bind(this));
},
makeContentDraggable: function(element) {

View 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));
}
};
});

View File

@@ -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;
});