main.js 13.8 KB
Newer Older
1
define(["js/views/validation", "codemirror", "underscore", "jquery", "jquery.ui", "js/utils/date_utils", "js/models/uploads",
2
    "js/views/uploads", "js/utils/change_on_enter", "jquery.timepicker", "date"],
3
    function(ValidatingView, CodeMirror, _, $, ui, DateUtils, FileUploadModel, FileUploadDialog, TriggerChangeEventOnEnter) {
4

5
var DetailsView = ValidatingView.extend({
6 7
    // Model class is CMS.Models.Settings.CourseDetails
    events : {
8 9 10
        "input input" : "updateModel",
        "input textarea" : "updateModel",
        // Leaving change in as fallback for older browsers
11 12
        "change input" : "updateModel",
        "change textarea" : "updateModel",
13
        "change select" : "updateModel",
14
        'click .remove-course-introduction-video' : "removeVideo",
15
        'focus #course-overview' : "codeMirrorize",
16
        'mouseover .timezone' : "updateTime",
17 18
        // would love to move to a general superclass, but event hashes don't inherit in backbone :-(
        'focus :input' : "inputFocus",
19 20
        'blur :input' : "inputUnfocus",
        'click .action-upload-image': "uploadImage"
21
    },
22

23
    initialize : function() {
24
        this.fileAnchorTemplate = _.template('<a href="<%= fullpath %>"> <i class="icon fa fa-file"></i><%= filename %></a>');
25
        // fill in fields
Don Mitchell committed
26 27 28
        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'));
29
        this.$el.find('.set-date').datepicker({ 'dateFormat': 'm/d/yy' });
30

Peter Fogg committed
31 32 33 34 35 36 37 38
        // 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();
        });

39
        this.listenTo(this.model, 'invalid', this.handleValidationError);
40
        this.listenTo(this.model, 'change', this.showNotificationBar);
41 42 43 44 45 46 47 48 49 50 51 52
        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]);

53 54
        this.$el.find('#' + this.fieldToSelectorMap['short_description']).val(this.model.get('short_description'));

55
        this.$el.find('.current-course-introduction-video iframe').attr('src', this.model.videosourceSample());
56
        this.$el.find('#' + this.fieldToSelectorMap['intro_video']).val(this.model.get('intro_video') || '');
57 58 59 60 61 62 63
        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'));

64
        var imageURL = this.model.get('course_image_asset_path');
65
        this.$el.find('#course-image-url').val(imageURL);
66
        this.$el.find('#course-image').attr('src', imageURL);
67

68 69 70
        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);
71

72 73 74 75 76 77 78 79 80 81
        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'));

82 83 84 85 86 87 88 89
        return this;
    },
    fieldToSelectorMap : {
        'start_date' : "course-start",
        'end_date' : 'course-end',
        'enrollment_start' : 'enrollment-start',
        'enrollment_end' : 'enrollment-end',
        'overview' : 'course-overview',
90
        'short_description' : 'course-short-description',
91
        'intro_video' : 'course-introduction-video',
92
        'effort' : "course-effort",
93
        'course_image_asset_path': 'course-image-url',
94 95 96
        'pre_requisite_courses': 'pre-requisite-course',
        'entrance_exam_enabled': 'entrance-exam-enabled',
        'entrance_exam_minimum_score_pct': 'entrance-exam-minimum-score-pct'
97
    },
cahrens committed
98

99
    updateTime : function(e) {
100 101 102 103 104 105 106 107 108
        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));
109
    },
cahrens committed
110 111 112 113

    setupDatePicker: function (fieldName) {
        var cacheModel = this.model;
        var div = this.$el.find('#' + this.fieldToSelectorMap[fieldName]);
114 115
        var datefield = $(div).find("input:.date");
        var timefield = $(div).find("input:.time");
cahrens committed
116
        var cachethis = this;
117
        var setfield = function () {
118 119
            var newVal = DateUtils.getDate(datefield, timefield),
                oldTime = new Date(cacheModel.get(fieldName)).getTime();
120
            if (newVal) {
121
                if (!cacheModel.has(fieldName) || oldTime !== newVal.getTime()) {
122 123
                    cachethis.clearValidationErrors();
                    cachethis.setAndValidate(fieldName, newVal);
cahrens committed
124
                }
125
            }
126 127 128 129
            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).
130 131
                cachethis.clearValidationErrors();
                cachethis.setAndValidate(fieldName, null);
132
            }
cahrens committed
133 134 135
        };

        // instrument as date and time pickers
136
        timefield.timepicker({'timeFormat' : 'H:i'});
