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>
79 lines
3.4 KiB
JavaScript
79 lines
3.4 KiB
JavaScript
'use strict';
|
|
/* prepTracks — направления подготовки (ЦТ и др.) и их контент («мастер-флаг»).
|
|
*
|
|
* Флаг student_prep(user_id, track) открывает ученику ВЕСЬ контент трека:
|
|
* - коллекцию флешкарт (flashcard_decks.collection)
|
|
* - курсы (content_type='course', content_ref=id курса)
|
|
* - экзамен-модули (content_type='exam', content_ref=exam_key)
|
|
* Доступ резолвится ДИНАМИЧЕСКИ (без материализации правил content_access):
|
|
* - contentAccess.resolve/allowedRefs читают trackGrants/trackRefs;
|
|
* - flashcardController.deckAccess/listDecks читают studentCollections.
|
|
*
|
|
* Добавить направление = добавить запись в TRACKS (+ при необходимости миграцию,
|
|
* проставляющую collection нужным колодам). Никаких изменений в резолверах. */
|
|
const db = require('../db/db');
|
|
|
|
/* Реестр треков. collection — ключ коллекции колод (flashcard_decks.collection);
|
|
content — что открывает трек, по типам content_access. */
|
|
const TRACKS = {
|
|
'ct-math': {
|
|
title: 'ЦТ/ЦЭ — математика',
|
|
label: 'Подготовка к ЦТ',
|
|
collection: 'ct-math',
|
|
content: { course: ['13'], exam: ['ctmath'] },
|
|
},
|
|
};
|
|
|
|
function isTrack(track) { return Object.prototype.hasOwnProperty.call(TRACKS, track); }
|
|
|
|
/* Множество известных треков, включённых у пользователя.
|
|
Inline-prepare + try/catch: до миграции 078 (нет таблицы student_prep) фича
|
|
просто не активна (пустой набор), а не падает на загрузке модуля/в тестах. */
|
|
function studentTracks(userId) {
|
|
try {
|
|
const rows = db.prepare('SELECT track FROM student_prep WHERE user_id = ?').all(userId);
|
|
return new Set(rows.map(r => r.track).filter(isTrack));
|
|
} catch (_) {
|
|
return new Set();
|
|
}
|
|
}
|
|
|
|
/* Множество collection-ключей, открытых пользователю его треками. */
|
|
function studentCollections(userId) {
|
|
const out = new Set();
|
|
for (const t of studentTracks(userId)) {
|
|
if (TRACKS[t].collection) out.add(TRACKS[t].collection);
|
|
}
|
|
return out;
|
|
}
|
|
|
|
/* Открывает ли какой-либо трек пользователя данный (type, ref)? */
|
|
function trackGrants(userId, type, ref) {
|
|
const r = String(ref);
|
|
for (const t of studentTracks(userId)) {
|
|
const refs = TRACKS[t].content && TRACKS[t].content[type];
|
|
if (refs && refs.includes(r)) return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/* content_ref'ы данного типа, открытые треками пользователя (для allowedRefs). */
|
|
function trackRefs(userId, type) {
|
|
const out = new Set();
|
|
for (const t of studentTracks(userId)) {
|
|
const refs = TRACKS[t].content && TRACKS[t].content[type];
|
|
if (refs) refs.forEach(x => out.add(String(x)));
|
|
}
|
|
return out;
|
|
}
|
|
|
|
/* Список треков для UI: [{ key, title, label }]. */
|
|
function listTracks() {
|
|
return Object.entries(TRACKS).map(([key, t]) => ({ key, title: t.title, label: t.label }));
|
|
}
|
|
|
|
module.exports = {
|
|
TRACKS, isTrack, studentTracks, studentCollections,
|
|
trackGrants, trackRefs, listTracks,
|
|
};
|