acfe002fbf
Co-authored-by: Cursor <cursoragent@cursor.com>
255 lines
6.0 KiB
Python
255 lines
6.0 KiB
Python
from datetime import date, datetime
|
|
from enum import Enum
|
|
from uuid import UUID
|
|
|
|
from pydantic import BaseModel, Field, field_validator
|
|
|
|
|
|
class ExamTypeEnum(str, Enum):
|
|
weekly = "weekly"
|
|
monthly = "monthly"
|
|
final = "final"
|
|
|
|
|
|
class WrongQuestionStatusEnum(str, Enum):
|
|
pending = "pending"
|
|
ocr_done = "ocr_done"
|
|
solved = "solved"
|
|
failed = "failed"
|
|
|
|
|
|
class WrongQuestionCategoryEnum(str, Enum):
|
|
regular = "regular"
|
|
olympiad = "olympiad"
|
|
|
|
|
|
class AIProviderEnum(str, Enum):
|
|
ollama = "ollama"
|
|
openai = "openai"
|
|
|
|
|
|
class SchoolLevelEnum(str, Enum):
|
|
junior_high = "junior_high"
|
|
senior_high = "senior_high"
|
|
|
|
|
|
class TokenResponse(BaseModel):
|
|
access_token: str
|
|
refresh_token: str
|
|
token_type: str = "bearer"
|
|
|
|
|
|
class UserRegister(BaseModel):
|
|
username: str = Field(min_length=3, max_length=64)
|
|
password: str = Field(min_length=6, max_length=128)
|
|
|
|
|
|
class UserLogin(BaseModel):
|
|
username: str
|
|
password: str
|
|
|
|
|
|
class RefreshRequest(BaseModel):
|
|
refresh_token: str
|
|
|
|
|
|
class UserOut(BaseModel):
|
|
id: UUID
|
|
username: str
|
|
is_superuser: bool = False
|
|
created_at: datetime
|
|
|
|
model_config = {"from_attributes": True}
|
|
|
|
|
|
class PublicSettingsOut(BaseModel):
|
|
registration_enabled: bool
|
|
|
|
|
|
class SystemSettingsOut(BaseModel):
|
|
registration_enabled: bool
|
|
ai_provider: AIProviderEnum
|
|
ollama_base_url: str | None = None
|
|
ollama_model: str | None = None
|
|
openai_base_url: str | None = None
|
|
openai_model: str | None = None
|
|
openai_api_key_set: bool = False
|
|
ocr_service_url: str | None = None
|
|
updated_at: datetime
|
|
|
|
model_config = {"from_attributes": True}
|
|
|
|
|
|
class SystemSettingsUpdate(BaseModel):
|
|
registration_enabled: bool | None = None
|
|
ai_provider: AIProviderEnum | None = None
|
|
ollama_base_url: str | None = None
|
|
ollama_model: str | None = None
|
|
openai_base_url: str | None = None
|
|
openai_model: str | None = None
|
|
openai_api_key: str | None = None
|
|
ocr_service_url: str | None = None
|
|
|
|
|
|
class AdminProfileUpdate(BaseModel):
|
|
username: str | None = Field(default=None, min_length=3, max_length=64)
|
|
current_password: str | None = None
|
|
password: str | None = Field(default=None, min_length=6, max_length=128)
|
|
|
|
|
|
class AdminUserCreate(BaseModel):
|
|
username: str = Field(min_length=3, max_length=64)
|
|
password: str = Field(min_length=6, max_length=128)
|
|
|
|
|
|
class AdminUserPasswordUpdate(BaseModel):
|
|
password: str = Field(min_length=6, max_length=128)
|
|
|
|
|
|
class AdminUserOut(BaseModel):
|
|
id: UUID
|
|
username: str
|
|
is_superuser: bool
|
|
created_at: datetime
|
|
|
|
model_config = {"from_attributes": True}
|
|
|
|
|
|
class StudentCreate(BaseModel):
|
|
name: str = Field(min_length=1, max_length=64)
|
|
school_level: SchoolLevelEnum = SchoolLevelEnum.junior_high
|
|
grade: str | None = None
|
|
class_name: str | None = None
|
|
|
|
|
|
class StudentUpdate(BaseModel):
|
|
name: str | None = None
|
|
school_level: SchoolLevelEnum | None = None
|
|
grade: str | None = None
|
|
class_name: str | None = None
|
|
|
|
|
|
class StudentOut(BaseModel):
|
|
id: UUID
|
|
name: str
|
|
school_level: SchoolLevelEnum
|
|
grade: str | None
|
|
class_name: str | None
|
|
created_at: datetime
|
|
|
|
model_config = {"from_attributes": True}
|
|
|
|
|
|
class SubjectOut(BaseModel):
|
|
id: int
|
|
name: str
|
|
|
|
model_config = {"from_attributes": True}
|
|
|
|
|
|
class ScoreInput(BaseModel):
|
|
subject_id: int
|
|
total_score: float
|
|
obtained_score: float
|
|
|
|
@field_validator("total_score")
|
|
@classmethod
|
|
def validate_total(cls, v: float) -> float:
|
|
if v <= 0:
|
|
raise ValueError("总分必须大于0")
|
|
return v
|
|
|
|
@field_validator("obtained_score")
|
|
@classmethod
|
|
def validate_obtained(cls, v: float, info) -> float:
|
|
total = info.data.get("total_score")
|
|
if total is not None and v > total:
|
|
raise ValueError("得分不能大于总分")
|
|
if v < 0:
|
|
raise ValueError("得分不能为负")
|
|
return v
|
|
|
|
|
|
class ScoreOut(BaseModel):
|
|
id: UUID
|
|
subject_id: int
|
|
subject_name: str | None = None
|
|
total_score: float
|
|
obtained_score: float
|
|
ratio: float
|
|
|
|
model_config = {"from_attributes": True}
|
|
|
|
|
|
class ExamCreate(BaseModel):
|
|
exam_type: ExamTypeEnum
|
|
exam_date: date
|
|
title: str | None = None
|
|
scores: list[ScoreInput] = []
|
|
|
|
|
|
class ExamUpdate(BaseModel):
|
|
exam_type: ExamTypeEnum | None = None
|
|
exam_date: date | None = None
|
|
title: str | None = None
|
|
scores: list[ScoreInput] | None = None
|
|
|
|
|
|
class ExamOut(BaseModel):
|
|
id: UUID
|
|
exam_type: ExamTypeEnum
|
|
exam_date: date
|
|
title: str | None
|
|
created_at: datetime
|
|
scores: list[ScoreOut] = []
|
|
|
|
model_config = {"from_attributes": True}
|
|
|
|
|
|
class TrendPoint(BaseModel):
|
|
exam_id: UUID
|
|
exam_type: ExamTypeEnum
|
|
exam_date: date
|
|
title: str | None
|
|
ratio: float
|
|
ratio_percent: float
|
|
delta: float | None = None
|
|
delta_percent: float | None = None
|
|
is_volatile: bool = False
|
|
direction: str | None = None
|
|
|
|
|
|
class TrendResponse(BaseModel):
|
|
subject_id: int
|
|
subject_name: str
|
|
threshold: float
|
|
points: list[TrendPoint]
|
|
|
|
|
|
class WrongQuestionOut(BaseModel):
|
|
id: UUID
|
|
student_id: UUID
|
|
subject_id: int
|
|
subject_name: str | None = None
|
|
category: WrongQuestionCategoryEnum
|
|
image_path: str
|
|
ocr_raw_text: str | None
|
|
question_text: str | None
|
|
solution_approach: str | None = None
|
|
solution_text: str | None
|
|
mark_regions: list[dict] | None = None
|
|
has_annotated_image: bool = False
|
|
has_cropped_image: bool = False
|
|
error_message: str | None = None
|
|
status: WrongQuestionStatusEnum
|
|
created_at: datetime
|
|
|
|
model_config = {"from_attributes": True}
|
|
|
|
|
|
class WrongQuestionUpdate(BaseModel):
|
|
question_text: str | None = None
|
|
solution_approach: str | None = None
|
|
solution_text: str | None = None
|
|
subject_id: int | None = None
|