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:
@@ -160,6 +160,8 @@
|
||||
delimiters: [
|
||||
{ left: '\\(', right: '\\)', display: false },
|
||||
{ left: '\\[', right: '\\]', display: true },
|
||||
{ left: '$$', right: '$$', display: true },
|
||||
{ left: '$', right: '$', display: false },
|
||||
],
|
||||
throwOnError: false,
|
||||
};
|
||||
@@ -312,7 +314,7 @@
|
||||
if (isCorrect && isChosen) { cls = 'chosen-correct'; icon = lsIcon('check', 14); }
|
||||
else if (isCorrect) { cls = 'correct-opt'; icon = lsIcon('check', 14); }
|
||||
else if (isChosen) { cls = 'chosen-wrong'; icon = lsIcon('x', 14); }
|
||||
return `<div class="review-opt ${cls}"><span class="review-opt-icon">${icon}</span>${esc(o.text)}</div>`;
|
||||
return `<div class="review-opt ${cls}"><span class="review-opt-icon">${icon}</span>${q.allow_html ? o.text : esc(o.text)}</div>`;
|
||||
}).join('') + '</div>';
|
||||
} else if (type === 'matching') {
|
||||
const pairs = (() => { try { return JSON.parse(q.answer_text || '{}'); } catch { return {}; } })();
|
||||
@@ -334,13 +336,14 @@
|
||||
let cls = '', icon = '<svg class="ic" viewBox="0 0 24 24"><circle cx="12" cy="12" r="8"/></svg>';
|
||||
if (isCorrect) { cls = 'correct-opt'; icon = lsIcon('check', 14); }
|
||||
else if (isChosen && !isCorrect) { cls = 'chosen-wrong'; icon = lsIcon('x', 14); }
|
||||
return `<div class="review-opt ${cls}"><span class="review-opt-icon">${icon}</span>${esc(o.text)}</div>`;
|
||||
return `<div class="review-opt ${cls}"><span class="review-opt-icon">${icon}</span>${q.allow_html ? o.text : esc(o.text)}</div>`;
|
||||
}).join('') + '</div>';
|
||||
}
|
||||
|
||||
const expl = q.explanation
|
||||
? `<div class="review-explanation"><strong>Пояснение:</strong> ${esc(q.explanation)}</div>`
|
||||
? `<div class="review-explanation"><strong>Пояснение:</strong> ${q.allow_html ? q.explanation : esc(q.explanation)}</div>`
|
||||
: '';
|
||||
const qText = q.allow_html ? q.text : esc(q.text);
|
||||
|
||||
list.innerHTML += `
|
||||
<div class="review-item ${status}">
|
||||
@@ -348,7 +351,7 @@
|
||||
<span class="review-qnum">Вопрос ${i + 1}</span>
|
||||
<span class="review-badge ${status}">${badgeText[status]}</span>
|
||||
</div>
|
||||
<div class="review-text">${esc(q.text)}</div>
|
||||
<div class="review-text">${qText}</div>
|
||||
${bodyHtml}
|
||||
${expl}
|
||||
</div>`;
|
||||
|
||||
Reference in New Issue
Block a user