<commit_before>if (!CMS.Views['Settings']) CMS.Views.Settings = {}; // ensure the pseudo pkg exists

CMS.Views.Settings.Grading = CMS.Views.ValidatingView.extend({
    // Model class is CMS.Models.Settings.CourseGradingPolicy
    events : {
        "input input" : "updateModel",
        "input textarea" : "updateModel",
        // Leaving change in as fallback for older browsers
        "change input" : "updateModel",
        "change textarea" : "updateModel",
        "input span[contenteditable]" : "updateDesignation",
        "click .settings-extra header" : "showSettingsExtras",
        "click .new-grade-button" : "addNewGrade",
        "click .remove-button" : "removeGrade",
        "click .add-grading-data" : "addAssignmentType",
        // would love to move to a general superclass, but event hashes don't inherit in backbone :-(
        'focus :input' : "inputFocus",
        'blur :input' : "inputUnfocus"
    },
    initialize : function() {
        //  load template for grading view
        var self = this;
        this.gradeCutoffTemplate = _.template('<li class="grade-specific-bar" style="width:<%= width %>%"><span class="letter-grade" contenteditable>' +
                '<%= descriptor %>' +
                '</span><span class="range"></span>' +
                '<% if (removable) {%><a href="#" class="remove-button">remove</a><% ;} %>' +
        '</li>');

        this.setupCutoffs();

        // Instrument grace period
        this.$el.find('#course-grading-graceperiod').timepicker();

        // instantiates an editor template for each update in the collection
        // Because this calls render, put it after everything which render may depend upon to prevent race condition.
        window.templateLoader.loadRemoteTemplate("course_grade_policy",
                "/static/client_templates/course_grade_policy.html",
                function (raw_template) {
            self.template = _.template(raw_template);
            self.render();
        }
        );
        this.listenTo(this.model, 'invalid', this.handleValidationError);
        this.listenTo(this.model, 'change', this.showNotificationBar);
        this.model.get('graders').on('reset', this.render, this);
        this.model.get('graders').on('add', this.render, this);
        this.selectorToField = _.invert(this.fieldToSelectorMap);
    },

    render: function() {
        // prevent bootstrap race condition by event dispatch
        if (!this.template) return;

        // Create and render the grading type subs
        var self = this;
        var gradelist = this.$el.find('.course-grading-assignment-list');
        // Undo the double invocation error. At some point, fix the double invocation
        $(gradelist).empty();
        var gradeCollection = this.model.get('graders');
        // We need to bind these events here (rather than in
        // initialize), or else we can only press the delete button
        // once due to the graders collection changing when we cancel
        // our changes.
        _.each(['change', 'remove', 'add'],
               function (event) {
                   gradeCollection.on(event, function() {
                       this.showNotificationBar();
                       // Since the change event gets fired every time
                       // we type in an input field, we don't need to
                       // (and really shouldn't) rerender the whole view.
                       if(event !== 'change') {
                           this.render();
                       }
                   }, this);
               },
               this);
        gradeCollection.each(function(gradeModel) {
            $(gradelist).append(self.template({model : gradeModel }));
            var newEle = gradelist.children().last();
            var newView = new CMS.Views.Settings.GraderView({el: newEle,
                model : gradeModel, collection : gradeCollection });
            // Listen in order to rerender when the 'cancel' button is
            // pressed
            self.listenTo(newView, 'revert', _.bind(self.render, self));
        });

        // render the grade cutoffs
        this.renderCutoffBar();

        var graceEle = this.$el.find('#course-grading-graceperiod');
        graceEle.timepicker({'timeFormat' : 'H:i'}); // init doesn't take setTime
        if (this.model.has('grace_period')) graceEle.timepicker('setTime', this.model.gracePeriodToDate());
        // remove any existing listeners to keep them from piling on b/c render gets called frequently
        graceEle.off('change', this.setGracePeriod);
        graceEle.on('change', this, this.setGracePeriod);

        return this;
    },
    addAssignmentType : function(e) {
        e.preventDefault();
        this.model.get('graders').push({});
    },
    fieldToSelectorMap : {
        'grace_period' : 'course-grading-graceperiod'
    },
    setGracePeriod : function(event) {
        var self = event.data;
        self.clearValidationErrors();
        var newVal = self.model.dateToGracePeriod($(event.currentTarget).timepicker('getTime'));
        self.model.set('grace_period', newVal, {validate: true});
    },
    updateModel : function(event) {
        if (!this.selectorToField[event.currentTarget.id]) return;

        switch (this.selectorToField[event.currentTarget.id]) {
        case 'grace_period': // handled above
            break;

        default:
            this.setField(event);
            break;
        }
    },

    // Grade sliders attributes and methods
    // Grade bars are li's ordered A -> F with A taking whole width, B overlaying it with its paint, ...
    // The actual cutoff for each grade is the width % of the next lower grade; so, the hack here
    // is to lay down a whole width bar claiming it's A and then lay down bars for each actual grade
    // starting w/ A but posting the label in the preceding li and setting the label of the last to "Fail" or "F"

    // A does not have a drag bar (cannot change its upper limit)
    // Need to insert new bars in right place.
    GRADES : ['A', 'B', 'C', 'D'],	// defaults for new grade designators
    descendingCutoffs : [],  // array of { designation : , cutoff : }
    gradeBarWidth : null, // cache of value since it won't change (more certain)

    renderCutoffBar: function() {
        var gradeBar =this.$el.find('.grade-bar');
        this.gradeBarWidth = gradeBar.width();
        var gradelist = gradeBar.children('.grades');
        // HACK fixing a duplicate call issue by undoing previous call effect. Need to figure out why called 2x
        gradelist.empty();
        var nextWidth = 100; // first width is 100%
        // Can probably be simplified to one variable now.
        var removable = false;
        var draggable = false; // first and last are not removable, first is not draggable
        _.each(this.descendingCutoffs,
                function(cutoff, index) {
            var newBar = this.gradeCutoffTemplate({
                descriptor : cutoff['designation'] ,
                width : nextWidth,
                removable : removable });
            gradelist.append(newBar);
            if (draggable) {
                newBar = gradelist.children().last(); // get the dom object not the unparsed string
                newBar.resizable({
                    handles: "e",
                    containment : "parent",
                    start : this.startMoveClosure(),
                    resize : this.moveBarClosure(),
                    stop : this.stopDragClosure()
                });
            }
            // prepare for next
            nextWidth = cutoff['cutoff'];
            removable = true; // first is not removable, all others are
            draggable = true;
        },
        this);
        // add fail which is not in data
        var failBar = this.gradeCutoffTemplate({ descriptor : this.failLabel(),
            width : nextWidth, removable : false});
        $(failBar).find("span[contenteditable=true]").attr("contenteditable", false);
        gradelist.append(failBar);
        gradelist.children().last().resizable({
            handles: "e",
            containment : "parent",
            start : this.startMoveClosure(),
            resize : this.moveBarClosure(),
            stop : this.stopDragClosure()
        });

        this.renderGradeRanges();
    },

    showSettingsExtras : function(event) {
        $(event.currentTarget).toggleClass('active');
        $(event.currentTarget).siblings.toggleClass('is-shown');
    },


    startMoveClosure : function() {
        // set min/max widths
        var cachethis = this;
        var widthPerPoint = cachethis.gradeBarWidth / 100;
        return function(event, ui) {
            var barIndex = ui.element.index();
            // min and max represent limits not labels (note, can's make smaller than 3 points wide)
            var min = (barIndex < cachethis.descendingCutoffs.length ? cachethis.descendingCutoffs[barIndex]['cutoff'] + 3 : 3);
            // minus 2 b/c minus 1 is the element we're effecting. It's max is just shy of the next one above it
            var max = (barIndex >= 2 ? cachethis.descendingCutoffs[barIndex - 2]['cutoff'] - 3 : 97);
            ui.element.resizable("option",{minWidth : min * widthPerPoint, maxWidth : max * widthPerPoint});
        };
    },

    moveBarClosure : function() {
        // 0th ele doesn't have a bar; so, will never invoke this
        var cachethis = this;
        return function(event, ui) {
            var barIndex = ui.element.index();
            // min and max represent limits not labels (note, can's make smaller than 3 points wide)
            var min = (barIndex < cachethis.descendingCutoffs.length ? cachethis.descendingCutoffs[barIndex]['cutoff'] + 3 : 3);
            // minus 2 b/c minus 1 is the element we're effecting. It's max is just shy of the next one above it
            var max = (barIndex >= 2 ? cachethis.descendingCutoffs[barIndex - 2]['cutoff'] - 3 : 100);
            var percentage = Math.min(Math.max(ui.size.width / cachethis.gradeBarWidth * 100, min), max);
            cachethis.descendingCutoffs[barIndex - 1]['cutoff'] = Math.round(percentage);
            cachethis.renderGradeRanges();
        };
    },

    renderGradeRanges: function() {
        // the labels showing the range e.g., 71-80
        var cutoffs = this.descendingCutoffs;
        this.$el.find('.range').each(function(i) {
            var min = (i < cutoffs.length ? cutoffs[i]['cutoff'] : 0);
            var max = (i > 0 ? cutoffs[i - 1]['cutoff'] : 100);
            $(this).text(min + '-' + max);
        });
    },

    stopDragClosure: function() {
        var cachethis = this;
        return function(event, ui) {
            // for some reason the resize is setting height to 0
            cachethis.saveCutoffs();
        };
    },

    saveCutoffs: function() {
        this.model.set('grade_cutoffs',
                _.reduce(this.descendingCutoffs,
                        function(object, cutoff) {
                    object[cutoff['designation']] = cutoff['cutoff'] / 100.0;
                    return object;
                },
                {}),
                {validate: true});
    },

    addNewGrade: function(e) {
        e.preventDefault();
        var gradeLength = this.descendingCutoffs.length; // cutoffs doesn't include fail/f so this is only the passing grades
        if(gradeLength > 3) {
            // TODO shouldn't we disable the button
            return;
        }
        var failBarWidth = this.descendingCutoffs[gradeLength - 1]['cutoff'];
        // going to split the grade above the insertion point in half leaving fail in same place
        var nextGradeTop = (gradeLength > 1 ? this.descendingCutoffs[gradeLength - 2]['cutoff'] : 100);
        var targetWidth = failBarWidth + ((nextGradeTop - failBarWidth) / 2);
        this.descendingCutoffs.push({designation: this.GRADES[gradeLength], cutoff: failBarWidth});
        this.descendingCutoffs[gradeLength - 1]['cutoff'] = Math.round(targetWidth);

        var $newGradeBar = this.gradeCutoffTemplate({ descriptor : this.GRADES[gradeLength],
            width : targetWidth, removable : true });
        var gradeDom = this.$el.find('.grades');
        gradeDom.children().last().before($newGradeBar);
        var newEle = gradeDom.children()[gradeLength];
        $(newEle).resizable({
            handles: "e",
            containment : "parent",
            start : this.startMoveClosure(),
            resize : this.moveBarClosure(),
            stop : this.stopDragClosure()
        });

        // Munge existing grade labels?
        // If going from Pass/Fail to 3 levels, change to Pass to A
        if (gradeLength === 1 && this.descendingCutoffs[0]['designation'] === 'Pass') {
            this.descendingCutoffs[0]['designation'] = this.GRADES[0];
            this.setTopGradeLabel();
        }
        this.setFailLabel();

        this.renderGradeRanges();
        this.saveCutoffs();
    },

    removeGrade: function(e) {
        e.preventDefault();
        var domElement = $(e.currentTarget).closest('li');
        var index = domElement.index();
        // copy the boundary up to the next higher grade then remove
        this.descendingCutoffs[index - 1]['cutoff'] = this.descendingCutoffs[index]['cutoff'];
        this.descendingCutoffs.splice(index, 1);
        domElement.remove();

        if (this.descendingCutoffs.length === 1 && this.descendingCutoffs[0]['designation'] === this.GRADES[0]) {
            this.descendingCutoffs[0]['designation'] = 'Pass';
            this.setTopGradeLabel();
        }
        this.setFailLabel();
        this.renderGradeRanges();
        this.saveCutoffs();
    },

    updateDesignation: function(e) {
        var index = $(e.currentTarget).closest('li').index();
        this.descendingCutoffs[index]['designation'] = $(e.currentTarget).html();
        this.saveCutoffs();
    },

    failLabel: function() {
        if (this.descendingCutoffs.length === 1) return 'Fail';
        else return 'F';
    },
    setFailLabel: function() {
        this.$el.find('.grades .letter-grade').last().html(this.failLabel());
    },
    setTopGradeLabel: function() {
        this.$el.find('.grades .letter-grade').first().html(this.descendingCutoffs[0]['designation']);
    },
    setupCutoffs: function() {
        // Instrument grading scale
        // convert cutoffs to inversely ordered list
        var modelCutoffs = this.model.get('grade_cutoffs');
        for (var cutoff in modelCutoffs) {
            this.descendingCutoffs.push({designation: cutoff, cutoff: Math.round(modelCutoffs[cutoff] * 100)});
        }
        this.descendingCutoffs = _.sortBy(this.descendingCutoffs,
                                          function (gradeEle) { return -gradeEle['cutoff']; });
    },
    revertView: function() {
        var self = this;
        this.model.fetch({
            success: function() {
                self.descendingCutoffs = [];
                self.setupCutoffs();
                self.render();
                self.renderCutoffBar();
            },
            reset: true,
            silent: true});
    },
    showNotificationBar: function() {
        // We always call showNotificationBar with the same args, just
        // delegate to superclass
        CMS.Views.ValidatingView.prototype.showNotificationBar.call(this,
                                                                    this.save_message,
                                                                    _.bind(this.saveView, this),
                                                                    _.bind(this.revertView, this));
    }
});

