from pydantic import BaseModel, EmailStr, Field from typing import Optional, List, Dict, Any from datetime import datetime # User schemas class UserBase(BaseModel): email: EmailStr class UserCreate(UserBase): password: str = Field(..., min_length=8, max_length=100) class UserLogin(BaseModel): email: EmailStr password: str class UserResponse(UserBase): id: int created_at: datetime is_active: bool is_developer: bool class Config: from_attributes = True # Token schemas class Token(BaseModel): access_token: str refresh_token: str token_type: str = "bearer" class TokenData(BaseModel): email: Optional[str] = None # Course schemas class CourseBase(BaseModel): title: str = Field(..., max_length=255) description: str level: str = Field(..., pattern="^(beginner|intermediate|advanced)$") duration: int = Field(0, ge=0) status: str = Field('draft', pattern="^(draft|published|archived)$") class CourseCreate(CourseBase): pass # Course builder schemas class CourseCreateRequest(BaseModel): title: str = Field(..., max_length=255) description: str level: str = Field(..., pattern="^(beginner|intermediate|advanced)$") duration: int = Field(..., ge=0) content: str = Field(..., min_length=1) structure: Optional[dict] = None class CourseUpdate(BaseModel): title: Optional[str] = Field(None, max_length=255) description: Optional[str] = None level: Optional[str] = Field(None, pattern="^(beginner|intermediate|advanced)$") duration: Optional[int] = Field(None, ge=0) content: Optional[str] = None status: Optional[str] = Field(None, pattern="^(draft|published|archived)$") class CourseResponse(CourseBase): id: int author_id: int content: Optional[str] = None status: str created_at: datetime updated_at: datetime class Config: from_attributes = True # Course response with full structure class CourseStructureResponse(CourseBase): id: int author_id: int content: Optional[str] = None status: str created_at: datetime updated_at: datetime modules: list['ModuleResponse'] = [] class Config: from_attributes = True # Module schemas class ModuleBase(BaseModel): title: str = Field(..., max_length=255) description: Optional[str] = None duration: Optional[str] = Field(None, max_length=100) order_index: int = 0 class ModuleCreate(ModuleBase): course_id: int class ModuleUpdate(BaseModel): title: Optional[str] = Field(None, max_length=255) description: Optional[str] = None duration: Optional[str] = Field(None, max_length=100) order_index: Optional[int] = None class ModuleResponse(ModuleBase): id: int course_id: int created_at: datetime updated_at: datetime class Config: from_attributes = True # Lesson schemas class LessonBase(BaseModel): title: str = Field(..., max_length=255) description: Optional[str] = None order_index: int = 0 class LessonCreate(LessonBase): module_id: int class LessonUpdate(BaseModel): title: Optional[str] = Field(None, max_length=255) description: Optional[str] = None order_index: Optional[int] = None class LessonResponse(LessonBase): id: int module_id: int created_at: datetime updated_at: datetime class Config: from_attributes = True # Lesson Step schemas class StepBase(BaseModel): step_type: str = Field(..., pattern="^(theory|practice|lab|test|presentation)$") title: Optional[str] = Field(None, max_length=255) content: Optional[str] = None content_html: Optional[str] = None file_url: Optional[str] = Field(None, max_length=500) order_index: int = 0 class StepCreate(StepBase): lesson_id: int class StepUpdate(BaseModel): step_type: Optional[str] = Field(None, pattern="^(theory|practice|lab|test|presentation)$") title: Optional[str] = Field(None, max_length=255) content: Optional[str] = None content_html: Optional[str] = None file_url: Optional[str] = Field(None, max_length=500) order_index: Optional[int] = None class StepResponse(StepBase): id: int lesson_id: int created_at: datetime updated_at: datetime class Config: from_attributes = True # Test schemas class TestSettings(BaseModel): passing_score: int = Field(70, ge=0, le=100) max_attempts: int = Field(0, ge=0) time_limit: int = Field(0, ge=0) show_answers: bool = False class TestAnswerBase(BaseModel): answer_text: str is_correct: bool = False order_index: int = 0 class TestAnswerCreate(TestAnswerBase): pass class TestAnswerResponse(TestAnswerBase): id: int question_id: int created_at: datetime class Config: from_attributes = True class TestMatchItemBase(BaseModel): left_text: str right_text: str order_index: int = 0 class TestMatchItemCreate(TestMatchItemBase): pass class TestMatchItemResponse(TestMatchItemBase): id: int question_id: int created_at: datetime class Config: from_attributes = True class TestQuestionBase(BaseModel): question_text: str question_type: str = Field(..., pattern="^(single_choice|multiple_choice|text_answer|match)$") points: int = Field(1, ge=1) order_index: int = 0 class TestQuestionCreate(TestQuestionBase): answers: List[TestAnswerCreate] = [] match_items: List[TestMatchItemCreate] = [] correct_answers: List[str] = [] # Для text_answer class TestQuestionUpdate(BaseModel): question_text: Optional[str] = None question_type: Optional[str] = Field(None, pattern="^(single_choice|multiple_choice|text_answer|match)$") points: Optional[int] = Field(None, ge=1) order_index: Optional[int] = None answers: Optional[List[TestAnswerCreate]] = None match_items: Optional[List[TestMatchItemCreate]] = None correct_answers: Optional[List[str]] = None class TestQuestionResponse(TestQuestionBase): id: int step_id: int created_at: datetime updated_at: datetime answers: List[TestAnswerResponse] = [] match_items: List[TestMatchItemResponse] = [] class Config: from_attributes = True class TestQuestionForViewer(TestQuestionResponse): """Вопрос без правильных ответов для отображения студенту""" answers: List[dict] = [] # {id, answer_text, order_index} без is_correct match_items: List[TestMatchItemResponse] = [] class TestAttemptCreate(BaseModel): step_id: int answers: Dict[str, Any] # {question_id: answer_id или [answer_ids] или text или {left_id: right_id}} class TestAttemptResponse(BaseModel): id: int score: float max_score: float passed: bool completed_at: datetime class Config: from_attributes = True class TestAttemptWithDetailsResponse(TestAttemptResponse): """Попытка с деталями для просмотра результатов""" answers: Dict[str, Any] # Full course structure schemas class CourseStructureResponse(CourseResponse): modules: list["ModuleWithLessonsResponse"] = [] class ModuleWithLessonsResponse(ModuleResponse): lessons: list["LessonWithStepsResponse"] = [] class LessonWithStepsResponse(LessonResponse): steps: list[StepResponse] = [] # Update forward references CourseStructureResponse.update_forward_refs() ModuleWithLessonsResponse.update_forward_refs() LessonWithStepsResponse.update_forward_refs() # Favorite schemas class FavoriteBase(BaseModel): course_id: int class FavoriteCreate(FavoriteBase): pass class FavoriteResponse(FavoriteBase): user_id: int added_at: datetime class Config: from_attributes = True # Progress schemas class ProgressBase(BaseModel): course_id: int completed_lessons: int = Field(..., ge=0) total_lessons: int = Field(..., gt=0) class ProgressCreate(ProgressBase): pass class ProgressUpdate(BaseModel): completed_lessons: int = Field(..., ge=0) total_lessons: int = Field(..., gt=0) class ProgressResponse(ProgressBase): user_id: int percentage: float updated_at: datetime class Config: from_attributes = True # Stats schemas class StatsResponse(BaseModel): total_users: int total_courses: int most_popular_course: Optional[str] = None # Response schemas class SuccessResponse(BaseModel): success: bool = True data: Optional[dict] = None message: Optional[str] = None # ErrorResponse class ErrorResponse(BaseModel): success: bool = False error: str details: Optional[dict] = None # Course builder schemas class CourseCreateRequest(BaseModel): title: str = Field(..., max_length=255) description: str level: str = Field(..., pattern="^(beginner|intermediate|advanced)$") duration: int = Field(..., ge=0) content: str = Field(..., min_length=1) structure: Optional[dict] = None # Course draft schemas class CourseDraftBase(BaseModel): title: str = Field(..., max_length=255) description: str level: str = Field(..., pattern="^(beginner|intermediate|advanced)$") content: str class CourseDraftCreate(CourseDraftBase): structure: Optional[str] = None # JSON string class CourseDraftUpdate(CourseDraftCreate): pass class CourseDraftResponse(CourseDraftBase): id: int user_id: int created_at: datetime updated_at: datetime class Config: from_attributes = True