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 @@
+
+
+
Correct Spelling Test
+
+
+ | Word | Typed | Mark correct |
+
+
+
+
+
+
+
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 =
+ `| Word | Typed | Mark 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) => {