Auto grading

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

安装此脚本?
作者推荐脚本

你可能也喜欢 USTC 助手

安装此脚本
  1. // ==UserScript==
  2. // @name Auto grading
  3. // @namespace http://tampermonkey.net/
  4. // @version 0.7.3
  5. // @description USTC 自动评教 tqm.ustc.edu.cn
  6. // @author PRO_2684
  7. // @match https://tqm.ustc.edu.cn/index.html*
  8. // @icon https://tqm.ustc.edu.cn/favicon.ico
  9. // @grant GM_getResourceText
  10. // @license gpl-3.0
  11. // @resource answers https://cdn.jsdelivr.net/gh/PRO-2684/gadgets/auto_grading/answers.json
  12. // ==/UserScript==
  13.  
  14. (function () {
  15. 'use strict';
  16. const INTERVAL = 500; // ms
  17. const log = console.log.bind(console, "[Auto grading]");
  18. const standard_answers = JSON.parse(GM_getResourceText("answers"));
  19. let bypass_timer = false;
  20. let menu_root;
  21. function clean(str) {
  22. // Remove spaces
  23. str = str.replace(/\s+/g, "");
  24. // Remove leading asterisk
  25. if (str[0] == '*') str = str.slice(1);
  26. // Remove leading serial number
  27. str = str.replace(/^\d*\./, "");
  28. // Remove "(单选题)"/"(多选题)"
  29. str = str.replace("(单选题)", "");
  30. str = str.replace("(多选题)", "");
  31. return str;
  32. }
  33. function on_bypass_click() {
  34. bypass_timer = !bypass_timer;
  35. this.textContent = `绕过倒计时 [${bypass_timer ? "✔" : "✘"}]`;
  36. }
  37. function add_item(display_name, hint, callback) {
  38. const new_item = menu_root.appendChild(document.createElement("li"));
  39. new_item.innerText = display_name;
  40. new_item.onclick = callback;
  41. new_item.className = "ant-menu-item";
  42. new_item.title = hint;
  43. }
  44. function help() {
  45. alert("食用方法:\n1. 进入未完成的评价问卷\n2. 侧栏选择你想要的操作或激活快捷键\n3. 等待脚本执行\n\n快捷键说明:\n- Enter: 智能执行以下中的一项: 下一位教师/选择标准答案/提交回答\n- Shift+Enter: 全自动评教\n- Backspace: 忽略并转到下一个");
  46. }
  47. function grade() {
  48. const questions = document.querySelectorAll("[class^='index_subject-']");
  49. const disabled = questions[0].querySelector(".ant-radio-wrapper-disabled");
  50. if (disabled) return false;
  51. let first_unchosen = null;
  52. questions.forEach((question) => {
  53. const required = Boolean(question.querySelector('[class^="index_necessary"]'));
  54. if (!required) return;
  55. const tmp = question.querySelector("[class^='index_title']");
  56. const remark = tmp.querySelector("[class^='index_remarks-']");
  57. const title = remark?.textContent || clean(tmp.querySelector("[class^='index_richTextContent']").textContent);
  58. const standard_answer = standard_answers[title];
  59. log(`${title}: ${standard_answer}`);
  60. let chosen = false;
  61. if (standard_answer) {
  62. const options = question.querySelectorAll('[style="width: 100%;"]');
  63. for (const option of options) {
  64. const is_standard_answer = (standard_answer.indexOf(option.innerText) >= 0);
  65. // const is_selected = option.querySelector(".ant-checkbox-checked") || option.querySelector(".ant-radio-checked");
  66. if (is_standard_answer) {
  67. option.firstChild.click();
  68. chosen = true;
  69. // break; // Compatible for multiple answers
  70. }
  71. }
  72. }
  73. if (!chosen && first_unchosen == null) first_unchosen = question;
  74. });
  75. if (first_unchosen != null) {
  76. first_unchosen.scrollIntoView({ behavior: "smooth" });
  77. return false;
  78. }
  79. return true;
  80. }
  81. function ignore() {
  82. const ignore_btn = root_node.querySelector("[class^='TaskDetailsMainContent_normalButton']");
  83. if (ignore_btn && ignore_btn.parentElement.parentElement.parentElement.getAttribute('aria-hidden') == 'false') {
  84. ignore_btn.click();
  85. } else {
  86. log("Cannot find ignore button!");
  87. }
  88. const tabs = root_node.querySelector("[class='ant-tabs-nav-scroll']");
  89. if (tabs) {
  90. tabs = tabs.children[0].children[0];
  91. } else {
  92. log("Cannot find teacher/TA list!");
  93. return;
  94. }
  95. let flag = false;
  96. let tab;
  97. for (tab of tabs.children) {
  98. if (flag) {
  99. tab.click();
  100. break;
  101. } else if (tab.getAttribute('aria-selected') == 'true') {
  102. flag = true;
  103. }
  104. }
  105. }
  106. async function auto() {
  107. if (await try_click("button[class^='ant-btn ant-btn-primary']")) // Confirm submission / Next teacher or course
  108. return true;
  109. if (grade()) { // Select standard answer
  110. await try_click("button[class^='ant-btn index_submit']"); // Submit
  111. return true;
  112. }
  113. return false;
  114. }
  115. async function full_auto() {
  116. // Wait INTERVAL ms between auto() resolves and next auto() call
  117. while (await auto()) {
  118. await new Promise((resolve) => setTimeout(resolve, INTERVAL));
  119. }
  120. alert("Success!");
  121. }
  122. function dump() {
  123. const questions = document.querySelectorAll("[class^='index_subject-']");
  124. const disabled = questions[0].querySelector(".ant-radio-wrapper-disabled");
  125. if (disabled) return false;
  126. let data = {};
  127. questions.forEach((question) => {
  128. const required = Boolean(question.querySelector('[class^="index_necessary"]'));
  129. if (!required) return;
  130. const tmp = question.querySelector("[class^='index_title']");
  131. const remark = tmp.querySelector("[class^='index_remarks-']");
  132. const title = remark?.textContent || clean(tmp.querySelector("[class^='index_richTextContent']").textContent);
  133. const options = question.querySelectorAll('[style="width: 100%;"]');
  134. data[title] = [];
  135. for (const option of options) {
  136. data[title].push(option.innerText);
  137. }
  138. });
  139. log(JSON.stringify(data));
  140. }
  141. function is_displayed(ele) {
  142. let displayed = true;
  143. let node = ele;
  144. while (node) {
  145. if (node.style.display == "none") {
  146. displayed = false;
  147. break;
  148. }
  149. node = node.parentElement;
  150. }
  151. return displayed;
  152. }
  153. function force_enable(ele) {
  154. ele.removeAttribute("disabled");
  155. const prefix = "__reactEventHandlers$";
  156. for (const key of Object.getOwnPropertyNames(ele)) {
  157. if (key.startsWith(prefix)) {
  158. ele[key].disabled = false;
  159. }
  160. }
  161. }
  162. async function until_enabled(ele) {
  163. return new Promise((resolve) => {
  164. if (!ele.hasAttribute("disabled")) {
  165. resolve();
  166. return;
  167. } else if (bypass_timer) {
  168. force_enable(ele);
  169. resolve();
  170. return;
  171. }
  172. log("Waiting for button to be enabled...", ele);
  173. const observer = new MutationObserver((mutations, observer) => {
  174. if (!ele.hasAttribute("disabled")) {
  175. observer.disconnect();
  176. log("Button is enabled!", ele);
  177. resolve();
  178. }
  179. });
  180. observer.observe(ele, { attributes: true });
  181. });
  182. }
  183. async function try_click(selector) {
  184. const eles = document.querySelectorAll(selector);
  185. for (const ele of eles) {
  186. if (ele && is_displayed(ele)) {
  187. await until_enabled(ele);
  188. ele.click();
  189. return true;
  190. }
  191. }
  192. return false;
  193. }
  194. // Side bar
  195. const root_node = document.getElementById('root');
  196. const config = { attributes: false, childList: true, subtree: true };
  197. const callback = function (mutations, observer) {
  198. menu_root = root_node.querySelector('.ant-menu-root');
  199. if (menu_root) {
  200. observer.disconnect();
  201. add_item("使用说明", "自动评教脚本使用说明", help);
  202. add_item("绕过倒计时 [✘]", "(实验性功能)在 Enter 以及全自动评教时绕过 5 秒倒计时", on_bypass_click);
  203. add_item("自动评价", "自动选择标准答案", grade);
  204. add_item("忽略并转到下一个", "(若可能)忽略当前助教并转到下一个助教", ignore);
  205. add_item("全自动评教", "(实验性功能)彻底解放双手", full_auto);
  206. add_item("输出答案", "(调试用)输出当前问卷的所有答案", dump);
  207. }
  208. }
  209. const observer = new MutationObserver(callback);
  210. observer.observe(root_node, config);
  211. // Shortcut
  212. document.addEventListener("keyup", (e) => {
  213. if (document.activeElement.nodeName != "INPUT" || document.activeElement.nodeName != "TEXTAREA") { // Don't trigger when typing
  214. switch (e.key) {
  215. case "Enter":
  216. if (!e.shiftKey) {
  217. auto();
  218. } else {
  219. full_auto();
  220. }
  221. break;
  222. case "Backspace":
  223. ignore();
  224. break;
  225. default:
  226. break;
  227. }
  228. }
  229. });
  230. })();