Миграция 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,
|
'text': q.text,
|
||||||
'questionOrder': q.question_order,
|
'questionOrder': q.question_order,
|
||||||
'hasMultipleAnswers': bool(q.has_multiple_answers),
|
'hasMultipleAnswers': bool(q.has_multiple_answers),
|
||||||
|
'aiHint': (q.ai_hint or '').strip() or None,
|
||||||
'options': options,
|
'options': options,
|
||||||
})
|
})
|
||||||
return out
|
return out
|
||||||
|
|||||||
@@ -122,12 +122,17 @@ def _replace_version_content(session: Session, version: TestVersion, payload: di
|
|||||||
questions_payload = payload.get('questions') or []
|
questions_payload = payload.get('questions') or []
|
||||||
for i, qp in enumerate(questions_payload):
|
for i, qp in enumerate(questions_payload):
|
||||||
q_text = (qp.get('text') or '').strip()
|
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(
|
new_q = Question(
|
||||||
test_version_id=version.id,
|
test_version_id=version.id,
|
||||||
text=q_text,
|
text=q_text,
|
||||||
question_order=qp.get('question_order') or (i + 1),
|
question_order=qp.get('question_order') or (i + 1),
|
||||||
has_multiple_answers=bool(qp.get('hasMultipleAnswers')),
|
has_multiple_answers=bool(qp.get('hasMultipleAnswers')),
|
||||||
ai_hint=old_hints.get(q_text),
|
ai_hint=hint_val,
|
||||||
)
|
)
|
||||||
session.add(new_q)
|
session.add(new_q)
|
||||||
session.flush()
|
session.flush()
|
||||||
|
|||||||
@@ -109,6 +109,14 @@
|
|||||||
syncOptionInputTypes(node);
|
syncOptionInputTypes(node);
|
||||||
updateOptionsCounter(node);
|
updateOptionsCounter(node);
|
||||||
updateAiButtonLabel(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;
|
return node;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -207,6 +215,8 @@
|
|||||||
const qTextEl = $('.q-text', node);
|
const qTextEl = $('.q-text', node);
|
||||||
qTextEl.value = '';
|
qTextEl.value = '';
|
||||||
autoResize(qTextEl);
|
autoResize(qTextEl);
|
||||||
|
const qh = $('.q-hint', node);
|
||||||
|
if (qh) { qh.value = ''; autoResize(qh); }
|
||||||
$$('.opt-text', node).forEach((t) => { t.value = ''; autoResize(t); });
|
$$('.opt-text', node).forEach((t) => { t.value = ''; autoResize(t); });
|
||||||
$$('.opt-correct', node).forEach((c) => { c.checked = false; });
|
$$('.opt-correct', node).forEach((c) => { c.checked = false; });
|
||||||
updateAiButtonLabel(node);
|
updateAiButtonLabel(node);
|
||||||
@@ -406,16 +416,20 @@
|
|||||||
// ─── collect ───────────────────────────────────────────────────────
|
// ─── collect ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
function collectPayload() {
|
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(),
|
text: $('.q-text', li).value.trim(),
|
||||||
question_order: i + 1,
|
question_order: i + 1,
|
||||||
hasMultipleAnswers: $('.q-multi', li).checked,
|
hasMultipleAnswers: $('.q-multi', li).checked,
|
||||||
|
aiHint: hintVal || null,
|
||||||
options: $$('.opt-item', li).map((op, j) => ({
|
options: $$('.opt-item', li).map((op, j) => ({
|
||||||
text: $('.opt-text', op).value.trim(),
|
text: $('.opt-text', op).value.trim(),
|
||||||
isCorrect: $('.opt-correct', op).checked,
|
isCorrect: $('.opt-correct', op).checked,
|
||||||
option_order: j + 1,
|
option_order: j + 1,
|
||||||
})),
|
})),
|
||||||
}));
|
};
|
||||||
|
});
|
||||||
const payload = {
|
const payload = {
|
||||||
title: titleEl.value.trim() || null,
|
title: titleEl.value.trim() || null,
|
||||||
description: descEl.value.trim() || null,
|
description: descEl.value.trim() || null,
|
||||||
|
|||||||
@@ -346,6 +346,18 @@
|
|||||||
</button>
|
</button>
|
||||||
<span class="q-options-count text-xs text-ink-400"></span>
|
<span class="q-options-count text-xs text-ink-400"></span>
|
||||||
</div>
|
</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>
|
</li>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user