Auto grading

USTC 自动评价 tqm.ustc.edu.cn

目前為 2023-12-25 提交的版本,檢視 最新版本

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name         Auto grading
// @namespace    http://tampermonkey.net/
// @version      0.6.4
// @description  USTC 自动评价 tqm.ustc.edu.cn
// @author       PRO_2684
// @match        https://tqm.ustc.edu.cn/index.html*
// @icon         https://tqm.ustc.edu.cn/favicon.ico
// @grant        none
// @license      gpl-3.0
// @require      https://greasyfork.org/scripts/468177-%E6%95%99%E5%AD%A6%E8%B4%A8%E9%87%8F%E7%AE%A1%E7%90%86%E5%B9%B3%E5%8F%B0%E6%A0%87%E5%87%86%E7%AD%94%E6%A1%88/code/%E6%95%99%E5%AD%A6%E8%B4%A8%E9%87%8F%E7%AE%A1%E7%90%86%E5%B9%B3%E5%8F%B0%E6%A0%87%E5%87%86%E7%AD%94%E6%A1%88.js
// ==/UserScript==

(function () {
    'use strict';
    let menu_root;
    function clean(str) {
        // Remove spaces
        str = str.replace(/\s+/g, "");
        // Remove leading asterisk
        if (str[0] == '*') str = str.slice(1);
        // Remove leading serial number
        str = str.replace(/^\d*\./, "");
        // Remove "(单选题)"/"(多选题)"
        str = str.replace("(单选题)", "");
        str = str.replace("(多选题)", "");
        return str;
    }
    function add_item(display_name, hint, callback) {
        const new_item = menu_root.appendChild(document.createElement("li"));
        new_item.innerText = display_name;
        new_item.onclick = callback;
        new_item.className = "ant-menu-item";
        new_item.title = hint;
    }
    function help() {
        alert("食用方法:\n1. 进入未完成的评价问卷\n2. 侧栏选择你想要的操作或激活快捷键\n3. 等待脚本执行\n\n快捷键说明:\n- Enter: 智能执行以下中的一项: 下一位教师/选择标准答案/提交回答\n- Shift+Enter: 全自动评教\n- Backspace: 忽略并转到下一个");
    }
    function grade() {
        const questions = document.querySelectorAll("[class^='index_subject-']");
        const disabled = questions[0].querySelector(".ant-radio-wrapper-disabled");
        if (disabled) return false;
        let first_unchosen = null;
        questions.forEach((question) => {
            const required = Boolean(question.querySelector('[class^="index_necessary"]'));
            if (!required) return;
            const tmp = question.querySelector("[class^='index_title']");
            const remark = tmp.querySelector("[class^='index_remarks-']");
            const title = remark?.textContent || clean(tmp.querySelector("[class^='index_richTextContent']").textContent);
            const standard_answer = standard_answers[title];
            console.log(`[Auto grading] ${title}: ${standard_answer}`);
            let chosen = false;
            if (standard_answer) {
                const options = question.querySelectorAll('[style="width: 100%;"]');
                for (const option of options) {
                    const is_standard_answer = (standard_answer.indexOf(option.innerText) >= 0);
                    if (is_standard_answer) {
                        option.firstChild.click();
                        chosen = true;
                        // break; // Compatible for multiple answers
                    }
                }
            }
            if (!chosen && first_unchosen == null) first_unchosen = question;
        });
        if (first_unchosen != null) {
            first_unchosen.scrollIntoView({ behavior: "smooth" });
            return false;
        }
        return true;
    }
    function ignore() {
        const ignore_btn = root_node.querySelector("[class^='TaskDetailsMainContent_normalButton']");
        if (ignore_btn && ignore_btn.parentElement.parentElement.parentElement.getAttribute('aria-hidden') == 'false') {
            ignore_btn.click();
        } else {
            console.log("[Auto grading] 未找到忽略按钮!");
        }
        const tabs = root_node.querySelector("[class='ant-tabs-nav-scroll']");
        if (tabs) {
            tabs = tabs.children[0].children[0];
        } else {
            console.log("[Auto grading] 未找到教师/助教列表!");
            return;
        }
        let flag = false;
        let tab;
        for (tab of tabs.children) {
            if (flag) {
                tab.click();
                break;
            } else if (tab.getAttribute('aria-selected') == 'true') {
                flag = true;
            }
        }
    }
    function auto() {
        if (try_click("button[class^='ant-btn ant-btn-primary']")) // Next teacher/course
            return true;
        if (grade()) { // Select standard answer
            try_click("button[class^='ant-btn index_submit']"); // Submit
            return true;
        }
        return false;
    }
    function full_auto() {
        // while (auto()) { }
        const timer = window.setInterval(() => {
            if (!auto()) {
                window.clearInterval(timer);
                alert("执行结束!");
            }
        }, 500);
    }
    function dump() {
        const questions = document.querySelectorAll("[class^='index_subject-']");
        const disabled = questions[0].querySelector(".ant-radio-wrapper-disabled");
        if (disabled) return false;
        let data = {};
        questions.forEach((question) => {
            const required = Boolean(question.querySelector('[class^="index_necessary"]'));
            if (!required) return;
            const tmp = question.querySelector("[class^='index_title']");
            const remark = tmp.querySelector("[class^='index_remarks-']");
            const title = remark?.textContent || clean(tmp.querySelector("[class^='index_richTextContent']").textContent);
            const options = question.querySelectorAll('[style="width: 100%;"]');
            data[title] = [];
            for (const option of options) {
                data[title].push(option.innerText);
            }
        });
        console.log(JSON.stringify(data));
    }
    function try_click(selector) {
        const ele = document.querySelector(selector);
        if (ele && ele.checkVisibility()) {
            ele.click();
            return true;
        } else {
            return false;
        }
    }
    // Side bar
    const root_node = document.getElementById('root');
    const config = { attributes: false, childList: true, subtree: true };
    const callback = function (mutations, observer) {
        menu_root = root_node.querySelector('.ant-menu-root');
        if (menu_root) {
            observer.disconnect();
            add_item("使用说明", "自动评教脚本使用说明", help);
            add_item("自动评价", "自动选择标准答案", grade);
            add_item("忽略并转到下一个", "(若可能)忽略当前助教并转到下一个助教", ignore);
            add_item("全自动评教", "(实验性功能)彻底解放双手", full_auto);
            add_item("输出答案", "(调试用)输出当前问卷的所有答案", dump);
        }
    }
    const observer = new MutationObserver(callback);
    observer.observe(root_node, config);
    // Shortcut
    document.addEventListener("keyup", (e) => {
        if (document.activeElement.nodeName != "INPUT" || document.activeElement.nodeName != "TEXTAREA") {  // Don't trigger when typing
            switch (e.key) {
                case "Enter":
                    if (!e.shiftKey) {
                        auto();
                    } else {
                        full_auto();
                    }
                    break;
                case "Backspace":
                    ignore();
                    break;
                default:
                    break;
            }
        }
    });
})();