ReadTheory Average Grade Level Calculator (Past 15 Quizzes)

Calculates the average grade level from the past 15 (or fewer) quizzes available on ReadTheory

  1. // ==UserScript==
  2. // @name ReadTheory Average Grade Level Calculator (Past 15 Quizzes)
  3. // @namespace https://greasyfork.org/en/users/567951-stuart-saddler
  4. // @version 1.12
  5. // @description Calculates the average grade level from the past 15 (or fewer) quizzes available on ReadTheory
  6. // @author Stuart Saddler
  7. // @icon https://images-na.ssl-images-amazon.com/images/I/41Y-bktG5oL.png
  8. // @license MIT
  9. // @match *://readtheory.org/app/teacher/reports/*
  10. // @grant none
  11. // @run-at document-idle
  12. // ==/UserScript==
  13.  
  14. (function () {
  15. "use strict";
  16.  
  17. const NUM_RECENT = 15;
  18.  
  19. // Map words to numbers; also allow raw digits like "5" or "5.0"
  20. function gradeTextToNumber(gradeText) {
  21. if (!gradeText) return 0;
  22. const txt = String(gradeText).toLowerCase().trim();
  23. const map = {
  24. "one": 1, "two": 2, "three": 3, "four": 4, "five": 5, "six": 6,
  25. "seven": 7, "eight": 8, "nine": 9, "ten": 10, "eleven": 11, "twelve": 12,
  26. "thirteen": 13, "fourteen": 14, "fifteen": 15
  27. };
  28. if (map[txt] != null) return map[txt];
  29. const num = parseFloat(txt.replace(/[^0-9.]/g, ""));
  30. return Number.isFinite(num) ? num : 0;
  31. }
  32.  
  33. function calculateAverage(nums) {
  34. const valid = nums.filter(n => Number.isFinite(n) && n > 0);
  35. if (valid.length === 0) return "0.0";
  36. const total = valid.reduce((a, b) => a + b, 0);
  37. return (total / valid.length).toFixed(1);
  38. }
  39.  
  40. function findGradeColumnIndex(table) {
  41. // Look only at header row cells
  42. const ths = table.querySelectorAll("thead th, tr:first-child th");
  43. let idx = -1;
  44. ths.forEach((th, i) => {
  45. if (String(th.textContent || "").toLowerCase().includes("grade level")) {
  46. idx = i;
  47. }
  48. });
  49. return idx; // 0-based
  50. }
  51.  
  52. function getRecentGradeCells(table, gradeColIdx) {
  53. // Pull the first NUM_RECENT body rows from the grade column
  54. const rows = Array.from(table.querySelectorAll("tbody tr")).slice(0, NUM_RECENT);
  55. const tds = rows.map(r => r.querySelectorAll("td")[gradeColIdx]).filter(Boolean);
  56. return tds;
  57. }
  58.  
  59. function ensureFloatingWindow() {
  60. let el = document.getElementById("average-grade-floating-window");
  61. if (!el) {
  62. el = document.createElement("div");
  63. el.id = "average-grade-floating-window";
  64. Object.assign(el.style, {
  65. position: "fixed",
  66. bottom: "100px",
  67. right: "20px",
  68. padding: "10px",
  69. backgroundColor: "#f1f1f1",
  70. border: "2px solid #333",
  71. zIndex: "9999",
  72. boxShadow: "0px 4px 8px rgba(0, 0, 0, 0.1)",
  73. fontSize: "16px",
  74. color: "#333",
  75. maxWidth: "320px",
  76. borderRadius: "6px",
  77. lineHeight: "1.3"
  78. });
  79. document.body.appendChild(el);
  80. }
  81. return el;
  82. }
  83.  
  84. function calculateAndDisplayAverage() {
  85. try {
  86. const table = document.querySelector("table");
  87. if (!table) {
  88. console.warn("No table found yet.");
  89. return;
  90. }
  91.  
  92. const colIdx = findGradeColumnIndex(table);
  93. if (colIdx === -1) {
  94. console.error("Could not find the 'Grade Level' column.");
  95. return;
  96. }
  97.  
  98. const gradeCells = getRecentGradeCells(table, colIdx);
  99. if (gradeCells.length === 0) {
  100. console.warn("No grade cells found yet.");
  101. return;
  102. }
  103.  
  104. const grades = gradeCells.map(td => gradeTextToNumber(td.textContent));
  105. const avg = calculateAverage(grades);
  106.  
  107. const box = ensureFloatingWindow();
  108. box.textContent = "Average Grade Level (Last " + grades.filter(g => g > 0).length + " Quizzes): " + avg;
  109. console.log("Average grade level:", avg);
  110. } catch (e) {
  111. console.error("Average grade calculation failed:", e);
  112. }
  113. }
  114.  
  115. // Observe content changes and recalc once the table is present
  116. let initObserverStarted = false;
  117. function startInitObserver() {
  118. if (initObserverStarted) return;
  119. initObserverStarted = true;
  120.  
  121. const obs = new MutationObserver(function (ml, ob) {
  122. const table = document.querySelector("table");
  123. const hasHeader = table && table.querySelector("th");
  124. if (hasHeader) {
  125. ob.disconnect();
  126. calculateAndDisplayAverage();
  127. }
  128. });
  129. obs.observe(document.body, { childList: true, subtree: true });
  130. }
  131.  
  132. // Recalculate when student selection changes; avoid stacking observers
  133. let dropdownListenerAttached = false;
  134. function watchStudentDropdown() {
  135. if (dropdownListenerAttached) return;
  136. const selects = Array.from(document.querySelectorAll("select"));
  137. if (selects.length === 0) return;
  138.  
  139. const handler = function () {
  140. // Give the page a moment to swap in new rows
  141. setTimeout(calculateAndDisplayAverage, 1200);
  142. };
  143.  
  144. selects.forEach(sel => sel.addEventListener("change", handler));
  145. document.addEventListener("change", function (ev) {
  146. if (ev.target && ev.target.tagName === "SELECT") {
  147. setTimeout(calculateAndDisplayAverage, 1200);
  148. }
  149. });
  150. dropdownListenerAttached = true;
  151. }
  152.  
  153. // Watch a specific panel attribute if present
  154. let idObserver;
  155. function watchStudentIDAttribute() {
  156. const panel = document.querySelector(".quiz-history-panel");
  157. if (!panel) return;
  158. if (idObserver) idObserver.disconnect();
  159.  
  160. idObserver = new MutationObserver(function (ml) {
  161. for (const m of ml) {
  162. if (m.type === "attributes" && m.attributeName === "studentid") {
  163. setTimeout(calculateAndDisplayAverage, 600);
  164. break;
  165. }
  166. }
  167. });
  168. idObserver.observe(panel, { attributes: true, attributeFilter: ["studentid"] });
  169. }
  170.  
  171. function init() {
  172. startInitObserver();
  173. watchStudentDropdown();
  174. watchStudentIDAttribute();
  175. // First attempt after idle
  176. calculateAndDisplayAverage();
  177. }
  178.  
  179. if (document.readyState === "complete" || document.readyState === "interactive") {
  180. setTimeout(init, 0);
  181. } else {
  182. window.addEventListener("load", init);
  183. document.addEventListener("DOMContentLoaded", init);
  184. }
  185. })();