diff --git a/backend/scripts/open_ctmath_for_class.js b/backend/scripts/open_ctmath_for_class.js new file mode 100644 index 0000000..2882a4b --- /dev/null +++ b/backend/scripts/open_ctmath_for_class.js @@ -0,0 +1,103 @@ +'use strict'; +/* ─────────────────────────────────────────────────────────────────────────── + open_ctmath_for_class.js — открыть ЦТ-математику профильному классу. + + Делает (идемпотентно): + 1. courses.is_published = 1 для курса id=13 «ЦЭ/ЦТ — Математика». + 2. content_access: открыть классу доступ к + • курсу (content_type='course', content_ref='13') + • экзамену (content_type='exam', content_ref='ctmath') + (scope='class', allow=1; upsert по UNIQUE(content_type,content_ref,scope,target_id)). + + Модель доступа — ALLOWLIST (services/contentAccess.js): по умолчанию закрыто, + правило ученика > класса, админ/учитель видят всё. Поэтому без этих правил + ученики класса курс/экзамен НЕ видят, даже если курс опубликован. + + Цель — класс #4 «10Б · Математика» (выбор пользователя). Сменить — флагом + --class=. Скрипт сверяет имя класса и печатает его перед записью. + + Запуск: + node backend/scripts/open_ctmath_for_class.js # DRY-RUN + node backend/scripts/open_ctmath_for_class.js --apply # запись + node backend/scripts/open_ctmath_for_class.js --class=4 --apply + + ⚠️ Outward-facing: после --apply и рестарта сервера ученики класса увидят + курс и пробники. Массовую запись запускает ПОЛЬЗОВАТЕЛЬ вручную. + ─────────────────────────────────────────────────────────────────────────── */ + +const { DatabaseSync } = require('node:sqlite'); +const path = require('path'); + +const APPLY = process.argv.includes('--apply'); +const COURSE_ID = 13; +const EXAM_KEY = 'ctmath'; +const classArg = (process.argv.find(a => a.startsWith('--class=')) || '').split('=')[1]; +const CLASS_ID = Number.isInteger(+classArg) && +classArg > 0 ? +classArg : 4; + +const db = new DatabaseSync(path.join(__dirname, '..', 'data', 'learnspace.db')); +const get = (sql, ...a) => db.prepare(sql).get(...a); + +console.log(`\n=== open_ctmath_for_class (${APPLY ? 'APPLY' : 'DRY-RUN'}) ===\n`); + +/* ── Защитные проверки ─────────────────────────────────────────────────────── */ +const course = get('SELECT id, title, is_published, created_by FROM courses WHERE id=?', COURSE_ID); +if (!course) { console.error(`✗ Курс id=${COURSE_ID} не найден. Прерывание.`); db.close(); process.exit(1); } + +const klass = get('SELECT id, name FROM classes WHERE id=?', CLASS_ID); +if (!klass) { console.error(`✗ Класс id=${CLASS_ID} не найден. Прерывание.`); db.close(); process.exit(1); } + +const track = get('SELECT exam_key, enabled FROM exam_tracks WHERE exam_key=?', EXAM_KEY); +if (!track) { console.error(`✗ Трек '${EXAM_KEY}' не найден в exam_tracks. Прерывание.`); db.close(); process.exit(1); } + +const members = get('SELECT COUNT(*) n FROM class_members WHERE class_id=?', CLASS_ID).n; +console.log(`Курс: id=${course.id} «${course.title}» (is_published=${course.is_published})`); +console.log(`Класс: id=${klass.id} «${klass.name}» (учеников: ${members})`); +console.log(`Экзамен: ${track.exam_key} (enabled=${track.enabled})\n`); + +/* ── План действий ─────────────────────────────────────────────────────────── */ +const actions = []; + +if (course.is_published !== 1) { + actions.push({ desc: `опубликовать курс id=${COURSE_ID} (is_published 0 → 1)`, + run: () => db.prepare('UPDATE courses SET is_published=1 WHERE id=?').run(COURSE_ID) }); +} else { + console.log('• курс уже опубликован — пропуск'); +} + +const accessRow = db.prepare(`SELECT allow FROM content_access + WHERE content_type=? AND content_ref=? AND scope='class' AND target_id=?`); +const upsertAccess = db.prepare(` + INSERT INTO content_access (content_type, content_ref, scope, target_id, allow, created_by) + VALUES (?, ?, 'class', ?, 1, ?) + ON CONFLICT (content_type, content_ref, scope, target_id) + DO UPDATE SET allow=1, created_by=excluded.created_by, created_at=datetime('now')`); + +for (const [type, ref] of [['course', String(COURSE_ID)], ['exam', EXAM_KEY]]) { + const cur = accessRow.get(type, ref, CLASS_ID); + if (cur && cur.allow === 1) { console.log(`• доступ ${type}:${ref} классу #${CLASS_ID} уже открыт — пропуск`); continue; } + actions.push({ desc: `открыть доступ ${type}:${ref} классу #${CLASS_ID} (allow=1)`, + run: () => upsertAccess.run(type, ref, CLASS_ID, course.created_by || null) }); +} + +console.log(`\nК применению (${actions.length}):`); +actions.forEach(a => console.log(' - ' + a.desc)); + +if (!actions.length) { console.log('\nВсё уже в нужном состоянии — менять нечего.\n'); db.close(); process.exit(0); } + +if (!APPLY) { + console.log('\nDRY-RUN: ничего не записано. Для записи: node backend/scripts/open_ctmath_for_class.js --apply\n'); + db.close(); process.exit(0); +} + +db.exec('BEGIN'); +try { + for (const a of actions) a.run(); + db.exec('COMMIT'); + console.log(`\n✓ Применено: ${actions.length}. Курс и пробники ЦТ открыты классу «${klass.name}».`); + console.log(' (после рестарта сервера ученики класса увидят их в каталоге / на дашборде)\n'); +} catch (e) { + db.exec('ROLLBACK'); + console.error('\n✗ Ошибка, откат:', e.message); + process.exitCode = 1; +} +db.close();