9509a67e25
Система «готовится к ЦТ»: флаг 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>
114 lines
5.7 KiB
JavaScript
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,
|
|
};
|