Course Updates date validation
TNL-4115. Previously, course updates (which are intended to be posted with
dates, for sorting in the LMS) could be authored in studio with a valid
date, nothing, or a random string as the "date" for the update. As there
is no validation for this in studio, everything succeeded with no warning.
However, the LMS has problems parsing some of these values, and barfs when
loaded by learners.
The fix does two big things:
- gracefully handles invalid dates in LMS. These updates are now treated as
having a date of today, for sorting purposes.
- turns on validation in studio. Now, it is not only impossible to enter
invalid dates in studio, but notifications will draw the course author's
eye if any invalid updates were previously saved.
Test additions for this commit:
Adds:
- unit test for LMS parsing
- Jasmine test to confirm invalid dates cannot be set by the user
-also adds event to setAndValidate instead of using a global object
- fix for lettuce test
-It is no longer valid to enter the string "January 1, 2013" as this test
had been doing. Keyed-in entries must use MM/DD/YY format.
This commit is contained in:
@@ -1,9 +1,11 @@
|
||||
define(["js/views/baseview", "codemirror", "js/models/course_update",
|
||||
define(["js/views/validation", "codemirror", "js/models/course_update",
|
||||
"common/js/components/views/feedback_prompt", "common/js/components/views/feedback_notification",
|
||||
"js/views/course_info_helper", "js/utils/modal"],
|
||||
function(BaseView, CodeMirror, CourseUpdateModel, PromptView, NotificationView, CourseInfoHelper, ModalUtils) {
|
||||
"js/views/course_info_helper", "js/utils/modal", "js/utils/date_utils"],
|
||||
function(ValidatingView, CodeMirror, CourseUpdateModel, PromptView, NotificationView,
|
||||
CourseInfoHelper, ModalUtils, DateUtils) {
|
||||
|
||||
var CourseInfoUpdateView = BaseView.extend({
|
||||
'use strict';
|
||||
var CourseInfoUpdateView = ValidatingView.extend({
|
||||
|
||||
// collection is CourseUpdateCollection
|
||||
events: {
|
||||
@@ -19,6 +21,7 @@ define(["js/views/baseview", "codemirror", "js/models/course_update",
|
||||
this.render();
|
||||
// when the client refetches the updates as a whole, re-render them
|
||||
this.listenTo(this.collection, 'reset', this.render);
|
||||
this.listenTo(this.collection, 'invalid', this.handleValidationError);
|
||||
},
|
||||
|
||||
render: function () {
|
||||
@@ -27,22 +30,68 @@ define(["js/views/baseview", "codemirror", "js/models/course_update",
|
||||
// remove and then add all children
|
||||
$(updateEle).empty();
|
||||
var self = this;
|
||||
this.collection.each(function (update) {
|
||||
this.collection.each(function (update, index) {
|
||||
try {
|
||||
CourseInfoHelper.changeContentToPreview(
|
||||
update, 'content', self.options['base_asset_url']);
|
||||
// push notification is always disabled for existing updates
|
||||
var newEle = self.template({ updateModel : update, push_notification_enabled : false });
|
||||
$(updateEle).append(newEle);
|
||||
DateUtils.setupDatePicker("date", self, index);
|
||||
update.isValid();
|
||||
} catch (e) {
|
||||
// ignore
|
||||
}
|
||||
});
|
||||
this.$el.find(".new-update-form").hide();
|
||||
this.$el.find('.date').datepicker({ 'dateFormat': 'MM d, yy' });
|
||||
return this;
|
||||
},
|
||||
|
||||
collectionSelector: function(uid) {
|
||||
return "course-update-list li[name=" + uid + "]";
|
||||
},
|
||||
|
||||
setAndValidate: function(attr, value, event) {
|
||||
if (attr === 'date') {
|
||||
// If the value to be set was typed, validate that entry rather than the current datepicker value
|
||||
if (this.dateEntry(event).length > 0) {
|
||||
value = DateUtils.parseDateFromString(this.dateEntry(event).val());
|
||||
if (value && isNaN(value.getTime())) {
|
||||
value = "";
|
||||
}
|
||||
}
|
||||
value = $.datepicker.formatDate("MM d, yy", value);
|
||||
}
|
||||
var targetModel = this.collection.get(this.$currentPost.attr('name'));
|
||||
var prevValue = targetModel.get(attr);
|
||||
if (prevValue !== value) {
|
||||
targetModel.set(attr, value);
|
||||
this.validateModel(targetModel);
|
||||
}
|
||||
},
|
||||
|
||||
handleValidationError : function(model, error) {
|
||||
var ele = this.$el.find('#course-update-list li[name=\"'+model.cid+'\"');
|
||||
$(ele).find('.message-error').remove();
|
||||
for (var field in error) {
|
||||
if (error.hasOwnProperty(field)) {
|
||||
$(ele).find('#update-date-'+model.cid).parent().append(
|
||||
this.errorTemplate({message : error[field]})
|
||||
);
|
||||
$(ele).find('.date-display').parent().append(this.errorTemplate({message : error[field]}));
|
||||
}
|
||||
}
|
||||
$(ele).find('.save-button').addClass('is-disabled');
|
||||
},
|
||||
|
||||
validateModel: function(model) {
|
||||
if (model.isValid()) {
|
||||
var ele = this.$el.find('#course-update-list li[name=\"' + model.cid + '\"');
|
||||
$(ele).find('.message-error').remove();
|
||||
$(ele).find('.save-button').removeClass('is-disabled');
|
||||
}
|
||||
},
|
||||
|
||||
onNew: function(event) {
|
||||
event.preventDefault();
|
||||
var self = this;
|
||||
@@ -75,15 +124,15 @@ define(["js/views/baseview", "codemirror", "js/models/course_update",
|
||||
// Binding empty function to prevent default hideModal.
|
||||
});
|
||||
|
||||
$('.date').datepicker('destroy');
|
||||
$('.date').datepicker({ 'dateFormat': 'MM d, yy' });
|
||||
DateUtils.setupDatePicker("date", this, 0);
|
||||
},
|
||||
|
||||
onSave: function(event) {
|
||||
event.preventDefault();
|
||||
var targetModel = this.eventModel(event);
|
||||
targetModel.set({
|
||||
date : this.dateEntry(event).val(),
|
||||
// translate short-form date (for input) into long form date (for display)
|
||||
date : $.datepicker.formatDate("MM d, yy", new Date(this.dateEntry(event).val())),
|
||||
content : this.$codeMirror.getValue(),
|
||||
push_notification_selected : this.push_notification_selected(event)
|
||||
});
|
||||
@@ -112,11 +161,13 @@ define(["js/views/baseview", "codemirror", "js/models/course_update",
|
||||
|
||||
onCancel: function(event) {
|
||||
event.preventDefault();
|
||||
// change editor contents back to model values and hide the editor
|
||||
$(this.editor(event)).hide();
|
||||
// If the model was never created (user created a new update, then pressed Cancel),
|
||||
// we wish to remove it from the DOM.
|
||||
// Since we're cancelling, the model should be using it's previous attributes
|
||||
var targetModel = this.eventModel(event);
|
||||
targetModel.set(targetModel.previousAttributes());
|
||||
this.validateModel(targetModel);
|
||||
// Hide the editor
|
||||
$(this.editor(event)).hide();
|
||||
// targetModel will be lacking an id if it was newly created
|
||||
this.closeEditor(!targetModel.id);
|
||||
},
|
||||
|
||||
@@ -129,6 +180,13 @@ define(["js/views/baseview", "codemirror", "js/models/course_update",
|
||||
$(this.editor(event)).show();
|
||||
var $textArea = this.$currentPost.find(".new-update-content").first();
|
||||
var targetModel = this.eventModel(event);
|
||||
// translate long-form date (for viewing) into short-form date (for input)
|
||||
if (targetModel.get('date') && targetModel.isValid()) {
|
||||
$(this.dateEntry(event)).val($.datepicker.formatDate("mm/dd/yy", new Date(targetModel.get('date'))));
|
||||
}
|
||||
else {
|
||||
$(this.dateEntry(event)).val("MM/DD/YY");
|
||||
}
|
||||
this.$codeMirror = CourseInfoHelper.editWithCodeMirror(
|
||||
targetModel, 'content', self.options['base_asset_url'], $textArea.get(0));
|
||||
|
||||
@@ -138,6 +196,9 @@ define(["js/views/baseview", "codemirror", "js/models/course_update",
|
||||
self.closeEditor(false);
|
||||
}
|
||||
);
|
||||
|
||||
// Ensure validity is marked appropriately
|
||||
targetModel.isValid();
|
||||
},
|
||||
|
||||
onDelete: function(event) {
|
||||
@@ -189,6 +250,8 @@ define(["js/views/baseview", "codemirror", "js/models/course_update",
|
||||
closeEditor: function(removePost) {
|
||||
var targetModel = this.collection.get(this.$currentPost.attr('name'));
|
||||
|
||||
// If the model was never created (user created a new update, then pressed Cancel),
|
||||
// we wish to remove it from the DOM.
|
||||
if(removePost) {
|
||||
this.$currentPost.remove();
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
define(["js/views/validation", "codemirror", "underscore", "jquery", "jquery.ui", "js/utils/date_utils", "js/models/uploads",
|
||||
"js/views/uploads", "js/utils/change_on_enter", "js/views/license", "js/models/license",
|
||||
"js/views/uploads", "js/views/license", "js/models/license",
|
||||
"common/js/components/views/feedback_notification", "jquery.timepicker", "date", "gettext"],
|
||||
function(ValidatingView, CodeMirror, _, $, ui, DateUtils, FileUploadModel,
|
||||
FileUploadDialog, TriggerChangeEventOnEnter, LicenseView, LicenseModel, NotificationView,
|
||||
FileUploadDialog, LicenseView, LicenseModel, NotificationView,
|
||||
timepicker, date, gettext) {
|
||||
|
||||
var DetailsView = ValidatingView.extend({
|
||||
@@ -63,10 +63,10 @@ var DetailsView = ValidatingView.extend({
|
||||
},
|
||||
|
||||
render: function() {
|
||||
this.setupDatePicker('start_date');
|
||||
this.setupDatePicker('end_date');
|
||||
this.setupDatePicker('enrollment_start');
|
||||
this.setupDatePicker('enrollment_end');
|
||||
DateUtils.setupDatePicker('start_date', this);
|
||||
DateUtils.setupDatePicker('end_date', this);
|
||||
DateUtils.setupDatePicker('enrollment_start', this);
|
||||
DateUtils.setupDatePicker('enrollment_end', this);
|
||||
|
||||
this.$el.find('#' + this.fieldToSelectorMap['overview']).val(this.model.get('overview'));
|
||||
this.codeMirrorize(null, $('#course-overview')[0]);
|
||||
@@ -147,51 +147,6 @@ var DetailsView = ValidatingView.extend({
|
||||
}, true));
|
||||
},
|
||||
|
||||
setupDatePicker: function (fieldName) {
|
||||
var cacheModel = this.model;
|
||||
var div = this.$el.find('#' + this.fieldToSelectorMap[fieldName]);
|
||||
var datefield = $(div).find("input.date");
|
||||
var timefield = $(div).find("input.time");
|
||||
var cachethis = this;
|
||||
var setfield = function () {
|
||||
var newVal = DateUtils.getDate(datefield, timefield),
|
||||
oldTime = new Date(cacheModel.get(fieldName)).getTime();
|
||||
if (newVal) {
|
||||
if (!cacheModel.has(fieldName) || oldTime !== newVal.getTime()) {
|
||||
cachethis.clearValidationErrors();
|
||||
cachethis.setAndValidate(fieldName, newVal);
|
||||
}
|
||||
}
|
||||
else {
|
||||
// Clear date (note that this clears the time as well, as date and time are linked).
|
||||
// Note also that the validation logic prevents us from clearing the start date
|
||||
// (start date is required by the back end).
|
||||
cachethis.clearValidationErrors();
|
||||
cachethis.setAndValidate(fieldName, null);
|
||||
}
|
||||
};
|
||||
|
||||
// instrument as date and time pickers
|
||||
timefield.timepicker({'timeFormat' : 'H:i'});
|
||||
datefield.datepicker();
|
||||
|
||||
// Using the change event causes setfield to be triggered twice, but it is necessary
|
||||
// to pick up when the date is typed directly in the field.
|
||||
datefield.change(setfield).keyup(TriggerChangeEventOnEnter);
|
||||
timefield.on('changeTime', setfield);
|
||||
timefield.on('input', setfield);
|
||||
|
||||
date = this.model.get(fieldName)
|
||||
// timepicker doesn't let us set null, so check that we have a time
|
||||
if (date) {
|
||||
DateUtils.setDate(datefield, timefield, date);
|
||||
} // but reset fields either way
|
||||
else {
|
||||
timefield.val('');
|
||||
datefield.val('');
|
||||
}
|
||||
},
|
||||
|
||||
updateModel: function(event) {
|
||||
switch (event.currentTarget.id) {
|
||||
case 'course-language':
|
||||
|
||||
Reference in New Issue
Block a user