From 0744ba9e39924c09c7652e5e37da046997fafe68 Mon Sep 17 00:00:00 2001 From: Nate Emmert Date: Fri, 21 Aug 2026 08:56:28 -0400 Subject: [PATCH] Add spelling test correction feature for admins Adds a "Correct" button to each test in the Spelling Test History table. Clicking it opens a modal showing each word, what the student typed, and a checkbox to mark it correct or incorrect. Saving PATCHes the server, which updates test_answers, recalculates the test score, and keeps any linked course submission in sync. Co-Authored-By: Claude Sonnet 4.6 --- db.js | 1 + public/admin.html | 13 +++++++++++++ public/admin.js | 47 ++++++++++++++++++++++++++++++++++++++++++++++- server.js | 37 ++++++++++++++++++++++++++++++++++--- 4 files changed, 94 insertions(+), 4 deletions(-) diff --git a/db.js b/db.js index 406f5b2..29177f0 100644 --- a/db.js +++ b/db.js @@ -242,6 +242,7 @@ tryAlter(`ALTER TABLE students ADD COLUMN young_learner INTEGER NOT NULL DEFAULT tryAlter(`ALTER TABLE students ADD COLUMN birthday TEXT`); tryAlter(`ALTER TABLE lists ADD COLUMN group_name TEXT NOT NULL DEFAULT ''`); tryAlter(`ALTER TABLE decks ADD COLUMN group_name TEXT NOT NULL DEFAULT ''`); +tryAlter(`ALTER TABLE tests ADD COLUMN item_id INTEGER`); export const sha256 = (s) => createHash('sha256').update(String(s)).digest('hex'); diff --git a/public/admin.html b/public/admin.html index a9f9e0f..d880fdb 100644 --- a/public/admin.html +++ b/public/admin.html @@ -413,6 +413,19 @@ + diff --git a/public/admin.js b/public/admin.js index 8416762..e5bd532 100644 --- a/public/admin.js +++ b/public/admin.js @@ -2071,7 +2071,10 @@ async function loadSpellingTests() { ${new Date(t.at + 'Z').toLocaleDateString()} ${esc(t.list)} ${t.score}/${t.total} (${pct}%) - + + + + `; }) .join(''); @@ -2082,7 +2085,49 @@ async function loadSpellingTests() { container.querySelectorAll('[data-print]').forEach((btn) => btn.addEventListener('click', () => printSpellingReport(btn.dataset.print)) ); + container.querySelectorAll('[data-correct]').forEach((btn) => + btn.addEventListener('click', () => openCorrectTestModal(btn.dataset.correct)) + ); } + +// ---- correction modal ---- +let correctTestId = null; + +async function openCorrectTestModal(testId) { + correctTestId = testId; + const r = await api(`/api/test-report/${testId}`); + $('#correct-test-meta').textContent = + `${r.student} · ${r.list} · ${new Date(r.at + 'Z').toLocaleDateString()}`; + const tbody = r.answers.map((a) => ` + + ${esc(a.word)} + ${a.typed ? esc(a.typed) : 'blank'} + + + + `).join(''); + $('#correct-test-table').innerHTML = + `WordTypedMark correct${tbody}`; + $('#correct-test-modal').hidden = false; +} + +$('#correct-test-cancel').addEventListener('click', () => { $('#correct-test-modal').hidden = true; }); + +$('#correct-test-save').addEventListener('click', async () => { + const checkboxes = $('#correct-test-table').querySelectorAll('input[type=checkbox]'); + const corrections = Array.from(checkboxes).map((cb) => ({ + wordId: Number(cb.dataset.wordId), + correct: cb.checked, + })); + const { score, total } = await api(`/api/tests/${correctTestId}/correct`, { + method: 'PATCH', + body: { corrections }, + }); + $('#correct-test-modal').hidden = true; + msg(`Score updated: ${score}/${total}`); + loadSpellingTests(); +}); + $('#gradebook-course').addEventListener('change', () => renderGradebook($('#gradebook-course').value)); $('#gradebook-csv-btn').addEventListener('click', () => { if (!gradebookCourseId) return; diff --git a/server.js b/server.js index 38db476..c517966 100644 --- a/server.js +++ b/server.js @@ -485,8 +485,8 @@ app.post('/api/test/:studentId', (req, res) => { }); const score = graded.filter((g) => g.correct).length; - const testId = db.prepare(`INSERT INTO tests (student_id, list_id, score, total) VALUES (?, ?, ?, ?)`) - .run(studentId, listId, score, graded.length).lastInsertRowid; + const testId = db.prepare(`INSERT INTO tests (student_id, list_id, score, total, item_id) VALUES (?, ?, ?, ?, ?)`) + .run(studentId, listId, score, graded.length, itemId || null).lastInsertRowid; const insAnswer = db.prepare(`INSERT INTO test_answers (test_id, word_id, typed, correct) VALUES (?, ?, ?, ?)`); for (const g of graded) { insAnswer.run(testId, g.wordId, String(g.typed), g.correct ? 1 : 0); @@ -520,7 +520,7 @@ app.get('/api/test-report/:testId', (req, res) => { if (!test) return res.status(404).json({ error: 'No such test' }); test.answers = db.prepare(` - SELECT w.word, ta.typed, ta.correct + SELECT w.id AS word_id, w.word, ta.typed, ta.correct FROM test_answers ta JOIN words w ON w.id = ta.word_id WHERE ta.test_id = ? ORDER BY w.word @@ -528,6 +528,37 @@ app.get('/api/test-report/:testId', (req, res) => { res.json(test); }); +// Correct individual answers on an existing test (admin only) +app.patch('/api/tests/:testId/correct', requirePin, (req, res) => { + const { corrections } = req.body; // [{ wordId, correct }] + if (!Array.isArray(corrections) || corrections.length === 0) { + return res.status(400).json({ error: 'No corrections provided' }); + } + const testId = Number(req.params.testId); + const test = db.prepare(`SELECT id, student_id, total, item_id FROM tests WHERE id = ?`).get(testId); + if (!test) return res.status(404).json({ error: 'Test not found' }); + + const upd = db.prepare(`UPDATE test_answers SET correct = ? WHERE test_id = ? AND word_id = ?`); + db.transaction(() => { + for (const c of corrections) { + upd.run(c.correct ? 1 : 0, testId, c.wordId); + } + })(); + + const newScore = db.prepare(`SELECT COUNT(*) AS n FROM test_answers WHERE test_id = ? AND correct = 1`).get(testId).n; + db.prepare(`UPDATE tests SET score = ? WHERE id = ?`).run(newScore, testId); + + // Keep linked course submission in sync + if (test.item_id) { + db.prepare(` + UPDATE submissions SET score = ?, points_possible = ?, graded_at = datetime('now') + WHERE student_id = ? AND item_id = ? + `).run(newScore, test.total, test.student_id, test.item_id); + } + + res.json({ score: newScore, total: test.total }); +}); + // ---------- spelling practice activity (parent view) ---------- app.get('/api/spelling-activity', requirePin, (req, res) => {