Add spelling test polish: date filter, CSV export, student history, planner badges
- Gradebook: period dropdown (all time / 30 / 90 days) and CSV export for Spelling Test History section - Student portal spelling tab: shows last 10 tests as a Recent Tests list - Planner: completed standalone spelling tests appear as read-only ⭐ badges on the day they were taken (week and month views) - Removed dead s.tests query from /api/overview (now served by dedicated endpoints) - New endpoints: GET /api/spelling-tests/csv, GET /api/students/:id/tests, GET /api/admin/students/:id/tests-range Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
1 parent
183b43b452
commit
b048ebbd85
6 files changed
+110
-20
No files matched your search
@@ -445,11 +445,6 @@ app.get('/api/overview', requirePin, (req, res) => {
|
||||
WHERE w.list_id = ?
|
||||
`).get(s.id, s.assignment.id);
|
||||
}
|
||||
s.tests = db.prepare(`
|
||||
SELECT t.id, t.score, t.total, t.at, l.name AS list
|
||||
FROM tests t JOIN lists l ON l.id = t.list_id
|
||||
WHERE t.student_id = ? ORDER BY t.at DESC LIMIT 20
|
||||
`).all(s.id);
|
||||
}
|
||||
res.json({ students });
|
||||
});
|
||||
@@ -970,17 +965,61 @@ app.get('/api/gradebook/:courseId/csv', requirePin, (req, res) => {
|
||||
|
||||
// All standalone spelling test results grouped by student
|
||||
app.get('/api/spelling-tests', requirePin, (req, res) => {
|
||||
const days = parseInt(req.query.since) || null;
|
||||
const cutoff = days ? new Date(Date.now() - days * 86400000).toISOString().slice(0, 10) : null;
|
||||
const students = db.prepare(`SELECT id, name, emoji FROM students ORDER BY name`).all();
|
||||
for (const s of students) {
|
||||
s.tests = db.prepare(`
|
||||
SELECT t.id, t.score, t.total, t.at, l.name AS list
|
||||
FROM tests t JOIN lists l ON l.id = t.list_id
|
||||
WHERE t.student_id = ? ORDER BY t.at DESC LIMIT 20
|
||||
`).all(s.id);
|
||||
s.tests = cutoff
|
||||
? db.prepare(`SELECT t.id, t.score, t.total, t.at, l.name AS list FROM tests t JOIN lists l ON l.id = t.list_id WHERE t.student_id = ? AND date(t.at) >= ? ORDER BY t.at DESC LIMIT 20`).all(s.id, cutoff)
|
||||
: db.prepare(`SELECT t.id, t.score, t.total, t.at, l.name AS list FROM tests t JOIN lists l ON l.id = t.list_id WHERE t.student_id = ? ORDER BY t.at DESC LIMIT 20`).all(s.id);
|
||||
}
|
||||
res.json({ students });
|
||||
});
|
||||
|
||||
app.get('/api/spelling-tests/csv', requirePin, (req, res) => {
|
||||
const days = parseInt(req.query.since) || null;
|
||||
const cutoff = days ? new Date(Date.now() - days * 86400000).toISOString().slice(0, 10) : null;
|
||||
const rows = db.prepare(`
|
||||
SELECT s.emoji, s.name AS student, l.name AS list, t.score, t.total, t.at
|
||||
FROM tests t JOIN students s ON s.id = t.student_id JOIN lists l ON l.id = t.list_id
|
||||
${cutoff ? 'WHERE date(t.at) >= ?' : ''} ORDER BY t.at DESC
|
||||
`).all(...(cutoff ? [cutoff] : []));
|
||||
const q = (v) => `"${String(v ?? '').replace(/"/g, '""')}"`;
|
||||
const header = 'Date,Student,List,Score,Total,%';
|
||||
const data = rows.map((r) => [
|
||||
q(new Date(r.at + 'Z').toLocaleDateString()),
|
||||
q(`${r.emoji} ${r.student}`),
|
||||
q(r.list), r.score, r.total,
|
||||
Math.round((r.score / r.total) * 100) + '%',
|
||||
].join(',')).join('\n');
|
||||
res.setHeader('Content-Type', 'text/csv');
|
||||
res.setHeader('Content-Disposition', 'attachment; filename="spelling-test-history.csv"');
|
||||
res.send(header + '\n' + data);
|
||||
});
|
||||
|
||||
// Student-facing: recent tests for the spelling tab (no PIN — student portal)
|
||||
app.get('/api/students/:id/tests', (req, res) => {
|
||||
const tests = db.prepare(`
|
||||
SELECT t.id, t.score, t.total, t.at, l.name AS list
|
||||
FROM tests t JOIN lists l ON l.id = t.list_id
|
||||
WHERE t.student_id = ? ORDER BY t.at DESC LIMIT 10
|
||||
`).all(req.params.id);
|
||||
res.json({ tests });
|
||||
});
|
||||
|
||||
// Admin: tests within a date range for a single student (used by planner)
|
||||
app.get('/api/admin/students/:id/tests-range', requirePin, (req, res) => {
|
||||
const { from, to } = req.query;
|
||||
if (!from || !to) return res.status(400).json({ error: 'from and to required' });
|
||||
const tests = db.prepare(`
|
||||
SELECT t.id, t.score, t.total, t.at, date(t.at) AS date, l.name AS list
|
||||
FROM tests t JOIN lists l ON l.id = t.list_id
|
||||
WHERE t.student_id = ? AND date(t.at) BETWEEN ? AND ?
|
||||
ORDER BY t.at
|
||||
`).all(req.params.id, from, to);
|
||||
res.json({ tests });
|
||||
});
|
||||
|
||||
// Full evidence for a submission (photo can be large, not included in queue list)
|
||||
app.get('/api/grading-queue/:id/evidence', requirePin, (req, res) => {
|
||||
const row = db.prepare(`SELECT evidence_notes, evidence_photo FROM submissions WHERE id = ?`).get(req.params.id);
|
||||
|
||||
Reference in new issue
Block a user