Initial commit: secondary school grade archive system.
Add FastAPI/React app with Docker deployment, Ubuntu one-click install, and docs for junior/senior high score tracking and mistake bank. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,183 @@
|
||||
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 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
|
||||
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
|
||||
image_path: str
|
||||
ocr_raw_text: str | None
|
||||
question_text: str | None
|
||||
solution_text: str | None
|
||||
status: WrongQuestionStatusEnum
|
||||
created_at: datetime
|
||||
|
||||
model_config = {"from_attributes": True}
|
||||
|
||||
|
||||
class WrongQuestionUpdate(BaseModel):
|
||||
question_text: str | None = None
|
||||
solution_text: str | None = None
|
||||
subject_id: int | None = None
|
||||
Reference in New Issue
Block a user