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
from uuid import uuid4
from datetime import datetime
from pytz import UTC
import json
from collections import OrderedDict
from functools import partial
......@@ -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
published = modulestore().has_item(xblock.location, revision=ModuleStoreEnum.RevisionOption.published_only)
graders = CourseGradingModel.fetch(xblock.location.course_key).graders
xblock_info = {
"id": unicode(xblock.location),
"display_name": xblock.display_name_with_default,
......@@ -653,7 +655,13 @@ def create_xblock_info(xblock, data=None, metadata=None, include_ancestor_info=F
'studio_url': xblock_studio_url(xblock),
"released_to_students": datetime.now(UTC) > xblock.start,
"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:
xblock_info["data"] = data
......
......@@ -1168,6 +1168,11 @@ class TestXBlockInfo(ItemTest):
self.assertEqual(xblock_info['display_name'], 'Week 1')
self.assertTrue(xblock_info['published'])
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
self.validate_xblock_info_consistency(xblock_info, has_child_info=has_child_info)
......
......@@ -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",
......
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")
);
......
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;
......
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) {
// 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();
......
/**
* 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() {
......
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) {
......@@ -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.
*/
getXBlockVisibilityClass = function(visibilityState) {
......@@ -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);
......
......@@ -28,7 +28,8 @@ from contentstore.utils import reverse_usage_url
</%block>
<%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">
<%static:include path="js/${template_name}.underscore" />
</script>
......
......@@ -4,7 +4,7 @@
aria-hidden=""
role="dialog">
<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="modal-header">
<h2 class="title modal-window-title"><%= title %></h2>
......
......@@ -42,8 +42,7 @@ if (statusType === 'warning') {
<% } else { %>
<h3 class="<%= xblockType %>-header-details">
<% } %>
<% if (category === 'vertical') { %>
<% if (xblockInfo.isVertical()) { %>
<span class="unit-title item-title">
<a href="<%= xblockInfo.get('studio_url') %>"><%= xblockInfo.get('display_name') %></a>
</span>
......@@ -56,6 +55,14 @@ if (statusType === 'warning') {
<div class="<%= xblockType %>-header-actions">
<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">
<a href="#" data-tooltip="<%= gettext('Delete') %>" class="delete-button action-button">
<i class="icon icon-trash"></i>
......@@ -66,7 +73,7 @@ if (statusType === 'warning') {
</div>
</div>
<div class="<%= xblockType %>-status">
<% if (category !== 'vertical') { %>
<% if (!xblockInfo.isVertical()) { %>
<div class="status-release">
<p>
<span class="sr status-release-label"><%= gettext('Release Status:') %></span>
......@@ -85,6 +92,17 @@ if (statusType === 'warning') {
<%= xblockInfo.get('release_date') %>
<% } %>
</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>
</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):
)
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):
"""
Return a list of (points, max_points) tuples representing the scores
......
"""
Course Outline page in Studio.
"""
import datetime
from bok_choy.page_object import PageObject
from bok_choy.promise import EmptyPromise
from selenium.webdriver.support.ui import Select
from .course_page import CoursePage
from .container import ContainerPage
from .utils import set_input_value_and_save, click_css, confirm_prompt, set_input_value
......@@ -19,6 +23,7 @@ class CourseOutlineItem(object):
NAME_INPUT_SELECTOR = '.xblock-field-input'
NAME_FIELD_WRAPPER_SELECTOR = '.xblock-title .wrapper-xblock-field'
STATUS_MESSAGE_SELECTOR = '> div[class$="status"] .status-message'
CONFIGURATION_BUTTON_SELECTOR = '.action-item .configure-button'
def __repr__(self):
# CourseOutlineItem is also used as a mixin for CourseOutlinePage, which doesn't have a locator
......@@ -94,6 +99,27 @@ class CourseOutlineItem(object):
css=self._bounded_selector(self.NAME_FIELD_WRAPPER_SELECTOR)
)[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):
"""
......@@ -413,3 +439,115 @@ class CourseOutlinePage(CoursePage, CourseOutlineContainer):
return ExpandCollapseLinkState.COLLAPSE
else:
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):
Install a course with no content using a fixture.
"""
super(StudioCourseTest, self).setUp()
self.course_fixture = CourseFixture(
self.course_info['org'],
self.course_info['number'],
......
......@@ -13,6 +13,13 @@ from ..pages.lms.courseware import CoursewarePage
from ..fixtures.course import XBlockFixtureDesc
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):
......@@ -20,8 +27,6 @@ class CourseOutlineTest(StudioCourseTest):
Base class for all course outline tests
"""
COURSE_ID_SEPARATOR = "."
def setUp(self):
"""
Install a course with no content using a fixture.
......@@ -34,9 +39,10 @@ class CourseOutlineTest(StudioCourseTest):
def populate_course_fixture(self, course_fixture):
""" Install a course with sections/problems, tabs, updates, and handouts """
course_fixture.add_children(
XBlockFixtureDesc('chapter', 'Test Section').add_children(
XBlockFixtureDesc('sequential', 'Test Subsection').add_children(
XBlockFixtureDesc('vertical', 'Test Unit').add_children(
XBlockFixtureDesc('chapter', SECTION_NAME).add_children(
XBlockFixtureDesc('sequential', SUBSECTION_NAME).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('discussion', 'Test Discussion Component')
)
......@@ -49,7 +55,7 @@ class WarningMessagesTest(CourseOutlineTest):
"""
Feature: Warning messages on sections, subsections, and units
"""
__test__ = True
STAFF_ONLY_WARNING = 'Contains staff only content'
......@@ -247,6 +253,129 @@ class WarningMessagesTest(CourseOutlineTest):
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):
"""
Feature: Click-to-edit section/subsection names
......@@ -790,9 +919,10 @@ class DefaultStatesContentTest(CourseOutlineTest):
self.course_outline_page.view_live()
courseware = CoursewarePage(self.browser, self.course_id)
courseware.wait_for_page()
self.assertEqual(courseware.num_xblock_components, 2)
self.assertEqual(courseware.xblock_component_type(0), 'html')
self.assertEqual(courseware.xblock_component_type(1), 'discussion')
self.assertEqual(courseware.num_xblock_components, 3)
self.assertEqual(courseware.xblock_component_type(0), 'problem')
self.assertEqual(courseware.xblock_component_type(1), 'html')
self.assertEqual(courseware.xblock_component_type(2), 'discussion')
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