feat: exam9 — назначение варианта как ДЗ + импорт нечётных в банк

Импорт 40 нечётных вариантов (v01, v03, ..., v79) в банк вопросов:
- 400 questions с allow_html=1, source_type='экзамен 9', year=2025
- 540 options (single-choice) + correct_text (short_answer)
- 40 tests (по 1 на вариант), title="Экзамен 9 — Вариант N"
- exam9_variant_tests маппинг для назначения

Назначение варианта как ДЗ на /exam9 (для учителей/админов):
- Кнопка «Назначить как ДЗ» под заголовком варианта (только если test_id есть)
- Модалка выбора классов + опциональный deadline
- POST /api/assignments/bulk с test_id из exam9_variant_tests

Поддержка HTML/SVG в вопросах банка через флаг questions.allow_html:
- Миграция 003: ALTER TABLE questions ADD COLUMN allow_html
- sessionController: SELECT возвращают allow_html и image
- test-run.html: рендер q.text и opt.text как HTML при allow_html=1
- test-result.html: то же для explanation и opt.text
- KaTeX: добавлены $...$ и $$...$$ delimiters в обеих страницах

Бонус-фикс: bulkSchema требовал class_id (single), контроллер ждёт
class_ids (array). Существующий вызов из classes.html был сломан;
исправлено вместе.

Команда: node backend/scripts/import-exam9.js  (--all для всех 80)
This commit is contained in:
Maxim Dolgolyov
2026-05-16 13:13:06 +03:00
parent 6cff327e88
commit 31a51956b6
10 changed files with 461 additions and 13 deletions
+2 -2
View File
@@ -417,7 +417,7 @@ function loadQuestionsForSession(ids) {
const ph = _placeholders(ids.length);
const questions = db.prepare(
`SELECT id, text, type, difficulty FROM questions WHERE id IN (${ph})`
`SELECT id, text, type, difficulty, allow_html, image FROM questions WHERE id IN (${ph})`
).all(...ids);
const allOptions = db.prepare(
@@ -449,7 +449,7 @@ function buildReview(session_id) {
const ph = _placeholders(ids.length);
const questions = db.prepare(
`SELECT id, text, type, explanation, correct_text FROM questions WHERE id IN (${ph})`
`SELECT id, text, type, explanation, correct_text, allow_html, image FROM questions WHERE id IN (${ph})`
).all(...ids);
const answers = db.prepare(
@@ -0,0 +1,8 @@
-- Allow HTML in question text and explanation (for curated content with formulas, SVG, etc.)
ALTER TABLE questions ADD COLUMN allow_html INTEGER NOT NULL DEFAULT 0;
-- Mapping: exam9 variant number → test_id (so teachers can assign a variant as homework)
CREATE TABLE exam9_variant_tests (
variant INTEGER PRIMARY KEY,
test_id INTEGER NOT NULL REFERENCES tests(id) ON DELETE CASCADE
);
+4 -4
View File
@@ -31,10 +31,10 @@ const directSchema = { body: {
}};
const bulkSchema = { body: {
title: { type: 'string', required: true, minLen: 1, maxLen: 200 },
class_id: { type: 'number', required: true, min: 1 },
mode: { type: 'string', oneOf: MODES },
count: { type: 'number', min: 1, max: 200 },
title: { type: 'string', required: true, minLen: 1, maxLen: 200 },
class_ids: { type: 'array', required: true },
mode: { type: 'string', oneOf: MODES },
count: { type: 'number', min: 1, max: 200 },
}};
router.use(authMiddleware);
+18
View File
@@ -0,0 +1,18 @@
'use strict';
const router = require('express').Router();
const db = require('../db/db');
const { authMiddleware } = require('../middleware/auth');
router.use(authMiddleware);
/* GET /api/exam9/variants — { variant: test_id } map for assigning variants as homework */
router.get('/variants', (_req, res) => {
const rows = db.prepare(
'SELECT evt.variant, evt.test_id FROM exam9_variant_tests evt JOIN tests t ON t.id = evt.test_id ORDER BY evt.variant'
).all();
const map = {};
for (const r of rows) map[r.variant] = r.test_id;
res.json({ variants: map });
});
module.exports = router;
+2
View File
@@ -50,6 +50,7 @@ const petRoutes = require('./routes/pet');
const collectionRoutes = require('./routes/collection');
const redBookRoutes = require('./routes/red-book');
const parentRoutes = require('./routes/parent');
const exam9Routes = require('./routes/exam9');
const { requestId, errorHandler } = require('./middleware/errorHandler');
@@ -166,6 +167,7 @@ app.use('/api/collection', collectionRoutes);
app.use('/api/red-book', redBookRoutes);
app.use('/api/biochem', require('./routes/biochem'));
app.use('/api/parent', parentRoutes);
app.use('/api/exam9', exam9Routes);
/* ── Public features endpoint (merges global + per-class for authenticated students) ── */
const _featDb = require('./db/db');