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 <noreply@anthropic.com>
This commit is contained in:
1 parent
4574bd3785
commit
0744ba9e39
4 files changed
+94
-4
No files matched your search
@@ -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');
|
||||
|
||||
|
||||
@@ -413,6 +413,19 @@
|
||||
<button id="history-modal-close" class="secondary">Close</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="correct-test-modal" class="modal" hidden>
|
||||
<div class="modal-box">
|
||||
<h3>Correct Spelling Test</h3>
|
||||
<p id="correct-test-meta" class="hint"></p>
|
||||
<table class="results" id="correct-test-table">
|
||||
<tr><th>Word</th><th>Typed</th><th>Mark correct</th></tr>
|
||||
</table>
|
||||
<div class="inline-form" style="margin-top:1rem">
|
||||
<button id="correct-test-save">Save corrections</button>
|
||||
<button id="correct-test-cancel" class="secondary">Cancel</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Spelling (legacy word lists + weekly assignment) -->
|
||||
|
||||
+46
-1
@@ -2071,7 +2071,10 @@ async function loadSpellingTests() {
|
||||
<td>${new Date(t.at + 'Z').toLocaleDateString()}</td>
|
||||
<td>${esc(t.list)}</td>
|
||||
<td class="${pct >= appSettings.passingPct ? 'score-good' : 'score-bad'}">${t.score}/${t.total} (${pct}%)</td>
|
||||
<td><button data-print="${t.id}">🖨 Print</button></td>
|
||||
<td>
|
||||
<button data-print="${t.id}">🖨 Print</button>
|
||||
<button class="secondary small" data-correct="${t.id}">✏ Correct</button>
|
||||
</td>
|
||||
</tr>`;
|
||||
})
|
||||
.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) => `
|
||||
<tr>
|
||||
<td>${esc(a.word)}</td>
|
||||
<td>${a.typed ? esc(a.typed) : '<em class="hint">blank</em>'}</td>
|
||||
<td style="text-align:center">
|
||||
<input type="checkbox" data-word-id="${a.word_id}" ${a.correct ? 'checked' : ''}>
|
||||
</td>
|
||||
</tr>`).join('');
|
||||
$('#correct-test-table').innerHTML =
|
||||
`<tr><th>Word</th><th>Typed</th><th>Mark correct</th></tr>${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;
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
Reference in new issue
Block a user