Commit a63cc0b3 by Alexander Kryklia Committed by cahrens

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.
parent 8ca4c685
...@@ -6,6 +6,7 @@ import logging ...@@ -6,6 +6,7 @@ import logging
from uuid import uuid4 from uuid import uuid4
from datetime import datetime from datetime import datetime
from pytz import UTC from pytz import UTC
import json
from collections import OrderedDict from collections import OrderedDict
from functools import partial from functools import partial
...@@ -643,6 +644,7 @@ def create_xblock_info(xblock, data=None, metadata=None, include_ancestor_info=F ...@@ -643,6 +644,7 @@ def create_xblock_info(xblock, data=None, metadata=None, include_ancestor_info=F
release_date = get_default_time_display(xblock.start) if xblock.start != DEFAULT_START_DATE else None release_date = get_default_time_display(xblock.start) if xblock.start != DEFAULT_START_DATE else None
published = modulestore().has_item(xblock.location, revision=ModuleStoreEnum.RevisionOption.published_only) published = modulestore().has_item(xblock.location, revision=ModuleStoreEnum.RevisionOption.published_only)
graders = CourseGradingModel.fetch(xblock.location.course_key).graders
xblock_info = { xblock_info = {
"id": unicode(xblock.location), "id": unicode(xblock.location),
"display_name": xblock.display_name_with_default, "display_name": xblock.display_name_with_default,
...@@ -653,7 +655,13 @@ def create_xblock_info(xblock, data=None, metadata=None, include_ancestor_info=F ...@@ -653,7 +655,13 @@ def create_xblock_info(xblock, data=None, metadata=None, include_ancestor_info=F
'studio_url': xblock_studio_url(xblock), 'studio_url': xblock_studio_url(xblock),
"released_to_students": datetime.now(UTC) > xblock.start, "released_to_students": datetime.now(UTC) > xblock.start,
"release_date": release_date, "release_date": release_date,
"visibility_state": _compute_visibility_state(xblock, child_info, is_unit_with_changes) if not xblock.category == 'course' else None "visibility_state": _compute_visibility_state(xblock, child_info, is_unit_with_changes) if not xblock.category == 'course' else None,
"start": xblock.fields['start'].to_json(xblock.start),
"graded": xblock.graded,
"due_date": get_default_time_display(xblock.due),
"due": xblock.fields['due'].to_json(xblock.due),
"format": xblock.format,
"course_graders": json.dumps([grader.get('type') for grader in graders]),
} }
if data is not None: if data is not None:
xblock_info["data"] = data xblock_info["data"] = data
......
...@@ -1168,6 +1168,11 @@ class TestXBlockInfo(ItemTest): ...@@ -1168,6 +1168,11 @@ class TestXBlockInfo(ItemTest):
self.assertEqual(xblock_info['display_name'], 'Week 1') self.assertEqual(xblock_info['display_name'], 'Week 1')
self.assertTrue(xblock_info['published']) self.assertTrue(xblock_info['published'])
self.assertIsNone(xblock_info.get('edited_by', None)) self.assertIsNone(xblock_info.get('edited_by', None))
self.assertEqual(xblock_info['course_graders'], '["Homework", "Lab", "Midterm Exam", "Final Exam"]')
self.assertEqual(xblock_info['start'], '2030-01-01T00:00:00Z')
self.assertEqual(xblock_info['graded'], False)
self.assertEqual(xblock_info['due'], None)
self.assertEqual(xblock_info['format'], None)
# Finally, validate the entire response for consistency # Finally, validate the entire response for consistency
self.validate_xblock_info_consistency(xblock_info, has_child_info=has_child_info) self.validate_xblock_info_consistency(xblock_info, has_child_info=has_child_info)
......
...@@ -213,6 +213,7 @@ define([ ...@@ -213,6 +213,7 @@ define([
"js/spec/models/component_template_spec", "js/spec/models/component_template_spec",
"js/spec/models/explicit_url_spec", "js/spec/models/explicit_url_spec",
"js/spec/models/group_configuration_spec", "js/spec/models/group_configuration_spec",
"js/spec/models/xblock_info_spec",
"js/spec/utils/drag_and_drop_spec", "js/spec/utils/drag_and_drop_spec",
"js/spec/utils/handle_iframe_binding_spec", "js/spec/utils/handle_iframe_binding_spec",
......
require(["domReady", "jquery", "underscore", "gettext", "js/views/feedback_notification", "js/views/feedback_prompt", 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"], "jquery.leanModal", "jquery.form", "jquery.smoothScroll"],
function(domReady, $, _, gettext, NotificationView, PromptView, DateUtils, ModuleUtils, IframeUtils, TriggerChangeEventOnEnter) function(domReady, $, _, gettext, NotificationView, PromptView, DateUtils, ModuleUtils, IframeUtils, TriggerChangeEventOnEnter)
{ {
...@@ -196,7 +196,7 @@ function saveSubsection() { ...@@ -196,7 +196,7 @@ function saveSubsection() {
// get datetimes for start and due, stick into metadata // get datetimes for start and due, stick into metadata
_(["start", "due"]).each(function(name) { _(["start", "due"]).each(function(name) {
var datetime = DateUtils( var datetime = DateUtils.getDate(
document.getElementById(name+"_date"), document.getElementById(name+"_date"),
document.getElementById(name+"_time") document.getElementById(name+"_time")
); );
......
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({ var XBlockInfo = Backbone.Model.extend({
urlRoot: ModuleUtils.urlRoot, urlRoot: ModuleUtils.urlRoot,
...@@ -6,32 +9,32 @@ define(["backbone", "underscore", "js/utils/module"], function(Backbone, _, Modu ...@@ -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 // 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. // and discard changes actions. Therefore 'publish' cannot be introduced as an attribute.
defaults: { defaults: {
"id": null, 'id': null,
"display_name": null, 'display_name': null,
"category": null, 'category': null,
"data": null, 'data': null,
"metadata" : null, 'metadata' : null,
/** /**
* The Studio URL for this xblock, or null if it doesn't have one. * 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 * An optional object with information about the children as well as about
* the primary xblock type that is supported as a child. * 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. * 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. * 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. * 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. * True iff a published version of the xblock exists.
*/ */
...@@ -39,11 +42,11 @@ define(["backbone", "underscore", "js/utils/module"], function(Backbone, _, Modu ...@@ -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. * 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. * User who last published the xblock, or null if never published.
*/ */
"published_by": null, 'published_by': null,
/** /**
* True if the xblock has changes. * True if the xblock has changes.
* Note: this is not always provided as a performance optimization. It is only provided for * 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 ...@@ -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. * 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. * 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. * 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, * 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 will either be the parent subsection or the grandparent section.
* This can be null if the release date is unscheduled. * 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 * True if this xblock is currently visible to students. This is computed server-side
* so that the logic isn't duplicated on the client. * 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) { parse: function(response) {
...@@ -100,6 +133,31 @@ define(["backbone", "underscore", "js/utils/module"], function(Backbone, _, Modu ...@@ -100,6 +133,31 @@ define(["backbone", "underscore", "js/utils/module"], function(Backbone, _, Modu
hasChildren: function() { hasChildren: function() {
var childInfo = this.get('child_info'); var childInfo = this.get('child_info');
return childInfo && childInfo.children.length > 0; 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; return XBlockInfo;
......
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);
});
});
}
);
define(["jquery", "jquery.ui", "jquery.timepicker"], function($) { define(["jquery", "date", "jquery.ui", "jquery.timepicker"], function($, date) {
var getDate = function (datepickerInput, timepickerInput) { var getDate = function (datepickerInput, timepickerInput) {
// given a pair of inputs (datepicker and timepicker), return a JS Date // given a pair of inputs (datepicker and timepicker), return a JS Date
// object that corresponds to the datetime.js that they represent. Assume // object that corresponds to the datetime.js that they represent. Assume
...@@ -14,5 +14,19 @@ define(["jquery", "jquery.ui", "jquery.timepicker"], function($) { ...@@ -14,5 +14,19 @@ define(["jquery", "jquery.ui", "jquery.timepicker"], function($) {
return null; 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 @@ ...@@ -9,8 +9,9 @@
* - adding units will automatically redirect to the unit page rather than showing them inline * - 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", define(["jquery", "underscore", "js/views/xblock_outline", "js/views/utils/view_utils",
"js/models/xblock_outline_info"], "js/models/xblock_outline_info",
function($, _, XBlockOutlineView, ViewUtils, XBlockOutlineInfo) { "js/views/modals/edit_outline_item"],
function($, _, XBlockOutlineView, ViewUtils, XBlockOutlineInfo, EditSectionXBlockModal) {
var CourseOutlineView = XBlockOutlineView.extend({ var CourseOutlineView = XBlockOutlineView.extend({
// takes XBlockOutlineInfo as a model // takes XBlockOutlineInfo as a model
...@@ -23,13 +24,12 @@ define(["jquery", "underscore", "js/views/xblock_outline", "js/views/utils/view_ ...@@ -23,13 +24,12 @@ define(["jquery", "underscore", "js/views/xblock_outline", "js/views/utils/view_
return true; return true;
} }
// Only expand the course and its chapters (aka sections) initially // Only expand the course and its chapters (aka sections) initially
var category = this.model.get('category'); return this.model.isCourse() || this.model.isChapter();
return category === 'course' || category === 'chapter';
}, },
shouldRenderChildren: function() { shouldRenderChildren: function() {
// Render all nodes up to verticals but not below // Render all nodes up to verticals but not below
return this.model.get('category') !== 'vertical'; return !this.model.isVertical();
}, },
createChildView: function(xblockInfo, parentInfo, parentView) { createChildView: function(xblockInfo, parentInfo, parentView) {
...@@ -64,7 +64,7 @@ define(["jquery", "underscore", "js/views/xblock_outline", "js/views/utils/view_ ...@@ -64,7 +64,7 @@ define(["jquery", "underscore", "js/views/xblock_outline", "js/views/utils/view_
var getViewToRefresh, view, expandedLocators; var getViewToRefresh, view, expandedLocators;
getViewToRefresh = function(view) { getViewToRefresh = function(view) {
if (view.model.get('category') === 'chapter' || !view.parentView) { if (view.model.isChapter() || !view.parentView) {
return view; return view;
} }
return getViewToRefresh(view.parentView); return getViewToRefresh(view.parentView);
...@@ -119,11 +119,10 @@ define(["jquery", "underscore", "js/views/xblock_outline", "js/views/utils/view_ ...@@ -119,11 +119,10 @@ define(["jquery", "underscore", "js/views/xblock_outline", "js/views/utils/view_
onChildDeleted: function(childView) { onChildDeleted: function(childView) {
var xblockInfo = this.model, var xblockInfo = this.model,
childCategory = childView.model.get('category'),
children = xblockInfo.get('child_info') && xblockInfo.get('child_info').children; 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 // If deleting a section that isn't the final one, just remove it for efficiency
// as it cannot visually effect the other sections. // 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(); childView.$el.remove();
children.splice(children.indexOf(childView.model), 1); children.splice(children.indexOf(childView.model), 1);
} else { } else {
...@@ -138,6 +137,23 @@ define(["jquery", "underscore", "js/views/xblock_outline", "js/views/utils/view_ ...@@ -138,6 +137,23 @@ define(["jquery", "underscore", "js/views/xblock_outline", "js/views/utils/view_
expanded_locators: [ locator ], expanded_locators: [ locator ],
scroll_offset: scrollOffset || 0 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"], ...@@ -15,7 +15,9 @@ define(["jquery", "underscore", "gettext", "js/views/baseview"],
modalName: 'basic', modalName: 'basic',
modalType: 'generic', modalType: 'generic',
modalSize: 'lg', modalSize: 'lg',
title: '' title: '',
// A list of class names, separated by space.
viewSpecificClasses: ''
}), }),
initialize: function() { initialize: function() {
...@@ -39,7 +41,8 @@ define(["jquery", "underscore", "gettext", "js/views/baseview"], ...@@ -39,7 +41,8 @@ define(["jquery", "underscore", "gettext", "js/views/baseview"],
name: this.options.modalName, name: this.options.modalName,
type: this.options.modalType, type: this.options.modalType,
size: this.options.modalSize, size: this.options.modalSize,
title: this.options.title title: this.options.title,
viewSpecificClasses: this.options.viewSpecificClasses
})); }));
this.addActionButtons(); this.addActionButtons();
this.renderContents(); this.renderContents();
......
/**
* 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 ...@@ -14,7 +14,8 @@ define(["jquery", "underscore", "gettext", "js/views/modals/base_modal", "js/vie
options: $.extend({}, BaseModal.prototype.options, { options: $.extend({}, BaseModal.prototype.options, {
modalName: 'edit-xblock', modalName: 'edit-xblock',
addSaveButton: true addSaveButton: true,
viewSpecificClasses: 'modal-editor confirm'
}), }),
initialize: function() { initialize: function() {
......
define(["domReady", "jquery", "jquery.ui", "underscore", "gettext", "js/views/feedback_notification", "js/utils/drag_and_drop", 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, function (domReady, $, ui, _, gettext, NotificationView, ContentDragger, CancelOnEscape,
DateUtils, ModuleUtils) { DateUtils, ModuleUtils) {
...@@ -61,7 +61,7 @@ define(["domReady", "jquery", "jquery.ui", "underscore", "gettext", "js/views/fe ...@@ -61,7 +61,7 @@ define(["domReady", "jquery", "jquery.ui", "underscore", "gettext", "js/views/fe
var saveSetSectionScheduleDate = function (e) { var saveSetSectionScheduleDate = function (e) {
e.preventDefault(); e.preventDefault();
var datetime = DateUtils( var datetime = DateUtils.getDate(
$('.edit-section-publish-settings .start-date'), $('.edit-section-publish-settings .start-date'),
$('.edit-section-publish-settings .start-time') $('.edit-section-publish-settings .start-time')
); );
......
...@@ -9,7 +9,8 @@ define(["jquery", "underscore", "gettext", "js/views/modals/base_modal", "jquery ...@@ -9,7 +9,8 @@ define(["jquery", "underscore", "gettext", "js/views/modals/base_modal", "jquery
options: $.extend({}, BaseModal.prototype.options, { options: $.extend({}, BaseModal.prototype.options, {
modalName: 'assetupload', modalName: 'assetupload',
modalSize: 'med', modalSize: 'med',
successMessageTimeout: 2000 // 2 seconds successMessageTimeout: 2000, // 2 seconds
viewSpecificClasses: 'confirm'
}), }),
initialize: function() { initialize: function() {
......
...@@ -4,7 +4,7 @@ ...@@ -4,7 +4,7 @@
define(["jquery", "underscore", "gettext", "js/views/utils/view_utils", "js/utils/module"], define(["jquery", "underscore", "gettext", "js/views/utils/view_utils", "js/utils/module"],
function($, _, gettext, ViewUtils, ModuleUtils) { function($, _, gettext, ViewUtils, ModuleUtils) {
var addXBlock, deleteXBlock, createUpdateRequestData, updateXBlockField, VisibilityState, var addXBlock, deleteXBlock, createUpdateRequestData, updateXBlockField, VisibilityState,
getXBlockVisibilityClass, getXBlockListTypeClass; getXBlockVisibilityClass, getXBlockListTypeClass, updateXBlockFields;
/** /**
* Represents the possible visibility states for an xblock: * Represents the possible visibility states for an xblock:
...@@ -105,9 +105,9 @@ define(["jquery", "underscore", "gettext", "js/views/utils/view_utils", "js/util ...@@ -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. * Updates the specified field of an xblock to a new value.
* @param xblockInfo The XBlockInfo model representing the xblock. * @param {Backbone Model} xblockInfo The XBlockInfo model representing the xblock.
* @param fieldName The xblock field name to be updated. * @param {String} fieldName The xblock field name to be updated.
* @param newValue The new value for the field. * @param {*} newValue The new value for the field.
* @returns {jQuery promise} A promise representing the updating of the field. * @returns {jQuery promise} A promise representing the updating of the field.
*/ */
updateXBlockField = function(xblockInfo, fieldName, newValue) { updateXBlockField = function(xblockInfo, fieldName, newValue) {
...@@ -119,6 +119,22 @@ define(["jquery", "underscore", "gettext", "js/views/utils/view_utils", "js/util ...@@ -119,6 +119,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&hellip;'),
function() {
return xblockInfo.save(xblockData, options);
}
);
};
/**
* Returns the CSS class to represent the specified xblock visibility state. * Returns the CSS class to represent the specified xblock visibility state.
*/ */
getXBlockVisibilityClass = function(visibilityState) { getXBlockVisibilityClass = function(visibilityState) {
...@@ -155,6 +171,7 @@ define(["jquery", "underscore", "gettext", "js/views/utils/view_utils", "js/util ...@@ -155,6 +171,7 @@ define(["jquery", "underscore", "gettext", "js/views/utils/view_utils", "js/util
'deleteXBlock': deleteXBlock, 'deleteXBlock': deleteXBlock,
'updateXBlockField': updateXBlockField, 'updateXBlockField': updateXBlockField,
'getXBlockVisibilityClass': getXBlockVisibilityClass, 'getXBlockVisibilityClass': getXBlockVisibilityClass,
'getXBlockListTypeClass': getXBlockListTypeClass 'getXBlockListTypeClass': getXBlockListTypeClass,
'updateXBlockFields': updateXBlockFields
}; };
}); });
...@@ -5,6 +5,7 @@ ...@@ -5,6 +5,7 @@
[class*="view-"] { [class*="view-"] {
// basic modal content // basic modal content
// ------------------------
.modal-window { .modal-window {
@extend %ui-depth3; @extend %ui-depth3;
@include box-sizing(border-box); @include box-sizing(border-box);
...@@ -51,6 +52,40 @@ ...@@ -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 // TODO: need to sync up (alongside general editing mode) with xblocks.scss UI
.modal-chin, .modal-chin,
.xblock-actions, .xblock-actions,
...@@ -86,6 +121,7 @@ ...@@ -86,6 +121,7 @@
// small modals - quick editors and dialogs // small modals - quick editors and dialogs
// ------------------------
.modal-sm { .modal-sm {
width: 30%; width: 30%;
min-width: ($baseline*15); min-width: ($baseline*15);
...@@ -96,6 +132,7 @@ ...@@ -96,6 +132,7 @@
} }
// medium modals - forms and interactives // medium modals - forms and interactives
// ------------------------
.modal-med { .modal-med {
width: 40%; width: 40%;
min-width: ($baseline*18); min-width: ($baseline*18);
...@@ -106,11 +143,16 @@ ...@@ -106,11 +143,16 @@
} }
// large modals - component editors and interactives // large modals - component editors and interactives
// ------------------------
.modal-lg { .modal-lg {
width: 70%; width: 70%;
min-width: ($baseline*27.5); min-width: ($baseline*27.5);
height: auto; height: auto;
.modal-content {
padding: $baseline;
}
&.modal-editor { &.modal-editor {
.modal-header { .modal-header {
...@@ -162,6 +204,7 @@ ...@@ -162,6 +204,7 @@
// specific modal overrides // specific modal overrides
// ------------------------
// upload modal // upload modal
.assetupload-modal { .assetupload-modal {
...@@ -191,6 +234,80 @@ ...@@ -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 // xblock custom actions
.modal-window .editor-with-buttons { .modal-window .editor-with-buttons {
margin-bottom: ($baseline*3); margin-bottom: ($baseline*3);
......
...@@ -28,7 +28,8 @@ from contentstore.utils import reverse_usage_url ...@@ -28,7 +28,8 @@ from contentstore.utils import reverse_usage_url
</%block> </%block>
<%block name="header_extras"> <%block name="header_extras">
% for template_name in ['course-outline', 'xblock-string-field-editor']: <link rel="stylesheet" type="text/css" href="${static.url('js/vendor/timepicker/jquery.timepicker.css')}" />
% for template_name in ['course-outline', 'xblock-string-field-editor', 'basic-modal', 'modal-button', 'edit-outline-item-modal']:
<script type="text/template" id="${template_name}-tpl"> <script type="text/template" id="${template_name}-tpl">
<%static:include path="js/${template_name}.underscore" /> <%static:include path="js/${template_name}.underscore" />
</script> </script>
......
...@@ -4,7 +4,7 @@ ...@@ -4,7 +4,7 @@
aria-hidden="" aria-hidden=""
role="dialog"> role="dialog">
<div class="modal-window-overlay"></div> <div class="modal-window-overlay"></div>
<div class="modal-window confirm modal-editor modal-<%= size %> modal-type-<%= type %>"> <div class="modal-window <%= viewSpecificClasses %> modal-<%= size %> modal-type-<%= type %>">
<div class="<%= name %>-modal"> <div class="<%= name %>-modal">
<div class="modal-header"> <div class="modal-header">
<h2 class="title modal-window-title"><%= title %></h2> <h2 class="title modal-window-title"><%= title %></h2>
......
...@@ -42,8 +42,7 @@ if (statusType === 'warning') { ...@@ -42,8 +42,7 @@ if (statusType === 'warning') {
<% } else { %> <% } else { %>
<h3 class="<%= xblockType %>-header-details"> <h3 class="<%= xblockType %>-header-details">
<% } %> <% } %>
<% if (xblockInfo.isVertical()) { %>
<% if (category === 'vertical') { %>
<span class="unit-title item-title"> <span class="unit-title item-title">
<a href="<%= xblockInfo.get('studio_url') %>"><%= xblockInfo.get('display_name') %></a> <a href="<%= xblockInfo.get('studio_url') %>"><%= xblockInfo.get('display_name') %></a>
</span> </span>
...@@ -56,6 +55,14 @@ if (statusType === 'warning') { ...@@ -56,6 +55,14 @@ if (statusType === 'warning') {
<div class="<%= xblockType %>-header-actions"> <div class="<%= xblockType %>-header-actions">
<ul class="actions-list"> <ul class="actions-list">
<% if (xblockInfo.isEditableOnCourseOutline()) { %>
<li class="action-item action-configure">
<a href="#" data-tooltip="<%= gettext('Configure') %>" class="configure-button action-button">
<i class="icon-gear"></i>
<span class="sr action-button-text"><%= gettext('Configure') %></span>
</a>
</li>
<% } %>
<li class="action-item action-delete"> <li class="action-item action-delete">
<a href="#" data-tooltip="<%= gettext('Delete') %>" class="delete-button action-button"> <a href="#" data-tooltip="<%= gettext('Delete') %>" class="delete-button action-button">
<i class="icon icon-trash"></i> <i class="icon icon-trash"></i>
...@@ -66,7 +73,7 @@ if (statusType === 'warning') { ...@@ -66,7 +73,7 @@ if (statusType === 'warning') {
</div> </div>
</div> </div>
<div class="<%= xblockType %>-status"> <div class="<%= xblockType %>-status">
<% if (category !== 'vertical') { %> <% if (!xblockInfo.isVertical()) { %>
<div class="status-release"> <div class="status-release">
<p> <p>
<span class="sr status-release-label"><%= gettext('Release Status:') %></span> <span class="sr status-release-label"><%= gettext('Release Status:') %></span>
...@@ -85,6 +92,17 @@ if (statusType === 'warning') { ...@@ -85,6 +92,17 @@ if (statusType === 'warning') {
<%= xblockInfo.get('release_date') %> <%= xblockInfo.get('release_date') %>
<% } %> <% } %>
</span> </span>
<% if (xblockInfo.get('due_date')) { %>
<span class="due-date">
<i class="icon-time"></i>
<%= gettext('Due date:') %> <%= xblockInfo.get('due_date') %>
</span>
<% } %>
<% if (xblockInfo.get('graded')) { %>
<span class="policy">
<%= gettext('Policy:') %> <%= xblockInfo.get('format') %>
</span>
<% } %>
</p> </p>
</div> </div>
<% } %> <% } %>
......
<div class="xblock-editor" data-locator="<%= xblockInfo.get('id') %>" data-course-key="<%= xblockInfo.get('courseKey') %>">
<div class="message modal-introduction">
<% if (xblockInfo.isChapter() || xblockInfo.isSequential()) { %>
<p>
<%= interpolate(gettext("Change the settings for %(display_name)s"), {display_name: xblockInfo.get('display_name')}, true) %>
</p>
<% } %>
</div>
<form class="edit-settings-form" action="#">
<div class="modal-section edit-settings-release scheduled-date-input">
<h3 class="modal-section-title"><%= gettext('Release Date and Time') %></h3>
<div class="modal-section-content has-actions">
<ul class="list-fields list-input datepair">
<li class="field field-text field-start-date field-release-date">
<label for="start_date" class="label"><%= gettext('Release Date:') %></label>
<input type="text" id="start_date" name="start_date"
value=""
placeholder="MM/DD/YYYY" class="start-date release-date date input input-text" autocomplete="off" />
</li>
<li class="field field-text field-start-time field-release-time">
<label for="start_time" class="label"><%= gettext('Release Time in UTC:') %></label>
<input type="text" id="start_time" name="start_time"
value=""
placeholder="HH:MM" class="start-time release-time time input input-text" autocomplete="off" />
</li>
</ul>
<% if (xblockInfo.isSequential()) { %>
<ul class="list-actions">
<li class="action-item">
<a href="#" data-tooltip="<%= gettext('Clear Release Date/Time') %>" class="clear-date action-button action-clear">
<i class="icon-remove-sign"></i>
<span class="sr"><%= gettext('Clear Release Date/Time') %></span>
</a>
</li>
</ul>
<% } %>
</div>
</div>
<% if (xblockInfo.isSequential()) { %>
<div class="modal-section edit-settings-grading">
<h3 class="modal-section-title"><%= gettext('Grading') %></h3>
<div class="modal-section-content grading-type">
<ul class="list-fields list-input">
<li class="field field-grading-type field-select">
<label for="grading_type" class="label"><%= gettext('Grade as:') %></label>
<select class="input" id="grading_type">
<option value="notgraded"><%= gettext('Not Graded') %></option>
<% _.each(graderTypes, function(grader) { %>
<option value="<%= grader %>"><%= grader %></option>
<% }); %>
</select>
</li>
</ul>
</div>
<div class="modal-section-content has-actions due-date-input grading-due-date">
<ul class="list-fields list-input datepair date-setter">
<li class="field field-text field-due-date">
<label for="due_date"><%= gettext('Due Date:') %></label>
<input type="text" id="due_date" name="due_date"
value=""
placeholder="MM/DD/YYYY" class="date input input-text" autocomplete="off"/>
</li>
<li class="field field-text field-due-time">
<label for="due_time"><%= gettext('Due Time in UTC:') %></label>
<input type="text" id="due_time" name="due_time"
value=""
placeholder="HH:MM" class="time input input-text" autocomplete="off" />
</li>
</ul>
<ul class="list-actions">
<li class="action-item">
<a href="#" data-tooltip="<%= gettext('Clear Grading Due Date') %>" class="clear-date action-button action-clear">
<i class="icon-remove-sign"></i>
<span class="sr"><%= gettext('Clear Grading Due Date') %></span>
</a>
</li>
</ul>
</div>
</div>
<% } %>
</form>
</div>
...@@ -18,6 +18,10 @@ class ProgressPage(CoursePage): ...@@ -18,6 +18,10 @@ class ProgressPage(CoursePage):
) )
return is_present return is_present
@property
def grading_formats(self):
return [label.replace(' Scores:', '') for label in self.q(css="div.scores h3").text]
def scores(self, chapter, section): def scores(self, chapter, section):
""" """
Return a list of (points, max_points) tuples representing the scores Return a list of (points, max_points) tuples representing the scores
......
""" """
Course Outline page in Studio. Course Outline page in Studio.
""" """
import datetime
from bok_choy.page_object import PageObject from bok_choy.page_object import PageObject
from bok_choy.promise import EmptyPromise from bok_choy.promise import EmptyPromise
from selenium.webdriver.support.ui import Select
from .course_page import CoursePage from .course_page import CoursePage
from .container import ContainerPage from .container import ContainerPage
from .utils import set_input_value_and_save, click_css, confirm_prompt, set_input_value from .utils import set_input_value_and_save, click_css, confirm_prompt, set_input_value
...@@ -19,6 +23,7 @@ class CourseOutlineItem(object): ...@@ -19,6 +23,7 @@ class CourseOutlineItem(object):
NAME_INPUT_SELECTOR = '.xblock-field-input' NAME_INPUT_SELECTOR = '.xblock-field-input'
NAME_FIELD_WRAPPER_SELECTOR = '.xblock-title .wrapper-xblock-field' NAME_FIELD_WRAPPER_SELECTOR = '.xblock-title .wrapper-xblock-field'
STATUS_MESSAGE_SELECTOR = '> div[class$="status"] .status-message' STATUS_MESSAGE_SELECTOR = '> div[class$="status"] .status-message'
CONFIGURATION_BUTTON_SELECTOR = '.action-item .configure-button'
def __repr__(self): def __repr__(self):
# CourseOutlineItem is also used as a mixin for CourseOutlinePage, which doesn't have a locator # CourseOutlineItem is also used as a mixin for CourseOutlinePage, which doesn't have a locator
...@@ -94,6 +99,27 @@ class CourseOutlineItem(object): ...@@ -94,6 +99,27 @@ class CourseOutlineItem(object):
css=self._bounded_selector(self.NAME_FIELD_WRAPPER_SELECTOR) css=self._bounded_selector(self.NAME_FIELD_WRAPPER_SELECTOR)
)[0].get_attribute("class") )[0].get_attribute("class")
def edit(self):
self.q(css=self._bounded_selector(self.CONFIGURATION_BUTTON_SELECTOR)).first.click()
modal = CourseOutlineModal(self)
EmptyPromise(lambda: modal.is_shown(), 'Modal is shown.')
return modal
@property
def release_date(self):
element = self.q(css=self._bounded_selector(".status-release-value"))
return element.first.text[0] if element.present else None
@property
def due_date(self):
element = self.q(css=self._bounded_selector(".due-date"))
return element.first.text[0] if element.present else None
@property
def policy(self):
element = self.q(css=self._bounded_selector(".policy"))
return element.first.text[0] if element.present else None
class CourseOutlineContainer(CourseOutlineItem): class CourseOutlineContainer(CourseOutlineItem):
""" """
...@@ -413,3 +439,115 @@ class CourseOutlinePage(CoursePage, CourseOutlineContainer): ...@@ -413,3 +439,115 @@ class CourseOutlinePage(CoursePage, CourseOutlineContainer):
return ExpandCollapseLinkState.COLLAPSE return ExpandCollapseLinkState.COLLAPSE
else: else:
return ExpandCollapseLinkState.EXPAND return ExpandCollapseLinkState.EXPAND
class CourseOutlineModal(object):
MODAL_SELECTOR = ".edit-outline-item-modal"
def __init__(self, page):
self.page = page
def _bounded_selector(self, selector):
"""
Returns `selector`, but limited to this particular `CourseOutlineModal` context.
"""
return " ".join([self.MODAL_SELECTOR, selector])
def is_shown(self):
return self.page.q(css=self.MODAL_SELECTOR).present
def find_css(self, selector):
return self.page.q(css=self._bounded_selector(selector))
def click(self, selector, index=0):
self.find_css(selector).nth(index).click()
def save(self):
self.click(".action-save")
self.page.wait_for_ajax()
def cancel(self):
self.click(".action-cancel")
def has_release_date(self):
return self.find_css("#start_date").present
def has_due_date(self):
return self.find_css("#due_date").present
def has_policy(self):
return self.find_css("#grading_type").present
def set_date(self, property_name, input_selector, date):
"""
Set `date` value to input pointed by `selector` and `property_name`.
"""
month, day, year = map(int, date.split('/'))
self.click(input_selector)
if getattr(self, property_name):
current_month, current_year = map(int, getattr(self, property_name).split('/')[1:])
else: # Use default timepicker values, which are current month and year.
current_month, current_year = datetime.datetime.today().month, datetime.datetime.today().year
date_diff = 12 * (year - current_year) + month - current_month
selector = "a.ui-datepicker-{}".format('next' if date_diff > 0 else 'prev')
for i in xrange(abs(date_diff)):
self.page.q(css=selector).click()
self.page.q(css="a.ui-state-default").nth(day - 1).click() # set day
EmptyPromise(
lambda: getattr(self, property_name) == u'{m}/{d}/{y}'.format(m=month, d=day, y=year),
"{} is updated in modal.".format(property_name)
).fulfill()
@property
def release_date(self):
return self.find_css("#start_date").first.attrs('value')[0]
@release_date.setter
def release_date(self, date):
"""
Date is "mm/dd/yyyy" string.
"""
self.set_date('release_date', "#start_date", date)
@property
def due_date(self):
return self.find_css("#due_date").first.attrs('value')[0]
@due_date.setter
def due_date(self, date):
"""
Date is "mm/dd/yyyy" string.
"""
self.set_date('due_date', "#due_date", date)
@property
def policy(self):
"""
Select the grading format with `value` in the drop-down list.
"""
element = self.find_css('#grading_type')[0]
return self.get_selected_option_text(element)
@policy.setter
def policy(self, grading_label):
"""
Select the grading format with `value` in the drop-down list.
"""
element = self.find_css('#grading_type')[0]
select = Select(element)
select.select_by_visible_text(grading_label)
EmptyPromise(
lambda: self.policy == grading_label,
"Grading label is updated.",
).fulfill()
def get_selected_option_text(self, element):
"""
Returns the text of the first selected option for the element.
"""
if element:
select = Select(element)
return select.first_selected_option.text
else:
return None
...@@ -14,7 +14,6 @@ class StudioCourseTest(UniqueCourseTest): ...@@ -14,7 +14,6 @@ class StudioCourseTest(UniqueCourseTest):
Install a course with no content using a fixture. Install a course with no content using a fixture.
""" """
super(StudioCourseTest, self).setUp() super(StudioCourseTest, self).setUp()
self.course_fixture = CourseFixture( self.course_fixture = CourseFixture(
self.course_info['org'], self.course_info['org'],
self.course_info['number'], self.course_info['number'],
......
...@@ -13,6 +13,13 @@ from ..pages.lms.courseware import CoursewarePage ...@@ -13,6 +13,13 @@ from ..pages.lms.courseware import CoursewarePage
from ..fixtures.course import XBlockFixtureDesc from ..fixtures.course import XBlockFixtureDesc
from .base_studio_test import StudioCourseTest from .base_studio_test import StudioCourseTest
from .helpers import load_data_str
from ..pages.lms.progress import ProgressPage
SECTION_NAME = 'Test Section'
SUBSECTION_NAME = 'Test Subsection'
UNIT_NAME = 'Test Unit'
class CourseOutlineTest(StudioCourseTest): class CourseOutlineTest(StudioCourseTest):
...@@ -20,8 +27,6 @@ class CourseOutlineTest(StudioCourseTest): ...@@ -20,8 +27,6 @@ class CourseOutlineTest(StudioCourseTest):
Base class for all course outline tests Base class for all course outline tests
""" """
COURSE_ID_SEPARATOR = "."
def setUp(self): def setUp(self):
""" """
Install a course with no content using a fixture. Install a course with no content using a fixture.
...@@ -34,9 +39,10 @@ class CourseOutlineTest(StudioCourseTest): ...@@ -34,9 +39,10 @@ class CourseOutlineTest(StudioCourseTest):
def populate_course_fixture(self, course_fixture): def populate_course_fixture(self, course_fixture):
""" Install a course with sections/problems, tabs, updates, and handouts """ """ Install a course with sections/problems, tabs, updates, and handouts """
course_fixture.add_children( course_fixture.add_children(
XBlockFixtureDesc('chapter', 'Test Section').add_children( XBlockFixtureDesc('chapter', SECTION_NAME).add_children(
XBlockFixtureDesc('sequential', 'Test Subsection').add_children( XBlockFixtureDesc('sequential', SUBSECTION_NAME).add_children(
XBlockFixtureDesc('vertical', 'Test Unit').add_children( XBlockFixtureDesc('vertical', UNIT_NAME).add_children(
XBlockFixtureDesc('problem', 'Test Problem 1', data=load_data_str('multiple_choice.xml')),
XBlockFixtureDesc('html', 'Test HTML Component'), XBlockFixtureDesc('html', 'Test HTML Component'),
XBlockFixtureDesc('discussion', 'Test Discussion Component') XBlockFixtureDesc('discussion', 'Test Discussion Component')
) )
...@@ -247,6 +253,129 @@ class WarningMessagesTest(CourseOutlineTest): ...@@ -247,6 +253,129 @@ class WarningMessagesTest(CourseOutlineTest):
unit.toggle_staff_lock() unit.toggle_staff_lock()
class EditingSectionsTest(CourseOutlineTest):
"""
Feature: Editing Release date, Due date and grading type.
"""
__test__ = True
def test_can_edit_subsection(self):
"""
Scenario: I can edit settings of subsection.
Given that I have created a subsection
Then I see release date, due date and grading policy of subsection in course outline
When I click on the configuration icon
Then edit modal window is shown
And release date, due date and grading policy fields present
And they have correct initial values
Then I set new values for these fields
And I click save button on the modal
Then I see release date, due date and grading policy of subsection in course outline
"""
self.course_outline_page.visit()
subsection = self.course_outline_page.section(SECTION_NAME).subsection(SUBSECTION_NAME)
# Verify that Release date visible by default
self.assertTrue(subsection.release_date)
# Verify that Due date and Policy hidden by default
self.assertFalse(subsection.due_date)
self.assertFalse(subsection.policy)
modal = subsection.edit()
# Verify fields
self.assertTrue(modal.has_release_date())
self.assertTrue(modal.has_due_date())
self.assertTrue(modal.has_policy())
# Verify initial values
self.assertEqual(modal.release_date, u'1/1/1970')
self.assertEqual(modal.due_date, u'')
self.assertEqual(modal.policy, u'Not Graded')
# Set new values
modal.release_date = '3/12/1972'
modal.due_date = '7/21/2014'
modal.policy = 'Lab'
modal.save()
self.assertIn(u'Released: Mar 12, 1972', subsection.release_date)
self.assertIn(u'Due date: Jul 21, 2014', subsection.due_date)
self.assertIn(u'Policy: Lab', subsection.policy)
def test_can_edit_section(self):
"""
Scenario: I can edit settings of section.
Given that I have created a section
Then I see release date of section in course outline
When I click on the configuration icon
Then edit modal window is shown
And release date field present
And it has correct initial value
Then I set new value for this field
And I click save button on the modal
Then I see release date of section in course outline
"""
self.course_outline_page.visit()
section = self.course_outline_page.section(SECTION_NAME)
# Verify that Release date visible by default
self.assertTrue(section.release_date)
# Verify that Due date and Policy are not present
self.assertFalse(section.due_date)
self.assertFalse(section.policy)
modal = section.edit()
# Verify fields
self.assertTrue(modal.has_release_date())
self.assertFalse(modal.has_due_date())
self.assertFalse(modal.has_policy())
# Verify initial value
self.assertEqual(modal.release_date, u'1/1/1970')
# Set new value
modal.release_date = '5/14/1969'
modal.save()
self.assertIn(u'Released: May 14, 1969', section.release_date)
# Verify that Due date and Policy are not present
self.assertFalse(section.due_date)
self.assertFalse(section.policy)
def test_subsection_is_graded_in_lms(self):
"""
Scenario: I can grade subsection from course outline page.
Given I visit progress page
And I see that problem in subsection has grading type "Practice"
Then I visit course outline page
And I click on the configuration icon of subsection
And I set grading policy to "Lab"
And I click save button on the modal
Then I visit progress page
And I see that problem in subsection has grading type "Problem"
"""
progress_page = ProgressPage(self.browser, self.course_id)
progress_page.visit()
progress_page.wait_for_page()
self.assertEqual(u'Practice', progress_page.grading_formats[0])
self.course_outline_page.visit()
subsection = self.course_outline_page.section(SECTION_NAME).subsection(SUBSECTION_NAME)
modal = subsection.edit()
# Set new values
modal.policy = 'Lab'
modal.save()
progress_page.visit()
self.assertEqual(u'Problem', progress_page.grading_formats[0])
class EditNamesTest(CourseOutlineTest): class EditNamesTest(CourseOutlineTest):
""" """
Feature: Click-to-edit section/subsection names Feature: Click-to-edit section/subsection names
...@@ -790,9 +919,10 @@ class DefaultStatesContentTest(CourseOutlineTest): ...@@ -790,9 +919,10 @@ class DefaultStatesContentTest(CourseOutlineTest):
self.course_outline_page.view_live() self.course_outline_page.view_live()
courseware = CoursewarePage(self.browser, self.course_id) courseware = CoursewarePage(self.browser, self.course_id)
courseware.wait_for_page() courseware.wait_for_page()
self.assertEqual(courseware.num_xblock_components, 2) self.assertEqual(courseware.num_xblock_components, 3)
self.assertEqual(courseware.xblock_component_type(0), 'html') self.assertEqual(courseware.xblock_component_type(0), 'problem')
self.assertEqual(courseware.xblock_component_type(1), 'discussion') self.assertEqual(courseware.xblock_component_type(1), 'html')
self.assertEqual(courseware.xblock_component_type(2), 'discussion')
class UnitNavigationTest(CourseOutlineTest): class UnitNavigationTest(CourseOutlineTest):
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment