LearnSpace: full-stack educational whiteboard platform
Node.js/Express backend + vanilla JS frontend. Features: real-time collaborative whiteboard (SSE), multi-page support, LaTeX formulas, shapes/connectors, coordinate systems, number lines, compass, zoom/pan, Catmull-Rom pencil smoothing, ruler/protractor with rotation & resize controls, minimap navigation overlay, auto-measurements, multi-page thumbnails sidebar, PNG export, page templates. Student/teacher workflows: classes, assignments, library, dashboard. Mobile responsive. SQLite (better-sqlite3). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,134 @@
|
||||
const db = require('../db/db');
|
||||
|
||||
/* ── GET /api/tests ─────────────────────────────────────────────────────── */
|
||||
function list(req, res) {
|
||||
const { subject } = req.query;
|
||||
const { role, id: uid } = req.user;
|
||||
const args = [];
|
||||
let where = '1=1';
|
||||
if (subject) { where += ' AND t.subject_slug = ?'; args.push(subject); }
|
||||
if (role !== 'admin') { where += ' AND t.created_by = ?'; args.push(uid); }
|
||||
|
||||
const rows = db.prepare(`
|
||||
SELECT t.id, t.title, t.subject_slug, t.description, t.created_at,
|
||||
u.name AS creator_name,
|
||||
COUNT(tq.question_id) AS question_count
|
||||
FROM tests t
|
||||
JOIN users u ON u.id = t.created_by
|
||||
LEFT JOIN test_questions tq ON tq.test_id = t.id
|
||||
WHERE ${where}
|
||||
GROUP BY t.id ORDER BY t.created_at DESC
|
||||
`).all(...args);
|
||||
res.json(rows);
|
||||
}
|
||||
|
||||
/* ── POST /api/tests ─────────────────────────────────────────────────────── */
|
||||
function create(req, res) {
|
||||
const { title, subject_slug, description, show_answers = 1, time_limit } = req.body;
|
||||
if (!title?.trim()) return res.status(400).json({ error: 'title required' });
|
||||
if (!subject_slug) return res.status(400).json({ error: 'subject_slug required' });
|
||||
const tl = time_limit ? Math.max(1, Math.min(600, Number(time_limit))) : null;
|
||||
const r = db.prepare(
|
||||
'INSERT INTO tests (title, subject_slug, description, show_answers, time_limit, created_by) VALUES (?, ?, ?, ?, ?, ?)'
|
||||
).run(title.trim(), subject_slug, description?.trim() || null, show_answers ? 1 : 0, tl, req.user.id);
|
||||
res.status(201).json({ id: r.lastInsertRowid });
|
||||
}
|
||||
|
||||
/* ── GET /api/tests/:id ──────────────────────────────────────────────────── */
|
||||
function getOne(req, res) {
|
||||
const t = db.prepare(`
|
||||
SELECT t.*, u.name AS creator_name
|
||||
FROM tests t JOIN users u ON u.id = t.created_by WHERE t.id = ?
|
||||
`).get(req.params.id);
|
||||
if (!t) return res.status(404).json({ error: 'Not found' });
|
||||
|
||||
const questions = db.prepare(`
|
||||
SELECT q.id, q.text, q.type, q.difficulty, q.explanation,
|
||||
tp.name AS topic, s.name AS subject_name,
|
||||
(SELECT json_group_array(json_object('id',o.id,'text',o.text,'is_correct',o.is_correct,'order_index',o.order_index,'match_pair',o.match_pair) ORDER BY o.order_index)
|
||||
FROM options o WHERE o.question_id = q.id) AS options_json,
|
||||
tq.order_index
|
||||
FROM test_questions tq
|
||||
JOIN questions q ON q.id = tq.question_id
|
||||
LEFT JOIN topics tp ON tp.id = q.topic_id
|
||||
LEFT JOIN subjects s ON s.id = q.subject_id
|
||||
WHERE tq.test_id = ? ORDER BY tq.order_index
|
||||
`).all(req.params.id).map(r => ({
|
||||
...r,
|
||||
options: JSON.parse(r.options_json || '[]'),
|
||||
options_json: undefined,
|
||||
}));
|
||||
|
||||
res.json({ ...t, questions });
|
||||
}
|
||||
|
||||
/* ── PUT /api/tests/:id ──────────────────────────────────────────────────── */
|
||||
function update(req, res) {
|
||||
const { title, subject_slug, description, show_answers, time_limit } = req.body;
|
||||
const t = req.resource; // ownership verified by requireOwnership middleware
|
||||
const tl = time_limit !== undefined ? (time_limit ? Math.max(1, Math.min(600, Number(time_limit))) : null) : undefined;
|
||||
db.prepare('UPDATE tests SET title = ?, subject_slug = ?, description = ?, show_answers = ?, time_limit = ? WHERE id = ?')
|
||||
.run(title?.trim(), subject_slug, description?.trim() || null, show_answers === undefined ? 1 : (show_answers ? 1 : 0),
|
||||
tl !== undefined ? tl : t.time_limit,
|
||||
t.id);
|
||||
res.json({ ok: true });
|
||||
}
|
||||
|
||||
/* ── DELETE /api/tests/:id ───────────────────────────────────────────────── */
|
||||
function remove(req, res) {
|
||||
db.prepare('DELETE FROM tests WHERE id = ?').run(req.resource.id);
|
||||
res.json({ ok: true });
|
||||
}
|
||||
|
||||
/* ── POST /api/tests/:id/questions ──────────────────────────────────────── */
|
||||
function addQuestions(req, res) {
|
||||
const { question_ids } = req.body;
|
||||
if (!Array.isArray(question_ids) || !question_ids.length)
|
||||
return res.status(400).json({ error: 'question_ids[] required' });
|
||||
|
||||
const testId = req.resource.id; // ownership verified by requireOwnership middleware
|
||||
|
||||
const { mx } = db.prepare(
|
||||
'SELECT COALESCE(MAX(order_index), -1) AS mx FROM test_questions WHERE test_id = ?'
|
||||
).get(testId);
|
||||
|
||||
let idx = mx + 1;
|
||||
const ins = db.prepare(
|
||||
'INSERT OR IGNORE INTO test_questions (test_id, question_id, order_index) VALUES (?, ?, ?)'
|
||||
);
|
||||
try {
|
||||
db.transaction(() => { question_ids.forEach(qid => ins.run(testId, qid, idx++)); })();
|
||||
} catch (e) {
|
||||
return res.status(500).json({ error: e.message });
|
||||
}
|
||||
res.json({ ok: true });
|
||||
}
|
||||
|
||||
/* ── DELETE /api/tests/:id/questions/:qid ───────────────────────────────── */
|
||||
function removeQuestion(req, res) {
|
||||
db.prepare('DELETE FROM test_questions WHERE test_id = ? AND question_id = ?')
|
||||
.run(req.resource.id, req.params.qid);
|
||||
res.json({ ok: true });
|
||||
}
|
||||
|
||||
/* ── PATCH /api/tests/:id/questions/reorder { ids: [qid, qid, ...] } ──── */
|
||||
function reorderQuestions(req, res) {
|
||||
const { ids } = req.body;
|
||||
if (!Array.isArray(ids) || !ids.length)
|
||||
return res.status(400).json({ error: 'ids[] required' });
|
||||
|
||||
const testId = req.resource.id;
|
||||
const upd = db.prepare(
|
||||
'UPDATE test_questions SET order_index = ? WHERE test_id = ? AND question_id = ?'
|
||||
);
|
||||
try {
|
||||
db.transaction(() => {
|
||||
ids.forEach((qid, i) => upd.run(i, testId, qid));
|
||||
})();
|
||||
} catch (e) {
|
||||
return res.status(500).json({ error: e.message });
|
||||
}
|
||||
res.json({ ok: true });
|
||||
}
|
||||
|
||||
module.exports = { list, create, getOne, update, remove, addQuestions, removeQuestion, reorderQuestions };
|
||||
Reference in New Issue
Block a user