Details now has validation except for youtube ids which cause
crossdomain policy violations when i try to validate.
This commit is contained in:
@@ -33,20 +33,57 @@ CMS.Models.Settings.CourseDetails = Backbone.Model.extend({
|
||||
return attributes;
|
||||
},
|
||||
|
||||
validate: function(newattrs) {
|
||||
// Returns either nothing (no return call) so that validate works or an object of {field: errorstring} pairs
|
||||
// A bit funny in that the video key validation is asynchronous; so, it won't stop the validation.
|
||||
var errors = {};
|
||||
if (newattrs.start_date && newattrs.end_date && newattrs.start_date >= newattrs.end_date) {
|
||||
errors.end_date = "The course end date cannot be before the course start date.";
|
||||
}
|
||||
if (newattrs.start_date && newattrs.enrollment_start && newattrs.start_date < newattrs.enrollment_start) {
|
||||
errors.enrollment_start = "The course start date cannot be before the enrollment start date.";
|
||||
}
|
||||
if (newattrs.enrollment_start && newattrs.enrollment_end && newattrs.enrollment_start >= newattrs.enrollment_end) {
|
||||
errors.enrollment_end = "The enrollment start date cannot be after the enrollment end date.";
|
||||
}
|
||||
if (newattrs.end_date && newattrs.enrollment_end && newattrs.end_date < newattrs.enrollment_end) {
|
||||
errors.enrollment_end = "The enrollment end date cannot be after the course end date.";
|
||||
}
|
||||
if (newattrs.intro_video && newattrs.intro_video != this.get('intro_video')) {
|
||||
var videos = this.parse_videosource(newattrs.intro_video);
|
||||
var vid_errors = new Array();
|
||||
var cachethis = this;
|
||||
for (var i=0; i<videos.length; i++) {
|
||||
// doesn't call parseFloat or Number b/c they stop on first non parsable and return what they have
|
||||
if (!isFinite(videos[i].speed)) vid_errors.push(videos[i].speed + " is not a valid speed.");
|
||||
// can't use get from client to test if video exists b/c of CORS (crossbrowser get not allowed)
|
||||
// GET "http://gdata.youtube.com/feeds/api/videos/" + videokey
|
||||
}
|
||||
if (!_.isEmpty(vid_errors)) {
|
||||
errors.intro_video = vid_errors.join('/n');
|
||||
}
|
||||
}
|
||||
if (!_.isEmpty(errors)) return errors;
|
||||
// NOTE don't return empty errors as that will be interpreted as an error state
|
||||
},
|
||||
|
||||
urlRoot: function() {
|
||||
var location = this.get('location');
|
||||
return '/' + location.get('org') + "/" + location.get('course') + '/settings/' + location.get('name') + '/section/details';
|
||||
},
|
||||
|
||||
_videoprefix : /\s*<video\s*youtube="/g,
|
||||
_videospeedparse : /\d+\.?\d*(?=:)/g,
|
||||
// the below is lax to enable validation
|
||||
_videospeedparse : /[^:]*/g, // /\d+\.?\d*(?=:)/g,
|
||||
_videokeyparse : /([^,\/]+)/g,
|
||||
_videonosuffix : /[^\"]+/g,
|
||||
_getNextMatch : function (regex, string, cursor) {
|
||||
regex.lastIndex = cursor;
|
||||
return regex.exec(string);
|
||||
var result = regex.exec(string);
|
||||
if (_.isArray(result)) return result[0];
|
||||
else return result;
|
||||
},
|
||||
// the whole string for editing
|
||||
// the whole string for editing (put in edit box)
|
||||
getVideoSource: function() {
|
||||
if (this.get('intro_video')) {
|
||||
var cursor = 0;
|
||||
@@ -95,10 +132,32 @@ CMS.Models.Settings.CourseDetails = Backbone.Model.extend({
|
||||
else return "";
|
||||
}
|
||||
},
|
||||
parse_videosource: function(videostring) {
|
||||
// used to validate before set so cannot get from model attr. Returns [{ speed: fff, key: sss }]
|
||||
var cursor = 0;
|
||||
this._getNextMatch(this._videoprefix, videostring, cursor);
|
||||
cursor = this._videoprefix.lastIndex;
|
||||
videostring = this._getNextMatch(this._videonosuffix, videostring, cursor);
|
||||
cursor = 0;
|
||||
// parsed to "fff:kkk,fff:kkk"
|
||||
var result = new Array();
|
||||
while (cursor < videostring.length) {
|
||||
var speed = this._getNextMatch(this._videospeedparse, videostring, cursor);
|
||||
if (speed) cursor = this._videospeedparse.lastIndex + 1;
|
||||
else return result;
|
||||
var key = this._getNextMatch(this._videokeyparse, videostring, cursor);
|
||||
cursor = this._videokeyparse.lastIndex + 1;
|
||||
// See the WTF above
|
||||
if (_.isArray(key)) key = key[0];
|
||||
result.push({speed: speed, key: key});
|
||||
}
|
||||
return result;
|
||||
},
|
||||
save_videosource: function(newsource) {
|
||||
// newsource either is <video youtube="speed:key, *"/> or just the "speed:key, *" string
|
||||
// returns the videosource for the preview which iss the key whose speed is closest to 1
|
||||
if (newsource == null) this.save({'intro_video': null});
|
||||
// TODO remove all whitespace w/in string
|
||||
else if (this._getNextMatch(this._videoprefix, newsource, 0)) this.save('intro_video', newsource);
|
||||
else this.save('intro_video', '<video youtube="' + newsource + '"/>');
|
||||
|
||||
|
||||
@@ -87,43 +87,85 @@ CMS.Views.Settings.Details = Backbone.View.extend({
|
||||
initialize : function() {
|
||||
// TODO move the html frag to a loaded asset
|
||||
this.fileAnchorTemplate = _.template('<a href="<%= fullpath %>"> <i class="ss-icon ss-standard">📄</i><%= filename %></a>');
|
||||
this.errorTemplate = _.template('<span class="message-error"><%= message %></span>');
|
||||
this.model.on('error', this.handleValidationError, this);
|
||||
},
|
||||
|
||||
render: function() {
|
||||
this.setupDatePicker('#course-start', 'start_date');
|
||||
this.setupDatePicker('#course-end', 'end_date');
|
||||
this.setupDatePicker('#enrollment-start', 'enrollment_start');
|
||||
this.setupDatePicker('#enrollment-end', 'enrollment_end');
|
||||
this.setupDatePicker('start_date')
|
||||
this.setupDatePicker('end_date')
|
||||
this.setupDatePicker('enrollment_start')
|
||||
this.setupDatePicker('enrollment_end')
|
||||
|
||||
if (this.model.has('syllabus')) {
|
||||
this.$el.find('.current-course-syllabus .doc-filename').html(
|
||||
this.$el.find(this.fieldToSelectorMap['syllabus']).html(
|
||||
this.fileAnchorTemplate({
|
||||
fullpath : this.model.get('syllabus'),
|
||||
filename: 'syllabus'}));
|
||||
this.$el.find('.remove-course-syllabus').show();
|
||||
}
|
||||
else {
|
||||
this.$el.find('.current-course-syllabus .doc-filename').html("");
|
||||
this.$el.find(this.fieldToSelectorMap['syllabus']).html("");
|
||||
this.$el.find('.remove-course-syllabus').hide();
|
||||
}
|
||||
|
||||
this.$el.find('#course-overview').val(this.model.get('overview'));
|
||||
this.$el.find(this.fieldToSelectorMap['overview']).val(this.model.get('overview'));
|
||||
|
||||
this.$el.find('.current-course-introduction-video iframe').attr('src', this.model.videosourceSample());
|
||||
if (this.model.has('intro_video')) {
|
||||
this.$el.find('.remove-course-introduction-video').show();
|
||||
this.$el.find('#course-introduction-video').val(this.model.getVideoSource());
|
||||
this.$el.find(this.fieldToSelectorMap['intro_video']).val(this.model.getVideoSource());
|
||||
}
|
||||
else this.$el.find('.remove-course-introduction-video').hide();
|
||||
|
||||
this.$el.find("#course-effort").val(this.model.get('effort'));
|
||||
this.$el.find(this.fieldToSelectorMap['effort']).val(this.model.get('effort'));
|
||||
|
||||
return this;
|
||||
},
|
||||
fieldToSelectorMap : {
|
||||
'start_date' : "#course-start",
|
||||
'end_date' : '#course-end',
|
||||
'enrollment_start' : '#enrollment-start',
|
||||
'enrollment_end' : '#enrollment-end',
|
||||
'syllabus' : '.current-course-syllabus .doc-filename',
|
||||
'overview' : '#course-overview',
|
||||
'intro_video' : '#course-introduction-video',
|
||||
'effort' : "#course-effort"
|
||||
},
|
||||
|
||||
setupDatePicker : function(elementName, fieldName) {
|
||||
_cacheValidationErrors : null,
|
||||
handleValidationError : function(model, error) {
|
||||
this._cacheValidationErrors = error;
|
||||
// error is object w/ fields and error strings
|
||||
for (var field in error) {
|
||||
var ele = this.$el.find(this.fieldToSelectorMap[field]);
|
||||
if ($(ele).is('div')) {
|
||||
// put error on the contained inputs
|
||||
$(ele).find('input, textarea').addClass('error');
|
||||
}
|
||||
else $(ele).addClass('error');
|
||||
$(ele).parent().append(this.errorTemplate({message : error[field]}));
|
||||
}
|
||||
},
|
||||
|
||||
clearValidationErrors : function() {
|
||||
if (this._cacheValidationErrors == null) return;
|
||||
// error is object w/ fields and error strings
|
||||
for (var field in this._cacheValidationErrors) {
|
||||
var ele = this.$el.find(this.fieldToSelectorMap[field]);
|
||||
if ($(ele).is('div')) {
|
||||
// put error on the contained inputs
|
||||
$(ele).find('input, textarea').removeClass('error');
|
||||
}
|
||||
else $(ele).removeClass('error');
|
||||
$(ele).nextAll('.message-error').remove();
|
||||
}
|
||||
this._cacheValidationErrors = null;
|
||||
},
|
||||
|
||||
setupDatePicker : function(fieldName) {
|
||||
var cacheModel = this.model;
|
||||
var div = this.$el.find(elementName);
|
||||
var div = this.$el.find(this.fieldToSelectorMap[fieldName]);
|
||||
var datefield = $(div).find(".date");
|
||||
var timefield = $(div).find(".time");
|
||||
var savefield = function() {
|
||||
@@ -143,7 +185,8 @@ CMS.Views.Settings.Details = Backbone.View.extend({
|
||||
},
|
||||
|
||||
updateModel: function(event) {
|
||||
// figure out which field
|
||||
this.clearValidationErrors();
|
||||
|
||||
switch (event.currentTarget.id) {
|
||||
case 'course-start-date': // handled via onSelect method
|
||||
case 'course-end-date':
|
||||
@@ -181,7 +224,7 @@ CMS.Views.Settings.Details = Backbone.View.extend({
|
||||
if (this.model.has('intro_video')) {
|
||||
this.model.save_videosource(null);
|
||||
this.$el.find(".current-course-introduction-video iframe").attr("src", "");
|
||||
this.$el.find('#course-introduction-video').val("");
|
||||
this.$el.find(this.fieldToSelectorMap['intro_video']).val("");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user