Auto grading

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

当前为 2023-12-25 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name Auto grading
  3. // @namespace http://tampermonkey.net/
  4. // @version 0.6.4
  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 none
  10. // @license gpl-3.0
  11. // @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
  12. // ==/UserScript==
  13.  
  14. (function () {
  15. 'use strict';
  16. let menu_root;
  17. function clean(str) {
  18. // Remove spaces
  19. str = str.replace(/\s+/g, "");
  20. // Remove leading asterisk
  21. if (str[0] == '*') str = str.slice(1);
  22. // Remove leading serial number
  23. str = str.replace(/^\d*\./, "");
  24. // Remove "(单选题)"/"(多选题)"
  25. str = str.replace("(单选题)", "");
  26. str = str.replace("(多选题)", "");
  27. return str;
  28. }
  29. function add_item(display_name, hint, callback) {
  30. const new_item = menu_root.appendChild(document.createElement("li"));
  31. new_item.innerText = display_name;
  32. new_item.onclick = callback;
  33. new_item.className = "ant-menu-item";
  34. new_item.title = hint;
  35. }
  36. function help() {
  37. alert("食用方法:\n1. 进入未完成的评价问卷\n2. 侧栏选择你想要的操作或激活快捷键\n3. 等待脚本执行\n\n快捷键说明:\n- Enter: 智能执行以下中的一项: 下一位教师/选择标准答案/提交回答\n- Shift+Enter: 全自动评教\n- Backspace: 忽略并转到下一个");
  38. }
  39. function grade() {
  40. const questions = document.querySelectorAll("[class^='index_subject-']");
  41. const disabled = questions[0].querySelector(".ant-radio-wrapper-disabled");
  42. if (disabled) return false;
  43. let first_unchosen = null;
  44. questions.forEach((question) => {
  45. const required = Boolean(question.querySelector('[class^="index_necessary"]'));
  46. if (!required) return;
  47. const tmp = question.querySelector("[class^='index_title']");
  48. const remark = tmp.querySelector("[class^='index_remarks-']");
  49. const title = remark?.textContent || clean(tmp.querySelector("[class^='index_richTextContent']").textContent);
  50. const standard_answer = standard_answers[title];
  51. console.log(`[Auto grading] ${title}: ${standard_answer}`);
  52. let chosen = false;
  53. if (standard_answer) {
  54. const options = question.querySelectorAll('[style="width: 100%;"]');
  55. for (const option of options) {
  56. const is_standard_answer = (standard_answer.indexOf(option.innerText) >= 0);
  57. if (is_standard_answer) {
  58. option.firstChild.click();
  59. chosen = true;
  60. // break; // Compatible for multiple answers
  61. }
  62. }
  63. }
  64. if (!chosen && first_unchosen == null) first_unchosen = question;
  65. });
  66. if (first_unchosen != null) {
  67. first_unchosen.scrollIntoView({ behavior: "smooth" });
  68. return false;
  69. }
  70. return true;
  71. }
  72. function ignore() {
  73. const ignore_btn = root_node.querySelector("[class^='TaskDetailsMainContent_normalButton']");
  74. if (ignore_btn && ignore_btn.parentElement.parentElement.parentElement.getAttribute('aria-hidden') == 'false') {
  75. ignore_btn.click();
  76. } else {
  77. console.log("[Auto grading] 未找到忽略按钮!");
  78. }
  79. const tabs = root_node.querySelector("[class='ant-tabs-nav-scroll']");
  80. if (tabs) {
  81. tabs = tabs.children[0].children[0];
  82. } else {
  83. console.log("[Auto grading] 未找到教师/助教列表!");
  84. return;
  85. }
  86. let flag = false;
  87. let tab;
  88. for (tab of tabs.children) {
  89. if (flag) {
  90. tab.click();
  91. break;
  92. } else if (tab.getAttribute('aria-selected') == 'true') {
  93. flag = true;
  94. }
  95. }
  96. }
  97. function auto() {
  98. if (try_click("button[class^='ant-btn ant-btn-primary']")) // Next teacher/course
  99. return true;
  100. if (grade()) { // Select standard answer
  101. try_click("button[class^='ant-btn index_submit']"); // Submit
  102. return true;
  103. }
  104. return false;
  105. }
  106. function full_auto() {
  107. // while (auto()) { }
  108. const timer = window.setInterval(() => {
  109. if (!auto()) {
  110. window.clearInterval(timer);
  111. alert("执行结束!");
  112. }
  113. }, 500);
  114. }
  115. function dump() {
  116. const questions = document.querySelectorAll("[class^='index_subject-']");
  117. const disabled = questions[0].querySelector(".ant-radio-wrapper-disabled");
  118. if (disabled) return false;
  119. let data = {};
  120. questions.forEach((question) => {
  121. const required = Boolean(question.querySelector('[class^="index_necessary"]'));
  122. if (!required) return;
  123. const tmp = question.querySelector("[class^='index_title']");
  124. const remark = tmp.querySelector("[class^='index_remarks-']");
  125. const title = remark?.textContent || clean(tmp.querySelector("[class^='index_richTextContent']").textContent);
  126. const options = question.querySelectorAll('[style="width: 100%;"]');
  127. data[title] = [];
  128. for (const option of options) {
  129. data[title].push(option.innerText);
  130. }
  131. });
  132. console.log(JSON.stringify(data));
  133. }
  134. function try_click(selector) {
  135. const ele = document.querySelector(selector);
  136. if (ele && ele.checkVisibility()) {
  137. ele.click();
  138. return true;
  139. } else {
  140. return false;
  141. }
  142. }
  143. // Side bar
  144. const root_node = document.getElementById('root');
  145. const config = { attributes: false, childList: true, subtree: true };
  146. const callback = function (mutations, observer) {
  147. menu_root = root_node.querySelector('.ant-menu-root');
  148. if (menu_root) {
  149. observer.disconnect();
  150. add_item("使用说明", "自动评教脚本使用说明", help);
  151. add_item("自动评价", "自动选择标准答案", grade);
  152. add_item("忽略并转到下一个", "(若可能)忽略当前助教并转到下一个助教", ignore);
  153. add_item("全自动评教", "(实验性功能)彻底解放双手", full_auto);
  154. add_item("输出答案", "(调试用)输出当前问卷的所有答案", dump);
  155. }
  156. }
  157. const observer = new MutationObserver(callback);
  158. observer.observe(root_node, config);
  159. // Shortcut
  160. document.addEventListener("keyup", (e) => {
  161. if (document.activeElement.nodeName != "INPUT" || document.activeElement.nodeName != "TEXTAREA") { // Don't trigger when typing
  162. switch (e.key) {
  163. case "Enter":
  164. if (!e.shiftKey) {
  165. auto();
  166. } else {
  167. full_auto();
  168. }
  169. break;
  170. case "Backspace":
  171. ignore();
  172. break;
  173. default:
  174. break;
  175. }
  176. }
  177. });
  178. })();