CFCodereviewer

Codereview codeforces

  1. // ==UserScript==
  2. // @name CFCodereviewer
  3. // @namespace http://tampermonkey.net/
  4. // @version 2.2.0
  5. // @description Codereview codeforces
  6. // @author kdzestelov
  7. // @license MIT
  8. // @match *://*codeforces.com/*
  9. // @grant GM_xmlhttpRequest
  10. // @updateUrl https://gist.githubusercontent.com/KhetagAb/000670a28c8a37b917ece858b142b747/raw/script.js
  11. // ==/UserScript==
  12.  
  13. const SHEET_URL = 'https://script.google.com/macros/s/AKfycbxGoJrsWBtwNFcYcXBNrk-PUTIOWrsRvPqDxBzBqT_TFpfaurt8IB5Dc9dEQU97-Eoyug/exec';
  14.  
  15. const SUB_RJ_BUTTON_CLASS = "submission-rj-button";
  16. const SUB_AC_BUTTON_CLASS = "submission-ac-button";
  17. const SUB_RG_BUTTON_CLASS = "submission-rejudge-form";
  18. const SUB_COMMENT_SEND_BUTTON_CLASS = "submission-comment-send-form";
  19.  
  20. function getLast(href) {
  21. return href.split("/").pop()
  22. }
  23.  
  24. function getGroupUrl() {
  25. const url = window.location.href
  26. return url.substring(0, url.indexOf("contest"))
  27. }
  28.  
  29. const getContestUrl = () => {
  30. const url = window.location.href
  31. return url.substring(0, url.indexOf("status"))
  32. }
  33.  
  34. function getContestId() {
  35. const url = window.location.href
  36. return url.substr(url.indexOf("contest") + 8, 6);
  37. }
  38.  
  39. function wrap(text) {
  40. return '[' + text + ']';
  41. }
  42.  
  43. function getSubRjButton(subId) {
  44. return $("[submissionid=" + subId + "] ." + SUB_RJ_BUTTON_CLASS);
  45. }
  46.  
  47. function getSubAcButton(subId) {
  48. return $("[submissionid=" + subId + "] ." + SUB_AC_BUTTON_CLASS);
  49. }
  50.  
  51. function getCommentSubAcButton(subId) {
  52. return $("." + SUB_COMMENT_SEND_BUTTON_CLASS)
  53. }
  54.  
  55. function getProblemIndex(subId) {
  56. return getLast($("tr[data-submission-id=" + subId + "] .status-small a").attr('href'));
  57. }
  58.  
  59. function getSubRow(subId) {
  60. return $('tr[data-submission-id="' + subId + '"]')
  61. }
  62.  
  63. function getHandle(subId) {
  64. return $("tr[data-submission-id=" + subId + "] .status-party-cell").text().trim();
  65. }
  66.  
  67. function getAllSubmissionsRow() {
  68. return $(".status-frame-datatable tbody tr")
  69. }
  70.  
  71. function getSubmissionRow(subId) {
  72. return $(".status-frame-datatable tbody tr[data-submission-id=" + subId + "]")
  73. }
  74.  
  75. function getSubsId() {
  76. return $(".information-box-link")
  77. }
  78.  
  79. function getCorrectSubs() {
  80. return $(".information-box-link .verdict-accepted").parent();
  81. }
  82.  
  83. function getSubButtons() {
  84. return $(".submission-action-form");
  85. }
  86.  
  87. function getSideBar() {
  88. return $("div[id=sidebar]")
  89. }
  90.  
  91. function getFilterBox() {
  92. return $(".status-filter-box");
  93. }
  94.  
  95. const getSheetSubmissions = () => {
  96. const fullUrl = SHEET_URL + "?type=get"
  97. GM_xmlhttpRequest({
  98. method: 'GET',
  99. url: fullUrl,
  100. onload: (response) => {
  101. if (response.status === 200) {
  102. try {
  103. console.log("Going to parse response: " + response.responseText);
  104. var submissions = JSON.parse(response.responseText)
  105. submissions = JSON.parse(response.responseText)
  106. localStorage.setItem("c_status", JSON.stringify(submissions));
  107. console.log("Submissions loaded from sheet: " + submissions);
  108. } catch (e) {
  109. console.error("Unable to parse submissions: " + e);
  110. }
  111. } else {
  112. console.error("Unable to load submissions from sheet: " + response.responseText);
  113. }
  114. },
  115. onerror: (error) => {
  116. console.error(error);
  117. }
  118. });
  119. }
  120.  
  121. const acceptSheetSubmission = (subId, button, type) => {
  122. const full_url = SHEET_URL + "?type=" + type + "&value=" + subId;
  123. GM_xmlhttpRequest({
  124. method: 'GET',
  125. url: full_url,
  126. timeout: 5000,
  127. onload: (response) => {
  128. if (response.status === 200) {
  129. const submissions = getSubmissions();
  130. submissions.push(subId);
  131. localStorage.setItem("c_status", JSON.stringify(submissions));
  132. button.style.backgroundColor = "#81D718";
  133. button.style.borderColor = "#81D718";
  134. if (showed_codes[subId] != null && showed_codes[subId].showed) {
  135. showed_codes[subId]["showButton"].click();
  136. }
  137. button.innerText = (type === "star" ? "🔥" : "Похвалить");
  138. } else {
  139. button.innerText = "Ошибка!"
  140. console.error(response)
  141. }
  142. },
  143. onerror: (error) => {
  144. console.error(error);
  145. }
  146. });
  147. }
  148. const getSubmissions = () => {
  149. return JSON.parse(localStorage.getItem("c_status") ?? "[]");
  150. };
  151. const isSubAccepted = (subId) => {
  152. return getSubmissions().includes(subId)
  153. }
  154.  
  155. const acceptSubmission = (subId, button) => {
  156. const type = isSubAccepted(subId) ? "star" : "accept";
  157. acceptSheetSubmission(subId, button, type);
  158. }
  159.  
  160. const showed_codes = {};
  161.  
  162. function createSubShowButton(subId, lang) {
  163. const button = document.createElement("button");
  164. button.className = "submission-show";
  165. button.style.marginTop = "4px";
  166. button.style.marginBottom = "4px";
  167. button.style.backgroundColor = "#176F95";
  168. button.style.border = "1px solid #176F95";
  169. button.style.borderRadius = "8px"
  170. button.style.boxShadow = "rgba(0, 0, 0, .1) 0 2px 4px 0"
  171. button.style.color = "#fff"
  172. button.style.cursor = "pointer"
  173. button.style.padding = "10px"
  174. button.style.width = "100%";
  175. button.innerText = "Показать " + lang;
  176. button.onclick = (_) => showButtonClick(subId, button);
  177. return button;
  178. }
  179.  
  180. function patchCodeSection(subId) {
  181. const patchLine = (i, line) => {
  182. line.addEventListener('click', () => {
  183. if(window.getSelection().toString() != "")
  184. return;
  185. const text = $("[data-submission-id=" + subId + "] textarea")
  186. text.val((text.val().length === 0 ? "" : text.val() + "\n") + "Строка " + (i + 1) + ": ")
  187. var x = window.scrollX, y = window.scrollY;
  188. text.focus();
  189. window.scrollTo(x, y);
  190. });
  191. return line;
  192. };
  193.  
  194. let pretty_code = $('[data-submission-id=' + subId + '] .program-source li');
  195. const code_lines_count = pretty_code.length
  196. pretty_code.each((i, line) => patchLine(i, line))
  197. pretty_code = pretty_code.parent().parent();
  198. pretty_code.before((_) => {
  199. const lines = document.createElement("pre");
  200. lines.style.width = '4%';
  201. lines.style.padding = "0.5em";
  202. lines.style.display = 'inline-block';
  203. const lineNs = [...Array(code_lines_count).keys()].map((i) => {
  204. const line = document.createElement("span");
  205. line.style.color = 'rgb(153, 153, 153)';
  206. line.innerText = "[" + (i + 1) + "]";
  207. line.style.display = "block";
  208. line.style.textAlign = "right";
  209. line.style.userSelect = "none";
  210. line.style.cursor = "pointer";
  211. return patchLine(i, line);
  212. })
  213. lines.append(...lineNs)
  214. return lines
  215. })
  216. pretty_code.css({'display': 'inline-block', 'width': '90%'})
  217. }
  218.  
  219. function showButtonClick(subId, button) {
  220. if (showed_codes[subId] != null) {
  221. if (showed_codes[subId].showed == true) {
  222. $(showed_codes[subId].commentSection).hide();
  223. $(showed_codes[subId].codeSection).hide();
  224. button.innerText = "Показать код";
  225. showed_codes[subId].showed = false;
  226. } else if (showed_codes[subId].showed == false) {
  227. $(showed_codes[subId].commentSection).show();
  228. $(showed_codes[subId].codeSection).show();
  229. button.innerText = "Скрыть код";
  230. showed_codes[subId].showed = true;
  231. }
  232. } else {
  233. button.innerText = showed_codes[subId] = "Загружаю код..."
  234. const requestUrl = getContestUrl() + 'submission/' + subId;
  235. console.log(requestUrl);
  236. $.get(requestUrl, function (html) {
  237. const codeHtml = $(html).find(".SubmissionDetailsFrameRoundBox-" + subId).html()
  238.  
  239. if (codeHtml == undefined) {
  240. button.innerText = "Ошибка!";
  241. //location.reload();
  242. return;
  243. }
  244.  
  245. const commentSection = createCommentSection(subId)
  246. const subCodeSection = createSubCodeSection(subId, codeHtml);
  247.  
  248. const subRow = getSubRow(subId);
  249. subRow.after(commentSection, subCodeSection)
  250.  
  251. prettyPrint(subId);
  252.  
  253. patchCodeSection(subId);
  254.  
  255. showed_codes[subId] = {
  256. "showed": true,
  257. "showButton": button,
  258. "commentSection": commentSection,
  259. "codeSection": subCodeSection
  260. }
  261. button.innerText = "Скрыть код"
  262. });
  263. }
  264. }
  265.  
  266. function createSubCodeSection(subId, codeHtml) {
  267. const trSubCode = document.createElement("tr");
  268. trSubCode.setAttribute('data-submission-id', subId);
  269. const tdSubCode = document.createElement("td");
  270. tdSubCode.setAttribute('colspan', '8');
  271. tdSubCode.innerHTML = codeHtml;
  272. tdSubCode.style.textAlign = "start"
  273. trSubCode.append(tdSubCode);
  274. return trSubCode;
  275. }
  276.  
  277. const createCommentSection = (subId) => {
  278. const subAcButton = getSubAcButton(subId)[0];
  279. const isAccepted = isSubAccepted(subId);
  280. const commentTextfield = createCommentTextfield()
  281. const commentAcButton = commentSendButtonTemplate(subId, (isAccepted ? "Похвалить" : "Принять") + " с комментарием", (isAccepted ? "#81D718" : "#13aa52"), (subId, button) => {
  282. const text = $(commentTextfield).val();
  283. if (text.length === 0) {
  284. button.innerText = "Принимаю...";
  285. acceptSubmission(subId, subAcButton);
  286. } else {
  287. button.innerText = "Отправляю...";
  288. $.post(getGroupUrl() + 'data/newAnnouncement', {
  289. contestId: getContestId(),
  290. englishText: "",
  291. russianText: text,
  292. submittedProblemIndex: getProblemIndex(subId),
  293. targetUserHandle: getHandle(subId),
  294. announceInPairContest: true,
  295. }, () => {
  296. acceptSubmission(subId, subAcButton);
  297. })
  298. }
  299. })
  300. const commentRjButton = commentSendButtonTemplate(subId, "Отклонить с комментарием", "#EC431A", (subId, button) => {
  301. const text = $(commentTextfield).val();
  302. if (text.length > 0) {
  303. button.innerText = "Отклоняю...";
  304. $.post(getGroupUrl() + 'data/newAnnouncement', {
  305. contestId: getContestId(),
  306. englishText: "",
  307. russianText: text,
  308. submittedProblemIndex: getProblemIndex(subId),
  309. targetUserHandle: getHandle(subId),
  310. announceInPairContest: true,
  311. }, () => {
  312. rejectSub(subId);
  313. if (showed_codes[subId] != null) {
  314. $(showed_codes[subId]["codeSection"]).hide();
  315. }
  316. button.innerText = "Отклонено";
  317. })
  318. }
  319. });
  320.  
  321. commentTextfield.addEventListener("keyup", (event) => {
  322. event.preventDefault();
  323. if (event.keyCode === 13) {
  324. commentRjButton.click();
  325. }
  326. });
  327.  
  328. const trSection = document.createElement("tr");
  329. trSection.setAttribute('data-submission-id', subId);
  330. const tdSection = document.createElement("td");
  331. tdSection.setAttribute('colspan', '8');
  332. const tdSectionTitle = document.createElement("div");
  333. tdSectionTitle.style.textAlign = "left";
  334. tdSectionTitle.className = "caption titled"
  335. tdSectionTitle.innerText = "→ Комментарий"
  336. tdSection.append(tdSectionTitle, commentTextfield, commentAcButton, commentRjButton);
  337. trSection.append(tdSection)
  338. return trSection;
  339. }
  340.  
  341. function createCommentTextfield() {
  342. const textField = document.createElement("textarea");
  343. textField.name = "russianText"
  344. textField.className = "bottom-space-small monospaced"
  345. textField.style.width = "80rem";
  346. textField.style.height = "5rem";
  347. textField.style.margin = "4px";
  348. return textField;
  349. }
  350.  
  351. const commentSendButtonTemplate = (subId, text, color, action) => {
  352. const button = document.createElement("button");
  353. button.className = SUB_COMMENT_SEND_BUTTON_CLASS;
  354. button.style.margin = "4px";
  355. button.style.width = "40%";
  356. button.style.backgroundColor = color;
  357. button.style.border = "1px solid " + color;
  358. button.style.borderRadius = "8px"
  359. button.style.boxShadow = "rgba(0, 0, 0, .1) 0 2px 4px 0"
  360. button.style.color = "#fff"
  361. button.style.cursor = "pointer"
  362. button.style.padding = "8px 0.5em"
  363. button.innerText = text;
  364. button.onclick = () => action(subId, button);
  365. return button
  366. }
  367.  
  368. const acButtonTemplate = (subId, action, text) => {
  369. const acButton = document.createElement("button");
  370. acButton.className = SUB_AC_BUTTON_CLASS;
  371. const color = (isSubAccepted(subId) ? "#81D718" : "#13aa52");
  372. acButton.style.backgroundColor = color;
  373. acButton.style.border = "1px solid " + color;
  374. acButton.style.borderRadius = "8px"
  375. acButton.style.boxShadow = "rgba(0, 0, 0, .1) 0 2px 4px 0"
  376. acButton.style.color = "#fff"
  377. acButton.style.cursor = "pointer"
  378. acButton.style.padding = "8px 0.5em"
  379. acButton.style.margin = "5px 5px 0 0";
  380. acButton.style.width = "59%";
  381. acButton.innerText = text !== undefined ? text : (isSubAccepted(subId) ? "Похвалить" : "AC");
  382. acButton.onclick = (_) => action(subId, acButton);
  383. return acButton;
  384. }
  385.  
  386. const createAcButton = (template, subId, ...args) => {
  387. return template(subId, (subId, button) => {
  388. button.innerText = "Подтверждаю...";
  389. button.style.borderColor = "gray";
  390. button.style.backgroundColor = "gray";
  391. acceptSubmission(subId, button);
  392. }, ...args);
  393. }
  394.  
  395. const createRjButton = (subId, text, action) => {
  396. const rjButton = document.createElement("button");
  397. rjButton.className = SUB_RJ_BUTTON_CLASS;
  398. rjButton.style.width = "38%";
  399. rjButton.style.backgroundColor = "#EC431A";
  400. rjButton.style.border = "1px solid #EC431A"
  401. rjButton.style.borderRadius = "8px"
  402. rjButton.style.padding = "8px 0.5em"
  403. rjButton.style.boxShadow = "rgba(0, 0, 0, .1) 0 2px 4px 0"
  404. rjButton.style.color = "#fff"
  405. rjButton.style.cursor = "pointer"
  406. rjButton.innerText = text;
  407. rjButton.onclick = (_) => action(subId, rjButton);
  408. return rjButton
  409. }
  410.  
  411. const createRgButton = (subId) => {
  412. const button = document.createElement("button");
  413. button.className = SUB_RG_BUTTON_CLASS;
  414. button.style.margin = "5px 0";
  415. button.style.padding = "4px"
  416. button.style.width = "100%";
  417. button.style.backgroundColor = "#176F95";
  418. button.style.border = "1px solid #176F95"
  419. button.style.borderRadius = "15px"
  420. button.style.boxShadow = "rgba(0, 0, 0, .1) 0 2px 4px 0"
  421. button.style.color = "#fff"
  422. button.style.cursor = "pointer"
  423. button.innerText = "Перетестировать";
  424. button.onclick = (_) => {
  425. const requestUrl = getContestUrl() + 'submission/' + subId
  426. const data = {action: "rejudge", submissionId: subId}
  427. $.post(requestUrl, data, (_) => location.reload());
  428. button.innerText = "Тестирую...";
  429. };
  430. return button;
  431. }
  432.  
  433. const rejectSub = (subId) => {
  434. const subRjButton = getSubRjButton(subId);
  435. subRjButton.innerText = "Отклоняю...";
  436. const subAcButton = getSubAcButton(subId);
  437. const commentSubAcButton = getCommentSubAcButton(subId);
  438. const requestUrl = getContestUrl() + 'submission/' + subId
  439. const data = {action: "reject", submissionId: subId}
  440. $.post(requestUrl, data, function (_) {
  441. $("[submissionid=" + subId + "] .verdict-accepted").remove()
  442. subAcButton.remove()
  443. subRjButton.remove()
  444. commentSubAcButton.remove();
  445. })
  446. }
  447.  
  448. const patchSubmissions = () => {
  449. const subsId = getSubsId();
  450. const languages = subsId.parent().prev()
  451.  
  452. languages.append((i, e) => {
  453. const subId = Number($(subsId[i])[0].getAttribute('submissionid'))
  454. const language = e.split('(')[0]
  455. $(languages[i]).empty()
  456. return createSubShowButton(subId, language);
  457. })
  458. }
  459.  
  460. const patchCorrectSubmissions = () => {
  461. const correctSubs = getCorrectSubs();
  462. correctSubs.parent().append((i) => {
  463. const subId = Number($(correctSubs[i]).attr('submissionid'))
  464.  
  465. const acButton = createAcButton(acButtonTemplate, subId)
  466. const rgButton = createRgButton(subId);
  467. const rjButton = createRjButton(subId, "RJ", (subId, _) => {
  468. rejectSub(subId);
  469. });
  470.  
  471. return [acButton, rjButton, rgButton]
  472. })
  473. }
  474.  
  475. const patchContestSidebar = () => {
  476. const contestsSidebar = $(".GroupContestsSidebarFrame ul a")
  477. contestsSidebar.before((i) => {
  478. const contestHref = $(contestsSidebar[i]).attr('href');
  479. return document.createTextNode(wrap(getLast(contestHref)));
  480. });
  481. }
  482.  
  483. const patchSubmission = () => {
  484. const buttons = getSubButtons()
  485. if (buttons.length > 0) {
  486. const subId = Number(getLast(location.pathname));
  487. const acButton = createAcButton(acButtonTemplate, subId);
  488. buttons[0].before(acButton);
  489. }
  490. }
  491.  
  492. const patchFilterBox = () => {
  493. const filterBox = getFilterBox();
  494. const sidebar = getSideBar();
  495. filterBox.detach().prependTo(sidebar);
  496. const filterBoxPart = filterBox.find(".status-filter-form-part")[0];
  497.  
  498. const correctSubsId = getCorrectSubs().map((i, e) => Number($(e).attr("submissionid"))).toArray();
  499. const filter = (checkbox) => {
  500. localStorage.setItem("filterPendingSubs", checkbox.checked);
  501. const filtered = correctSubsId.filter(subId => {
  502. console.log(subId + " " + !isSubAccepted(subId))
  503. return !isSubAccepted(subId)
  504. });
  505. console.log(filtered)
  506. getAllSubmissionsRow().each((i, e) => {
  507. if (checkbox.checked) {
  508. if (!filtered.includes(Number($(e).attr('data-submission-id')))) {
  509. $(e).hide();
  510. }
  511. } else {
  512. $(e).show()
  513. }
  514. });
  515. };
  516.  
  517. const template = createFilterPendingCheckboxTemplate(filter);
  518. const label = template[0]
  519. const checkbox = template[1]
  520. checkbox.checked = ('true' === localStorage.getItem("filterPendingSubs") ?? false);
  521. filter(checkbox);
  522. filterBoxPart.before(label);
  523. }
  524.  
  525. function createFilterPendingCheckboxTemplate(action) {
  526. const label = document.createElement("label");
  527. const checkbox = document.createElement("input");
  528. checkbox.type = "checkbox";
  529. checkbox.onclick = (_) => action(checkbox);
  530. const title = document.createElement("span");
  531. title.style.padding = "5px";
  532. title.className = "smaller";
  533. title.innerText = "Только непроверенные посылки";
  534. label.append(checkbox, title);
  535. return [label, checkbox];
  536. }
  537.  
  538.  
  539. (function () {
  540. getSheetSubmissions();
  541.  
  542. try {
  543. patchContestSidebar();
  544. } catch (e) {
  545. console.error(e);
  546. }
  547.  
  548. try {
  549. patchFilterBox();
  550. } catch (e) {
  551. console.error(e);
  552. }
  553.  
  554. try {
  555. patchCorrectSubmissions();
  556. } catch (e) {
  557. console.error(e);
  558. }
  559.  
  560. try {
  561. patchSubmissions();
  562. } catch (e) {
  563. console.error(e);
  564. }
  565.  
  566. try {
  567. patchSubmission();
  568. } catch (e) {
  569. console.error(e);
  570. }
  571. })();