From 111d6478a23ecbdc186c095f3db1c8a1b4336dfa Mon Sep 17 00:00:00 2001
From: PavelBegunkov <asml.silence@gmail.com>
Date: Tue, 2 Aug 2016 16:09:39 +0300
Subject: [PATCH] refs #128 Extract rating groups filtering Extract rating
 table highlighting Extract rating table cursor class Generalize position
 parsing Delete redundant statements downloading code Extract rating cell's
 initialization Extract highlighting Extract all common cells initializators
 Extract postRate Extract rate methods

---
 media/js/config.js                            |   6 +-
 media/js/discipline/rating/common.js          | 423 ++++++++++++++
 media/js/discipline/rating/exam.js            | 496 ++---------------
 media/js/discipline/rating/rate.js            | 516 +-----------------
 media/less/teacher/rating.less                |   5 +-
 .../classes/Controller/Handler/Rating.php     |   2 +
 .../views/teacher/discipline/rating/exam.twig |   3 +-
 .../views/teacher/discipline/rating/rate.twig |   5 +-
 8 files changed, 494 insertions(+), 962 deletions(-)
 create mode 100644 media/js/discipline/rating/common.js

diff --git a/media/js/config.js b/media/js/config.js
index 011b7ba23..5a23b44a0 100644
--- a/media/js/config.js
+++ b/media/js/config.js
@@ -13,10 +13,10 @@ $.postJSON = function (url, data, callback) {
 
 $.fn.extend({
     turnOn: function () {
-        return $(this).prop('disabled', false)
+        return $(this).prop('disabled', false);
     },
     turnOff: function () {
-        return $(this).prop('disabled', true)
+        return $(this).prop('disabled', true);
     },
     aggregate: function (foo, init = 0) {
         var res = init;
@@ -39,7 +39,7 @@ var Auth = (function () {
             return $.postJSON(URLdir + 'handler/sign/in', {
                 'login':    login,
                 'password': pass,
-            }).always(_ => {
+            }).always(() => {
                 allowRequest = true;
                 self.onAllow();
             });
diff --git a/media/js/discipline/rating/common.js b/media/js/discipline/rating/common.js
new file mode 100644
index 000000000..0ae0af794
--- /dev/null
+++ b/media/js/discipline/rating/common.js
@@ -0,0 +1,423 @@
+class Base {
+    static parseID($this) {
+        return +/\w_(\d+)/.exec($this.attr('id'))[1];
+    }
+
+    static parsePosition($this) {
+        let doubleNumReg = /[A-Za-z_]+(\d+)[A-Za-z_]+(\d+)/;
+        return doubleNumReg.exec($this.attr('id')).splice(1, 2);
+    }
+
+    static parseSettings() {
+        let $hiddenInfo = $('#hidden_div');
+        let $disciplineID = $('#disciplineID');
+        let $pageType = $('#pageType');
+
+        let settings = $.parseJSON($hiddenInfo.html());
+        settings.disciplineID = $disciplineID.val();
+        settings.pageType = $pageType.val();
+
+        $hiddenInfo.remove();
+        $disciplineID.remove();
+        $pageType.remove();
+
+        return settings;
+    }
+}
+
+class Cursor {
+    constructor() {
+        this.col = 0;
+        this.row = 0;
+    }
+
+    update(col, row) {
+        this.col = col;
+        this.row = row;
+    }
+
+    get() {
+        return [this.col, this.row];
+    }
+
+    parseLocation($this) {
+        let [col, row] = Base.parsePosition($this);
+        this.update(+col, +row);
+    }
+}
+
+class Cell {
+    constructor() {
+        this.update(0, 0, 0);
+    }
+
+    update(student, submodule, maxRate) {
+        this.student = student;
+        this.submodule = submodule;
+        this.maxRate = maxRate;
+    }
+}
+
+class RateInfo {
+    constructor(settings, cursor, cell) {
+        this.settings = settings;
+        this.cursor = cursor;
+        this.cell = cell;
+
+        this.$submodulesInfo = $('.RatingTableSubmodulesInfo');
+        this.$submodulesHead = $('.RatingTableSubmodulesHead');
+        this.$submodulesMaxRate = $('.RatingTableSubmodulesHeadMaxRate');
+        this.$modulesHead = $('.RatingTableModulesHead');
+
+        this.$tdInfoWrap = $('#tdInfo_wrap');
+        this.$tdInfo = this.$tdInfoWrap.children('#tdInfo');
+        this.$studentInfo = this.$tdInfo.children('#student').children('b');
+        this.$submoduleInfo = this.$tdInfo.children('#submodule').children('b');
+        this.$rateInfo = this.$tdInfo.children('#maxRate').children('b');
+    }
+
+    show($this) {
+        var disciplinePassRate = (this.settings.Type === "exam") ? 38 : 60;
+        var [col] = this.cursor.get();
+        // Получаем подмодуль
+        var jCurSubmoduleInfo = this.$submodulesInfo.children(`.col_${col}:first`);
+        var jCurSubmoduleHead = this.$submodulesHead.children(`.col_${col}:first`);
+
+        var submodule = +jCurSubmoduleInfo.attr('id');
+        var student = Base.parseID($this.siblings('.studentCell'));
+        var maxRate = +this.$submodulesMaxRate.children(`.col_${col}`).text();
+
+        // Проверяем допустимое значение (только для добора)
+        if ($this.hasClass('additionalCell')) {
+            var semesterRate = parseInt($this.siblings('.semesterRateResultCell').text());
+            maxRate = (semesterRate <= disciplinePassRate) ? disciplinePassRate - semesterRate : 0;
+        }
+        this.cell.update(student, submodule, maxRate);
+
+        let submoduleTitle = (jCurSubmoduleHead.length < 1 && this.$modulesHead.children('.bonus').length > 0)
+            ? 'Бонусные баллы'
+            : (jCurSubmoduleHead.length == 0) ? 'Добор баллов' : jCurSubmoduleHead.text(); // todo: убрать костыль
+        let stdName = $this.siblings('.studentCell').text();
+
+        this.$tdInfoWrap.show();
+        this.$studentInfo.html(stdName);
+        this.$submoduleInfo.html(submoduleTitle);
+        this.$rateInfo.html(this.cell.maxRate);
+    };
+
+    hide() {
+        this.$tdInfoWrap.hide();
+        this.cell.update(0, 0, 0);
+    };
+
+}
+
+const defaultCellBackground = '#fff';
+const focusCellBackground = '#f1f1f1';
+class Highlighting {
+    constructor(settings, cursor) {
+        this.settings = settings;
+        this.cursor = cursor;
+    }
+
+    static setRowBackground(color, [col, row]) {
+        let $cols = $(`td#col_${col}`);
+        let $rows = $(`tr#row_${row}`);
+        let $rowStatic = $rows.find('.staticCell');
+
+        $cols.filter('.commonCell')
+            .add($cols.filter('.staticCell'))
+            .add($rows.find('.commonCell'))
+            .add($rowStatic)
+            .children('input')
+            .add($rowStatic)
+            .each((_, elem) => $(elem).css('background-color', color));
+    }
+
+    // Ставим подстветку
+    focus($this) {
+        this.cursor.parseLocation($this);
+        Highlighting.setRowBackground(focusCellBackground, this.cursor.get());
+        $this.children('input').css('background-color', defaultCellBackground);
+    };
+
+    // Убираем подстветку
+    focusOut() {
+        Highlighting.setRowBackground(defaultCellBackground, this.cursor.get());
+    }
+}
+
+class GroupFilter {
+    constructor(settings, $table, groupID) {
+        this.settings = settings;
+        this.$table = $table.children('tbody');
+
+        this.filterGroups(+groupID);
+    }
+
+    // Скрываем все остальные группы
+    // 0 - показать все
+    filterGroups(groupID) {
+        let groupClass = `group_${groupID}`;
+        this.$table.children().slice(3).each((_, elem) => {
+            let matchGroup = groupID == 0 || $(elem).hasClass(groupClass);
+            matchGroup ? $(elem).show() : $(elem).hide();
+        });
+    }
+
+    listen($selector) {
+        let self = this;
+        $selector.change(function () {
+            let groupSelected = +$(this).val();
+            if (groupSelected >= 0) {
+                self.filterGroups(groupSelected);
+                let disciplineID = self.settings.disciplineID;
+                $.post(URLdir + 'handler/rating/SelectGroup', {disciplineID, groupSelected});
+            }
+        });
+    }
+}
+
+class TableAdjuster {
+    constructor($wrapper, $table) {
+        this.$wrapper = $wrapper;
+        this.$table = $table;
+    }
+
+    adjust() {
+        let tableWidth = this.$table.get(0).scrollWidth;
+        // check scroll bar
+        if (tableWidth <= this.$table.width() + 10)
+            return;
+
+        //correct
+        tableWidth *= 1.1;
+        let maxWidth = $(window).width() * 0.95;
+        let newWidth = (tableWidth > maxWidth) ? maxWidth : tableWidth;
+        this.$wrapper.css('max-width', newWidth);
+    };
+
+    autoResize() {
+        $(window).resize(() => this.adjust());
+    }
+}
+
+function Rating() {
+
+    let settings = Base.parseSettings();
+    let cell = new Cell();
+    let cursor = new Cursor();
+    let rateInfo = new RateInfo(settings, cursor, cell);
+    let highlighter = new Highlighting(settings, cursor);
+    let adjuster = new TableAdjuster($('.main_layer'), $('.main_content'));
+    let filter = new GroupFilter(settings, $('.studentsRate'), settings['GroupID_Filter']);
+
+    const Direction = {
+        Up: 0,
+        Right: 1,
+        Down: 2,
+        Left: 3
+    };
+
+    /**
+     * @param direction Direction в каком направлении искать следующую ячейку для перемещения фокуса
+     * @return jQuery клетка, в которую надо переместиться или пустой jquery list, если в этом направлении нет подходящих ячеек
+     */
+    let getDesiredCell = function (direction) {
+        let [col, row] = cursor.get();
+        let dx = (direction == Direction.Left) ? -1
+            : (direction == Direction.Right) ? 1 : 0;
+        let dy = (direction == Direction.Up) ? -1
+            : (direction == Direction.Down) ? 1 : 0;
+
+        let currentCell;
+        do {
+            row += dy;
+            col += dx;
+            currentCell = $(`#col_${col}_row_${row}`);
+        } while (currentCell.length > 0 && currentCell.children('input').attr('disabled') === 'disabled');
+        return currentCell;
+    };
+
+    let isFocusCell = false; // Стоит фокус на ячейке или нет
+    let oldRate = 0;
+    var cancelFlag = false;
+    let initGeneralCells = function (finalize) {
+        var jCommonCell = $('.commonCell').not('.absenceCell').not('.autoPass');
+
+        jCommonCell.mouseenter(function () {
+            if (isFocusCell === false)
+                highlighter.focus($(this));
+        });
+
+        jCommonCell.mouseleave(function () {
+            if (isFocusCell === false)
+                highlighter.focusOut();
+        });
+
+        jCommonCell.focusin(function () {
+            isFocusCell = true;
+            highlighter.focus($(this));
+            rateInfo.show($(this));
+
+            var value = $(this).children('input').val();
+            oldRate = (value !== '') ? +value : -1;
+        });
+
+        jCommonCell.focusout(function () {
+            isFocusCell = false;
+
+            if (cancelFlag) {
+                var str = (oldRate != -1) ? oldRate : '';
+                $(this).children('input').val(str);
+                cancelFlag = false;
+            } else
+                setRate($(this), +oldRate);
+
+            highlighter.focusOut();
+            rateInfo.hide($(this));
+            finalize($(this));
+        });
+
+        jCommonCell.children('input').focusin(function () {
+            $(this).select();
+        });
+
+        // При нажатии на элемент commonCell дочерный input получает фокус
+        jCommonCell.click(function () {
+            $(this).children('input').focus();
+        });
+
+        // В inputCredit (где баллы вводить) разрешаем вводить только цифры
+        jCommonCell.children('input').keydown(KeyDownOnlyNumber);
+
+        const keyDirs = {
+            27: Direction.Down, // esc
+            13: Direction.Down, // enter
+            40: Direction.Down, // down arrow
+            38: Direction.Up, // up arrow
+            39: Direction.Right, // right arrow
+            37: Direction.Left // left arrow
+        };
+
+        jCommonCell.keydown(function (e) {
+            cancelFlag = cancelFlag || +e.keyCode === 27; // escape
+            if (!(e.keyCode in keyDirs)) //
+                return;
+
+            let [col, row] = cursor.get();
+            let direction = keyDirs[e.keyCode];
+            let whereToMoveFocus = getDesiredCell(direction);
+
+            if (whereToMoveFocus.length > 0) {
+                highlighter.focusOut();
+                cursor.update(col, row);
+                whereToMoveFocus.children('input').focus();
+            } else
+                $(this).children('input').blur();
+        });
+
+
+    };
+
+
+    var postRate = function (rate, $this, oldRate, rateResult, studentID, submoduleID) {
+        $.postJSON(URLdir + 'handler/rating/setRate', {studentID, submoduleID, rate})
+            .success(() => {
+                let correctRate = (rateResult > 100) ? '100+' : rateResult;
+                $this.siblings('.rateResultCell').text(correctRate);
+
+                // Открываем доступ к след. ячейке добора баллов
+                if ($this.hasClass('additionalCell')) {
+                    let [col, row] = cursor.get();
+                    let nextAdditionalCell = $(`#col_${col + 1}_row_${row}`);
+                    let placeholderMaxVal = (rateResult < 60) ? (60 - rateResult) : 0;
+
+                    if (nextAdditionalCell.hasClass('additionalCell')) {
+                        let placeholderMax = (placeholderMaxVal > 0) ? 'макс. ' + placeholderMaxVal : '---';
+                        nextAdditionalCell.find('input').attr('placeholder', placeholderMax);
+                    }
+                }
+                Popup.success('Балл добавлен/изменен');
+            }).fail(jqXHR => {
+            $this.children('input').val(oldRate);
+            let status = +jqXHR.status;
+            let errorMsg = (status == 400) ? 'Не удалось добавить/изменить балл' :
+                            (status == 403) ? 'Сессия истекла' : '' + jqXHR.status; // null to 'null'
+            Popup.error(errorMsg);
+
+            if (status == 403)
+                window.location.replace(URLdir);
+        });
+    };
+
+    // todo: merge "if" branches
+    function recountScores(newRate, $this) {
+        let rateResult = Math.max(0, newRate);
+        let bonus = +$this.siblings('.bonus').text();
+
+        // считаем баллы по строке
+        if (settings.pageType === 'exam') //(jThis.attr('class').indexOf('attemptCell') >= 0)
+        {
+            // страница сессии
+            rateResult += +$this.siblings('.semesterRateResultCell').text();
+            rateResult += $this.siblings('.additionalCell').aggregate((init, elem) => +$(elem).children('input').val() + init);
+
+            if (newRate === -1)
+                rateResult += $this.siblings('.attemptCell').not('.autoPass').not('.absenceCell')
+                    .aggregate((init, elem) => Math.max(init, +$(elem).find('input').val()));
+        } else {
+            $this.siblings('.commonCell').each(function () { // добавим сумму баллов в соседних ячейках
+                rateResult += +$(this).children('input').val() || 0;
+            });
+            let extraRate = +$this.siblings('.extraCell').text() || 0;
+            let examRate = +$this.siblings('.examCell').text() || 0;
+            rateResult += extraRate + examRate;
+        }
+        return rateResult + (bonus || 0);
+    }
+
+    /**
+     * @param {jQuery} $this
+     * @param {int} oldRate
+     */
+    function setRate($this, oldRate) {
+        let $scoreInput = $this.children('input');
+        let inputVal = $scoreInput.val();
+
+        // если пустая строка в ячейке, значит ничего не поставлено
+        let newRate = (inputVal === '') ? -1 : +inputVal;
+        if (newRate === oldRate)
+            return;
+
+        // блокируем ячейку пока не обработаем коллбек
+        $scoreInput.turnOff();
+
+        var rateResult = recountScores(newRate, $this);
+
+        if (newRate <= cell.maxRate)
+            postRate(newRate, $this, oldRate, rateResult, cell.student, cell.submodule);
+        else {
+            let cellRate = (oldRate > cell.maxRate) ? '0' :
+                            (oldRate === -1 ? '' : oldRate);
+            $scoreInput.val(cellRate);
+            Popup.error('Текущий балл превышает максимальный для данного модуля');
+        }
+        // todo: move to post callback
+        $scoreInput.turnOn();
+    }
+
+    return {
+        initGeneralCells,
+
+        // + ID - id дисциплины
+        // + studyGroupID_Filter - studyGroupID для фильтра (Эффект памяти)
+        settings,
+        cell,
+        cursor,
+        rateInfo,
+        highlighter,
+        adjuster,
+        filter
+    }
+}
diff --git a/media/js/discipline/rating/exam.js b/media/js/discipline/rating/exam.js
index b551311a4..cd507c0ab 100644
--- a/media/js/discipline/rating/exam.js
+++ b/media/js/discipline/rating/exam.js
@@ -1,8 +1,9 @@
 var $ = jQuery;
 
 $(function () {
-    // Секция инициализации
-    var optionsViewData = {
+    var R = Rating();
+
+    let optionsViewData = {
         'absence': {
             sendingOption: 'absence',
             messageOK: 'Неявка выставлена',
@@ -25,30 +26,6 @@ $(function () {
         }
     };
 
-    var json_settings = (function () {
-        var $hiddenInfo = $('#hidden_div');
-        var data = $.parseJSON($hiddenInfo.html());
-        $hiddenInfo.remove();
-        return data;
-    })();
-    var pageType = $('#pageType').val();
-    var disciplineType = $('#ExamType').val();
-    var cancelFlag = false;
-    var g_col;
-    var g_row;
-    var g_isFocusCell = false; // Стоит фокус на ячейке или нет
-    var g_oldRateCell = null;
-    var g_submoduleID = null;
-    var g_studentID = null;
-    var g_submoduleTitle = '';
-    var g_submoduleMaxRate = 0;
-    var g_stdName = '';
-
-    var jCommonCell = $('.commonCell').not('.absenceCell').not('.autoPass');
-    var jTdInfo_wrap = $('#tdInfo_wrap');
-    var jTdInfo = jTdInfo_wrap.children('#tdInfo');
-    var g_disciplineID = $('#disciplineID').val();
-
     var StudentState = {
         successful: 0, // успевающий
         beforeExam: 1, // экзамен еще не состоялся
@@ -59,39 +36,21 @@ $(function () {
         admittedAndNotPassed: 6 // допуск получен, экзамен не сдал
     };
 
-    // on page loaded
-    // Настройки дисциплины:
-    // + ID - id дисциплины
-    // + studyGroupID_Filter - studyGroupID для фильтра (Эффект памяти)
-
-    controlVisualization();
-    $('div.main_content').ready(AdjustTable);
-    $(window).resize(AdjustTable);
-
-    // выставить фильтр по группам
     (function () {
-        var groupID = json_settings['GroupID_Filter'];
-        if (groupID) {
-            filterGroups(groupID);
-            $(".groupSelector [value='" + groupID + "']").attr('selected', 'selected');
-        }
+        $('.studentsRate').children().children('tr')
+            .each(function () {
+                let id = $(this).attr('id');
+                if (id && id.substr(0, 3) === 'row')
+                    controlRowVisualization($(this));
+            });
     })();
 
-    function AdjustTable() {
-        var jWrap = $('div.main_layer');
-        var jTable = $('div.main_content');
-        var tableWidth = jTable.get(0).scrollWidth;
-        // check scroll bar
-        if (tableWidth <= jTable.width() + 10) {
-            return;
-        }
+    R.adjuster.autoResize();
+    R.filter.listen($('.groupSelector'));
+    R.initGeneralCells($this => {
+        controlRowVisualization($this.parent());
+    });
 
-        //correct
-        tableWidth *= 1.1;
-        var maxWidth = $(window).width() * 0.95;
-        var newWidth = (tableWidth > maxWidth) ? maxWidth : tableWidth;
-        jWrap.css('max-width', newWidth);
-    }
 
     function deriveStudentState(semesterRate, extraRate, examRate, autopass, firstAttempt) {
         var admissionRate = semesterRate + extraRate;
@@ -117,17 +76,12 @@ $(function () {
     }
 
     function setCellAccess(cell, access) {
-        if (access)
-            $(cell).removeAttr('disabled');
-        else
-            $(cell).attr('disabled', true);
+        access ? $(cell).turnOn() : $(cell).turnOff();
     }
 
     function controlRowVisualization(jRow) {
-
-        if ((pageType !== 'exam') || (disciplineType !== 'exam')) { // || (json_settings.ExamType !== 'exam')) {
+        if (R.settings.Type !== 'exam')
             return;
-        }
 
         var jAutoPassCheckBox = jRow.children('.autoPass').children('.autoPassCheck');
         var jAbsenceCheckBoxes = jRow.children('.absenceCell').children('.absenceCheck');
@@ -136,16 +90,15 @@ $(function () {
         var semesterRate = parseInt(jRow.children('.semesterRateResultCell').text());
         var autopass = jAutoPassCheckBox[0].checked;
         var absence = jAbsenceCheckBoxes[0].checked;
-        if (semesterRate < 60 || absence) {
-            jAutoPassCheckBox.attr('disabled', true);
-        } else {
-            jAutoPassCheckBox.removeAttr('disabled');
-        }
+
+        if (semesterRate < 60 || absence)
+            jAutoPassCheckBox.turnOff();
+        else
+            jAutoPassCheckBox.turnOn();
 
         // суммарный добор
         var extraRate = jExtraInputs.aggregate((cur, elem) => +$(elem).val() + cur);
-        var row = +jRow.attr('id').substr(4); // одинаковая для всех
-
+   
         // Определим экзамен
         var curExamCell = 0;
         var curExamRate = 0;
@@ -155,8 +108,7 @@ $(function () {
         jAttemptCells.each(function () {
             var needToRemeber = (curExamCell == 0); // инициализация первым экзаменом
 
-            var id = $(this).attr('id');
-            var col = +id.substr(4, 1);
+            let [col, row] = Base.parsePosition($(this));
             var neighborAbsenceCheck = $($(`#absence_${col}_${row}`).children()[0]);
 
             dependentCells[index][0] = $(this);
@@ -218,347 +170,25 @@ $(function () {
         }
     }
 
-    function controlVisualization() {
-        $('.studentsRate').children().children('tr')
-            .each(function () {
-                if (($(this).prop('id') !== '') && ($(this).attr('id').substr(0, 3) === 'row')) {
-                    controlRowVisualization($(this));
-                }
-            });
-    }
-
-    // Скрываем все остальные группы
-    // 0 - показать все
-    function filterGroups(groupID) {
-        if (groupID == 0) {
-            $('.studentsRate tbody')
-                .children()
-                .each((_, elem) => $(elem).show());
-        } else {
-            $('.studentsRate tbody')
-                .children(':gt(2)')
-                .each(function () {
-                    if ($(this).hasClass(`group_${groupID}`))
-                        $(this).show();
-                    else
-                        $(this).hide();
-                });
-        }
-    }
-
-    var setCursorLocation = function (jThis) {
-        g_col = parseInt(jThis.attr('id').substr(4));
-        g_row = parseInt(jThis.parent('tr').attr('id').substr(4))
-    };
-
-    var setRowBackground = function (color) {
-        let $cols = $(`td#col_${g_col}`);
-        let $rows = $(`tr#row_${g_row}`);
-        let $rowStatic = $rows.find('.staticCell');
-
-        $cols.filter('.commonCell')
-            .add($cols.filter('.staticCell'))
-            .add($rows.find('.commonCell'))
-            .add($rowStatic)
-            .children('input')
-            .add($rowStatic)
-            .each((_, elem) => $(elem).css('background-color', color));
-    };
-
-    var defaultCellBackground = '#fff';
-    var focusCellBackground = '#f1f1f1';
-
-    // Ставим подстветку
-    function TdFocus(jThis) {
-        setCursorLocation(jThis);
-        g_oldRateCell = jThis.children('input').val();
-
-        setRowBackground(focusCellBackground);
-        jThis.children('input').css('background-color', defaultCellBackground);
-    }
-
-    // Убираем подстветку
-    function TdUnFocus() {
-        setRowBackground(defaultCellBackground);
-    }
-
-    function TdInfo(jThis) {
-        var disciplinePassRate = 60; // credit pass rate
-        if (disciplineType === 'exam')
-            disciplinePassRate = 38;
-
-        // Получаем подмодуль
-        var jCurSubmoduleInfo = $(`.RatingTableSubmodulesInfo .col_${g_col}:first`);
-        var jCurSubmoduleHead = $(`.RatingTableSubmodulesHead .col_${g_col}:first`);
-
-        g_submoduleID = parseInt(jCurSubmoduleInfo.attr('id'));
-        g_submoduleTitle = jCurSubmoduleHead.text();
-        if (jCurSubmoduleHead.length < 1 && $('.RatingTableModulesHead .bonus').length > 0)
-            g_submoduleTitle = 'Бонусные баллы';
-        g_submoduleMaxRate = parseInt($(`.RatingTableSubmodulesHeadMaxRate .col_${g_col}`).text());
-
-
-        // Проверяем допустимое значение (только для добора)
-        if (jThis.attr('class').indexOf('additionalCell') >= 0) {
-            var semesterRate = parseInt(jThis.siblings('.semesterRateResultCell').text());
-            if (semesterRate <= disciplinePassRate)
-                g_submoduleMaxRate = disciplinePassRate - semesterRate;
-            else
-                g_submoduleMaxRate = 0;
-        }
-
-        // Получаем студента
-        g_studentID = jThis.siblings('.studentCell').attr('id');
-        g_studentID = g_studentID.substr(8);
-
-        g_stdName = jThis.siblings('.studentCell').text();
-
-        jTdInfo_wrap.show();
-        jTdInfo.children('#student').children('b').html(g_stdName);
-        jTdInfo.children('#submodule').children('b').html(g_submoduleTitle);
-        jTdInfo.children('#maxRate').children('b').html(g_submoduleMaxRate);
-    }
-
-    function UnsetTdInfo(jThis) {
-        jTdInfo_wrap.hide();
-        g_submoduleID = null;
-        g_studentID = null;
-        g_submoduleMaxRate = 0;
-    }
-
-    var setRate = function(newRate, jThis, oldRate, rateResult, bonus) {
-        $.postJSON(URLdir + 'handler/rating/setRate',
-            {
-                studentID: g_studentID, 
-                submoduleID: g_submoduleID,
-                rate: newRate
-            }
-        ).success(data => {
-            if (data.success !== true) {
-                jThis.children('input').val(oldRate);
-                EventInspector.error('Не удалось добавить/изменить балл');
-                return;
-            }
-
-            var correctRate = (rateResult + bonus > 100) ? '100+' : rateResult + bonus;
-            jThis.siblings('.rateResultCell').text(correctRate);
 
-            // Открываем доступ к след. ячейке добора баллов
-            if (jThis.hasClass('additionalCell')) {
-                var nextAdditionalCell = $('#col_' + (g_col + 1) + '_row_' + g_row);
-                var placeholderMaxVal = (rateResult < 60) ? (60 - rateResult) : 0;//(60 - oldRate);
-
-                if (nextAdditionalCell.hasClass('additionalCell')) {
-                    var placeholderMax = (placeholderMaxVal > 0) ? placeholderMax = 'макс. ' + placeholderMaxVal : '---';
-                    nextAdditionalCell.find('input').attr('placeholder', placeholderMax);
-                }
-            }
-            EventInspector.success('Балл добавлен/изменен');
-        }).fail(jqXHR => {
-            jThis.children('input').val(oldRate);
-            switch (jqXHR.status) {
-                case 400:
-                    EventInspector.error(optionsViewData[option].messageFail);
-                    break;
-                case 403:
-                    EventInspector.error('Сессия истекла');
-                    window.location.replace(URLdir);
-                    break;
-                default:
-                    EventInspector.error(' ' + jqXHR.status);
-            }
-        });
-    };
-
-    function Rating(jThis, oldRate) {
-        oldRate = parseInt(oldRate);
-
-        // Здесь jThis - div rateCell, а не input, который является дочкой
-        if (cancelFlag) {
-            var str = '';
-            if (oldRate != -1)
-                str = oldRate;
-            jThis.children('input').val(str);
-            cancelFlag = false;
-            return;
-        }
-
-        var newRate = -1; // если пустая строка в ячейке, значит ничего не поставлено
-        if (jThis.children('input').val() !== '')
-            newRate = parseInt(jThis.children('input').val());
-        if (newRate == oldRate)
-            return;
-
-        // блокируем ячейку пока не обработаем коллбек
-        jThis.children('input').attr('disabled', true);
-
-        var rateResult = Math.max(0, newRate);
-        var bonus = parseInt(jThis.siblings('.bonus').text());
-
-        // считаем баллы по строке
-        if (pageType === 'exam') //(jThis.attr('class').indexOf('attemptCell') >= 0)
-        {
-            // страница сессии
-            rateResult += parseInt(jThis.siblings('.semesterRateResultCell').text());
-            jThis.siblings('.additionalCell').aggregate((init, elem) => +$(elem).children('input').val() + init);
-        }
-        if (newRate === -1)
-            rateResult += jThis.siblings('.attemptCell').not('.autoPass').not('.absenceCell')
-                .aggregate((init, elem) => Math.max(init, +$(elem).find('input').val()));
-
-        if (newRate <= g_submoduleMaxRate)
-            setRate(newRate, jThis, oldRate, rateResult, bonus);
-        else {
-            var cellRate = (oldRate <= g_submoduleMaxRate)
-                ? ((oldRate != -1) ? oldRate : '')
-                : '0';
-            jThis.children('input').val(cellRate);
-            EventInspector.error('Текущий балл превышает максимальный для данного модуля');
-        }
-        jThis.children('input').removeAttr('disabled');
-    }
-
-    jCommonCell.mouseenter(function () {
-        if (g_isFocusCell === false)
-            TdFocus($(this));
-    });
-
-    jCommonCell.mouseleave(function () {
-        if (g_isFocusCell === false)
-            TdUnFocus();
-    });
-
-    var oldRate = 0;
-    jCommonCell.focusin(function () {
-        g_isFocusCell = true;
-        TdFocus($(this));
-        TdInfo($(this));
-
-        var value = $(this).children('input').val();
-        oldRate = (value !== '') ? +value : -1;
-    });
-
-    jCommonCell.focusout(function () {
-        g_isFocusCell = false;
-        Rating($(this), oldRate);
-
-        TdUnFocus();
-        UnsetTdInfo($(this));
-        controlRowVisualization($(this).parent());
-    });
-
-    var Direction = {
-        Up: 0,
-        Right: 1,
-        Down: 2,
-        Left: 3
-    };
-
-    /**
-     * @param direction Direction в каком направлении искать следующую ячейку для перемещения фокуса
-     * @return jQuery клетка, в которую надо переместиться или null, если в этом направлении нет подходящих ячеек
-     */
-    function getDesiredCell(direction) {
-        /** Будем искать ячейку в направлении {@see direction} до тех пока не найдем её и пока не
-         *  кончатся ячейки в этом направлении */
-        var row = g_row;
-        var col = g_col;
-        var currentCell;
-        switch (direction) {
-            case Direction.Up:
-                do {
-                    row--;
-                    currentCell = $(`tr#row_${row} td#col_${g_col}.commonCell`)
-                } while ((currentCell.length > 0) &&
-                (currentCell.children('input').attr('disabled') == 'disabled'));
-                return currentCell;
-            case Direction.Right:
-                do {
-                    col++;
-                    currentCell = $(`tr#row_${g_row} td#col_${col}.commonCell`)
-                } while ((currentCell.length > 0) &&
-                (currentCell.children('input').attr('disabled') == 'disabled'));
-                return currentCell;
-            case Direction.Down:
-                do {
-                    row++;
-                    currentCell = $(`tr#row_${row} td#col_${g_col}.commonCell`)
-                } while ((currentCell.length > 0) &&
-                (currentCell.children('input').attr('disabled') == 'disabled'));
-                return currentCell;
-            case Direction.Left:
-                do {
-                    col--;
-                    currentCell = $(`tr#row_${g_row} td#col_${col}.commonCell`)
-                } while ((currentCell.length > 0) &&
-                (currentCell.children('input').attr('disabled') == 'disabled'));
-                return currentCell;
-        }
-        return null;
-    }
-
-    jCommonCell.keydown(function (e) {
-        var row = g_row;
-        var col = g_col;
-        var direction;
-        switch (e.keyCode) {
-            case 27:
-                cancelFlag = true; // esc
-            case 13: // enter
-            case 40: // down arrow
-                direction = Direction.Down;
-                break;
-            case 38: // up arrow
-                direction = Direction.Up;
-                break;
-            case 39: // right arrow
-                direction = Direction.Right;
-                break;
-            case 37: // left arrow
-                direction = Direction.Left;
-                break;
-            default:
-                return;
-        }
-        var whereToMoveFocus = getDesiredCell(direction);
-        if (whereToMoveFocus.length > 0) {
-            TdUnFocus();
-            g_row = row;
-            g_col = col;
-            whereToMoveFocus.children('input').focus();
-        } else
-            $(this).children('input').blur();
-    });
-
-    $('.commonCell input').focusin(function () {
-        $(this).select();
-    });
-
-    // При нажатии на элемент commonCell дочерный input получает фокус
-    jCommonCell.click(function () {
-        $(this).children('input').focus();
-    });
-
-    // В inputCredit (где баллы вводить) разрешаем вводить только цифры
-    jCommonCell.children('input').keydown(function (event) {
-        KeyDownOnlyNumber(event);
-    });
+    // ================================================
+    //          exam page specific
+    // ================================================
 
     // Нажатие на чекбокс "Неявка"
-    $('.absenceCheck').click(function (event) {
+    $('.absenceCheck').click(function () {
         // prepare
         var option = 'absence';
         var checked = $(this)[0].checked;
         if (!checked)
             option += '_unset';
         var thisTd = $(this).parent();
-        g_col = thisTd.attr('id').substr(8, 1); // absence_C
-        g_row = thisTd.attr('id').substr(10, 1); // absence_C_R
+        R.cursor.parseLocation(thisTd);
 
         // call
-        TdInfo(thisTd);
+        R.rateInfo.show(thisTd);
         var errCode = setExamPeriodOption($(this), option);
-        UnsetTdInfo(thisTd);
+        R.rateInfo.hide(thisTd);
 
         // test
         if (errCode > 0)
@@ -566,20 +196,19 @@ $(function () {
         controlRowVisualization(thisTd.parent());
     });
 
-    $('.autoPassCheck').click(function (event) {
+    $('.autoPassCheck').click(function () {
         // prepare
         var option = 'pass';
         var checked = $(this)[0].checked;
         if (!checked)
             option += '_unset';
         var thisTd = $(this).parent();
-        g_col = thisTd.attr('id').substr(9, 1); // autopass_C
-        g_row = thisTd.attr('id').substr(11, 1); // autopass_C_R
+        R.cursor.parseLocation(thisTd);
 
         // call
-        TdInfo(thisTd);
+        R.rateInfo.show(thisTd);
         var errCode = setExamPeriodOption($(this), option);
-        UnsetTdInfo(thisTd);
+        R.rateInfo.hide(thisTd);
 
         // test
         if (errCode > 0)
@@ -587,54 +216,35 @@ $(function () {
         controlRowVisualization(thisTd.parent());
     });
 
-
-    function setExamPeriodOption(jThis, option) {
-        jThis.attr('disabled', true);
+    function setExamPeriodOption($this, option) {
+        $this.turnOff();
 
         $.postJSON(URLdir + 'handler/rating/SetExamPeriodOption',
             {
-                studentID: g_studentID,
-                submoduleID: g_submoduleID,
+                studentID: R.cell.student,
+                submoduleID: R.cell.submodule,
                 option: '' + optionsViewData[option].sendingOption // cast null to "null"
             })
         .done(data => EventInspector.success(optionsViewData[option].messageOK))
         .fail(jqXHR => {
-            jThis.prop("checked", !jThis.prop("checked"));
-            switch (jqXHR.status) {
-                case 400:
-                    EventInspector.error(optionsViewData[option].messageFail);
-                    break;
-                case 403:
-                    EventInspector.error('Сессия истекла');
-                    window.location.replace(URLdir);
-                    break;
-                default:
-                    EventInspector.error(' ' + jqXHR.status);
-            }
-        }).always(() => jThis.removeAttr('disabled'));
-    }
+            $this.prop("checked", !$this.prop("checked"));
 
-    // Фильтр по группе
-    $('.groupSelector').change(function () {
-        var group = $(this).val();
-        if (!(group >= 0))
-            return;
+            let status = +jqXHR.status;
+            let errorMsg =  (status == 400) ? optionsViewData[option].messageFail :
+                            (status == 403) ? 'Сессия истекла' : '' + jqXHR.status; // null to 'null'
+            Popup.error(errorMsg);
 
-        filterGroups(group);
-        $.post(
-            URLdir + 'handler/rating/SelectGroup',
-            {
-                'disciplineID': g_disciplineID,
-                'groupSelected': group
-            }
-        );
-    });
+            if (status === 403)
+                window.location.replace(URLdir);
+
+        }).always(() => $this.turnOn());
+    }
 
     // Скачать таблицу оценивания в формате excel // depricated
     $('.downloadExcel').click(function () {
         $.fileDownload(URLdir + 'handler/FileCreator/GenerateExcelRatingTable', {
             httpMethod: 'POST',
-            data: {'disciplineID': g_disciplineID},
+            data: {'disciplineID': R.settings.disciplineID},
             successCallback: function () {},
             failCallback: function () {}
         });
@@ -643,15 +253,15 @@ $(function () {
     // Ведомость в формате excel
     $('body').on('click', '.downloadExcelStatement', function () {
 
-        var groupID = parseInt($(this).attr('id').substr(6));
-        var jStageSelector = $('#stageSelector_' + groupID);
+        var groupID = Base.parseID($(this));
+        var $stageSelector = $('#stageSelector_' + groupID);
 
         $.fileDownload(URLdir + 'handler/FileCreator/GenerateFinalForm', {
             httpMethod: 'POST',
             data: {
-                'disciplineID': g_disciplineID,
+                'disciplineID': R.settings.disciplineID,
                 'groupID': groupID,
-                'stage': parseInt(jStageSelector.val())
+                'stage': parseInt($stageSelector.val())
             },
             successCallback: function () {},
             failCallback: function () {}
diff --git a/media/js/discipline/rating/rate.js b/media/js/discipline/rating/rate.js
index 92f63133f..b7b538fe7 100644
--- a/media/js/discipline/rating/rate.js
+++ b/media/js/discipline/rating/rate.js
@@ -1,515 +1,9 @@
 var $ = jQuery;
 
-$(function () {
-    // Секция инициализации
-    var optionsViewData = {
-        'absence': {
-            sendingOption: 'absence',
-            messageOK: 'Неявка выставлена',
-            messageFail: 'Установка неявки не удалась'
-        },
-        'absence_unset': {
-            sendingOption: null,
-            messageOK: 'Неявка отменена',
-            messageFail: 'Отмена неявки не удалась'
-        },
-        'pass': {
-            sendingOption: 'pass',
-            messageOK: 'Автомат выставлен',
-            messageFail: 'Установка автомата не удалась'
-        },
-        'pass_unset': {
-            sendingOption: null,
-            messageOK: 'Автомат отменен',
-            messageFail: 'Отмена автомата не удалась'
-        }
-    };
-    var json_settings = $.parseJSON($("#hidden_div").html());
-    var cancelFlag = false;
-    var g_col;
-    var g_row;
-    var g_isFocusCell = false; // Стоит фокус на ячейки или нет
-    var g_oldRateCell = null;
-    var g_submoduleID = null;
-    var g_studentID = null;
-    var g_submoduleTitle = "";
-    var g_submoduleMaxRate = 0;
-    var g_stdName = "";
-
-    var jCommonCell = $('.commonCell');
-    var jTdInfo_wrap = $("#tdInfo_wrap");
-    var jTdInfo = jTdInfo_wrap.children('#tdInfo');
-    var g_URL = (window.location.href).split("/");
-    var g_disciplineID = $('#disciplineID').val();
-
-    var onCursorChange = function (cursor) {
-        // console.log('(' + cursor.row + ', ' + cursor.col + ')\n'); // FIXME
-    };
-
-    var cursor = (function (onChange) {
-        var col = NaN, row = NaN;
-        var self = this;
-
-        return {
-            col: function (val) {
-                if (typeof val !== 'undefined') {
-                    if (onChange) onChange(self);
-                    self.col = val;
-                }
-                return self.col;
-            },
-
-            row: function (val) {
-                if (typeof val !== 'undefined') {
-                    if (onChange) onChange(self);
-                    self.row = val;
-                }
-                return self.row;
-            }
-        }
-    })(onCursorChange);
-
-    //function getDesiredCell(direction) {
-    //    /** Будем искать ячейку в направлении {@see direction} до тех пока не найдем её и пока не
-    //     *  кончатся ячейки в этом направлении */
-    //    var row = g_row;
-    //    var col = g_col;
-    //    var newCell;
-    //
-    //
-    //    do {
-    //        switch (direction) {
-    //            case Direction.Up:
-    //                row--;
-    //                break;
-    //            case Direction.Down:
-    //                row++;
-    //                break;
-    //            case Direction.Left:
-    //                col--;
-    //                break;
-    //            case Direction.Right:
-    //                col++;
-    //                break;
-    //        }
-    //
-    //        newCell = $("tr#row_" + row + " td#col_" + g_col + ".commonCell");
-    //    } while ((newCell.length > 0) &&
-    //    (newCell.children("input").attr("disabled") == "disabled"));
-    //    return newCell;
-    //}
-
-    // on page loaded
-    // Настройки дисциплины:
-    // + ID - id дисциплины
-    // + studyGroupID_Filter - studyGroupID для фильтра (Эффект памяти)
-
-    $("div.main_content").ready(AdjustTable);
-    $(window).resize(AdjustTable);
-
-    filterGroups(json_settings.GroupID_Filter);
-    $(".groupSelector [value='" + json_settings.GroupID_Filter + "']").attr("selected", "selected");
-
-    function AdjustTable() {
-        var jWrap = $('div.main_layer');
-        var jTable = $("div.main_content");
-        var tableWidth = jTable.get(0).scrollWidth;
-        // check scroll bar
-        if (tableWidth <= jTable.width() + 10) {
-            return;
-        }
-
-        //correct
-        tableWidth *= 1.1;
-        var maxWidth = $(window).width() * 0.95;
-        var newWidth = (tableWidth > maxWidth) ? maxWidth : tableWidth;
-        jWrap.css("max-width", newWidth);
-    }
-
-    // Скрываем все остальные группы
-    // 0 - показать все
-    function filterGroups(groupID) {
-        if (groupID == 0) {
-            $(".studentsRate tbody")
-                .children()
-                .each(function () {
-                    $(this).show();
-                });
-        } else {
-            $(".studentsRate tbody")
-                .children(":gt(2)")
-                .each(function () {
-                    if ($(this).hasClass("group_" + groupID))
-                        $(this).show();
-                    else
-                        $(this).hide();
-                });
-        }
-    }
-
-    // Ставим подстветку
-    function TdFocus($this) {
-        var colID = $this.attr('id');
-        cursor.col(parseInt(colID.substr(4)));
-        var rowID = $this.parent('tr').attr('id');
-        cursor.row(parseInt(rowID.substr(4)));
-
-        g_oldRateCell = $this.children("input").val();
-
-        highlightCell(cursor.row(), cursor.col(), '#f1f1f1', '#f1f1f1');
-        $this.children('input').css('background-color', '#fff');
-    }
-
-    // Remove highlighting
-    function TdUnFocus() {
-        highlightCell(cursor.row(), cursor.col(), '#fff', '#fbfeff');
-    }
-
-    function highlightCell(row, col, commonColor, staticColor) {
-        var $row = $('tr#row_' + row);
-        var $column = $('td#col_' + col);
-
-        $column.filter('.commonCell')
-            .add($column.filter('.staticCell'))
-            .add($row.find(' .commonCell'))
-            .each(function () {
-                $(this).children('input').css('background-color', commonColor);
-            });
-
-        $row.find('.staticCell')
-            .each(function() {
-                $(this).css("background-color", staticColor);
-                $(this).children('input').css("background-color", commonColor);
-            });
-    }
-
-    function TdInfo(jThis) {
-        var disciplineType = json_settings.ExamType;
-        var disciplinePassRate = 60; // credit pass rate
-        if (disciplineType === "exam")
-            disciplinePassRate = 38;
-
-        // Получаем подмодуль
-        var jCurSubmoduleInfo = $(".RatingTableSubmodulesInfo .col_" + cursor.col() + ":first");
-        var jCurSubmoduleHead = $(".RatingTableSubmodulesHead .col_" + cursor.col() + ":first");
-
-        g_submoduleID = parseInt(jCurSubmoduleInfo.attr("id"));
-        g_submoduleTitle = jCurSubmoduleHead.text();
-        if (jCurSubmoduleHead.length < 1 && $(".RatingTableModulesHead .bonus").length > 0)
-            g_submoduleTitle = 'Бонусные баллы';
-        g_submoduleMaxRate = parseInt($(".RatingTableSubmodulesHeadMaxRate .col_" + cursor.col()).text());
-
-
-        // Проверяем допустимое значение (только для добора)
-        if (jThis.attr("class").indexOf("additionalCell") >= 0) {
-            var semesterRate = parseInt(jThis.siblings(".semesterRateResultCell").text());
-            if (semesterRate <= disciplinePassRate)
-                g_submoduleMaxRate = disciplinePassRate - semesterRate;
-            else
-                g_submoduleMaxRate = 0;
-        }
-
-        // Получаем студента
-        g_studentID = jThis.siblings('.studentCell').attr("id");
-        g_studentID = g_studentID.substr(8);
-
-        g_stdName = jThis.siblings('.studentCell').text();
-
-        jTdInfo_wrap.show();
-        jTdInfo.children("#student").children("b").html(g_stdName);
-        jTdInfo.children("#submodule").children("b").html(g_submoduleTitle);
-        jTdInfo.children("#maxRate").children("b").html(g_submoduleMaxRate);
-
-        //if (jThis.children('.tdInfo').length <= 0)
-        //  jThis.append("<div class='tdInfo'>"+g_submoduleTitle+"<br>"+g_stdName+"</div>");
-    }
-
-    function UnsetTdInfo(jThis) {
-        //jThis.children(".tdInfo").remove();
-        jTdInfo_wrap.hide();
-        g_submoduleID = null;
-        g_studentID = null;
-        g_submoduleMaxRate = 0;
-    }
-
-    function Rating(jThis, oldRate) {
-        oldRate = parseInt(oldRate);
-
-        // Здесь jThis - div rateCell, а не input, который является дочкой
-        if (cancelFlag) {
-            var str = "";
-            if (oldRate != -1)
-                str = oldRate;
-            jThis.children("input").val(str);
-            cancelFlag = false;
-            return;
-        }
-
-        var newRate = -1; // если пустая строка в ячейке, значит ничего не поставлено
-        if (jThis.children("input").val() !== "")
-            newRate = parseInt(jThis.children("input").val());
-        if (newRate == oldRate)
-            return;
-
-        // блокируем ячейку пока не обработаем коллбек
-        jThis.children("input").attr("disabled", true);
-
-        var rateResult = newRate>=0 ? newRate : 0;
-
-        // считаем баллы по строке
-        jThis.siblings(".commonCell").each(function () { // добавим сумму баллов в соседних ячейках
-            var rate = $(this).children("input").val();
-            if (rate)
-                rateResult += parseInt(rate);
-        });
-
-        var additionalRateStr = jThis.siblings(".extraCell").text();
-        if (additionalRateStr)
-            rateResult += parseInt(additionalRateStr);
-
-        additionalRateStr = jThis.siblings(".examCell").text();
-        if (additionalRateStr)
-            rateResult += parseInt(additionalRateStr);
-
-        if (newRate <= g_submoduleMaxRate) {
-            $.ajax({
-                type: "POST",
-                url: URLdir + "handler/rating/setRate",
-                data: "studentID=" + g_studentID + "&submoduleID=" + g_submoduleID + "&rate=" + newRate,
-                complete: function (jqXHR, textStatus) {
-                    switch (jqXHR.status) {
-                        case  403:
-                            EventInspector.error("Сессия истекла");
-                            jThis.children("input").val(oldRate);
-
-                            window.location.replace(URLdir);
-                            break;
-                        case 200:
-                            data = $.parseJSON(jqXHR.responseText);
-                            if (data.success === true) {
-                                var correctRate = (rateResult > 100) ? '100+' : rateResult;
-                                jThis.siblings(".rateResultCell").text(correctRate);
-
-                                // Открываем доступ к след. ячейке добора баллов
-                                if (jThis.hasClass("additionalCell")) {
-                                    nextAdditionalCell = $("#col_" + (cursor.col() + 1) + "_row_" + cursor.row());
-                                    placeholder_max = (rateResult <= 60) ? (60 - rateResult) : (60 - oldRate);
-
-                                    if (placeholder_max > 0 && nextAdditionalCell.hasClass("additionalCell")) {
-                                        nextAdditionalCell.find("input").attr("placeholder", "макс. " + (placeholder_max));
-                                    }
-                                }
-
-                                EventInspector.success("Балл добавлен/изменен");
-                            }
-                            else EventInspector.error("Не удалось добавить/изменить балл");
-                            break;
-                        default:
-                            EventInspector.error(" " + jqXHR.status);
-                    }
-                }
-            });
-        }
-        else {
-            if (oldRate <= g_submoduleMaxRate) {
-                if (oldRate != -1)
-                    jThis.children("input").val(oldRate);
-                else
-                    jThis.children("input").val("");
-            }
-            else
-                jThis.children("input").val("0");
-
-            EventInspector.error("Текущий балл превышает максимальный для данного модуля");
-        }
-        jThis.children("input").removeAttr("disabled");
-    }
-
-    jCommonCell.mouseenter(function () {
-        if (g_isFocusCell === false)
-            TdFocus($(this));
-    });
-
-    jCommonCell.mouseleave(function () {
-        if (g_isFocusCell === false)
-            TdUnFocus();
-    });
-
-    var oldRate = 0;
-    jCommonCell.focusin(function () {
-        g_isFocusCell = true;
-        TdFocus($(this));
-        TdInfo($(this));
-
-        if ($(this).children("input").val() !== "") {
-            oldRate = $(this).children("input").val();
-        }
-        else oldRate = -1;
-    });
-
-    jCommonCell.focusout(function () {
-        g_isFocusCell = false;
-        Rating($(this), oldRate);
-
-        TdUnFocus();
-        UnsetTdInfo($(this));
-    });
-
-    var Direction = {
-        Up: 0,
-        Right: 1,
-        Down: 2,
-        Left: 3
-    };
-
-    /**
-     * @param direction Direction в каком направлении искать следующую ячейку для перемещения фокуса
-     * @return jQuery клетка, в которую надо переместиться или null, если в этом направлении нет подходящих ячеек
-     */
-    function getDesiredCell(direction) {
-        /** Будем искать ячейку в направлении {@see direction} до тех пока не найдем её и пока не
-         *  кончатся ячейки в этом направлении */
-        var row = cursor.row();
-        var col = cursor.col();
-        var currentCell;
-        switch (direction) {
-            case Direction.Up:
-                do {
-                    row--;
-                    currentCell = $("tr#row_" + row + " td#col_" + cursor.col() + ".commonCell")
-                } while ((currentCell.length > 0) &&
-                (currentCell.children("input").attr("disabled") == "disabled"));
-                return currentCell;
-            case Direction.Right:
-                do {
-                    col++;
-                    currentCell = $("tr#row_" + cursor.row() + " td#col_" + col + ".commonCell")
-                } while ((currentCell.length > 0) &&
-                (currentCell.children("input").attr("disabled") == "disabled"));
-                return currentCell;
-            case Direction.Down:
-                do {
-                    row++;
-                    currentCell = $("tr#row_" + row + " td#col_" + cursor.col() + ".commonCell")
-                } while ((currentCell.length > 0) &&
-                (currentCell.children("input").attr("disabled") == "disabled"));
-                return currentCell;
-            case Direction.Left:
-                do {
-                    col--;
-                    currentCell = $("tr#row_" + cursor.row() + " td#col_" + col + ".commonCell")
-                } while ((currentCell.length > 0) &&
-                (currentCell.children("input").attr("disabled") == "disabled"));
-                return currentCell;
-        }
-        return null;
-    }
-
-    jCommonCell.keydown(function (e) {
-        var row = cursor.row();
-        var col = cursor.col();
-        var direction;
-        switch (e.keyCode) {
-            case 27:
-                cancelFlag = true; // esc
-            case 13: // enter
-            case 40: // down arrow
-                direction = Direction.Down;
-                break;
-            case 38: // up arrow
-                direction = Direction.Up;
-                break;
-            case 39: // right arrow
-                direction = Direction.Right;
-                break;
-            case 37: // left arrow
-                direction = Direction.Left;
-                break;
-            default:
-                return;
-        }
-        var whereToMoveFocus = getDesiredCell(direction);
-        if (whereToMoveFocus.length > 0) {
-            TdUnFocus();
-            cursor.col(col);
-            cursor.row(row);
-            whereToMoveFocus.children("input").focus();
-        } else
-            $(this).children("input").blur();
-    });
-
-    $(".commonCell input").focusin(function () {
-        $(this).select();
-    });
-
-    // При нажатии на элемент commonCell дочерный input получает фокус
-    jCommonCell.click(function () {
-        $(this).children("input").focus();
-    });
-
-    // В inputCredit (где баллы вводить) разрешаем вводить только цифры
-    jCommonCell.children("input").keydown(function (event) {
-        KeyDownOnlyNumber(event);
-    });
-
-    // Фильтр по группе
-    $(".groupSelector").change(function () {
-        var group = $(this).val();
-        if (group >= 0) {
-            filterGroups(group);
-            $.post(
-                URLdir + "handler/rating/SelectGroup",
-                {
-                    "disciplineID": g_disciplineID,
-                    "groupSelected": group
-                },
-                function (data) {
-                    data = $.parseJSON(data);
-                    if (data.success === true) {
-
-                    }
-                }
-            );
-        }
-    });
-
-    // Скачать таблицу оценивания в формате excel // depricated
-    $(".downloadExcel").click(function () {
-        $.fileDownload(URLdir + 'handler/FileCreator/GenerateExcelRatingTable', {
-            httpMethod: "POST",
-            data: {
-                'disciplineID': g_disciplineID
-            },
-            successCallback: function () {
-
-            },
-            failCallback: function () {
-
-            }
-        });
-    });
-
-    // Ведомость в формате excel
-    $('body').on('click', '.downloadExcelStatement', function () {
-
-        var groupID = parseInt($(this).attr("id").substr(6))
-        var jStageSelector = $("#stageSelector_" + groupID);
-
-        $.fileDownload(URLdir + 'handler/FileCreator/GenerateFinalForm', {
-            httpMethod: "POST",
-            data: {
-                "disciplineID": g_disciplineID,
-                "groupID": groupID,
-                "stage": parseInt(jStageSelector.val())
-            },
-            successCallback: function () {
-
-            },
-            failCallback: function () {
-
-            }
-        });
-    });
-
+$(() => {
+    let R = Rating();
 
+    R.adjuster.autoResize();
+    R.filter.listen($('.groupSelector'));
+    R.initGeneralCells(() => {});
 });
diff --git a/media/less/teacher/rating.less b/media/less/teacher/rating.less
index cf8685b7f..fabdebc15 100644
--- a/media/less/teacher/rating.less
+++ b/media/less/teacher/rating.less
@@ -24,7 +24,10 @@
 .RatingTableModulesHead { background: #f0f7fd } 
 .RatingTableSubmodulesHead { background: #f0f7fd } /* ffffe0 */
 .RatingTableSubmodulesHeadMaxRate { background: #f0f7fd } 
-.RatingTableSubmodulesInfo { empty-cells: hide }
+.RatingTableSubmodulesInfo {
+	empty-cells: hide;
+	display: none;
+}
 
 .title {
 	padding: 5px; text-align: center;
diff --git a/~dev_rating/application/classes/Controller/Handler/Rating.php b/~dev_rating/application/classes/Controller/Handler/Rating.php
index 891fa2e54..424685ccc 100644
--- a/~dev_rating/application/classes/Controller/Handler/Rating.php
+++ b/~dev_rating/application/classes/Controller/Handler/Rating.php
@@ -23,6 +23,8 @@ class Controller_Handler_Rating extends Controller_Handler
             $data['success'] = ($result->get('Num') == 0);
         }
 
+        if (!$data['success'])
+            throw HTTP_Exception::factory(400);
         $this->response->body(json_encode($data));
     }
 
diff --git a/~dev_rating/application/views/teacher/discipline/rating/exam.twig b/~dev_rating/application/views/teacher/discipline/rating/exam.twig
index d5f6ebe4e..5000f62be 100644
--- a/~dev_rating/application/views/teacher/discipline/rating/exam.twig
+++ b/~dev_rating/application/views/teacher/discipline/rating/exam.twig
@@ -2,6 +2,7 @@
 
 {% block title %}Сессия{% endblock %} {# head -> title #}
 {% block media %} {# head -> css, js #}
+	{{ HTML.script('static/js/discipline/rating/common.js')|raw }}
 	{{ HTML.script('static/js/discipline/rating/exam.js')|raw }}
 	{{ HTML.script('static/js/functions.js')|raw }}
 	{{ HTML.script('static/js/libs/jquery.fileDownload.js')|raw }}
@@ -51,7 +52,7 @@
 		</tr>
 
 		<tr class="RatingTableSubmodulesHead">
-			{% set col = 1 %}
+			{% set col = 2 %}
 			<td class="title">Мероприятие</td>
 			{% for i in 1..headerRate.ModulesCount %}
 				{% set colSpanGeneral = 1 %}
diff --git a/~dev_rating/application/views/teacher/discipline/rating/rate.twig b/~dev_rating/application/views/teacher/discipline/rating/rate.twig
index 99351c92a..e5ccabc29 100644
--- a/~dev_rating/application/views/teacher/discipline/rating/rate.twig
+++ b/~dev_rating/application/views/teacher/discipline/rating/rate.twig
@@ -4,6 +4,7 @@
 {% block media %} {# head -> css, js #}
 	{{ HTML.script('static/js/functions.js')|raw }}
 	{{ HTML.script('static/js/libs/jquery.fileDownload.js')|raw }}
+	{{ HTML.script('static/js/discipline/rating/common.js')|raw }}
 	{{ HTML.script('static/js/discipline/rating/rate.js')|raw }}
 
 	{{ HTML.style('static/css/teacher/rating.css')|raw }}
@@ -25,8 +26,6 @@
     #}
 	<h2 class="h2_titleSubject">{{ Discipline.SubjectName }}</h2>
 
-	<button class="downloadExcel" style="display: none">Скачать в excel формате [dev version]</button>
-
 	{{ HTML.anchor('discipline/' ~ Discipline.ID ~ '/exam', 'Перейти к сессии →', {
 		'title': 'Сессия', 'class': 'exam_a'
 	})|raw }}
@@ -158,7 +157,7 @@
 						{% set j = j + 1 %}
 
 						{% if Module.Type == 'regular' or Module.Type == 'bonus' %}
-							<td id="col_{{ j }}" class="{{ Discipline.Milestone ? 'staticCell' : 'commonCell' }}">
+							<td id="col_{{ j }}_row_{{row}}" class="{{ Discipline.Milestone ? 'staticCell' : 'commonCell' }}">
 								<input type="text" value="{{ rate }}">
 							</td>
 						{% else %}
-- 
GitLab