feat: Sprint 2 — test taking + results
Backend:
- Models: TestAttempt, AttemptAnswer (migration 002)
- POST /api/attempts: start attempt, shuffle questions/answers,
hide is_correct, expose is_multiple for UI hints
- POST /api/attempts/{id}/submit: save answers, calculate score,
strict matching (selected == correct), return full result
- GET /api/attempts/{id}/result: fetch saved result
- Register attempts router in main.py
Frontend:
- api/attempts.ts: types + API functions
- TestTake page: one question at a time, progress bar, timer
with auto-submit, back navigation controlled by test setting,
radio/checkbox based on is_multiple
- AttemptResult page: score, pass/fail, per-question breakdown
with correct/selected/missed answer highlighting
- App.tsx: add /tests/:testId/take and /attempts/:id/result routes
- TestDetail: add "Пройти тест" button
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,89 @@
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
# ── Начало попытки ──────────────────────────────────────────
|
||||
|
||||
class AttemptStart(BaseModel):
|
||||
test_id: int
|
||||
|
||||
|
||||
class AnswerForTest(BaseModel):
|
||||
"""Вариант ответа без поля is_correct — не раскрываем правильные ответы."""
|
||||
id: int
|
||||
text: str
|
||||
|
||||
model_config = {"from_attributes": True}
|
||||
|
||||
|
||||
class QuestionForTest(BaseModel):
|
||||
id: int
|
||||
text: str
|
||||
is_multiple: bool # True = несколько правильных ответов → показываем чекбоксы
|
||||
answers: list[AnswerForTest] # перемешаны случайно
|
||||
|
||||
model_config = {"from_attributes": True}
|
||||
|
||||
|
||||
class AttemptStarted(BaseModel):
|
||||
"""Возвращается после старта попытки."""
|
||||
id: int
|
||||
test_id: int
|
||||
test_title: str
|
||||
test_description: Optional[str]
|
||||
started_at: datetime
|
||||
time_limit: Optional[int] # минуты, из теста
|
||||
allow_navigation_back: bool # из теста
|
||||
questions: list[QuestionForTest] # перемешаны случайно
|
||||
|
||||
model_config = {"from_attributes": True}
|
||||
|
||||
|
||||
# ── Сдача попытки ────────────────────────────────────────────
|
||||
|
||||
class QuestionAnswer(BaseModel):
|
||||
"""Ответы сотрудника на один вопрос."""
|
||||
question_id: int
|
||||
answer_ids: list[int] # выбранные варианты (может быть пустым)
|
||||
|
||||
|
||||
class AttemptSubmitDto(BaseModel):
|
||||
answers: list[QuestionAnswer]
|
||||
|
||||
|
||||
# ── Результат ────────────────────────────────────────────────
|
||||
|
||||
class AnswerResult(BaseModel):
|
||||
id: int
|
||||
text: str
|
||||
is_correct: bool # правильный ли ответ вообще
|
||||
is_selected: bool # выбрал ли его сотрудник
|
||||
|
||||
model_config = {"from_attributes": True}
|
||||
|
||||
|
||||
class QuestionResult(BaseModel):
|
||||
id: int
|
||||
text: str
|
||||
is_answered_correctly: bool # вся комбинация ответов верна
|
||||
answers: list[AnswerResult]
|
||||
|
||||
model_config = {"from_attributes": True}
|
||||
|
||||
|
||||
class AttemptResult(BaseModel):
|
||||
id: int
|
||||
test_id: int
|
||||
test_title: str
|
||||
started_at: datetime
|
||||
finished_at: datetime
|
||||
score: float # процент правильных ответов
|
||||
passed: bool # преодолён ли порог зачёта
|
||||
passing_score: int # порог из теста
|
||||
correct_count: int
|
||||
total_count: int
|
||||
questions: list[QuestionResult]
|
||||
|
||||
model_config = {"from_attributes": True}
|
||||
Reference in New Issue
Block a user