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); } // Экзаменационные варианты — это служебные строки в tests (см. import-exam9.js), // не показываем их во вкладке «Тесты (шаблоны)» админки. where += ' AND t.id NOT IN (SELECT test_id FROM exam9_variant_tests)'; 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, })); // Hide is_correct from students — only teachers/admins see correct answers const isPrivileged = req.user.role === 'teacher' || req.user.role === 'admin'; if (!isPrivileged) { questions.forEach(q => { q.options.forEach(o => { delete o.is_correct; }); delete q.explanation; }); } 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) { console.error('[testController] addQuestions error:', e.message); return res.status(500).json({ error: 'Ошибка добавления вопросов' }); } 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) { console.error('[testController] reorderQuestions error:', e.message); return res.status(500).json({ error: 'Ошибка сортировки' }); } res.json({ ok: true }); } module.exports = { list, create, getOne, update, remove, addQuestions, removeQuestion, reorderQuestions };