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:
Aleksey Razorvin
2026-03-21 12:05:04 +05:00
parent 054376bca7
commit 8b17c5d3c4
35 changed files with 1370 additions and 0 deletions
View File
+85
View File
@@ -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}