fix(ctmath): чистка банка — год-пачки убраны из пикера пробников
- exam-prep.js: MOCK_VARIANT_RANGE — для ctmath показываем как пробники только чистые 30-задачные варианты [101;1999]; год-пачки (variant=год 2011-2024 и 0, до 114 задач) остаются пулом для тренажёра по темам, но скрыты из пикера/mock-start/просмотра вариантов. math9 (1..80) не затронут (диапазон только для ctmath). - mock.js: пикер «По варианту» — выпадающий список реальных вариантов (через listVariants) вместо number-input 1..N; раньше для ctmath он предлагал 1..18 и не доходил до 101 → пробник по варианту не запускался. - cleanup_ctmath_bank.js: идемпотентный скрипт — ретайр битого id=1419 (mc с противоречивым ответом → long), variants_count → 3 (чистых вариантов). - seed_*: variants_count считается по диапазону [101;1999] (консистентно с роутом). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,88 @@
|
|||||||
|
'use strict';
|
||||||
|
/* ───────────────────────────────────────────────────────────────────────────
|
||||||
|
cleanup_ctmath_bank.js — точечная чистка банка exam-prep ctmath.
|
||||||
|
|
||||||
|
Что делает (идемпотентно):
|
||||||
|
1. id=1248 (вычисление 5^lg2·2^lg5): дефектная задача (варианты «а» и «д»
|
||||||
|
одинаковы, верного ответа нет) — уже переведена в 'long'; чистим
|
||||||
|
литеральное answer="null" → NULL.
|
||||||
|
2. id=1419 (var 2024, «укажите номера пар»): битый mc — сохранённый ответ «а»
|
||||||
|
(«3 и 4») противоречит решению («4 и 5»), причём «4 и 5» вообще нет среди
|
||||||
|
вариантов; единственная подходящая пара — №4, ни один mc-вариант не верен.
|
||||||
|
Ретайрим в 'long' (self-check): убирается из авто-проверки тренажёра/пробника
|
||||||
|
(там берутся только mc/open), но текст и разбор сохраняются.
|
||||||
|
3. variants_count трека ctmath → число «чистых» вариантов-пробников (variant≥101),
|
||||||
|
чтобы шапка («N вариантов») соответствовала пикеру (год-пачки скрыты роутом).
|
||||||
|
|
||||||
|
Год-пачки (variant=год) НЕ удаляются — они остаются пулом задач для тренажёра
|
||||||
|
по темам (он отбирает по subtopic). «Указательные» opts (["1","1"]…) НЕ трогаем —
|
||||||
|
они рабочие (ученик выбирает номер).
|
||||||
|
|
||||||
|
Запуск: node backend/scripts/cleanup_ctmath_bank.js [--apply]
|
||||||
|
─────────────────────────────────────────────────────────────────────────── */
|
||||||
|
|
||||||
|
const { DatabaseSync } = require('node:sqlite');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
const APPLY = process.argv.includes('--apply');
|
||||||
|
const EXAM = 'ctmath';
|
||||||
|
// Чистые варианты-пробники: 3-значные [101;1999]; год-пачки — 4-значные годы
|
||||||
|
// (≥2011) и 0 — исключены. Совпадает с MOCK_VARIANT_RANGE.ctmath в routes/exam-prep.js.
|
||||||
|
const MOCK_LO = 101, MOCK_HI = 1999;
|
||||||
|
|
||||||
|
const db = new DatabaseSync(path.join(__dirname, '..', 'data', 'learnspace.db'));
|
||||||
|
const get = (sql, ...a) => db.prepare(sql).get(...a);
|
||||||
|
|
||||||
|
console.log(`\n=== cleanup_ctmath_bank (${APPLY ? 'APPLY' : 'DRY-RUN'}) ===\n`);
|
||||||
|
|
||||||
|
const actions = [];
|
||||||
|
|
||||||
|
// 1. id=1248 answer="null" → NULL
|
||||||
|
const t1248 = get(`SELECT id, task_type, answer FROM exam_tasks WHERE id=1248 AND exam_key=?`, EXAM);
|
||||||
|
if (t1248 && t1248.answer === 'null') {
|
||||||
|
actions.push({ desc: `id=1248: answer "null" → NULL (тип ${t1248.task_type})`,
|
||||||
|
run: () => db.prepare(`UPDATE exam_tasks SET answer=NULL WHERE id=1248`).run() });
|
||||||
|
} else {
|
||||||
|
console.log(`• id=1248: пропуск (answer=${t1248 ? JSON.stringify(t1248.answer) : 'нет строки'})`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. id=1419 битый mc → long, answer/opts NULL
|
||||||
|
const t1419 = get(`SELECT id, task_type FROM exam_tasks WHERE id=1419 AND exam_key=?`, EXAM);
|
||||||
|
if (t1419 && t1419.task_type === 'mc') {
|
||||||
|
actions.push({ desc: `id=1419: битый mc → 'long' (answer/opts → NULL, текст и разбор сохраняются)`,
|
||||||
|
run: () => db.prepare(`UPDATE exam_tasks SET task_type='long', answer=NULL, opts_json=NULL WHERE id=1419`).run() });
|
||||||
|
} else {
|
||||||
|
console.log(`• id=1419: пропуск (тип ${t1419 ? t1419.task_type : 'нет строки'})`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. variants_count = число чистых вариантов (≥101)
|
||||||
|
const cleanCnt = get(`SELECT COUNT(DISTINCT variant) c FROM exam_tasks WHERE exam_key=? AND variant BETWEEN ? AND ?`, EXAM, MOCK_LO, MOCK_HI).c;
|
||||||
|
const curCnt = get(`SELECT variants_count vc FROM exam_tracks WHERE exam_key=?`, EXAM).vc;
|
||||||
|
if (curCnt !== cleanCnt) {
|
||||||
|
actions.push({ desc: `exam_tracks.variants_count: ${curCnt} → ${cleanCnt} (чистых вариантов [${MOCK_LO};${MOCK_HI}])`,
|
||||||
|
run: () => db.prepare(`UPDATE exam_tracks SET variants_count=? WHERE exam_key=?`).run(cleanCnt, EXAM) });
|
||||||
|
} else {
|
||||||
|
console.log(`• variants_count: пропуск (уже ${curCnt})`);
|
||||||
|
}
|
||||||
|
|
||||||
|
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/cleanup_ctmath_bank.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}.\n`);
|
||||||
|
} catch (e) {
|
||||||
|
db.exec('ROLLBACK');
|
||||||
|
console.error('\n✗ Ошибка, откат:', e.message);
|
||||||
|
process.exitCode = 1;
|
||||||
|
}
|
||||||
|
db.close();
|
||||||
@@ -369,8 +369,8 @@ try {
|
|||||||
);
|
);
|
||||||
n++;
|
n++;
|
||||||
}
|
}
|
||||||
// обновить variants_count = реальное число различных вариантов
|
// variants_count = число «чистых» вариантов-пробников [101;1999]; год-пачки (годы≥2011, 0) скрыты роутом
|
||||||
const distinct = db.prepare(`SELECT COUNT(DISTINCT variant) c FROM exam_tasks WHERE exam_key=?`).get(EXAM).c;
|
const distinct = db.prepare(`SELECT COUNT(DISTINCT variant) c FROM exam_tasks WHERE exam_key=? AND variant BETWEEN 101 AND 1999`).get(EXAM).c;
|
||||||
db.prepare(`UPDATE exam_tracks SET variants_count=? WHERE exam_key=?`).run(distinct, EXAM);
|
db.prepare(`UPDATE exam_tracks SET variants_count=? WHERE exam_key=?`).run(distinct, EXAM);
|
||||||
db.exec('COMMIT');
|
db.exec('COMMIT');
|
||||||
console.log(`\n✓ Записано/обновлено ${n} заданий (variant=${VARIANT}).`);
|
console.log(`\n✓ Записано/обновлено ${n} заданий (variant=${VARIANT}).`);
|
||||||
|
|||||||
@@ -289,7 +289,7 @@ const upsert = db.prepare(`
|
|||||||
let n = 0; db.exec('BEGIN');
|
let n = 0; db.exec('BEGIN');
|
||||||
try {
|
try {
|
||||||
for (const t of TASKS) { upsert.run(EXAM, VARIANT, t.idx, t.type, t.text, t.fig || null, t.type === 'mc' ? JSON.stringify(t.opts) : null, t.answer, buildSolution(t), t.topic, t.subtopic, t.diff); n++; }
|
for (const t of TASKS) { upsert.run(EXAM, VARIANT, t.idx, t.type, t.text, t.fig || null, t.type === 'mc' ? JSON.stringify(t.opts) : null, t.answer, buildSolution(t), t.topic, t.subtopic, t.diff); n++; }
|
||||||
const distinct = db.prepare(`SELECT COUNT(DISTINCT variant) c FROM exam_tasks WHERE exam_key=?`).get(EXAM).c;
|
const distinct = db.prepare(`SELECT COUNT(DISTINCT variant) c FROM exam_tasks WHERE exam_key=? AND variant BETWEEN 101 AND 1999`).get(EXAM).c;
|
||||||
db.prepare(`UPDATE exam_tracks SET variants_count=? WHERE exam_key=?`).run(distinct, EXAM);
|
db.prepare(`UPDATE exam_tracks SET variants_count=? WHERE exam_key=?`).run(distinct, EXAM);
|
||||||
db.exec('COMMIT');
|
db.exec('COMMIT');
|
||||||
console.log(`\n✓ Записано/обновлено ${n} заданий (variant=${VARIANT}). variants_count=${distinct}.`);
|
console.log(`\n✓ Записано/обновлено ${n} заданий (variant=${VARIANT}). variants_count=${distinct}.`);
|
||||||
|
|||||||
@@ -285,7 +285,7 @@ const upsert = db.prepare(`
|
|||||||
let n = 0; db.exec('BEGIN');
|
let n = 0; db.exec('BEGIN');
|
||||||
try {
|
try {
|
||||||
for (const t of TASKS) { upsert.run(EXAM, VARIANT, t.idx, t.type, t.text, t.fig || null, t.type === 'mc' ? JSON.stringify(t.opts) : null, t.answer, buildSolution(t), t.topic, t.subtopic, t.diff); n++; }
|
for (const t of TASKS) { upsert.run(EXAM, VARIANT, t.idx, t.type, t.text, t.fig || null, t.type === 'mc' ? JSON.stringify(t.opts) : null, t.answer, buildSolution(t), t.topic, t.subtopic, t.diff); n++; }
|
||||||
const distinct = db.prepare(`SELECT COUNT(DISTINCT variant) c FROM exam_tasks WHERE exam_key=?`).get(EXAM).c;
|
const distinct = db.prepare(`SELECT COUNT(DISTINCT variant) c FROM exam_tasks WHERE exam_key=? AND variant BETWEEN 101 AND 1999`).get(EXAM).c;
|
||||||
db.prepare(`UPDATE exam_tracks SET variants_count=? WHERE exam_key=?`).run(distinct, EXAM);
|
db.prepare(`UPDATE exam_tracks SET variants_count=? WHERE exam_key=?`).run(distinct, EXAM);
|
||||||
db.exec('COMMIT');
|
db.exec('COMMIT');
|
||||||
console.log(`\n✓ Записано/обновлено ${n} заданий (variant=${VARIANT}). variants_count=${distinct}.`);
|
console.log(`\n✓ Записано/обновлено ${n} заданий (variant=${VARIANT}). variants_count=${distinct}.`);
|
||||||
|
|||||||
@@ -15,6 +15,21 @@ router.param('examKey', (req, res, next, examKey) => {
|
|||||||
next();
|
next();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/* ── Mock/variant picker: какие variant считаются «пробниками» ──────
|
||||||
|
ctmath: год-пачки (variant=год 2011–2024 и 0) — это тематический ПУЛ для
|
||||||
|
тренажёра по темам, а НЕ чистые 30-задачные варианты (у части до 114 задач).
|
||||||
|
Чистые варианты-пробники нумеруются 3-значно (101, 102, …), а год-пачки —
|
||||||
|
4-значными годами (≥2011) и 0, поэтому фильтр — ДИАПАЗОН [101;1999], а не
|
||||||
|
просто порог (год 2024 > 101 и иначе бы прошёл!). В пикере пробников,
|
||||||
|
mock/start и просмотре вариантов показываем только чистые. Тренажёр по темам
|
||||||
|
отбирает по subtopic и этот фильтр НЕ использует — пул задач не теряется.
|
||||||
|
Для остальных треков (math9: варианты 1..80) диапазона нет — показываются все. */
|
||||||
|
const MOCK_VARIANT_RANGE = { ctmath: [101, 1999] };
|
||||||
|
const isMockVariant = (examKey, v) => {
|
||||||
|
const r = MOCK_VARIANT_RANGE[examKey];
|
||||||
|
return r ? (v >= r[0] && v <= r[1]) : (v >= 1);
|
||||||
|
};
|
||||||
|
|
||||||
/* ── Statements (prepared once) ────────────────────────────────── */
|
/* ── Statements (prepared once) ────────────────────────────────── */
|
||||||
const SQL = {
|
const SQL = {
|
||||||
listTracks: db.prepare(`
|
listTracks: db.prepare(`
|
||||||
@@ -478,7 +493,7 @@ router.get('/:examKey/info', (req, res) => {
|
|||||||
router.get('/:examKey/variants', (req, res) => {
|
router.get('/:examKey/variants', (req, res) => {
|
||||||
const { examKey } = req.params;
|
const { examKey } = req.params;
|
||||||
if (!SQL.getTrack.get(examKey)) return res.status(404).json({ error: 'Unknown exam track' });
|
if (!SQL.getTrack.get(examKey)) return res.status(404).json({ error: 'Unknown exam track' });
|
||||||
const rows = SQL.listVariants.all(req.user.id, examKey);
|
const rows = SQL.listVariants.all(req.user.id, examKey).filter(r => isMockVariant(examKey, r.variant));
|
||||||
const variants = rows.map(r => ({
|
const variants = rows.map(r => ({
|
||||||
n: r.variant,
|
n: r.variant,
|
||||||
label: `Вариант ${r.variant}`,
|
label: `Вариант ${r.variant}`,
|
||||||
@@ -498,6 +513,7 @@ router.get('/:examKey/variants/:n/tasks', (req, res) => {
|
|||||||
const { examKey } = req.params;
|
const { examKey } = req.params;
|
||||||
const n = parseInt(req.params.n, 10);
|
const n = parseInt(req.params.n, 10);
|
||||||
if (!Number.isFinite(n) || n < 1) return res.status(400).json({ error: 'Bad variant number' });
|
if (!Number.isFinite(n) || n < 1) return res.status(400).json({ error: 'Bad variant number' });
|
||||||
|
if (!isMockVariant(examKey, n)) return res.status(404).json({ error: 'Variant not found or empty' });
|
||||||
|
|
||||||
const rows = SQL.getVariantTasks.all(examKey, n);
|
const rows = SQL.getVariantTasks.all(examKey, n);
|
||||||
if (!rows.length) return res.status(404).json({ error: 'Variant not found or empty' });
|
if (!rows.length) return res.status(404).json({ error: 'Variant not found or empty' });
|
||||||
@@ -1180,7 +1196,7 @@ router.post('/:examKey/mock/start', (req, res) => {
|
|||||||
|
|
||||||
if (source === 'variant') {
|
if (source === 'variant') {
|
||||||
variant = Number(req.body?.variant);
|
variant = Number(req.body?.variant);
|
||||||
if (!Number.isInteger(variant) || variant < 1) {
|
if (!Number.isInteger(variant) || !isMockVariant(examKey, variant)) {
|
||||||
return res.status(400).json({ error: 'Variant number required' });
|
return res.status(400).json({ error: 'Variant number required' });
|
||||||
}
|
}
|
||||||
const rows = SQL.getTasksByVariant.all(examKey, variant);
|
const rows = SQL.getTasksByVariant.all(examKey, variant);
|
||||||
|
|||||||
@@ -44,11 +44,16 @@
|
|||||||
/* ════════════════════════════════════════════════════════════
|
/* ════════════════════════════════════════════════════════════
|
||||||
PHASE 1: SETUP
|
PHASE 1: SETUP
|
||||||
════════════════════════════════════════════════════════════ */
|
════════════════════════════════════════════════════════════ */
|
||||||
function renderSetup() {
|
async function renderSetup() {
|
||||||
const title = EP.info?.track?.title || 'Пробный экзамен';
|
const title = EP.info?.track?.title || 'Пробный экзамен';
|
||||||
const dur = EP.info?.track?.duration_min || 180;
|
const dur = EP.info?.track?.duration_min || 180;
|
||||||
const tpv = EP.info?.track?.tasks_per_variant || 10;
|
const tpv = EP.info?.track?.tasks_per_variant || 10;
|
||||||
const vc = EP.info?.track?.variants_count || 80;
|
// Реальный список вариантов-пробников (бэкенд уже отфильтровал год-пачки):
|
||||||
|
// номера вариантов могут быть не подряд (ctmath: 101, 102, …), поэтому
|
||||||
|
// показываем выпадающий список реальных вариантов, а не диапазон 1..N.
|
||||||
|
let vlist = [];
|
||||||
|
try { vlist = (await EP.api.listVariants(examKey)).variants || []; } catch {}
|
||||||
|
const vOpts = vlist.map(v => `<option value="${v.n}">${v.label}</option>`).join('');
|
||||||
|
|
||||||
main.innerHTML = `
|
main.innerHTML = `
|
||||||
<div class="ep-card mk-setup">
|
<div class="ep-card mk-setup">
|
||||||
@@ -65,10 +70,10 @@
|
|||||||
<span>По варианту</span>
|
<span>По варианту</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="mk-source-body">
|
<div class="mk-source-body">
|
||||||
<label>Номер варианта:
|
<label>Вариант:
|
||||||
<input type="number" min="1" max="${vc}" value="1" id="mk-variant-input" class="mk-input" />
|
<select id="mk-variant-input" class="mk-input">${vOpts || '<option value="">—</option>'}</select>
|
||||||
</label>
|
</label>
|
||||||
<div class="mk-source-hint">Один из ${vc} реальных вариантов целиком.</div>
|
<div class="mk-source-hint">Один из ${vlist.length} готовых вариантов целиком.</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -116,7 +121,7 @@
|
|||||||
if (!Number.isInteger(v) || v < 1) {
|
if (!Number.isInteger(v) || v < 1) {
|
||||||
btn.disabled = false; btn.innerHTML = '<i data-lucide="play"></i> Начать пробник';
|
btn.disabled = false; btn.innerHTML = '<i data-lucide="play"></i> Начать пробник';
|
||||||
if (window.lucide) lucide.createIcons();
|
if (window.lucide) lucide.createIcons();
|
||||||
return alert('Введите номер варианта');
|
return alert('Выберите вариант');
|
||||||
}
|
}
|
||||||
body.variant = v;
|
body.variant = v;
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
Reference in New Issue
Block a user