Auto grading

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

Installera detta skript?
Upphovsmannens rekommenderade skript

Du kanske också gillar USTC Helper.

Installera detta skript

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

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

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

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

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

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name         Auto grading
// @namespace    http://tampermonkey.net/
// @version      0.7.3
// @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        GM_getResourceText
// @license      gpl-3.0
// @resource     answers https://cdn.jsdelivr.net/gh/PRO-2684/gadgets/auto_grading/answers.json
// ==/UserScript==

(function () {
    'use strict';
    const INTERVAL = 500; // ms
    const log = console.log.bind(console, "[Auto grading]");
    const standard_answers = JSON.parse(GM_getResourceText("answers"));
    let bypass_timer = false;
    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 on_bypass_click() {
        bypass_timer = !bypass_timer;
        this.textContent = `绕过倒计时 [${bypass_timer ? "✔" : "✘"}]`;
    }
    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];
            log(`${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);
                    // const is_selected = option.querySelector(".ant-checkbox-checked") || option.querySelector(".ant-radio-checked");
                    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 {
            log("Cannot find ignore button!");
        }
        const tabs = root_node.querySelector("[class='ant-tabs-nav-scroll']");
        if (tabs) {
            tabs = tabs.children[0].children[0];
        } else {
            log("Cannot find teacher/TA list!");
            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;
            }
        }
    }
    async function auto() {
        if (await try_click("button[class^='ant-btn ant-btn-primary']")) // Confirm submission / Next teacher or course
            return true;
        if (grade()) { // Select standard answer
            await try_click("button[class^='ant-btn index_submit']"); // Submit
            return true;
        }
        return false;
    }
    async function full_auto() {
        // Wait INTERVAL ms between auto() resolves and next auto() call
        while (await auto()) {
            await new Promise((resolve) => setTimeout(resolve, INTERVAL));
        }
        alert("Success!");
    }
    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);
            }
        });
        log(JSON.stringify(data));
    }
    function is_displayed(ele) {
        let displayed = true;
        let node = ele;
        while (node) {
            if (node.style.display == "none") {
                displayed = false;
                break;
            }
            node = node.parentElement;
        }
        return displayed;
    }
    function force_enable(ele) {
        ele.removeAttribute("disabled");
        const prefix = "__reactEventHandlers$";
        for (const key of Object.getOwnPropertyNames(ele)) {
            if (key.startsWith(prefix)) {
                ele[key].disabled = false;
            }
        }
    }
    async function until_enabled(ele) {
        return new Promise((resolve) => {
            if (!ele.hasAttribute("disabled")) {
                resolve();
                return;
            } else if (bypass_timer) {
                force_enable(ele);
                resolve();
                return;
            }
            log("Waiting for button to be enabled...", ele);
            const observer = new MutationObserver((mutations, observer) => {
                if (!ele.hasAttribute("disabled")) {
                    observer.disconnect();
                    log("Button is enabled!", ele);
                    resolve();
                }
            });
            observer.observe(ele, { attributes: true });
        });
    }
    async function try_click(selector) {
        const eles = document.querySelectorAll(selector);
        for (const ele of eles) {
            if (ele && is_displayed(ele)) {
                await until_enabled(ele);
                ele.click();
                return true;
            }
        }
        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("绕过倒计时 [✘]", "(实验性功能)在 Enter 以及全自动评教时绕过 5 秒倒计时", on_bypass_click);
            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;
            }
        }
    });
})();