Skip to content
Snippets Groups Projects
rating.js 23 KiB
Newer Older
// распознает строки вида t_2
const singleNumRegexp = /\w_(\d+)/;
// распознает строки вида row_3_1
const doubleNumReg = /.*_(\d+)_(\d+)$/;

class Base {
    static parseid($cell) {
        singleNumRegexp.lastIndex = 0;
        let found = singleNumRegexp.exec($cell.attr('id'));
        return found ? parseInt(found[1]) : 0;
    }

    static parseClass($cell) {
        singleNumRegexp.lastIndex = 0;
        let found = singleNumRegexp.exec($cell.attr('class'));
        return found ? parseInt(found[1]) : 0;
    }

    static parsePosition($cell) {
        doubleNumReg.lastIndex = 0;
        let [col, row] = doubleNumReg.exec($cell.attr('id')).splice(1, 2);
        return [parseInt(col), parseInt(row)];
    }

    static parseSettings() {
        let $discipline = $('#json_discipline');
        settings.discipline = {};
        let disciplineRaw = $.parseJSON($discipline.html() || '{}');
        Object.keys(disciplineRaw).forEach((keyRaw) => {
            let key = keyRaw.slice(0, 1).toLowerCase() + keyRaw.slice(1);
            if (keyRaw === "ID") key = 'id';
            settings.discipline[key] = +disciplineRaw[keyRaw];
    static parseRateForCell($cell, server) {
        if ($cell.hasClass('static')) {
            return this.parseRate($cell, $cell.text(), server);
            return this.parseRate($cell, $cell.children('input').val(), server);
    static parseRate($cell, rate, server) {
        let def = server ? '-1' : '0';
        return parseInt(rate || def);
    }

    static filterNumbers($input) {
        let scoreInputVal = $input.val();
        let numbers = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'];
        scoreInputVal = Array.prototype.filter.call(scoreInputVal, chr => chr in numbers).join('');
        $input.val(scoreInputVal);
    }
}

class Cursor {
    constructor() {
        this.col = 0;
        this.row = 0;
    }

    update(col, row) {
        this.col = col;
        this.row = row;
    }

    get() {
        return [this.col, this.row];
    }

    // TODO: REFACTOR
    // нреправильное название parseLocation для функции, которая
    // еще и обновляет значения стобца и строки
    parseLocation($cell) {
        let [col, row] = Base.parsePosition($cell);
        this.update(col, row);
    }

    isEmpty() {
        return (this.col === 0) && (this.row === 0);
    }
}

class Cell {
    constructor() {
        this.update(0, 0, 0);
    }

    update(recordBook, submodule, maxRate) {
        this.recordBook = recordBook;
        this.submodule = submodule;
        this.maxRate = maxRate;
    }
}

class CellInfo {
    constructor(cursor, cell) {
        this.cursor = cursor;
        this.cell = cell;

        this.$modulesHead = $('#modulesHead');
        this.$submodulesHead = $('#submodulesHead');
        this.$submodulesMaxRate = $('#submodulesHeadMaxRate');
    }

    /**
     *
     * @param $cell выбранная ячейка
     * @desc определяет мероприятие, максимальный балл и id студента для ячейки таблицы баллов
     * делает это по id ячейки, тексту в заголовке таблицы и id ячейки с именем студента
     */
    select($cell) {
        this.cursor.parseLocation($cell);

        let [col] = this.cursor.get();
        let $curSubmoduleMaxRate = this.$submodulesMaxRate.children(`.col_${col}`);
        let recordBook = Base.parseid($cell.siblings('.name'));
        let submodule = Base.parseid($curSubmoduleMaxRate);
        let maxRate = parseInt($curSubmoduleMaxRate.text());
        if ($cell.hasClass('extra')) {
            maxRate -= Base.parseRateForCell($cell.siblings('.semester'));
            maxRate -= Base.parseRateForCell($cell.prev('.extra'));
            maxRate = Math.max(0, maxRate);
        }
        this.cell.update(recordBook, submodule, maxRate);
/**
 * @desc Класс, контролирующий строку состояния таблицы баллов
 * показывает студента, мероприятие и макс. балл, если выделена ячейка
 *
 */
class RateInfo {
    constructor(cursor, cell) {
        this.cursor = cursor;
        this.cell = cell;

        this.$modulesHead = $('#modulesHead');
        this.$submodulesHead = $('#submodulesHead');
        this.$submodulesMaxRate = $('#submodulesHeadMaxRate');

        this.$cellInfo = $('#cellInfo');
        this.$studentInfo = this.$cellInfo.find('#student .value');
        this.$submoduleInfo = this.$cellInfo.find('#submodule .value');
        this.$rateInfo = this.$cellInfo.find('#maxRate .value');
    }

    /**
     * @desc обновляет имя студента, мероприятие и макс. балл по выбранной ячейке, выбирая текст из столбца и строки таблицы
     * @param $cell ячейка, информацию по которой нужно вывести в строке состояния
     *
     */
        let [col] = this.cursor.get();

        let studentName = $cell.siblings('.name').text();
        let submoduleTitle = this.$submodulesHead.children(`.col_${col}`).text();

        this.$studentInfo.html(studentName);
        this.$submoduleInfo.html(submoduleTitle);
        this.$rateInfo.html(this.cell.maxRate);
        this.$cellInfo.show();
    }

    hide() {
        this.$cellInfo.hide();
    }
}

/**
 * @desc класс, управляющий фильтром по группам в таблице баллов
 * следит за выпадающим списком групп и скрывает или показывает строки таблицы
 */
class GroupFilter {
    constructor(settings, $table, $selector) {
        this.settings = settings;
        this.$table = $table.children('tbody');

        let groupFilter = JSON.parse(localStorage.groupFilter || "{}");
        let groupid = groupFilter[this.settings.discipline.id] || 0;
        $selector.children('[value=' + groupid + ']').prop('selected', true);
        this.filterGroups(groupid);
    filterGroups(groupid) {
        let $trs = this.$table.children();
        $trs.show();

        if (groupid) {
            $trs.not(`.group_${groupid}`).hide();
        }
    }

    listen($selector) {
        $selector.change(() => {
            let groupid = parseInt($selector.val());

            let groupFilter = JSON.parse(localStorage.groupFilter || "{}");
            groupFilter[this.settings.discipline.id] = groupid;
            localStorage.groupFilter = JSON.stringify(groupFilter);

            this.filterGroups(groupid);
/**
 * @desc класс, управляющий вводом баллов в таблицу
 */
class Rating {
    constructor() {
        this.settings = Base.parseSettings();
        this.cell = new Cell();
        this.cursor = new Cursor();
        this.cellInfo = new CellInfo(this.cursor, this.cell);
        this.rateInfo = new RateInfo(this.cursor, this.cell);
        this.filter = new GroupFilter(this.settings, $('#studentsRate'), $('#groupSelector'));
        this.undoManager = new ActionHistory();
        this.Direction = {
            Up: 0,
            Right: 1,
            Down: 2,
            Left: 3
        };

        let keyDirs = {
            13: this.Direction.Down,  // enter
            40: this.Direction.Down,  // down arrow
            38: this.Direction.Up,    // up arrow
            39: this.Direction.Right, // right arrow
            37: this.Direction.Left   // left arrow
        };

        let self = this;
        let oldInputVal = '';
        // при нажатии на ячейку таблицы передаем фокус элементу input
        // обовляем курсор
        // обновляем строку статуса
        // запоминаем предыдущее значение в нем
        $('#studentsRate tbody').on('focusin', '.rate', function () {
            $(this).children('input').select();

            self.cellInfo.select($(this));
            self.rateInfo.show($(this));
            $(this).parents('tr').addClass('focus');

            oldInputVal = $(this).children('input').val();
        }).on('focusout', '.rate', function () {
            self.rateInfo.hide($(this));
            self.cellInfo.reset();
            $(this).parents('tr').removeClass('focus');
        }).on('keydown', '.rate', function (event) {
            // Разрешаем нажимать только цифры
            keyDownOnlyNumber(event);

            let key = event.keyCode;

            if (key == 90 && event.ctrlKey === true) {
                let didUndo = self.undoManager.undo();
                if (!didUndo) {
                    Popup.error("Еще нельзя отменить");
                }
                return;
            }
            if (key == 89 && event.ctrlKey === true) {
                let didRedo = self.undoManager.redo();
                if (!didRedo) {
                    Popup.error("Еще нельзя повторить");
                }
                return;
            }

            if (key === 27) {
                $(this).children('input').val(oldInputVal);
                $(this).children('input').blur();
            }

            if (!(key in keyDirs)) {
                return;
            }

            let direction = keyDirs[key];
            let $cell = self.getDesiredCell(direction);
            if ($cell.length !== 0) { // Если ячейка найдена
                self.selectCell($cell);
                $cell = $(this);
                let scoreInputVal = $cell.children('input').val();
                if (scoreInputVal !== oldInputVal) {
                    $cell.one('turnOn', () => self.selectCell($cell));
                    $cell.chlidren('input').blur();
                } else {
                    self.selectCell($cell);
                }
            }
        }).on('change', '.rate', function () {
            Base.filterNumbers($(this).children('input'));
            self.setRate($(this), oldInputVal);
        }).on('change', '.checkbox', function () {
            self.cellInfo.select($(this));
            self.setExam($(this));
            self.cellInfo.reset();
        }).on('click', '.download button', function () {
Korvin's avatar
Korvin committed
            $.fileDownload(g_URLdir + 'handler/FileCreator/GenerateFinalForm', {
                httpMethod: 'POST',
                data: {
                    'disciplineID': self.settings.discipline.id,
                    'groupID': Base.parseid($(this)),
                    'stage': parseInt($(this).siblings('select').val()),
    /**
     * @desc находит соседнюю ячейку в заданном направлении
     * @param direction направление движения
     * @returns {*}
     */
    getDesiredCell(direction) {
        let [col, row] = this.cursor.get();

        let $cell;
        do {
            if (direction === this.Direction.Left)  col--;
            if (direction === this.Direction.Right) col++;
            if (direction === this.Direction.Up)    row--;
            if (direction === this.Direction.Down)  row++;
            $cell = $(`#col_row_${col}_${row}`);
        } while ($cell.length && !$cell.hasClass('rate'));
    selectCell($cell) {
        $cell.children('input').focus();
        // Выбрать весь текст в ячейке
        setTimeout(() => $cell.children('input').select(), 0);
    }


    /**
     * @desc отправляет выставленный в ячейку балл на сервер и выводит сообщение об ошибке/успехе
     * @param $cell
     * @param oldInputVal
     */
    setRate($cell, oldInputVal) {
        let scoreInputVal = $cell.children('input').val();
        if (scoreInputVal === oldInputVal) return;
        if (!this.settings.discipline.isLocked) {
            if (window.confirm("Если выставить первый балл, то невозможно будет редактировать учебную карту дисциплины. Продолжить?")) {
                this.settings.discipline.isLocked = 1;
            } else {
                $cell.children('input').val(oldInputVal);
                return;
            }
        }

        let rate = Base.parseRateForCell($cell, true);
        if (scoreInputVal !== '' && oldInputVal !== '') {
            successText = 'Балл обновлен';
            successUndoText = 'Балл исправлен';
            errorText = 'Не удалось обновить балл';
        } else if (scoreInputVal !== '' && oldInputVal === '') {
            successText = 'Балл добавлен';
            successUndoText = 'Балл удален';
            errorText = 'Не удалось добавить балл';
        } else {
            successText = 'Балл удален';
            successUndoText = 'Балл добавлен';
            errorText = 'Не удалось удалить балл';
        }

        let self = this;

        let oldGrade = Base.parseRate($cell, oldInputVal, true);
        let newGrade = rate;
        const newGradeDisplay = newGrade === -1? '' : newGrade;
        const oldGradeDisplay = oldGrade === -1? '' : oldGrade;
        let targetCellId = $cell.attr("id");

        let newAction = this.undoManager.addAction(targetCellId,
            function (action) { // redo
            $targetCell = $("#" + targetCellId);
            let $input = $cell.children('input');
            $input.turnOff();
            $input.turnOn();
            self.cellInfo.select($targetCell);
            action.deactivate();
            self.sendRate($targetCell, 'handler/rating/SetRate', {rate: newGrade},
                function () {
                    action.activate();
                },
                function ($input) {
                    action.remove();
                }, successText, errorText);
            self.selectCell($targetCell);
        }, function (action) { // undo
            $targetCell = $("#" + targetCellId);
            let $input = $cell.children('input');
            $input.turnOff();
            $input.turnOn();
            self.cellInfo.select($targetCell);
            action.deactivate();
            self.sendRate($targetCell, 'handler/rating/SetRate', {rate: oldGrade},
                function () {
                    action.activate();
                },
                function ($input) {
                    action.remove();
                }, successUndoText, errorText
            );
            self.selectCell($targetCell);
        });

        this.sendRate($cell, 'handler/rating/SetRate', {rate},
            function () {
                newAction.activate()
            },
            function ($input) {
                newAction.remove();
                $input.val(oldInputVal);
            },
            successText, errorText);
    /**
     * @desc отправляет выставленную неявку или автомат на экзамене на сервер и выводит сообщение об ошибке/успехе
     * @param $cell
     */
    setExam($cell) {
        let optionVal;
        if ($cell.hasClass('absence')) optionVal = 'absence';
        if ($cell.hasClass('autopass')) optionVal = 'pass';
        if (!$cell.children('input').prop('checked')) optionVal = `drop_${optionVal}`;

        let successText;
        let errorText;
        let option;
        let oldOption;
        switch (optionVal) {
            case 'absence':
                successText = 'Неявка установлена';
                errorText = 'Не удалось установить неявку';
                option = 'absence';
                oldOption = 'null';
                break;

            case 'drop_absence':
                successText = 'Неявка удалена';
                errorText = 'Не удалось удалить неявку';
                option = 'null';
                oldOption = 'absence';
                break;

            case 'pass':
                successText = 'Автомат установлен';
                errorText = 'Не удалось установить автомат';
                option = 'pass'
                oldOption = 'null';;
                break;

            case 'drop_pass':
                successText = 'Автомат удален';
                errorText = 'Не удалось удалить автомат';
                option = 'null';
                oldOption = 'pass';
        let self = this;
        let targetCellId = $cell.attr("id");


        // let newAction = this.undoManager.addAction(targetCellId,
        //     function (action) {
        //         $targetCell = $("#" + targetCellId);
        //         let $input = $cell.children('input');
        //         $input.prop("checked", !$input.prop("checked"));
        //         //self.cellInfo.select($targetCell);
        //         self.sendRate($targetCell, 'handler/rating/SetExamPeriodOption', {option},
        //             function () {
        //                 action.activate();
        //             },
        //             function ($input) {
        //                 $input.prop("checked", !$input.prop("checked"))
        //                 action.remove();
        //             }, successText, errorText);
        //         //self.selectCell($targetCell);
        //     },
        //     function (action) {
        //         $targetCell = $("#" + targetCellId);
        //         let $input = $cell.children('input');
        //         $input.prop("checked", !$input.prop("checked"));
        //         //self.cellInfo.select($targetCell);
        //         self.sendRate($targetCell, 'handler/rating/SetExamPeriodOption', {option: oldOption},
        //             function () {
        //                 action.activate();
        //             },
        //             function ($input) {
        //                 $input.prop("checked", !$input.prop("checked"))
        //                 action.remove();
        //             }, successText, errorText);
        //         //self.selectCell($targetCell);
        //     });

        this.sendRate($cell, 'handler/rating/SetExamPeriodOption', {option},
            function () {
                //newAction.activate()
            },
            function ($input) {
                //newAction.remove();
                $input.prop("checked", !$input.prop("checked"));
            },
            successText, errorText);
    sendRate($cell, url, data, onSuccess, onFail, successText, errorText) {
        let $input = $cell.children('input');
        // Действия необходимые для безопасной обработки в асинхронном режиме
        window.onbeforeunload = () => {
            return "Запрос на добавление/изменение/удалени баллов еще не обработан!";
        };
        data.recordBookID = this.cell.recordBook;
        data.disciplineID = this.settings.discipline.id;
        data.submoduleID = this.cell.submodule;
Korvin's avatar
Korvin committed
        $.postJSON(g_URLdir + url, data).success(() => {
            this.recountScores($cell, data.submoduleID);
            Popup.success(successText);
            window.onbeforeunload = undefined;
            onSuccess();
        }).fail(jqXHR => {
            onFail($input);
            const status = parseInt(jqXHR.status);
            let message = errorText;
                if (status === 400)
                    message = JSON.parse(jqXHR.responseText).message;
            } catch(error) {
            if (status !== 0)
                Popup.error(message);
            window.onbeforeunload = undefined;
        }).always(() => $input.turnOn());
    recountScores($cell, submoduleID) {
        let $cells = $cell.parent().children();
        let [col, row] = Base.parsePosition($cell);
        let examCol = Base.parseClass($(`#exam_${submoduleID}`));
        let absenceCol = Base.parseClass($(`#absence_${submoduleID}`));
        let autopassCol = Base.parseClass($(`#autopass_${submoduleID}`));

        if ($cell.hasClass('exam')) {
            $cells.filter(`#col_row_${absenceCol}_${row}`).children('input').prop('checked', false);
            $cells.filter(`#col_row_${autopassCol}_${row}`).children('input').prop('checked', false);
        if ($cell.hasClass('absence')) {
            $cells.filter(`#col_row_${examCol}_${row}`).children('input').val('');
            $cells.filter(`#col_row_${autopassCol}_${row}`).children('input').prop('checked', false);
        if ($cell.hasClass('autopass')) {
            $cells.filter(`#col_row_${examCol}_${row}`).children('input').val('');
            $cells.filter(`#col_row_${absenceCol}_${row}`).children('input').prop('checked', false);
        let $regularCells = $cells.filter('.regular');
        if ($regularCells.length) {
            let semesterRate = $regularCells.aggregate((init, elem) => init + Base.parseRateForCell($(elem)));
            $cells.filter('.semester').text(semesterRate);
        let semesterRate = Base.parseRateForCell($cells.filter('.semester'));
        let bonusRate = Base.parseRateForCell($cells.filter('.bonus'));
        let extraRate = $cells.filter('.extra').aggregate((init, elem) => init + Base.parseRateForCell($(elem)));
        let examRate = $cells.filter('.exam').aggregate((init, elem) => Math.max(init, Base.parseRateForCell($(elem))));
        let rateResult = semesterRate + bonusRate + extraRate + examRate;


        let rateResultText = (rateResult > 100) ? '100+' : rateResult;
        $cells.filter('.result').text(rateResultText);
        if ($cell.hasClass('extra')) {
            let $nextExtraCell = $cell.next('.extra');
            if ($nextExtraCell.length) {
                let maxExtraRate = this.settings.discipline.type == 'exam' ? 38 : 60;
                let curRate = Base.parseRateForCell($cell, true);
                let maxVal = maxExtraRate - semesterRate - curRate;
                if (maxVal > 0 && curRate !== -1) {
                    $nextExtraCell.find('input').attr('placeholder', 'макс. ' + maxVal);
                } else {
                    $nextExtraCell.find('input').attr('placeholder', '').val('');
                }
            }
$(() => new Rating());