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,49 @@
|
||||
from datetime import datetime
|
||||
|
||||
from sqlalchemy import Boolean, DateTime, Float, ForeignKey, Integer, String
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
from sqlalchemy.sql import func
|
||||
|
||||
from app.database import Base
|
||||
|
||||
|
||||
class TestAttempt(Base):
|
||||
__tablename__ = "test_attempts"
|
||||
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True)
|
||||
test_id: Mapped[int] = mapped_column(Integer, ForeignKey("tests.id"), nullable=False)
|
||||
started_at: Mapped[datetime] = mapped_column(
|
||||
DateTime(timezone=True), server_default=func.now()
|
||||
)
|
||||
finished_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
|
||||
score: Mapped[float | None] = mapped_column(Float, nullable=True) # процент 0–100
|
||||
passed: Mapped[bool | None] = mapped_column(Boolean, nullable=True)
|
||||
correct_count: Mapped[int | None] = mapped_column(Integer, nullable=True)
|
||||
total_count: Mapped[int | None] = mapped_column(Integer, nullable=True)
|
||||
# in_progress | finished
|
||||
status: Mapped[str] = mapped_column(String(20), default="in_progress", nullable=False)
|
||||
|
||||
test: Mapped["Test"] = relationship("Test") # type: ignore[name-defined]
|
||||
answers: Mapped[list["AttemptAnswer"]] = relationship(
|
||||
"AttemptAnswer", back_populates="attempt", cascade="all, delete-orphan"
|
||||
)
|
||||
|
||||
|
||||
class AttemptAnswer(Base):
|
||||
"""Одна запись = один выбранный вариант ответа сотрудника."""
|
||||
|
||||
__tablename__ = "attempt_answers"
|
||||
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True)
|
||||
attempt_id: Mapped[int] = mapped_column(
|
||||
Integer, ForeignKey("test_attempts.id"), nullable=False
|
||||
)
|
||||
question_id: Mapped[int] = mapped_column(
|
||||
Integer, ForeignKey("questions.id"), nullable=False
|
||||
)
|
||||
answer_id: Mapped[int] = mapped_column(
|
||||
Integer, ForeignKey("answers.id"), nullable=False
|
||||
)
|
||||
|
||||
attempt: Mapped["TestAttempt"] = relationship("TestAttempt", back_populates="answers")
|
||||
answer: Mapped["Answer"] = relationship("Answer") # type: ignore[name-defined]
|
||||
Reference in New Issue
Block a user