您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Calculates the average grade level from the past 15 (or fewer) quizzes available on ReadTheory
- // ==UserScript==
- // @name ReadTheory Average Grade Level Calculator (Past 15 Quizzes)
- // @namespace https://greasyfork.org/en/users/567951-stuart-saddler
- // @version 1.12
- // @description Calculates the average grade level from the past 15 (or fewer) quizzes available on ReadTheory
- // @author Stuart Saddler
- // @icon https://images-na.ssl-images-amazon.com/images/I/41Y-bktG5oL.png
- // @license MIT
- // @match *://readtheory.org/app/teacher/reports/*
- // @grant none
- // @run-at document-idle
- // ==/UserScript==
- (function () {
- "use strict";
- const NUM_RECENT = 15;
- // Map words to numbers; also allow raw digits like "5" or "5.0"
- function gradeTextToNumber(gradeText) {
- if (!gradeText) return 0;
- const txt = String(gradeText).toLowerCase().trim();
- const map = {
- "one": 1, "two": 2, "three": 3, "four": 4, "five": 5, "six": 6,
- "seven": 7, "eight": 8, "nine": 9, "ten": 10, "eleven": 11, "twelve": 12,
- "thirteen": 13, "fourteen": 14, "fifteen": 15
- };
- if (map[txt] != null) return map[txt];
- const num = parseFloat(txt.replace(/[^0-9.]/g, ""));
- return Number.isFinite(num) ? num : 0;
- }
- function calculateAverage(nums) {
- const valid = nums.filter(n => Number.isFinite(n) && n > 0);
- if (valid.length === 0) return "0.0";
- const total = valid.reduce((a, b) => a + b, 0);
- return (total / valid.length).toFixed(1);
- }
- function findGradeColumnIndex(table) {
- // Look only at header row cells
- const ths = table.querySelectorAll("thead th, tr:first-child th");
- let idx = -1;
- ths.forEach((th, i) => {
- if (String(th.textContent || "").toLowerCase().includes("grade level")) {
- idx = i;
- }
- });
- return idx; // 0-based
- }
- function getRecentGradeCells(table, gradeColIdx) {
- // Pull the first NUM_RECENT body rows from the grade column
- const rows = Array.from(table.querySelectorAll("tbody tr")).slice(0, NUM_RECENT);
- const tds = rows.map(r => r.querySelectorAll("td")[gradeColIdx]).filter(Boolean);
- return tds;
- }
- function ensureFloatingWindow() {
- let el = document.getElementById("average-grade-floating-window");
- if (!el) {
- el = document.createElement("div");
- el.id = "average-grade-floating-window";
- Object.assign(el.style, {
- position: "fixed",
- bottom: "100px",
- right: "20px",
- padding: "10px",
- backgroundColor: "#f1f1f1",
- border: "2px solid #333",
- zIndex: "9999",
- boxShadow: "0px 4px 8px rgba(0, 0, 0, 0.1)",
- fontSize: "16px",
- color: "#333",
- maxWidth: "320px",
- borderRadius: "6px",
- lineHeight: "1.3"
- });
- document.body.appendChild(el);
- }
- return el;
- }
- function calculateAndDisplayAverage() {
- try {
- const table = document.querySelector("table");
- if (!table) {
- console.warn("No table found yet.");
- return;
- }
- const colIdx = findGradeColumnIndex(table);
- if (colIdx === -1) {
- console.error("Could not find the 'Grade Level' column.");
- return;
- }
- const gradeCells = getRecentGradeCells(table, colIdx);
- if (gradeCells.length === 0) {
- console.warn("No grade cells found yet.");
- return;
- }
- const grades = gradeCells.map(td => gradeTextToNumber(td.textContent));
- const avg = calculateAverage(grades);
- const box = ensureFloatingWindow();
- box.textContent = "Average Grade Level (Last " + grades.filter(g => g > 0).length + " Quizzes): " + avg;
- console.log("Average grade level:", avg);
- } catch (e) {
- console.error("Average grade calculation failed:", e);
- }
- }
- // Observe content changes and recalc once the table is present
- let initObserverStarted = false;
- function startInitObserver() {
- if (initObserverStarted) return;
- initObserverStarted = true;
- const obs = new MutationObserver(function (ml, ob) {
- const table = document.querySelector("table");
- const hasHeader = table && table.querySelector("th");
- if (hasHeader) {
- ob.disconnect();
- calculateAndDisplayAverage();
- }
- });
- obs.observe(document.body, { childList: true, subtree: true });
- }
- // Recalculate when student selection changes; avoid stacking observers
- let dropdownListenerAttached = false;
- function watchStudentDropdown() {
- if (dropdownListenerAttached) return;
- const selects = Array.from(document.querySelectorAll("select"));
- if (selects.length === 0) return;
- const handler = function () {
- // Give the page a moment to swap in new rows
- setTimeout(calculateAndDisplayAverage, 1200);
- };
- selects.forEach(sel => sel.addEventListener("change", handler));
- document.addEventListener("change", function (ev) {
- if (ev.target && ev.target.tagName === "SELECT") {
- setTimeout(calculateAndDisplayAverage, 1200);
- }
- });
- dropdownListenerAttached = true;
- }
- // Watch a specific panel attribute if present
- let idObserver;
- function watchStudentIDAttribute() {
- const panel = document.querySelector(".quiz-history-panel");
- if (!panel) return;
- if (idObserver) idObserver.disconnect();
- idObserver = new MutationObserver(function (ml) {
- for (const m of ml) {
- if (m.type === "attributes" && m.attributeName === "studentid") {
- setTimeout(calculateAndDisplayAverage, 600);
- break;
- }
- }
- });
- idObserver.observe(panel, { attributes: true, attributeFilter: ["studentid"] });
- }
- function init() {
- startInitObserver();
- watchStudentDropdown();
- watchStudentIDAttribute();
- // First attempt after idle
- calculateAndDisplayAverage();
- }
- if (document.readyState === "complete" || document.readyState === "interactive") {
- setTimeout(init, 0);
- } else {
- window.addEventListener("load", init);
- document.addEventListener("DOMContentLoaded", init);
- }
- })();