fc684e7c7d
- Миграция 005: user_id в test_attempts (дефолт 1 = Гость) - GET /api/attempts с фильтрами по тесту, дате и пагинацией - Страница /tracker: таблица попыток, фильтры, пагинация - Ссылка «Трекер» в шапке приложения Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
116 lines
3.5 KiB
Python
116 lines
3.5 KiB
Python
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}
|
|
|
|
|
|
# ── Трекер результатов ────────────────────────────────────────
|
|
|
|
class AttemptListItem(BaseModel):
|
|
id: int
|
|
test_id: int
|
|
test_title: str
|
|
test_version: int
|
|
user_id: int
|
|
user_name: str
|
|
started_at: datetime
|
|
finished_at: Optional[datetime]
|
|
score: Optional[float]
|
|
correct_count: Optional[int]
|
|
total_count: Optional[int]
|
|
passed: Optional[bool]
|
|
|
|
model_config = {"from_attributes": True}
|
|
|
|
|
|
class AttemptListResponse(BaseModel):
|
|
items: list[AttemptListItem]
|
|
total: int
|
|
page: int
|
|
page_size: int
|