b2a3bda01b
Backend:
- migration 003: add parent_id to tests table
- PUT /api/tests/{id}: edit in place if no attempts, create new version otherwise
- GET /api/tests: show only latest versions (no successor)
Frontend:
- TestForm: extracted reusable form component
- TestCreate: refactored to use TestForm
- TestEdit: full edit mode with pre-populated form, version redirect on new version
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
92 lines
2.2 KiB
Python
92 lines
2.2 KiB
Python
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
|
|
parent_id: Optional[int]
|
|
created_at: datetime
|
|
questions: list[QuestionOut] = []
|
|
|
|
model_config = {"from_attributes": True}
|
|
|
|
|
|
class TestUpdateResponse(BaseModel):
|
|
test: TestOut
|
|
is_new_version: bool
|
|
|
|
|
|
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}
|