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:
Aleksey Razorvin
2026-03-21 12:53:11 +05:00
parent 5551202d6f
commit d5f6abb5ad
12 changed files with 973 additions and 4 deletions
+68
View File
@@ -0,0 +1,68 @@
import client from './client'
export interface AnswerForTest {
id: number
text: string
}
export interface QuestionForTest {
id: number
text: string
is_multiple: boolean // true → показываем чекбоксы, false → радио-кнопки
answers: AnswerForTest[]
}
export interface AttemptStarted {
id: number
test_id: number
test_title: string
test_description: string | null
started_at: string
time_limit: number | null // минуты
allow_navigation_back: boolean
questions: QuestionForTest[]
}
export interface QuestionAnswer {
question_id: number
answer_ids: number[]
}
export interface AnswerResult {
id: number
text: string
is_correct: boolean
is_selected: boolean
}
export interface QuestionResult {
id: number
text: string
is_answered_correctly: boolean
answers: AnswerResult[]
}
export interface AttemptResult {
id: number
test_id: number
test_title: string
started_at: string
finished_at: string
score: number
passed: boolean
passing_score: number
correct_count: number
total_count: number
questions: QuestionResult[]
}
export const attemptsApi = {
start: (test_id: number) =>
client.post<AttemptStarted>('/attempts', { test_id }),
submit: (attempt_id: number, answers: QuestionAnswer[]) =>
client.post<AttemptResult>(`/attempts/${attempt_id}/submit`, { answers }),
getResult: (attempt_id: number) =>
client.get<AttemptResult>(`/attempts/${attempt_id}/result`),
}