cahrens committed
137 138
        datefield.datepicker();

139
        // Using the change event causes setfield to be triggered twice, but it is necessary
cahrens committed
140
        // to pick up when the date is typed directly in the field.
141
        datefield.change(setfield).keyup(TriggerChangeEventOnEnter);
142
        timefield.on('changeTime', setfield);
143
        timefield.on('input', setfield);
cahrens committed
144

145
        date = this.model.get(fieldName)
146
        // timepicker doesn't let us set null, so check that we have a time
147 148 149
        if (date) {
            DateUtils.setDate(datefield, timefield, date);
        } // but reset fields either way
150 151
        else {
            timefield.val('');
152
            datefield.val('');
153
        }
cahrens committed
154
    },
155 156 157

    updateModel: function(event) {
        switch (event.currentTarget.id) {
158 159 160 161 162 163 164 165 166 167 168
        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;
169
        case 'course-effort':
170
            this.setField(event);
171
            break;
172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188
        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;
189 190 191
        case 'course-short-description':
            this.setField(event);
            break;
192 193 194 195 196
        case 'pre-requisite-course':
            var value = $(event.currentTarget).val();
            value = value == "" ? [] : [value];
            this.model.set('pre_requisite_courses', value);
            break;
197
        // Don't make the user reload the page to check the Youtube ID.
198
        // Wait for a second to load the video, avoiding egregious AJAX calls.
199 200
        case 'course-introduction-video':
            this.clearValidationErrors();
201
            var previewsource = this.model.set_videosource($(event.currentTarget).val());
202 203 204 205 206 207 208 209 210 211
            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);
212
            break;
213
        default: // Everything else is handled by datepickers and CodeMirror.
214 215 216 217
            break;
        }
    },

218 219
    removeVideo: function(event) {
        event.preventDefault();
220
        if (this.model.has('intro_video')) {
221
            this.model.set_videosource(null);
222 223 224 225 226 227
            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 : {},
cahrens committed
228 229 230 231 232
    codeMirrorize: function (e, forcedTarget) {
        var thisTarget;
        if (forcedTarget) {
            thisTarget = forcedTarget;
            thisTarget.id = $(thisTarget).attr('id');
233
        } else if (e !== null) {
cahrens committed
234
            thisTarget = e.currentTarget;
235 236
        } else
        {
Chris Dodge committed
237 238 239 240 241
            // 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.
242
            return;
cahrens committed
243
        }
244

cahrens committed
245 246 247 248
        if (!this.codeMirrors[thisTarget.id]) {
            var cachethis = this;
            var field = this.selectorToField[thisTarget.id];
            this.codeMirrors[thisTarget.id] = CodeMirror.fromTextArea(thisTarget, {
249 250
                mode: "text/html", lineNumbers: true, lineWrapping: true});
            this.codeMirrors[thisTarget.id].on('change', function (mirror) {
cahrens committed
251 252 253
                    mirror.save();
                    cachethis.clearValidationErrors();
                    var newVal = mirror.getValue();
254
                    if (cachethis.model.get(field) != newVal) {
255
                        cachethis.setAndValidate(field, newVal);
256
                    }
cahrens committed
257 258
            });
        }
259
    },
260

261 262 263 264 265 266 267 268 269 270 271 272 273 274
    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));
                       });
            },
275 276
            reset: true,
            silent: true});
277 278 279 280 281 282 283 284 285 286
    },
    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();
287 288 289 290 291
    },

    showNotificationBar: function() {
        // We always call showNotificationBar with the same args, just
        // delegate to superclass
292 293 294 295
        ValidatingView.prototype.showNotificationBar.call(this,
                                                          this.save_message,
                                                          _.bind(this.saveView, this),
                                                          _.bind(this.revertView, this));
296 297 298 299
    },

    uploadImage: function(event) {
        event.preventDefault();
300
        var upload = new FileUploadModel({
301
            title: gettext("Upload your course image."),
302
            message: gettext("Files must be in JPEG or PNG format."),
303
            mimeTypes: ['image/jpeg', 'image/png']
304 305
        });
        var self = this;
306
        var modal = new FileUploadDialog({
307 308 309
            model: upload,
            onSuccess: function(response) {
                var options = {
310 311 312
                    'course_image_name': response.asset.display_name,
                    'course_image_asset_path': response.asset.url
                };
313 314
                self.model.set(options);
                self.render();
315
                $('#course-image').attr('src', self.model.get('course_image_asset_path'));
316 317
            }
        });
318
        modal.show();
319
    }
Don Mitchell committed
320 321
});

322 323 324
return DetailsView;

}); // end define()