diff --git a/media/js/config.js b/media/js/config.js
index 011b7ba23341122668f8f60320ce2ed4a5811ca2..5a23b44a0c507207c02b96c50fab41887ebd4dc4 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 0000000000000000000000000000000000000000..0ae0af794018124c706dda651b6e35b8300b4f47
--- /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 b551311a439e1a29e9bf0c557205548a50df6606..cd507c0abfbfaed9dba8f7ffcb695a96695dc44b 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 92f63133f5b27306746eaa82e0c81f4be358660e..b7b538fe70ff876f1436f41efbd17f6e357ae117 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 cf8685b7f7bb51ef8a5596601f0c51a83c1f27c4..fabdebc15a5de17e3416b511e484d2db12a7192a 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 891fa2e546e1db3b880cff28697185e7400e82fb..424685ccc98ab8aeae2daf6db5e9646b06bedb47 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 d5f6ebe4ec7490d76bcf1222f021d021a5072120..5000f62be9e6cf2deebbb34a4c959f1decaa8ff0 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 99351c92a09686721e4d634de3d6b921b4e516b9..e5ccabc2996d0f86da8e58c9cc75724ef1052e2a 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 %}