feat: Sprint 1 — infrastructure + test creation
Backend:
- FastAPI + SQLAlchemy 2.0 async + Alembic
- Models: Test, Question, Answer
- API: GET /api/tests, GET /api/tests/{id}, POST /api/tests
- Pydantic validation: min 7 questions, min 3 answers, ≥1 correct
Frontend:
- React 18 + TypeScript + Vite + Ant Design + TanStack Query
- Pages: TestList, TestCreate (nested Form.List), TestDetail
Infrastructure:
- Docker Compose: db (postgres:16), backend, frontend, nginx
- Nginx: /api/ → FastAPI, / → Vite dev server with HMR
- Alembic migration 001_init: tests, questions, answers tables
- entrypoint.sh: wait for db, migrate, start uvicorn
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,85 @@
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
|
||||
from pydantic import BaseModel, Field, field_validator
|
||||
|
||||
|
||||
class AnswerCreate(BaseModel):
|
||||
text: str = Field(min_length=1)
|
||||
is_correct: bool
|
||||
|
||||
|
||||
class AnswerOut(BaseModel):
|
||||
id: int
|
||||
text: str
|
||||
is_correct: bool
|
||||
|
||||
model_config = {"from_attributes": True}
|
||||
|
||||
|
||||
class QuestionCreate(BaseModel):
|
||||
text: str = Field(min_length=1)
|
||||
answers: list[AnswerCreate]
|
||||
|
||||
@field_validator("answers")
|
||||
@classmethod
|
||||
def validate_answers(cls, v: list[AnswerCreate]) -> list[AnswerCreate]:
|
||||
if len(v) < 3:
|
||||
raise ValueError("Минимум 3 варианта ответа на вопрос")
|
||||
if not any(a.is_correct for a in v):
|
||||
raise ValueError("Хотя бы один ответ должен быть правильным")
|
||||
return v
|
||||
|
||||
|
||||
class QuestionOut(BaseModel):
|
||||
id: int
|
||||
text: str
|
||||
order: int
|
||||
answers: list[AnswerOut]
|
||||
|
||||
model_config = {"from_attributes": True}
|
||||
|
||||
|
||||
class TestCreate(BaseModel):
|
||||
title: str = Field(min_length=1, max_length=255)
|
||||
description: Optional[str] = None
|
||||
passing_score: int = Field(ge=0, le=100)
|
||||
time_limit: Optional[int] = Field(None, ge=1)
|
||||
allow_navigation_back: bool = True
|
||||
questions: list[QuestionCreate]
|
||||
|
||||
@field_validator("questions")
|
||||
@classmethod
|
||||
def validate_questions(cls, v: list[QuestionCreate]) -> list[QuestionCreate]:
|
||||
if len(v) < 7:
|
||||
raise ValueError("Минимум 7 вопросов в тесте")
|
||||
return v
|
||||
|
||||
|
||||
class TestOut(BaseModel):
|
||||
id: int
|
||||
title: str
|
||||
description: Optional[str]
|
||||
passing_score: int
|
||||
time_limit: Optional[int]
|
||||
allow_navigation_back: bool
|
||||
is_active: bool
|
||||
version: int
|
||||
created_at: datetime
|
||||
questions: list[QuestionOut] = []
|
||||
|
||||
model_config = {"from_attributes": True}
|
||||
|
||||
|
||||
class TestListItem(BaseModel):
|
||||
id: int
|
||||
title: str
|
||||
description: Optional[str]
|
||||
passing_score: int
|
||||
time_limit: Optional[int]
|
||||
is_active: bool
|
||||
version: int
|
||||
created_at: datetime
|
||||
questions_count: int = 0
|
||||
|
||||
model_config = {"from_attributes": True}
|
||||
Reference in New Issue
Block a user