Files
Learn_System/backend/src/controllers/prepController.js
T
Maxim Dolgolyov 9509a67e25 feat(prep): мастер-флаг подготовки к направлению (ЦТ) + коллекции колод — бэкенд
Система «готовится к ЦТ»: флаг student_prep(user_id,track) открывает ученику
ВЕСЬ контент трека (карточки + курс + пробники) динамически, без материализации.
- мигр.078: таблица student_prep + flashcard_decks.collection + разметка ЦТ-колод 'ct-math'
- services/prepTracks.js: реестр треков (трек→коллекция/курсы/экзамены), устойчив до миграции
- contentAccess.resolve/allowedRefs: учитывают мастер-флаг (явный запрет ученика побеждает)
- flashcardController.deckAccess/listDecks: колоды коллекции открыты по флагу
- prepController + /api/prep: учитель (своим) и админ ставят/снимают флаг (ученику/классу)
- js/api.js: LS.prep* обёртки

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 15:29:00 +03:00

114 lines
5.7 KiB
JavaScript

'use strict';
/* prepController — управление флагом «подготовки к направлению» (мастер-флаг).
*
* Флаг student_prep(user_id, track) открывает ученику весь контент трека
* (карточки + курс + пробники) — резолв в services/prepTracks.js + contentAccess.js.
*
* Управление: учитель — СВОИМ ученикам (из своих классов или персональным),
* админ — любым. Read own-статус доступен самому ученику (GET /me).
*/
const db = require('../db/db');
const prepTracks = require('../services/prepTracks');
/* Ученик «свой» для учителя: член одного из его классов ИЛИ персональный ученик. */
function canManageStudent(user, studentId) {
if (user.role === 'admin') return true;
if (user.role !== 'teacher') return false;
return !!db.prepare(`
SELECT 1 FROM class_members cm JOIN classes c ON c.id = cm.class_id
WHERE cm.user_id = ? AND c.teacher_id = ?
UNION
SELECT 1 FROM teacher_students WHERE student_id = ? AND teacher_id = ?
LIMIT 1
`).get(studentId, user.id, studentId, user.id);
}
/* Класс «свой» для учителя (admin — любой). */
function canManageClass(user, classId) {
if (user.role === 'admin') return true;
if (user.role !== 'teacher') return false;
return !!db.prepare(`SELECT 1 FROM classes WHERE id = ? AND teacher_id = ?`).get(classId, user.id);
}
/* ── GET /api/prep/tracks — список доступных направлений (для UI-меток) ── */
function listTracks(req, res) {
res.json({ tracks: prepTracks.listTracks() });
}
/* ── GET /api/prep/me — мои треки (ученик видит свой статус) ── */
function myTracks(req, res) {
const rows = db.prepare(`SELECT track FROM student_prep WHERE user_id = ?`).all(req.user.id);
res.json({ tracks: rows.map(r => r.track).filter(prepTracks.isTrack) });
}
/* ── GET /api/prep/student/:id — треки ученика (учитель своего / админ) ── */
function studentTracks(req, res) {
const studentId = Number(req.params.id) || 0;
if (!canManageStudent(req.user, studentId)) return res.status(403).json({ error: 'Forbidden' });
const rows = db.prepare(`SELECT track FROM student_prep WHERE user_id = ?`).all(studentId);
res.json({ tracks: rows.map(r => r.track).filter(prepTracks.isTrack) });
}
/* ── POST /api/prep/student/:id { track } — включить флаг ── */
function setStudent(req, res) {
const studentId = Number(req.params.id) || 0;
const track = String(req.body.track || '');
if (!prepTracks.isTrack(track)) return res.status(400).json({ error: 'Неизвестный трек' });
if (!canManageStudent(req.user, studentId)) return res.status(403).json({ error: 'Forbidden' });
// Ставим флаг только реально существующему ученику.
const stu = db.prepare(`SELECT id, role FROM users WHERE id = ?`).get(studentId);
if (!stu) return res.status(404).json({ error: 'Ученик не найден' });
db.prepare(`INSERT OR IGNORE INTO student_prep (user_id, track, created_by) VALUES (?,?,?)`)
.run(studentId, track, req.user.id);
res.json({ ok: true });
}
/* ── DELETE /api/prep/student/:id?track= — снять флаг ── */
function unsetStudent(req, res) {
const studentId = Number(req.params.id) || 0;
const track = String(req.query.track || '');
if (!prepTracks.isTrack(track)) return res.status(400).json({ error: 'Неизвестный трек' });
if (!canManageStudent(req.user, studentId)) return res.status(403).json({ error: 'Forbidden' });
db.prepare(`DELETE FROM student_prep WHERE user_id = ? AND track = ?`).run(studentId, track);
res.json({ ok: true });
}
/* ── GET /api/prep/class/:id?track= — статус флага у членов класса ── */
function classStatus(req, res) {
const classId = Number(req.params.id) || 0;
const track = String(req.query.track || '');
if (!prepTracks.isTrack(track)) return res.status(400).json({ error: 'Неизвестный трек' });
if (!canManageClass(req.user, classId)) return res.status(403).json({ error: 'Forbidden' });
const rows = db.prepare(`
SELECT u.id, u.name,
CASE WHEN sp.id IS NULL THEN 0 ELSE 1 END AS prep
FROM class_members cm
JOIN users u ON u.id = cm.user_id
LEFT JOIN student_prep sp ON sp.user_id = u.id AND sp.track = ?
WHERE cm.class_id = ?
ORDER BY u.name
`).all(track, classId);
res.json({ students: rows });
}
/* ── POST /api/prep/class/:id { track, on } — массово по классу ── */
function setClass(req, res) {
const classId = Number(req.params.id) || 0;
const track = String(req.body.track || '');
const on = !!req.body.on;
if (!prepTracks.isTrack(track)) return res.status(400).json({ error: 'Неизвестный трек' });
if (!canManageClass(req.user, classId)) return res.status(403).json({ error: 'Forbidden' });
const members = db.prepare(`SELECT user_id FROM class_members WHERE class_id = ?`).all(classId);
const ins = db.prepare(`INSERT OR IGNORE INTO student_prep (user_id, track, created_by) VALUES (?,?,?)`);
const del = db.prepare(`DELETE FROM student_prep WHERE user_id = ? AND track = ?`);
const run = db.transaction(() => {
for (const m of members) on ? ins.run(m.user_id, track, req.user.id) : del.run(m.user_id, track);
});
run();
res.json({ ok: true, count: members.length });
}
module.exports = {
listTracks, myTracks, studentTracks, setStudent, unsetStudent, classStatus, setClass,
};