Миграция questions.ai_hint и подсказки в редакторе теста
- Alembic 0003: колонка ai_hint (TEXT NULL) - API черновика: отдаём aiHint, сохраняем из payload - Карточка вопроса: textarea подсказки для прохождения Made-with: Cursor
This commit is contained in:
@@ -0,0 +1,35 @@
|
||||
"""Добавление ai_hint в questions.
|
||||
|
||||
Revision ID: 0003_question_ai_hint
|
||||
Revises: 0002_tests_hints_result_mode
|
||||
Create Date: 2026-04-29
|
||||
|
||||
Колонка опциональная (TEXT NULL), как в ORM Question.ai_hint.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
|
||||
revision: str = "0003_question_ai_hint"
|
||||
down_revision: Union[str, None] = "0002_tests_hints_result_mode"
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
op.execute(
|
||||
"""
|
||||
ALTER TABLE questions
|
||||
ADD COLUMN IF NOT EXISTS ai_hint TEXT NULL;
|
||||
"""
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.execute(
|
||||
"""
|
||||
ALTER TABLE questions DROP COLUMN IF EXISTS ai_hint;
|
||||
"""
|
||||
)
|
||||
@@ -51,6 +51,7 @@ def load_questions_for_version(session, test_version_id, *, include_correct: boo
|
||||
'text': q.text,
|
||||
'questionOrder': q.question_order,
|
||||
'hasMultipleAnswers': bool(q.has_multiple_answers),
|
||||
'aiHint': (q.ai_hint or '').strip() or None,
|
||||
'options': options,
|
||||
})
|
||||
return out
|
||||
|
||||
@@ -122,12 +122,17 @@ def _replace_version_content(session: Session, version: TestVersion, payload: di
|
||||
questions_payload = payload.get('questions') or []
|
||||
for i, qp in enumerate(questions_payload):
|
||||
q_text = (qp.get('text') or '').strip()
|
||||
hint_in = qp.get('aiHint')
|
||||
if hint_in is not None:
|
||||
hint_val = (str(hint_in).strip() or None)
|
||||
else:
|
||||
hint_val = old_hints.get(q_text)
|
||||
new_q = Question(
|
||||
test_version_id=version.id,
|
||||
text=q_text,
|
||||
question_order=qp.get('question_order') or (i + 1),
|
||||
has_multiple_answers=bool(qp.get('hasMultipleAnswers')),
|
||||
ai_hint=old_hints.get(q_text),
|
||||
ai_hint=hint_val,
|
||||
)
|
||||
session.add(new_q)
|
||||
session.flush()
|
||||
|
||||
@@ -109,6 +109,14 @@
|
||||
syncOptionInputTypes(node);
|
||||
updateOptionsCounter(node);
|
||||
updateAiButtonLabel(node);
|
||||
|
||||
const hintEl = $('.q-hint', node);
|
||||
if (hintEl) {
|
||||
hintEl.value = q.aiHint || '';
|
||||
const rh = () => autoResize(hintEl);
|
||||
hintEl.addEventListener('input', () => { rh(); scheduleDirtyCheck(); });
|
||||
requestAnimationFrame(rh);
|
||||
}
|
||||
return node;
|
||||
}
|
||||
|
||||
@@ -207,6 +215,8 @@
|
||||
const qTextEl = $('.q-text', node);
|
||||
qTextEl.value = '';
|
||||
autoResize(qTextEl);
|
||||
const qh = $('.q-hint', node);
|
||||
if (qh) { qh.value = ''; autoResize(qh); }
|
||||
$$('.opt-text', node).forEach((t) => { t.value = ''; autoResize(t); });
|
||||
$$('.opt-correct', node).forEach((c) => { c.checked = false; });
|
||||
updateAiButtonLabel(node);
|
||||
@@ -406,16 +416,20 @@
|
||||
// ─── collect ───────────────────────────────────────────────────────
|
||||
|
||||
function collectPayload() {
|
||||
const questions = $$('#questions .q-item:not(.q-removed)').map((li, i) => ({
|
||||
const questions = $$('#questions .q-item:not(.q-removed)').map((li, i) => {
|
||||
const hintVal = ($('.q-hint', li) && $('.q-hint', li).value.trim()) || '';
|
||||
return {
|
||||
text: $('.q-text', li).value.trim(),
|
||||
question_order: i + 1,
|
||||
hasMultipleAnswers: $('.q-multi', li).checked,
|
||||
aiHint: hintVal || null,
|
||||
options: $$('.opt-item', li).map((op, j) => ({
|
||||
text: $('.opt-text', op).value.trim(),
|
||||
isCorrect: $('.opt-correct', op).checked,
|
||||
option_order: j + 1,
|
||||
})),
|
||||
}));
|
||||
};
|
||||
});
|
||||
const payload = {
|
||||
title: titleEl.value.trim() || null,
|
||||
description: descEl.value.trim() || null,
|
||||
|
||||
@@ -346,6 +346,18 @@
|
||||
</button>
|
||||
<span class="q-options-count text-xs text-ink-400"></span>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 pt-3 border-t border-ink-200/80 q-hint-block">
|
||||
<label class="block">
|
||||
<span class="text-xs text-ink-600 font-medium mb-1 block">Подсказка при прохождении</span>
|
||||
<textarea class="q-hint form-input w-full rounded-lg border border-ink-300 px-3 py-2 text-sm
|
||||
focus:border-brand-500 focus:ring-2 focus:ring-brand-500/20"
|
||||
rows="2" maxlength="8000"
|
||||
placeholder="Краткий текст подсказки (если в параметрах теста включены подсказки и режим «Сразу после ответа»)"
|
||||
style="resize:none; overflow:hidden; font-family:inherit;"></textarea>
|
||||
</label>
|
||||
<p class="mt-1.5 text-xs text-ink-400 leading-snug">Необязательно. Показывается участнику во всплывающем окне при верном ответе.</p>
|
||||
</div>
|
||||
</li>
|
||||
</template>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user