CMS.Views.Settings.GraderView = CMS.Views.ValidatingView.extend({
    // Model class is CMS.Models.Settings.CourseGrader
    events : {
        "input input" : "updateModel",
        "input textarea" : "updateModel",
        // Leaving change in as fallback for older browsers
        "change input" : "updateModel",
        "change textarea" : "updateModel",
        "click .remove-grading-data" : "deleteModel",
        // would love to move to a general superclass, but event hashes don't inherit in backbone :-(
        'focus :input' : "inputFocus",
        'blur :input' : "inputUnfocus"
    },
    initialize : function() {
        this.listenTo(this.model, 'invalid', this.handleValidationError);
        this.selectorToField = _.invert(this.fieldToSelectorMap);
        this.render();
    },

    render: function() {
        return this;
    },
    fieldToSelectorMap : {
        'type' : 'course-grading-assignment-name',
        'short_label' : 'course-grading-assignment-shortname',
        'min_count' : 'course-grading-assignment-totalassignments',
        'drop_count' : 'course-grading-assignment-droppable',
        'weight' : 'course-grading-assignment-gradeweight'
    },
    updateModel: function(event) {
        // HACK to fix model sometimes losing its pointer to the collection [I think I fixed this but leaving
        // this in out of paranoia. If this error ever happens, the user will get a warning that they cannot
        // give 2 assignments the same name.]
        if (!this.model.collection) {
            this.model.collection = this.collection;
        }

        switch (event.currentTarget.id) {
        case 'course-grading-assignment-totalassignments':
            this.$el.find('#course-grading-assignment-droppable').attr('max', $(event.currentTarget).val());
            this.setField(event);
            break;
        case 'course-grading-assignment-name':
            // Keep the original name, until we save
            this.oldName = this.oldName === undefined ? this.model.get('type') : this.oldName;
            // If the name has changed, alert the user to change all subsection names.
            if (this.setField(event) != this.oldName && !_.isEmpty(this.oldName)) {
                // overload the error display logic
                this._cacheValidationErrors.push(event.currentTarget);
                $(event.currentTarget).parent().append(
                        this.errorTemplate({message : 'For grading to work, you must change all "' + this.oldName +
                            '" subsections to "' + this.model.get('type') + '".'}));
            }
            break;
        default:
            this.setField(event);
        break;
        }
    },
    deleteModel : function(e) {
        e.preventDefault();
        this.collection.remove(this.model);
    }
});
<commit_msg>Fix failing grade label being editable but not saved.<commit_after>if (!CMS.Views['Settings']) CMS.Views.Settings = {}; // ensure the pseudo pkg exists

