"""SQLAlchemy ORM models for ClassLens. All tables are isolated in a dedicated schema (``classlens`` on Supabase) so they never collide with other apps sharing the same Postgres instance. """ from __future__ import annotations from datetime import datetime from typing import Optional from sqlalchemy import ( JSON, Boolean, DateTime, Float, ForeignKey, Integer, MetaData, String, Text, UniqueConstraint, func, ) from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column from .db import active_schema SCHEMA = active_schema() def _fk(target: str) -> str: """Schema-qualified FK target, e.g. 'classlens.teachers.id' or 'teachers.id'.""" return f"{SCHEMA}.{target}" if SCHEMA else target class Base(DeclarativeBase): metadata = MetaData(schema=SCHEMA) class Teacher(Base): __tablename__ = "teachers" id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) email: Mapped[str] = mapped_column(String(255), unique=True, nullable=False, index=True) password_hash: Mapped[str] = mapped_column(String(255), nullable=False) display_name: Mapped[str] = mapped_column(String(255), default="") full_name: Mapped[str] = mapped_column(String(255), default="") school: Mapped[str] = mapped_column(String(255), default="") is_admin: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False) created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now()) last_login: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True), nullable=True) class Student(Base): __tablename__ = "students" __table_args__ = ( UniqueConstraint("teacher_id", "normalized_name", name="uq_student_teacher_name"), ) id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) teacher_id: Mapped[int] = mapped_column(ForeignKey(_fk("teachers.id"), ondelete="CASCADE"), index=True) name: Mapped[str] = mapped_column(String(255), nullable=False) normalized_name: Mapped[str] = mapped_column(String(255), nullable=False, index=True) external_id: Mapped[Optional[str]] = mapped_column(String(128), nullable=True) created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now()) class Quiz(Base): __tablename__ = "quizzes" id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) teacher_id: Mapped[int] = mapped_column(ForeignKey(_fk("teachers.id"), ondelete="CASCADE"), index=True) title: Mapped[str] = mapped_column(String(512), default="Untitled Session") subject: Mapped[Optional[str]] = mapped_column(String(255), nullable=True) status: Mapped[str] = mapped_column(String(32), default="draft") created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now()) updated_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), server_default=func.now(), onupdate=func.now() ) class RawFile(Base): __tablename__ = "raw_files" id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) quiz_id: Mapped[int] = mapped_column(ForeignKey(_fk("quizzes.id"), ondelete="CASCADE"), index=True) data_type: Mapped[str] = mapped_column(String(64), nullable=False) file_name: Mapped[str] = mapped_column(String(512), default="") content_type: Mapped[str] = mapped_column(String(128), default="") size_bytes: Mapped[int] = mapped_column(Integer, default=0) storage_path: Mapped[str] = mapped_column(String(1024), default="") created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now()) class ParsedData(Base): __tablename__ = "parsed_data" id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) quiz_id: Mapped[int] = mapped_column(ForeignKey(_fk("quizzes.id"), ondelete="CASCADE"), index=True) data_type: Mapped[str] = mapped_column(String(64), nullable=False) file_name: Mapped[str] = mapped_column(String(512), default="") raw_text: Mapped[str] = mapped_column(Text, default="") structured_data: Mapped[dict] = mapped_column(JSON, nullable=False) created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now()) class StudentResult(Base): """One student's outcome on one quiz — the backbone for cross-quiz analytics.""" __tablename__ = "student_results" __table_args__ = ( UniqueConstraint("quiz_id", "student_id", name="uq_result_quiz_student"), ) id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) quiz_id: Mapped[int] = mapped_column(ForeignKey(_fk("quizzes.id"), ondelete="CASCADE"), index=True) student_id: Mapped[int] = mapped_column(ForeignKey(_fk("students.id"), ondelete="CASCADE"), index=True) answers: Mapped[dict] = mapped_column(JSON, default=dict) total_questions: Mapped[int] = mapped_column(Integer, default=0) correct_count: Mapped[int] = mapped_column(Integer, default=0) score: Mapped[float] = mapped_column(Float, default=0.0) created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now()) class Prompt(Base): __tablename__ = "prompts" id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) teacher_id: Mapped[int] = mapped_column(ForeignKey(_fk("teachers.id"), ondelete="CASCADE"), index=True) name: Mapped[str] = mapped_column(String(255), nullable=False) content: Mapped[str] = mapped_column(Text, nullable=False) is_default: Mapped[bool] = mapped_column(Boolean, default=False) created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now()) updated_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), server_default=func.now(), onupdate=func.now() ) class Report(Base): __tablename__ = "reports" id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) quiz_id: Mapped[int] = mapped_column(ForeignKey(_fk("quizzes.id"), ondelete="CASCADE"), index=True) student_id: Mapped[Optional[int]] = mapped_column( ForeignKey(_fk("students.id"), ondelete="SET NULL"), nullable=True, index=True ) prompt_id: Mapped[Optional[int]] = mapped_column( ForeignKey(_fk("prompts.id"), ondelete="SET NULL"), nullable=True ) html_content: Mapped[str] = mapped_column(Text, nullable=False) model: Mapped[str] = mapped_column(String(64), default="") created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())