define(["js/views/validation", "codemirror", "underscore", "jquery", "jquery.ui", "js/utils/date_utils", "js/models/uploads", "js/views/uploads", "js/utils/change_on_enter", "jquery.timepicker", "date"], function(ValidatingView, CodeMirror, _, $, ui, DateUtils, FileUploadModel, FileUploadDialog, TriggerChangeEventOnEnter) { var DetailsView = ValidatingView.extend({ // Model class is CMS.Models.Settings.CourseDetails events : { "input input" : "updateModel", "input textarea" : "updateModel", // Leaving change in as fallback for older browsers "change input" : "updateModel", "change textarea" : "updateModel", "change select" : "updateModel", 'click .remove-course-introduction-video' : "removeVideo", 'focus #course-overview' : "codeMirrorize", 'mouseover .timezone' : "updateTime", // would love to move to a general superclass, but event hashes don't inherit in backbone :-( 'focus :input' : "inputFocus", 'blur :input' : "inputUnfocus", 'click .action-upload-image': "uploadImage" }, initialize : function() { this.fileAnchorTemplate = _.template(' <%= filename %>'); // fill in fields this.$el.find("#course-organization").val(this.model.get('org')); this.$el.find("#course-number").val(this.model.get('course_id')); this.$el.find("#course-name").val(this.model.get('run')); this.$el.find('.set-date').datepicker({ 'dateFormat': 'm/d/yy' }); // Avoid showing broken image on mistyped/nonexistent image this.$el.find('img.course-image').error(function() { $(this).hide(); }); this.$el.find('img.course-image').load(function() { $(this).show(); }); this.listenTo(this.model, 'invalid', this.handleValidationError); this.listenTo(this.model, 'change', this.showNotificationBar); this.selectorToField = _.invert(this.fieldToSelectorMap); }, render: function() { this.setupDatePicker('start_date'); this.setupDatePicker('end_date'); this.setupDatePicker('enrollment_start'); this.setupDatePicker('enrollment_end'); this.$el.find('#' + this.fieldToSelectorMap['overview']).val(this.model.get('overview')); this.codeMirrorize(null, $('#course-overview')[0]); this.$el.find('#' + this.fieldToSelectorMap['short_description']).val(this.model.get('short_description')); this.$el.find('.current-course-introduction-video iframe').attr('src', this.model.videosourceSample()); this.$el.find('#' + this.fieldToSelectorMap['intro_video']).val(this.model.get('intro_video') || ''); if (this.model.has('intro_video')) { this.$el.find('.remove-course-introduction-video').show(); } else this.$el.find('.remove-course-introduction-video').hide(); this.$el.find('#' + this.fieldToSelectorMap['effort']).val(this.model.get('effort')); var imageURL = this.model.get('course_image_asset_path'); this.$el.find('#course-image-url').val(imageURL); this.$el.find('#course-image').attr('src', imageURL); var pre_requisite_courses = this.model.get('pre_requisite_courses'); pre_requisite_courses = pre_requisite_courses.length > 0 ? pre_requisite_courses : ''; this.$el.find('#' + this.fieldToSelectorMap['pre_requisite_courses']).val(pre_requisite_courses); if (this.model.get('entrance_exam_enabled') == 'true') { this.$('#' + this.fieldToSelectorMap['entrance_exam_enabled']).attr('checked', this.model.get('entrance_exam_enabled')); this.$('.div-grade-requirements').show(); } else { this.$('#' + this.fieldToSelectorMap['entrance_exam_enabled']).removeAttr('checked'); this.$('.div-grade-requirements').hide(); } this.$('#' + this.fieldToSelectorMap['entrance_exam_minimum_score_pct']).val(this.model.get('entrance_exam_minimum_score_pct')); return this; }, fieldToSelectorMap : { 'start_date' : "course-start", 'end_date' : 'course-end', 'enrollment_start' : 'enrollment-start', 'enrollment_end' : 'enrollment-end', 'overview' : 'course-overview', 'short_description' : 'course-short-description', 'intro_video' : 'course-introduction-video', 'effort' : "course-effort", 'course_image_asset_path': 'course-image-url', 'pre_requisite_courses': 'pre-requisite-course', 'entrance_exam_enabled': 'entrance-exam-enabled', 'entrance_exam_minimum_score_pct': 'entrance-exam-minimum-score-pct' }, updateTime : function(e) { var now = new Date(), hours = now.getUTCHours(), minutes = now.getUTCMinutes(), currentTimeText = gettext('%(hours)s:%(minutes)s (current UTC time)'); $(e.currentTarget).attr('title', interpolate(currentTimeText, { 'hours': hours, 'minutes': minutes }, 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-image-url': this.setField(event); var url = $(event.currentTarget).val(); var image_name = _.last(url.split('/')); this.model.set('course_image_name', image_name); // Wait to set the image src until the user stops typing clearTimeout(this.imageTimer); this.imageTimer = setTimeout(function() { $('#course-image').attr('src', $(event.currentTarget).val()); }, 1000); break; case 'course-effort': this.setField(event); break; case 'entrance-exam-enabled': if($(event.currentTarget).is(":checked")){ this.$('.div-grade-requirements').show(); }else{ this.$('.div-grade-requirements').hide(); } this.setField(event); break; case 'entrance-exam-minimum-score-pct': // If the val is an empty string then update model with default value. if ($(event.currentTarget).val() === '') { this.model.set('entrance_exam_minimum_score_pct', this.model.defaults.entrance_exam_minimum_score_pct); } else { this.setField(event); } break; case 'course-short-description': this.setField(event); break; case 'pre-requisite-course': var value = $(event.currentTarget).val(); value = value == "" ? [] : [value]; this.model.set('pre_requisite_courses', value); break; // Don't make the user reload the page to check the Youtube ID. // Wait for a second to load the video, avoiding egregious AJAX calls. case 'course-introduction-video': this.clearValidationErrors(); var previewsource = this.model.set_videosource($(event.currentTarget).val()); clearTimeout(this.videoTimer); this.videoTimer = setTimeout(_.bind(function() { this.$el.find(".current-course-introduction-video iframe").attr("src", previewsource); if (this.model.has('intro_video')) { this.$el.find('.remove-course-introduction-video').show(); } else { this.$el.find('.remove-course-introduction-video').hide(); } }, this), 1000); break; default: // Everything else is handled by datepickers and CodeMirror. break; } }, removeVideo: function(event) { event.preventDefault(); if (this.model.has('intro_video')) { this.model.set_videosource(null); this.$el.find(".current-course-introduction-video iframe").attr("src", ""); this.$el.find('#' + this.fieldToSelectorMap['intro_video']).val(""); this.$el.find('.remove-course-introduction-video').hide(); } }, codeMirrors : {}, codeMirrorize: function (e, forcedTarget) { var thisTarget; if (forcedTarget) { thisTarget = forcedTarget; thisTarget.id = $(thisTarget).attr('id'); } else if (e !== null) { thisTarget = e.currentTarget; } else { // e and forcedTarget can be null so don't deference it // This is because in cases where we have a marketing site // we don't display the codeMirrors for editing the marketing // materials, except we do need to show the 'set course image' // workflow. So in this case e = forcedTarget = null. return; } if (!this.codeMirrors[thisTarget.id]) { var cachethis = this; var field = this.selectorToField[thisTarget.id]; this.codeMirrors[thisTarget.id] = CodeMirror.fromTextArea(thisTarget, { mode: "text/html", lineNumbers: true, lineWrapping: true}); this.codeMirrors[thisTarget.id].on('change', function (mirror) { mirror.save(); cachethis.clearValidationErrors(); var newVal = mirror.getValue(); if (cachethis.model.get(field) != newVal) { cachethis.setAndValidate(field, newVal); } }); } }, revertView: function() { // Make sure that the CodeMirror instance has the correct // data from its corresponding textarea var self = this; this.model.fetch({ success: function() { self.render(); _.each(self.codeMirrors, function(mirror) { var ele = mirror.getTextArea(); var field = self.selectorToField[ele.id]; mirror.setValue(self.model.get(field)); }); }, reset: true, silent: true}); }, setAndValidate: function(attr, value) { // If we call model.set() with {validate: true}, model fields // will not be set if validation fails. This puts the UI and // the model in an inconsistent state, and causes us to not // see the right validation errors the next time validate() is // called on the model. So we set *without* validating, then // call validate ourselves. this.model.set(attr, value); this.model.isValid(); }, showNotificationBar: function() { // We always call showNotificationBar with the same args, just // delegate to superclass ValidatingView.prototype.showNotificationBar.call(this, this.save_message, _.bind(this.saveView, this), _.bind(this.revertView, this)); }, uploadImage: function(event) { event.preventDefault(); var upload = new FileUploadModel({ title: gettext("Upload your course image."), message: gettext("Files must be in JPEG or PNG format."), mimeTypes: ['image/jpeg', 'image/png'] }); var self = this; var modal = new FileUploadDialog({ model: upload, onSuccess: function(response) { var options = { 'course_image_name': response.asset.display_name, 'course_image_asset_path': response.asset.url }; self.model.set(options); self.render(); $('#course-image').attr('src', self.model.get('course_image_asset_path')); } }); modal.show(); } }); return DetailsView; }); // end define()