-
Notifications
You must be signed in to change notification settings - Fork 8
/
Copy pathauto_grading.js
230 lines (229 loc) · 9.29 KB
/
auto_grading.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
// ==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;
}
}
});
})();