Edit section/subsection release dates and grading types.
Fix bok_choy test by changing course separator. Change format of the modal title to '[Subsection/Section Name] Settings'. Improve bok_choy test stability. Studio: correcting modal window size name for outline settings editor Specify full date in bok_choy tests. Refactor bok_choy tests. Remove .modal-editor from basic-modal.underscore Set classes in modal window dynamically. Studio: revising outline edit modal tip content and overall size Rename isEditable to isEditableOnCourseOutline. Interpolate display name. Use graded instead of format as flag. Studio: revising outline settings edit modal size Fix selectors in bok_choy tests.
This commit is contained in:
committed by
cahrens
parent
8ca4c68580
commit
a63cc0b30f
@@ -213,6 +213,7 @@ define([
|
||||
"js/spec/models/component_template_spec",
|
||||
"js/spec/models/explicit_url_spec",
|
||||
"js/spec/models/group_configuration_spec",
|
||||
"js/spec/models/xblock_info_spec",
|
||||
|
||||
"js/spec/utils/drag_and_drop_spec",
|
||||
"js/spec/utils/handle_iframe_binding_spec",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
require(["domReady", "jquery", "underscore", "gettext", "js/views/feedback_notification", "js/views/feedback_prompt",
|
||||
"js/utils/get_date", "js/utils/module", "js/utils/handle_iframe_binding", "js/utils/change_on_enter", "jquery.ui",
|
||||
"js/utils/date_utils", "js/utils/module", "js/utils/handle_iframe_binding", "js/utils/change_on_enter", "jquery.ui",
|
||||
"jquery.leanModal", "jquery.form", "jquery.smoothScroll"],
|
||||
function(domReady, $, _, gettext, NotificationView, PromptView, DateUtils, ModuleUtils, IframeUtils, TriggerChangeEventOnEnter)
|
||||
{
|
||||
@@ -196,7 +196,7 @@ function saveSubsection() {
|
||||
// get datetimes for start and due, stick into metadata
|
||||
_(["start", "due"]).each(function(name) {
|
||||
|
||||
var datetime = DateUtils(
|
||||
var datetime = DateUtils.getDate(
|
||||
document.getElementById(name+"_date"),
|
||||
document.getElementById(name+"_time")
|
||||
);
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
define(["backbone", "underscore", "js/utils/module"], function(Backbone, _, ModuleUtils) {
|
||||
define(
|
||||
['backbone', 'underscore', 'underscore.string', 'js/utils/module'],
|
||||
function(Backbone, _, str, ModuleUtils) {
|
||||
'use strict';
|
||||
var XBlockInfo = Backbone.Model.extend({
|
||||
|
||||
urlRoot: ModuleUtils.urlRoot,
|
||||
@@ -6,32 +9,32 @@ define(["backbone", "underscore", "js/utils/module"], function(Backbone, _, Modu
|
||||
// NOTE: 'publish' is not an attribute on XBlockInfo, but it is used to signal the publish
|
||||
// and discard changes actions. Therefore 'publish' cannot be introduced as an attribute.
|
||||
defaults: {
|
||||
"id": null,
|
||||
"display_name": null,
|
||||
"category": null,
|
||||
"data": null,
|
||||
"metadata" : null,
|
||||
'id': null,
|
||||
'display_name': null,
|
||||
'category': null,
|
||||
'data': null,
|
||||
'metadata' : null,
|
||||
/**
|
||||
* The Studio URL for this xblock, or null if it doesn't have one.
|
||||
*/
|
||||
"studio_url": null,
|
||||
'studio_url': null,
|
||||
/**
|
||||
* An optional object with information about the children as well as about
|
||||
* the primary xblock type that is supported as a child.
|
||||
*/
|
||||
"child_info": null,
|
||||
'child_info': null,
|
||||
/**
|
||||
* An optional object with information about each of the ancestors.
|
||||
*/
|
||||
"ancestor_info": null,
|
||||
'ancestor_info': null,
|
||||
/**
|
||||
* Date of the last edit to this xblock or any of its descendants.
|
||||
*/
|
||||
"edited_on":null,
|
||||
'edited_on':null,
|
||||
/**
|
||||
* User who last edited the xblock or any of its descendants.
|
||||
*/
|
||||
"edited_by":null,
|
||||
'edited_by':null,
|
||||
/**
|
||||
* True iff a published version of the xblock exists.
|
||||
*/
|
||||
@@ -39,11 +42,11 @@ define(["backbone", "underscore", "js/utils/module"], function(Backbone, _, Modu
|
||||
/**
|
||||
* Date of the last publish of this xblock, or null if never published.
|
||||
*/
|
||||
"published_on": null,
|
||||
'published_on': null,
|
||||
/**
|
||||
* User who last published the xblock, or null if never published.
|
||||
*/
|
||||
"published_by": null,
|
||||
'published_by': null,
|
||||
/**
|
||||
* True if the xblock has changes.
|
||||
* Note: this is not always provided as a performance optimization. It is only provided for
|
||||
@@ -58,23 +61,53 @@ define(["backbone", "underscore", "js/utils/module"], function(Backbone, _, Modu
|
||||
/**
|
||||
* True iff the release date of the xblock is in the past.
|
||||
*/
|
||||
"released_to_students": null,
|
||||
'released_to_students': null,
|
||||
/**
|
||||
* If the xblock is published, the date on which it will be released to students.
|
||||
* This can be null if the release date is unscheduled.
|
||||
*/
|
||||
"release_date": null,
|
||||
'release_date': null,
|
||||
/**
|
||||
* The xblock which is determining the release date. For instance, for a unit,
|
||||
* this will either be the parent subsection or the grandparent section.
|
||||
* This can be null if the release date is unscheduled.
|
||||
*/
|
||||
"release_date_from":null,
|
||||
'release_date_from':null,
|
||||
/**
|
||||
* True if this xblock is currently visible to students. This is computed server-side
|
||||
* so that the logic isn't duplicated on the client.
|
||||
*/
|
||||
"currently_visible_to_students": null
|
||||
'currently_visible_to_students': null,
|
||||
/**
|
||||
* If xblock is graded, the date after which student assessment will be evaluated.
|
||||
* It has same format as release date, for example: 'Jan 02, 2015 at 00:00 UTC'.
|
||||
*/
|
||||
'due_date': null,
|
||||
/**
|
||||
* Grading policy for xblock.
|
||||
*/
|
||||
'format': null,
|
||||
/**
|
||||
* List of course graders names.
|
||||
*/
|
||||
'course_graders': null,
|
||||
/**
|
||||
* True if this xblock contributes to the final course grade.
|
||||
*/
|
||||
'graded': null,
|
||||
/**
|
||||
* The same as `release_date` but as an ISO-formatted date string.
|
||||
*/
|
||||
'start': null,
|
||||
/**
|
||||
* The same as `due_date` but as an ISO-formatted date string.
|
||||
*/
|
||||
'due': null
|
||||
},
|
||||
|
||||
initialize: function () {
|
||||
// Extend our Model by helper methods.
|
||||
_.extend(this, this.getCategoryHelpers());
|
||||
},
|
||||
|
||||
parse: function(response) {
|
||||
@@ -100,6 +133,31 @@ define(["backbone", "underscore", "js/utils/module"], function(Backbone, _, Modu
|
||||
hasChildren: function() {
|
||||
var childInfo = this.get('child_info');
|
||||
return childInfo && childInfo.children.length > 0;
|
||||
},
|
||||
|
||||
/**
|
||||
* Return a list of convenience methods to check affiliation to the category.
|
||||
* @return {Array}
|
||||
*/
|
||||
getCategoryHelpers: function () {
|
||||
var categories = ['course', 'chapter', 'sequential', 'vertical'],
|
||||
helpers = {};
|
||||
|
||||
_.each(categories, function (item) {
|
||||
helpers['is' + str.titleize(item)] = function () {
|
||||
return this.get('category') === item;
|
||||
};
|
||||
}, this);
|
||||
|
||||
return helpers;
|
||||
},
|
||||
|
||||
/**
|
||||
* Check if we can edit current XBlock or not on Course Outline page.
|
||||
* @return {Boolean}
|
||||
*/
|
||||
isEditableOnCourseOutline: function() {
|
||||
return this.isSequential() || this.isChapter();
|
||||
}
|
||||
});
|
||||
return XBlockInfo;
|
||||
|
||||
12
cms/static/js/spec/models/xblock_info_spec.js
Normal file
12
cms/static/js/spec/models/xblock_info_spec.js
Normal file
@@ -0,0 +1,12 @@
|
||||
define(['backbone', 'js/models/xblock_info'],
|
||||
function(Backbone, XBlockInfo) {
|
||||
describe('XblockInfo isEditableOnCourseOutline', function() {
|
||||
it('works correct', function() {
|
||||
expect(new XBlockInfo({'category': 'chapter'}).isEditableOnCourseOutline()).toBe(true);
|
||||
expect(new XBlockInfo({'category': 'course'}).isEditableOnCourseOutline()).toBe(false);
|
||||
expect(new XBlockInfo({'category': 'sequential'}).isEditableOnCourseOutline()).toBe(true);
|
||||
expect(new XBlockInfo({'category': 'vertical'}).isEditableOnCourseOutline()).toBe(false);
|
||||
});
|
||||
});
|
||||
}
|
||||
);
|
||||
@@ -1,6 +1,6 @@
|
||||
define(["jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/view_helpers", "js/views/utils/view_utils",
|
||||
"js/views/pages/course_outline", "js/models/xblock_outline_info"],
|
||||
function ($, create_sinon, view_helpers, ViewUtils, CourseOutlinePage, XBlockOutlineInfo) {
|
||||
"js/views/pages/course_outline", "js/models/xblock_outline_info", "js/utils/date_utils", "js/spec_helpers/edit_helpers"],
|
||||
function ($, create_sinon, view_helpers, ViewUtils, CourseOutlinePage, XBlockOutlineInfo, DateUtils, edit_helpers) {
|
||||
|
||||
describe("CourseOutlinePage", function() {
|
||||
var createCourseOutlinePage, displayNameInput, model, outlinePage, requests,
|
||||
@@ -56,6 +56,7 @@ define(["jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/view_helpers"
|
||||
published: true,
|
||||
edited_on: 'Jul 02, 2014 at 20:56 UTC',
|
||||
edited_by: 'MockUser',
|
||||
course_graders: '["Lab", "Howework"]',
|
||||
child_info: {
|
||||
category: 'vertical',
|
||||
display_name: 'Unit',
|
||||
@@ -109,6 +110,9 @@ define(["jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/view_helpers"
|
||||
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');
|
||||
appendSetFixtures(mockOutlinePage);
|
||||
mockCourseJSON = createMockCourseJSON('mock-course', 'Mock Course', [
|
||||
createMockSectionJSON('mock-section', 'Mock Section', [
|
||||
@@ -134,6 +138,7 @@ define(["jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/view_helpers"
|
||||
|
||||
afterEach(function () {
|
||||
view_helpers.removeMockAnalytics();
|
||||
edit_helpers.cancelModalIfShowing();
|
||||
});
|
||||
|
||||
describe('Initial display', function() {
|
||||
@@ -342,15 +347,101 @@ define(["jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/view_helpers"
|
||||
expandItemsAndVerifyState('section');
|
||||
collapseItemsAndVerifyState('section');
|
||||
});
|
||||
|
||||
it('can be edited', function() {
|
||||
createCourseOutlinePage(this, mockCourseJSON, false);
|
||||
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();
|
||||
|
||||
// Section does not contain due_date or grading type selector
|
||||
expect($("due_date")).not.toExist();
|
||||
expect($("grading_format")).not.toExist();
|
||||
|
||||
$(".edit-outline-item-modal .action-save").click();
|
||||
|
||||
create_sinon.expectJsonRequest(requests, 'POST', '/xblock/mock-section', {
|
||||
"metadata":{
|
||||
"start":"2015-01-02T00:00:00.000Z",
|
||||
}
|
||||
});
|
||||
expect(requests[0].requestHeaders['X-HTTP-Method-Override']).toBe('PATCH');
|
||||
|
||||
// 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'
|
||||
}
|
||||
])
|
||||
]),
|
||||
{
|
||||
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.
|
||||
create_sinon.respondWithJson(requests, mockResponseSectionJSON);
|
||||
|
||||
expect($(".outline-section .status-release-value")).toContainText("Jan 02, 2015 at 00:00 UTC");
|
||||
});
|
||||
});
|
||||
|
||||
describe("Subsection", function() {
|
||||
var getDisplayNameWrapper;
|
||||
var getDisplayNameWrapper, setEditModalValues, mockServerValuesJson;
|
||||
|
||||
getDisplayNameWrapper = function() {
|
||||
return getItemHeaders('subsection').find('.wrapper-xblock-field');
|
||||
};
|
||||
|
||||
setEditModalValues = function (start_date, due_date, grading_type) {
|
||||
$("#start_date").val(start_date);
|
||||
$("#due_date").val(due_date);
|
||||
$("#grading_type").val(grading_type);
|
||||
}
|
||||
|
||||
// Contains hard-coded dates because dates are presented in different formats.
|
||||
var 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'
|
||||
}
|
||||
])
|
||||
]),
|
||||
{
|
||||
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"
|
||||
}]
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
it('can be deleted', function() {
|
||||
var promptSpy = view_helpers.createPromptSpy();
|
||||
createCourseOutlinePage(this, mockCourseJSON);
|
||||
@@ -409,6 +500,80 @@ define(["jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/view_helpers"
|
||||
collapseItemsAndVerifyState('subsection');
|
||||
expandItemsAndVerifyState('subsection');
|
||||
});
|
||||
|
||||
it('can be edited', function() {
|
||||
createCourseOutlinePage(this, mockCourseJSON, false);
|
||||
outlinePage.$('.outline-subsection .configure-button').click();
|
||||
setEditModalValues("7/9/2014", "7/10/2014", "Lab");
|
||||
$(".edit-outline-item-modal .action-save").click();
|
||||
create_sinon.expectJsonRequest(requests, 'POST', '/xblock/mock-subsection', {
|
||||
"graderType":"Lab",
|
||||
"metadata":{
|
||||
"start":"2014-07-09T00:00:00.000Z",
|
||||
"due":"2014-07-10T00:00:00.000Z"
|
||||
}
|
||||
});
|
||||
expect(requests[0].requestHeaders['X-HTTP-Method-Override']).toBe('PATCH');
|
||||
|
||||
// This is the response for the change operation.
|
||||
create_sinon.respondWithJson(requests, {});
|
||||
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.
|
||||
create_sinon.respondWithJson(requests, mockServerValuesJson);
|
||||
|
||||
expect($(".outline-subsection .status-release-value")).toContainText("Jul 09, 2014 at 00:00 UTC");
|
||||
expect($(".outline-subsection .due-date")).toContainText("Due date: Jul 10, 2014 at 00:00 UTC");
|
||||
expect($(".outline-subsection .policy")).toContainText("Policy: Lab");
|
||||
|
||||
expect($(".outline-item .outline-subsection .policy")).toContainText("Policy: Lab");
|
||||
outlinePage.$('.outline-item .outline-subsection .configure-button').click();
|
||||
expect($("#start_date").val()).toBe('7/9/2014');
|
||||
expect($("#due_date").val()).toBe('7/10/2014');
|
||||
expect($("#grading_type").val()).toBe('Lab');
|
||||
});
|
||||
|
||||
it('release date, due date and grading type can be cleared.', function() {
|
||||
createCourseOutlinePage(this, mockCourseJSON, false);
|
||||
outlinePage.$('.outline-item .outline-subsection .configure-button').click();
|
||||
setEditModalValues("7/9/2014", "7/10/2014", "Lab");
|
||||
$(".edit-outline-item-modal .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, mockServerValuesJson);
|
||||
|
||||
expect($(".outline-subsection .status-release-value")).toContainText("Jul 09, 2014 at 00:00 UTC");
|
||||
expect($(".outline-subsection .due-date")).toContainText("Due date: Jul 10, 2014 at 00:00 UTC");
|
||||
expect($(".outline-subsection .policy")).toContainText("Policy: Lab");
|
||||
|
||||
outlinePage.$('.outline-subsection .configure-button').click();
|
||||
expect($("#start_date").val()).toBe('7/9/2014');
|
||||
expect($("#due_date").val()).toBe('7/10/2014');
|
||||
expect($("#grading_type").val()).toBe('Lab');
|
||||
|
||||
$(".edit-outline-item-modal .scheduled-date-input .action-clear").click();
|
||||
$(".edit-outline-item-modal .due-date-input .action-clear").click();
|
||||
expect($("#start_date").val()).toBe('');
|
||||
expect($("#due_date").val()).toBe('');
|
||||
|
||||
$("#grading_type").val('notgraded');
|
||||
|
||||
$(".edit-outline-item-modal .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', [])
|
||||
])
|
||||
);
|
||||
expect($(".outline-subsection .status-release-value")).not.toContainText("Jul 09, 2014 at 00:00 UTC");
|
||||
expect($(".outline-subsection .due-date")).not.toExist();
|
||||
expect($(".outline-subsection .policy")).not.toExist();
|
||||
});
|
||||
});
|
||||
|
||||
// Note: most tests for units can be found in Bok Choy
|
||||
@@ -434,5 +599,21 @@ define(["jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/view_helpers"
|
||||
expect(unitAnchor.attr('href')).toBe('/container/mock-unit');
|
||||
});
|
||||
});
|
||||
|
||||
describe("Date and Time picker", function() {
|
||||
// Two datetime formats can came from server: '%Y-%m-%dT%H:%M:%SZ' and %Y-%m-%dT%H:%M:%S+TZ:TZ'
|
||||
it('can parse dates in both formats that can come from server', function() {
|
||||
createCourseOutlinePage(this, mockCourseJSON, false);
|
||||
outlinePage.$('.outline-subsection .configure-button').click();
|
||||
expect($("#start_date").val()).toBe('');
|
||||
expect($("#start_time").val()).toBe('');
|
||||
DateUtils.setDate($("#start_date"), ("#start_time"), "2015-08-10T05:10:00Z");
|
||||
expect($("#start_date").val()).toBe('8/10/2015');
|
||||
expect($("#start_time").val()).toBe('05:10');
|
||||
DateUtils.setDate($("#start_date"), ("#start_time"), "2014-07-09T00:00:00+00:00");
|
||||
expect($("#start_date").val()).toBe('7/9/2014');
|
||||
expect($("#start_time").val()).toBe('00:00');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
define(["jquery", "jquery.ui", "jquery.timepicker"], function($) {
|
||||
define(["jquery", "date", "jquery.ui", "jquery.timepicker"], function($, date) {
|
||||
var getDate = function (datepickerInput, timepickerInput) {
|
||||
// given a pair of inputs (datepicker and timepicker), return a JS Date
|
||||
// object that corresponds to the datetime.js that they represent. Assume
|
||||
@@ -14,5 +14,19 @@ define(["jquery", "jquery.ui", "jquery.timepicker"], function($) {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
return getDate;
|
||||
|
||||
var setDate = function (datepickerInput, timepickerInput, datetime) {
|
||||
// given a pair of inputs (datepicker and timepicker) and the date as an
|
||||
// ISO-formatted date string.
|
||||
datetime = date.parse(datetime);
|
||||
if (datetime) {
|
||||
$(datepickerInput).datepicker("setDate", datetime);
|
||||
$(timepickerInput).timepicker("setTime", datetime);
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
getDate: getDate,
|
||||
setDate: setDate
|
||||
};
|
||||
});
|
||||
@@ -9,8 +9,9 @@
|
||||
* - 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"],
|
||||
function($, _, XBlockOutlineView, ViewUtils, XBlockOutlineInfo) {
|
||||
"js/models/xblock_outline_info",
|
||||
"js/views/modals/edit_outline_item"],
|
||||
function($, _, XBlockOutlineView, ViewUtils, XBlockOutlineInfo, EditSectionXBlockModal) {
|
||||
|
||||
var CourseOutlineView = XBlockOutlineView.extend({
|
||||
// takes XBlockOutlineInfo as a model
|
||||
@@ -23,13 +24,12 @@ define(["jquery", "underscore", "js/views/xblock_outline", "js/views/utils/view_
|
||||
return true;
|
||||
}
|
||||
// Only expand the course and its chapters (aka sections) initially
|
||||
var category = this.model.get('category');
|
||||
return category === 'course' || category === 'chapter';
|
||||
return this.model.isCourse() || this.model.isChapter();
|
||||
},
|
||||
|
||||
shouldRenderChildren: function() {
|
||||
// Render all nodes up to verticals but not below
|
||||
return this.model.get('category') !== 'vertical';
|
||||
return !this.model.isVertical();
|
||||
},
|
||||
|
||||
createChildView: function(xblockInfo, parentInfo, parentView) {
|
||||
@@ -64,7 +64,7 @@ define(["jquery", "underscore", "js/views/xblock_outline", "js/views/utils/view_
|
||||
var getViewToRefresh, view, expandedLocators;
|
||||
|
||||
getViewToRefresh = function(view) {
|
||||
if (view.model.get('category') === 'chapter' || !view.parentView) {
|
||||
if (view.model.isChapter() || !view.parentView) {
|
||||
return view;
|
||||
}
|
||||
return getViewToRefresh(view.parentView);
|
||||
@@ -119,11 +119,10 @@ define(["jquery", "underscore", "js/views/xblock_outline", "js/views/utils/view_
|
||||
|
||||
onChildDeleted: function(childView) {
|
||||
var xblockInfo = this.model,
|
||||
childCategory = childView.model.get('category'),
|
||||
children = xblockInfo.get('child_info') && xblockInfo.get('child_info').children;
|
||||
// If deleting a section that isn't the final one, just remove it for efficiency
|
||||
// as it cannot visually effect the other sections.
|
||||
if (childCategory === 'chapter' && children && children.length > 1) {
|
||||
if (childView.model.isChapter() && children && children.length > 1) {
|
||||
childView.$el.remove();
|
||||
children.splice(children.indexOf(childView.model), 1);
|
||||
} else {
|
||||
@@ -138,6 +137,23 @@ define(["jquery", "underscore", "js/views/xblock_outline", "js/views/utils/view_
|
||||
expanded_locators: [ locator ],
|
||||
scroll_offset: scrollOffset || 0
|
||||
};
|
||||
},
|
||||
|
||||
editXBlock: function() {
|
||||
var modal = new EditSectionXBlockModal({
|
||||
model: this.model,
|
||||
onSave: this.refresh.bind(this)
|
||||
});
|
||||
|
||||
modal.show();
|
||||
},
|
||||
|
||||
addButtonActions: function(element) {
|
||||
XBlockOutlineView.prototype.addButtonActions.apply(this, arguments);
|
||||
element.find('.configure-button').click(function(event) {
|
||||
event.preventDefault();
|
||||
this.editXBlock();
|
||||
}.bind(this));
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -15,7 +15,9 @@ define(["jquery", "underscore", "gettext", "js/views/baseview"],
|
||||
modalName: 'basic',
|
||||
modalType: 'generic',
|
||||
modalSize: 'lg',
|
||||
title: ''
|
||||
title: '',
|
||||
// A list of class names, separated by space.
|
||||
viewSpecificClasses: ''
|
||||
}),
|
||||
|
||||
initialize: function() {
|
||||
@@ -39,7 +41,8 @@ define(["jquery", "underscore", "gettext", "js/views/baseview"],
|
||||
name: this.options.modalName,
|
||||
type: this.options.modalType,
|
||||
size: this.options.modalSize,
|
||||
title: this.options.title
|
||||
title: this.options.title,
|
||||
viewSpecificClasses: this.options.viewSpecificClasses
|
||||
}));
|
||||
this.addActionButtons();
|
||||
this.renderContents();
|
||||
|
||||
232
cms/static/js/views/modals/edit_outline_item.js
Normal file
232
cms/static/js/views/modals/edit_outline_item.js
Normal file
@@ -0,0 +1,232 @@
|
||||
/**
|
||||
* The EditSectionXBlockModal 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/modals/base_modal',
|
||||
'date', 'js/views/utils/xblock_utils', 'js/utils/date_utils'
|
||||
],
|
||||
function(
|
||||
$, Backbone, _, gettext, BaseModal, date, XBlockViewUtils, DateUtils
|
||||
) {
|
||||
'use strict';
|
||||
var EditSectionXBlockModal, BaseDateView, ReleaseDateView, DueDateView,
|
||||
GradingView;
|
||||
|
||||
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: 'lg',
|
||||
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 () {
|
||||
if (this.model.isChapter() || this.model.isSequential()) {
|
||||
return _.template(
|
||||
gettext('<%= sectionName %> Settings'),
|
||||
{sectionName: this.model.get('display_name')});
|
||||
} else {
|
||||
return '';
|
||||
}
|
||||
},
|
||||
|
||||
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()
|
||||
});
|
||||
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
|
||||
}, 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 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',
|
||||
|
||||
getValue: function () {
|
||||
return DateUtils.getDate(this.$('#start_date'), this.$('#start_time'));
|
||||
},
|
||||
|
||||
clearValue: function (event) {
|
||||
event.preventDefault();
|
||||
this.$('#start_time, #start_date').val('');
|
||||
},
|
||||
|
||||
getMetadata: function () {
|
||||
return {
|
||||
'start': this.getValue()
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
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'))
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
return EditSectionXBlockModal;
|
||||
});
|
||||
@@ -14,7 +14,8 @@ define(["jquery", "underscore", "gettext", "js/views/modals/base_modal", "js/vie
|
||||
|
||||
options: $.extend({}, BaseModal.prototype.options, {
|
||||
modalName: 'edit-xblock',
|
||||
addSaveButton: true
|
||||
addSaveButton: true,
|
||||
viewSpecificClasses: 'modal-editor confirm'
|
||||
}),
|
||||
|
||||
initialize: function() {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
define(["domReady", "jquery", "jquery.ui", "underscore", "gettext", "js/views/feedback_notification", "js/utils/drag_and_drop",
|
||||
"js/utils/cancel_on_escape", "js/utils/get_date", "js/utils/module"],
|
||||
"js/utils/cancel_on_escape", "js/utils/date_utils", "js/utils/module"],
|
||||
function (domReady, $, ui, _, gettext, NotificationView, ContentDragger, CancelOnEscape,
|
||||
DateUtils, ModuleUtils) {
|
||||
|
||||
@@ -61,7 +61,7 @@ define(["domReady", "jquery", "jquery.ui", "underscore", "gettext", "js/views/fe
|
||||
var saveSetSectionScheduleDate = function (e) {
|
||||
e.preventDefault();
|
||||
|
||||
var datetime = DateUtils(
|
||||
var datetime = DateUtils.getDate(
|
||||
$('.edit-section-publish-settings .start-date'),
|
||||
$('.edit-section-publish-settings .start-time')
|
||||
);
|
||||
|
||||
@@ -9,7 +9,8 @@ define(["jquery", "underscore", "gettext", "js/views/modals/base_modal", "jquery
|
||||
options: $.extend({}, BaseModal.prototype.options, {
|
||||
modalName: 'assetupload',
|
||||
modalSize: 'med',
|
||||
successMessageTimeout: 2000 // 2 seconds
|
||||
successMessageTimeout: 2000, // 2 seconds
|
||||
viewSpecificClasses: 'confirm'
|
||||
}),
|
||||
|
||||
initialize: function() {
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
define(["jquery", "underscore", "gettext", "js/views/utils/view_utils", "js/utils/module"],
|
||||
function($, _, gettext, ViewUtils, ModuleUtils) {
|
||||
var addXBlock, deleteXBlock, createUpdateRequestData, updateXBlockField, VisibilityState,
|
||||
getXBlockVisibilityClass, getXBlockListTypeClass;
|
||||
getXBlockVisibilityClass, getXBlockListTypeClass, updateXBlockFields;
|
||||
|
||||
/**
|
||||
* Represents the possible visibility states for an xblock:
|
||||
@@ -105,9 +105,9 @@ define(["jquery", "underscore", "gettext", "js/views/utils/view_utils", "js/util
|
||||
|
||||
/**
|
||||
* Updates the specified field of an xblock to a new value.
|
||||
* @param xblockInfo The XBlockInfo model representing the xblock.
|
||||
* @param fieldName The xblock field name to be updated.
|
||||
* @param newValue The new value for the field.
|
||||
* @param {Backbone Model} xblockInfo The XBlockInfo model representing the xblock.
|
||||
* @param {String} fieldName The xblock field name to be updated.
|
||||
* @param {*} newValue The new value for the field.
|
||||
* @returns {jQuery promise} A promise representing the updating of the field.
|
||||
*/
|
||||
updateXBlockField = function(xblockInfo, fieldName, newValue) {
|
||||
@@ -118,6 +118,22 @@ define(["jquery", "underscore", "gettext", "js/views/utils/view_utils", "js/util
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Updates the specified fields of an xblock to a new values.
|
||||
* @param {Backbone Model} xblockInfo The XBlockInfo model representing the xblock.
|
||||
* @param {Object} xblockData Object representing xblock data as accepted on server.
|
||||
* @param {Object} [options] Hash with options.
|
||||
* @returns {jQuery promise} A promise representing the updating of the xblock values.
|
||||
*/
|
||||
updateXBlockFields = function(xblockInfo, xblockData, options) {
|
||||
options = _.extend({}, { patch: true }, options);
|
||||
return ViewUtils.runOperationShowingMessage(gettext('Saving…'),
|
||||
function() {
|
||||
return xblockInfo.save(xblockData, options);
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns the CSS class to represent the specified xblock visibility state.
|
||||
*/
|
||||
@@ -155,6 +171,7 @@ define(["jquery", "underscore", "gettext", "js/views/utils/view_utils", "js/util
|
||||
'deleteXBlock': deleteXBlock,
|
||||
'updateXBlockField': updateXBlockField,
|
||||
'getXBlockVisibilityClass': getXBlockVisibilityClass,
|
||||
'getXBlockListTypeClass': getXBlockListTypeClass
|
||||
'getXBlockListTypeClass': getXBlockListTypeClass,
|
||||
'updateXBlockFields': updateXBlockFields
|
||||
};
|
||||
});
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
[class*="view-"] {
|
||||
|
||||
// basic modal content
|
||||
// ------------------------
|
||||
.modal-window {
|
||||
@extend %ui-depth3;
|
||||
@include box-sizing(border-box);
|
||||
@@ -51,6 +52,40 @@
|
||||
}
|
||||
}
|
||||
|
||||
// sections within a modal
|
||||
.modal-section {
|
||||
margin-bottom: ($baseline/2);
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.modal-section-title {
|
||||
@extend %t-title6;
|
||||
margin: 0 0 ($baseline/2) 0;
|
||||
border-bottom: 1px solid $gray-l4;
|
||||
padding-bottom: ($baseline/4);
|
||||
color: $gray-d2;
|
||||
}
|
||||
|
||||
.modal-section-content {
|
||||
|
||||
.list-fields, .list-actions {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.list-actions {
|
||||
@extend %actions-list;
|
||||
margin-left: ($baseline/4);
|
||||
|
||||
.action-button {
|
||||
@extend %t-icon4;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: need to sync up (alongside general editing mode) with xblocks.scss UI
|
||||
.modal-chin,
|
||||
.xblock-actions,
|
||||
@@ -86,6 +121,7 @@
|
||||
|
||||
|
||||
// small modals - quick editors and dialogs
|
||||
// ------------------------
|
||||
.modal-sm {
|
||||
width: 30%;
|
||||
min-width: ($baseline*15);
|
||||
@@ -96,6 +132,7 @@
|
||||
}
|
||||
|
||||
// medium modals - forms and interactives
|
||||
// ------------------------
|
||||
.modal-med {
|
||||
width: 40%;
|
||||
min-width: ($baseline*18);
|
||||
@@ -106,11 +143,16 @@
|
||||
}
|
||||
|
||||
// large modals - component editors and interactives
|
||||
// ------------------------
|
||||
.modal-lg {
|
||||
width: 70%;
|
||||
min-width: ($baseline*27.5);
|
||||
height: auto;
|
||||
|
||||
.modal-content {
|
||||
padding: $baseline;
|
||||
}
|
||||
|
||||
&.modal-editor {
|
||||
|
||||
.modal-header {
|
||||
@@ -162,6 +204,7 @@
|
||||
|
||||
|
||||
// specific modal overrides
|
||||
// ------------------------
|
||||
|
||||
// upload modal
|
||||
.assetupload-modal {
|
||||
@@ -191,6 +234,80 @@
|
||||
}
|
||||
}
|
||||
|
||||
// edit outline item settings
|
||||
.edit-outline-item-modal {
|
||||
|
||||
.list-fields {
|
||||
|
||||
.field {
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
margin-right: ($baseline/2);
|
||||
margin-bottom: ($baseline/4);
|
||||
|
||||
|
||||
// TODO: refactor the _forms.scss partial to allow for this area to inherit from it
|
||||
label, input, textarea {
|
||||
display: block;
|
||||
}
|
||||
|
||||
label {
|
||||
@extend %t-copy-sub1;
|
||||
@include transition(color $tmg-f3 ease-in-out 0s);
|
||||
margin: 0 0 ($baseline/4) 0;
|
||||
font-weight: 600;
|
||||
|
||||
&.is-focused {
|
||||
color: $blue;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
input, textarea {
|
||||
@extend %t-copy-base;
|
||||
@include transition(all $tmg-f2 ease-in-out 0s);
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
padding: ($baseline/2);
|
||||
|
||||
// CASE: long length
|
||||
&.long {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
// CASE: short length
|
||||
&.short {
|
||||
width: 25%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// CASE: select input
|
||||
.field-select {
|
||||
|
||||
.label, .input {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.label {
|
||||
margin-right: ($baseline/2);
|
||||
}
|
||||
|
||||
.input {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.edit-settings-grading {
|
||||
|
||||
.grading-type {
|
||||
margin-bottom: $baseline;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// xblock custom actions
|
||||
.modal-window .editor-with-buttons {
|
||||
margin-bottom: ($baseline*3);
|
||||
|
||||
Reference in New Issue
Block a user