feat(ctmath): скрипт открытия ЦТ-математики классу (publish курса 13 + доступ)

Идемпотентно: courses.is_published=1 (курс 13) + content_access classу #4
«10Б · Математика» на курс (course:13) и экзамен-модуль (exam:ctmath).
Модель — allowlist (без правил ученики не видят даже опубликованный курс).
Цель класса флагом --class=<id> (деф. 4), сверка имени. DRY-RUN по умолчанию,
запись с --apply (outward-facing, запускает пользователь).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Maxim Dolgolyov
2026-06-19 10:06:51 +03:00
parent 17c1c92490
commit fd656ed63f
+103
View File
@@ -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=<id>. Скрипт сверяет имя класса и печатает его перед записью.
Запуск:
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();