CMS.Views.Settings.Grading = CMS.Views.ValidatingView.extend({
    // Model class is CMS.Models.Settings.CourseGradingPolicy
    events : {
        "input input" : "updateModel",
        "input textarea" : "updateModel",
        // Leaving change in as fallback for older browsers
        "change input" : "updateModel",
        "change textarea" : "updateModel",
        "input span[contenteditable=true]" : "updateDesignation",
        "click .settings-extra header" : "showSettingsExtras",
        "click .new-grade-button" : "addNewGrade",
        "click .remove-button" : "removeGrade",
        "click .add-grading-data" : "addAssignmentType",
        // would love to move to a general superclass, but event hashes don't inherit in backbone :-(
        'focus :input' : "inputFocus",
        'blur :input' : "inputUnfocus"
    },
    initialize : function() {
        //  load template for grading view
        var self = this;
        this.gradeCutoffTemplate = _.template('<li class="grade-specific-bar" style="width:<%= width %>%"><span class="letter-grade" contenteditable="true">' +
                '<%= descriptor %>' +
                '</span><span class="range"></span>' +
                '<% if (removable) {%><a href="#" class="remove-button">remove</a><% ;} %>' +
        '</li>');

        this.setupCutoffs();

        // Instrument grace period
        this.$el.find('#course-grading-graceperiod').timepicker();

        // instantiates an editor template for each update in the collection
        // Because this calls render, put it after everything which render may depend upon to prevent race condition.
        window.templateLoader.loadRemoteTemplate("course_grade_policy",
                "/static/client_templates/course_grade_policy.html",
                function (raw_template) {
            self.template = _.template(raw_template);
            self.render();
        }
        );
        this.listenTo(this.model, 'invalid', this.handleValidationError);
        this.listenTo(this.model, 'change', this.showNotificationBar);
        this.model.get('graders').on('reset', this.render, this);
        this.model.get('graders').on('add', this.render, this);
        this.selectorToField = _.invert(this.fieldToSelectorMap);
    },

    render: function() {
        // prevent bootstrap race condition by event dispatch
        if (!this.template) return;

        // Create and render the grading type subs
        var self = this;
        var gradelist = this.$el.find('.course-grading-assignment-list');
        // Undo the double invocation error. At some point, fix the double invocation
        $(gradelist).empty();
        var gradeCollection = this.model.get('graders');
        // We need to bind these events here (rather than in
        // initialize), or else we can only press the delete button
        // once due to the graders collection changing when we cancel
        // our changes.
        _.each(['change', 'remove', 'add'],
               function (event) {
                   gradeCollection.on(event, function() {
                       this.showNotificationBar();
                       // Since the change event gets fired every time
                       // we type in an input field, we don't need to
                       // (and really shouldn't) rerender the whole view.
                       if(event !== 'change') {
                           this.render();
                       }
                   }, this);
               },
               this);
        gradeCollection.each(function(gradeModel) {
            $(gradelist).append(self.template({model : gradeModel }));
            var newEle = gradelist.children().last();
            var newView = new CMS.Views.Settings.GraderView({el: newEle,
                model : gradeModel, collection : gradeCollection });
            // Listen in order to rerender when the 'cancel' button is
            // pressed
            self.listenTo(newView, 'revert', _.bind(self.render, self));
        });

        // render the grade cutoffs
        this.renderCutoffBar();

        var graceEle = this.$el.find('#course-grading-graceperiod');
        graceEle.timepicker({'timeFormat' : 'H:i'}); // init doesn't take setTime
        if (this.model.has('grace_period')) graceEle.timepicker('setTime', this.model.gracePeriodToDate());
        // remove any existing listeners to keep them from piling on b/c render gets called frequently
        graceEle.off('change', this.setGracePeriod);
        graceEle.on('change', this, this.setGracePeriod);

        return this;
    },
    addAssignmentType : function(e) {
        e.preventDefault();
        this.model.get('graders').push({});
    },
    fieldToSelectorMap : {
        'grace_period' : 'course-grading-graceperiod'
    },
    setGracePeriod : function(event) {
        var self = event.data;
        self.clearValidationErrors();
        var newVal = self.model.dateToGracePeriod($(event.currentTarget).timepicker('getTime'));
        self.model.set('grace_period', newVal, {validate: true});
    },
    updateModel : function(event) {
        if (!this.selectorToField[event.currentTarget.id]) return;

        switch (this.selectorToField[event.currentTarget.id]) {
        case 'grace_period': // handled above
            break;

        default:
            this.setField(event);
            break;
        }
    },

    // Grade sliders attributes and methods
    // Grade bars are li's ordered A -> F with A taking whole width, B overlaying it with its paint, ...
    // The actual cutoff for each grade is the width % of the next lower grade; so, the hack here
    // is to lay down a whole width bar claiming it's A and then lay down bars for each actual grade
    // starting w/ A but posting the label in the preceding li and setting the label of the last to "Fail" or "F"

    // A does not have a drag bar (cannot change its upper limit)
    // Need to insert new bars in right place.
    GRADES : ['A', 'B', 'C', 'D'],	// defaults for new grade designators
    descendingCutoffs : [],  // array of { designation : , cutoff : }
    gradeBarWidth : null, // cache of value since it won't change (more certain)

    renderCutoffBar: function() {
        var gradeBar =this.$el.find('.grade-bar');
        this.gradeBarWidth = gradeBar.width();
        var gradelist = gradeBar.children('.grades');
        // HACK fixing a duplicate call issue by undoing previous call effect. Need to figure out why called 2x
        gradelist.empty();
        var nextWidth = 100; // first width is 100%
        // Can probably be simplified to one variable now.
        var removable = false;
        var draggable = false; // first and last are not removable, first is not draggable
        _.each(this.descendingCutoffs,
                function(cutoff, index) {
            var newBar = this.gradeCutoffTemplate({
                descriptor : cutoff['designation'] ,
                width : nextWidth,
                removable : removable });
            gradelist.append(newBar);
            if (draggable) {
                newBar = gradelist.children().last(); // get the dom object not the unparsed string
                newBar.resizable({
                    handles: "e",
                    containment : "parent",
                    start : this.startMoveClosure(),
                    resize : this.moveBarClosure(),
                    stop : this.stopDragClosure()
                });
            }
            // prepare for next
            nextWidth = cutoff['cutoff'];
            removable = true; // first is not removable, all others are
            draggable = true;
        },
        this);
        // add fail which is not in data
        var failBar = $(this.gradeCutoffTemplate({
            descriptor : this.failLabel(),
            width : nextWidth,
            removable : false
        }));
        failBar.find("span[contenteditable=true]").attr("contenteditable", false);
        gradelist.append(failBar);
        gradelist.children().last().resizable({
            handles: "e",
            containment : "parent",
            start : this.startMoveClosure(),
            resize : this.moveBarClosure(),
            stop : this.stopDragClosure()
        });

        this.renderGradeRanges();
    },

    showSettingsExtras : function(event) {
        $(event.currentTarget).toggleClass('active');
        $(event.currentTarget).siblings.toggleClass('is-shown');
    },


    startMoveClosure : function() {
        // set min/max widths
        var cachethis = this;
        var widthPerPoint = cachethis.gradeBarWidth / 100;
        return function(event, ui) {
            var barIndex = ui.element.index();
            // min and max represent limits not labels (note, can's make smaller than 3 points wide)
            var min = (barIndex < cachethis.descendingCutoffs.length ? cachethis.descendingCutoffs[barIndex]['cutoff'] + 3 : 3);
            // minus 2 b/c minus 1 is the element we're effecting. It's max is just shy of the next one above it
            var max = (barIndex >= 2 ? cachethis.descendingCutoffs[barIndex - 2]['cutoff'] - 3 : 97);
            ui.element.resizable("option",{minWidth : min * widthPerPoint, maxWidth : max * widthPerPoint});
        };
    },

    moveBarClosure : function() {
        // 0th ele doesn't have a bar; so, will never invoke this
        var cachethis = this;
        return function(event, ui) {
            var barIndex = ui.element.index();
            // min and max represent limits not labels (note, can's make smaller than 3 points wide)
            var min = (barIndex < cachethis.descendingCutoffs.length ? cachethis.descendingCutoffs[barIndex]['cutoff'] + 3 : 3);
            // minus 2 b/c minus 1 is the element we're effecting. It's max is just shy of the next one above it
            var max = (barIndex >= 2 ? cachethis.descendingCutoffs[barIndex - 2]['cutoff'] - 3 : 100);
            var percentage = Math.min(Math.max(ui.size.width / cachethis.gradeBarWidth * 100, min), max);
            cachethis.descendingCutoffs[barIndex - 1]['cutoff'] = Math.round(percentage);
            cachethis.renderGradeRanges();
        };
    },

    renderGradeRanges: function() {
        // the labels showing the range e.g., 71-80
        var cutoffs = this.descendingCutoffs;
        this.$el.find('.range').each(function(i) {
            var min = (i < cutoffs.length ? cutoffs[i]['cutoff'] : 0);
            var max = (i > 0 ? cutoffs[i - 1]['cutoff'] : 100);
            $(this).text(min + '-' + max);
        });
    },

    stopDragClosure: function() {
        var cachethis = this;
        return function(event, ui) {
            // for some reason the resize is setting height to 0
            cachethis.saveCutoffs();
        };
    },

    saveCutoffs: function() {
        this.model.set('grade_cutoffs',
                _.reduce(this.descendingCutoffs,
                        function(object, cutoff) {
                    object[cutoff['designation']] = cutoff['cutoff'] / 100.0;
                    return object;
                },
                {}),
                {validate: true});
    },

    addNewGrade: function(e) {
        e.preventDefault();
        var gradeLength = this.descendingCutoffs.length; // cutoffs doesn't include fail/f so this is only the passing grades
        if(gradeLength > 3) {
            // TODO shouldn't we disable the button
            return;
        }
        var failBarWidth = this.descendingCutoffs[gradeLength - 1]['cutoff'];
        // going to split the grade above the insertion point in half leaving fail in same place
        var nextGradeTop = (gradeLength > 1 ? this.descendingCutoffs[gradeLength - 2]['cutoff'] : 100);
        var targetWidth = failBarWidth + ((nextGradeTop - failBarWidth) / 2);
        this.descendingCutoffs.push({designation: this.GRADES[gradeLength], cutoff: failBarWidth});
        this.descendingCutoffs[gradeLength - 1]['cutoff'] = Math.round(targetWidth);

        var $newGradeBar = this.gradeCutoffTemplate({ descriptor : this.GRADES[gradeLength],
            width : targetWidth, removable : true });
        var gradeDom = this.$el.find('.grades');
        gradeDom.children().last().before($newGradeBar);
        var newEle = gradeDom.children()[gradeLength];
        $(newEle).resizable({
            handles: "e",
            containment : "parent",
            start : this.startMoveClosure(),
            resize : this.moveBarClosure(),
            stop : this.stopDragClosure()
        });

        // Munge existing grade labels?
        // If going from Pass/Fail to 3 levels, change to Pass to A
        if (gradeLength === 1 && this.descendingCutoffs[0]['designation'] === 'Pass') {
            this.descendingCutoffs[0]['designation'] = this.GRADES[0];
            this.setTopGradeLabel();
        }
        this.setFailLabel();

        this.renderGradeRanges();
        this.saveCutoffs();
    },

    removeGrade: function(e) {
        e.preventDefault();
        var domElement = $(e.currentTarget).closest('li');
        var index = domElement.index();
        // copy the boundary up to the next higher grade then remove
        this.descendingCutoffs[index - 1]['cutoff'] = this.descendingCutoffs[index]['cutoff'];
        this.descendingCutoffs.splice(index, 1);
        domElement.remove();

        if (this.descendingCutoffs.length === 1 && this.descendingCutoffs[0]['designation'] === this.GRADES[0]) {
            this.descendingCutoffs[0]['designation'] = 'Pass';
            this.setTopGradeLabel();
        }
        this.setFailLabel();
        this.renderGradeRanges();
        this.saveCutoffs();
    },

    updateDesignation: function(e) {
        var index = $(e.currentTarget).closest('li').index();
        this.descendingCutoffs[index]['designation'] = $(e.currentTarget).html();
        this.saveCutoffs();
    },

    failLabel: function() {
        if (this.descendingCutoffs.length === 1) return 'Fail';
        else return 'F';
    },
    setFailLabel: function() {
        this.$el.find('.grades .letter-grade').last().html(this.failLabel());
    },
    setTopGradeLabel: function() {
        this.$el.find('.grades .letter-grade').first().html(this.descendingCutoffs[0]['designation']);
    },
    setupCutoffs: function() {
        // Instrument grading scale
        // convert cutoffs to inversely ordered list
        var modelCutoffs = this.model.get('grade_cutoffs');
        for (var cutoff in modelCutoffs) {
            this.descendingCutoffs.push({designation: cutoff, cutoff: Math.round(modelCutoffs[cutoff] * 100)});
        }
        this.descendingCutoffs = _.sortBy(this.descendingCutoffs,
                                          function (gradeEle) { return -gradeEle['cutoff']; });
    },
    revertView: function() {
        var self = this;
        this.model.fetch({
            success: function() {
                self.descendingCutoffs = [];
                self.setupCutoffs();
                self.render();
                self.renderCutoffBar();
            },
            reset: true,
            silent: true});
    },
    showNotificationBar: function() {
        // We always call showNotificationBar with the same args, just
        // delegate to superclass
        CMS.Views.ValidatingView.prototype.showNotificationBar.call(this,
                                                                    this.save_message,
                                                                    _.bind(this.saveView, this),
                                                                    _.bind(this.revertView, this));
    }
});

