import enum import uuid from datetime import date, datetime, timezone from sqlalchemy import Date, DateTime, Enum, ForeignKey, Integer, Numeric, String, Text, UniqueConstraint from sqlalchemy.dialects.postgresql import UUID from sqlalchemy.orm import Mapped, mapped_column, relationship from app.core.database import Base class ExamType(str, enum.Enum): weekly = "weekly" monthly = "monthly" final = "final" class WrongQuestionStatus(str, enum.Enum): pending = "pending" ocr_done = "ocr_done" solved = "solved" failed = "failed" class WrongQuestionCategory(str, enum.Enum): regular = "regular" olympiad = "olympiad" class CompositionStatus(str, enum.Enum): pending = "pending" generating = "generating" done = "done" failed = "failed" class CompositionInputMode(str, enum.Enum): manual = "manual" ocr = "ocr" class AIProvider(str, enum.Enum): ollama = "ollama" openai = "openai" class SchoolLevel(str, enum.Enum): junior_high = "junior_high" senior_high = "senior_high" class User(Base): __tablename__ = "users" id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) username: Mapped[str] = mapped_column(String(64), unique=True, index=True) password_hash: Mapped[str] = mapped_column(String(255)) is_superuser: Mapped[bool] = mapped_column(default=False) created_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), default=lambda: datetime.now(timezone.utc) ) students: Mapped[list["Student"]] = relationship(back_populates="user", cascade="all, delete-orphan") class Student(Base): __tablename__ = "students" id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) user_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), ForeignKey("users.id"), index=True) name: Mapped[str] = mapped_column(String(64)) school_level: Mapped[SchoolLevel] = mapped_column( Enum(SchoolLevel), default=SchoolLevel.junior_high ) grade: Mapped[str | None] = mapped_column(String(32), nullable=True) class_name: Mapped[str | None] = mapped_column(String(32), nullable=True) created_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), default=lambda: datetime.now(timezone.utc) ) user: Mapped["User"] = relationship(back_populates="students") exam_records: Mapped[list["ExamRecord"]] = relationship( back_populates="student", cascade="all, delete-orphan" ) wrong_questions: Mapped[list["WrongQuestion"]] = relationship( back_populates="student", cascade="all, delete-orphan" ) compositions: Mapped[list["Composition"]] = relationship( back_populates="student", cascade="all, delete-orphan" ) class Subject(Base): __tablename__ = "subjects" id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) name: Mapped[str] = mapped_column(String(32), unique=True) scores: Mapped[list["SubjectScore"]] = relationship(back_populates="subject") wrong_questions: Mapped[list["WrongQuestion"]] = relationship(back_populates="subject") class ExamRecord(Base): __tablename__ = "exam_records" id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) student_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), ForeignKey("students.id"), index=True) exam_type: Mapped[ExamType] = mapped_column(Enum(ExamType)) exam_date: Mapped[date] = mapped_column(Date) title: Mapped[str | None] = mapped_column(String(128), nullable=True) created_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), default=lambda: datetime.now(timezone.utc) ) student: Mapped["Student"] = relationship(back_populates="exam_records") scores: Mapped[list["SubjectScore"]] = relationship( back_populates="exam_record", cascade="all, delete-orphan" ) class SubjectScore(Base): __tablename__ = "subject_scores" __table_args__ = (UniqueConstraint("exam_record_id", "subject_id", name="uq_exam_subject"),) id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) exam_record_id: Mapped[uuid.UUID] = mapped_column( UUID(as_uuid=True), ForeignKey("exam_records.id"), index=True ) subject_id: Mapped[int] = mapped_column(Integer, ForeignKey("subjects.id")) total_score: Mapped[float] = mapped_column(Numeric(8, 2)) obtained_score: Mapped[float] = mapped_column(Numeric(8, 2)) ratio: Mapped[float] = mapped_column(Numeric(8, 4)) review_statuses_json: Mapped[str | None] = mapped_column(Text, nullable=True) exam_record: Mapped["ExamRecord"] = relationship(back_populates="scores") subject: Mapped["Subject"] = relationship(back_populates="scores") class WrongQuestion(Base): __tablename__ = "wrong_questions" id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) student_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), ForeignKey("students.id"), index=True) subject_id: Mapped[int] = mapped_column(Integer, ForeignKey("subjects.id")) image_path: Mapped[str] = mapped_column(String(512)) ocr_raw_text: Mapped[str | None] = mapped_column(Text, nullable=True) question_text: Mapped[str | None] = mapped_column(Text, nullable=True) solution_approach: Mapped[str | None] = mapped_column(Text, nullable=True) solution_text: Mapped[str | None] = mapped_column(Text, nullable=True) mark_regions_json: Mapped[str | None] = mapped_column(Text, nullable=True) annotated_image_path: Mapped[str | None] = mapped_column(String(512), nullable=True) cropped_image_path: Mapped[str | None] = mapped_column(String(512), nullable=True) error_message: Mapped[str | None] = mapped_column(Text, nullable=True) status: Mapped[WrongQuestionStatus] = mapped_column( Enum(WrongQuestionStatus), default=WrongQuestionStatus.pending ) category: Mapped[WrongQuestionCategory] = mapped_column( Enum(WrongQuestionCategory), default=WrongQuestionCategory.regular ) created_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), default=lambda: datetime.now(timezone.utc) ) student: Mapped["Student"] = relationship(back_populates="wrong_questions") subject: Mapped["Subject"] = relationship(back_populates="wrong_questions") class Composition(Base): __tablename__ = "compositions" id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) student_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), ForeignKey("students.id"), index=True) topic: Mapped[str] = mapped_column(Text) input_mode: Mapped[CompositionInputMode] = mapped_column( Enum(CompositionInputMode), default=CompositionInputMode.manual ) writing_plan: Mapped[str | None] = mapped_column(Text, nullable=True) sample_essay: Mapped[str | None] = mapped_column(Text, nullable=True) error_message: Mapped[str | None] = mapped_column(Text, nullable=True) status: Mapped[CompositionStatus] = mapped_column( Enum(CompositionStatus), default=CompositionStatus.pending ) created_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), default=lambda: datetime.now(timezone.utc) ) updated_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), default=lambda: datetime.now(timezone.utc) ) student: Mapped["Student"] = relationship(back_populates="compositions") class SystemSettings(Base): __tablename__ = "system_settings" id: Mapped[int] = mapped_column(Integer, primary_key=True, default=1) registration_enabled: Mapped[bool] = mapped_column(default=True) ai_review_enabled: Mapped[bool] = mapped_column(default=True) ai_provider: Mapped[str] = mapped_column(String(16), default="ollama") ollama_base_url: Mapped[str | None] = mapped_column(String(256), nullable=True) ollama_model: Mapped[str | None] = mapped_column(String(128), nullable=True) openai_base_url: Mapped[str | None] = mapped_column(String(256), nullable=True) openai_model: Mapped[str | None] = mapped_column(String(128), nullable=True) openai_api_key: Mapped[str | None] = mapped_column(String(512), nullable=True) ocr_service_url: Mapped[str | None] = mapped_column(String(256), nullable=True) updated_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), default=lambda: datetime.now(timezone.utc) )