CMS.Views.Settings.GraderView = CMS.Views.ValidatingView.extend({
    // Model class is CMS.Models.Settings.CourseGrader
    events : {
        "input input" : "updateModel",
        "input textarea" : "updateModel",
        // Leaving change in as fallback for older browsers
        "change input" : "updateModel",
        "change textarea" : "updateModel",
        "click .remove-grading-data" : "deleteModel",
        // would love to move to a general superclass, but event hashes don't inherit in backbone :-(
        'focus :input' : "inputFocus",
        'blur :input' : "inputUnfocus"
    },
    initialize : function() {
        this.listenTo(this.model, 'invalid', this.handleValidationError);
        this.selectorToField = _.invert(this.fieldToSelectorMap);
        this.render();
    },

    render: function() {
        return this;
    },
    fieldToSelectorMap : {
        'type' : 'course-grading-assignment-name',
        'short_label' : 'course-grading-assignment-shortname',
        'min_count' : 'course-grading-assignment-totalassignments',
        'drop_count' : 'course-grading-assignment-droppable',
        'weight' : 'course-grading-assignment-gradeweight'
    },
    updateModel: function(event) {
        // HACK to fix model sometimes losing its pointer to the collection [I think I fixed this but leaving
        // this in out of paranoia. If this error ever happens, the user will get a warning that they cannot
        // give 2 assignments the same name.]
        if (!this.model.collection) {
            this.model.collection = this.collection;
        }

        switch (event.currentTarget.id) {
        case 'course-grading-assignment-totalassignments':
            this.$el.find('#course-grading-assignment-droppable').attr('max', $(event.currentTarget).val());
            this.setField(event);
            break;
        case 'course-grading-assignment-name':
            // Keep the original name, until we save
            this.oldName = this.oldName === undefined ? this.model.get('type') : this.oldName;
            // If the name has changed, alert the user to change all subsection names.
            if (this.setField(event) != this.oldName && !_.isEmpty(this.oldName)) {
                // overload the error display logic
                this._cacheValidationErrors.push(event.currentTarget);
                $(event.currentTarget).parent().append(
                        this.errorTemplate({message : 'For grading to work, you must change all "' + this.oldName +
                            '" subsections to "' + this.model.get('type') + '".'}));
            }
            break;
        default:
            this.setField(event);
        break;
        }
    },
    deleteModel : function(e) {
        e.preventDefault();
        this.collection.remove(this.model);
    }
});

{'fraction_non_alphanumeric': 0.12157915939304417, 'fraction_numerical': 0.00413589364844904, 'mean_word_length': 2.0694913857060424, 'pattern_counts': {'":': 0, '<': 33, '<?xml version=': 0, '>': 39, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 16, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '6536258', 'n_tokens_mistral': 10267, 'n_tokens_neox': 9731, 'n_words': 2618}