""" Tryout model with configuration for assessment sessions. Represents tryout exams with configurable scoring, selection, and normalization modes. """ from datetime import datetime from typing import Literal, Union from sqlalchemy import ( Boolean, CheckConstraint, DateTime, Float, ForeignKey, Integer, String, UniqueConstraint, func, ) from sqlalchemy.orm import Mapped, mapped_column, relationship from app.database import Base class Tryout(Base): """ Tryout model with configuration for assessment sessions. Supports multiple scoring modes (CTT, IRT, hybrid), selection strategies (fixed, adaptive, hybrid), and normalization modes (static, dynamic, hybrid). Attributes: id: Primary key website_id: Website identifier tryout_id: Tryout identifier (unique per website) name: Tryout name description: Tryout description scoring_mode: Scoring algorithm (ctt, irt, hybrid) selection_mode: Item selection strategy (fixed, adaptive, hybrid) normalization_mode: Normalization method (static, dynamic, hybrid) min_sample_for_dynamic: Minimum sample size for dynamic normalization static_rataan: Static mean value for manual normalization static_sb: Static standard deviation for manual normalization AI_generation_enabled: Enable/disable AI question generation hybrid_transition_slot: Slot number to transition from fixed to adaptive min_calibration_sample: Minimum responses needed for IRT calibration theta_estimation_method: Method for estimating theta (mle, map, eap) fallback_to_ctt_on_error: Fallback to CTT if IRT fails created_at: Record creation timestamp updated_at: Record update timestamp website: Website relationship items: Items in this tryout sessions: Sessions for this tryout stats: Tryout statistics """ __tablename__ = "tryouts" # Primary key id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True) # Foreign keys website_id: Mapped[int] = mapped_column( ForeignKey("websites.id", ondelete="CASCADE", onupdate="CASCADE"), nullable=False, index=True, comment="Website identifier", ) # Tryout identifier (unique per website) tryout_id: Mapped[str] = mapped_column( String(255), nullable=False, index=True, comment="Tryout identifier (unique per website)", ) # Basic information name: Mapped[str] = mapped_column( String(255), nullable=False, comment="Tryout name" ) description: Mapped[Union[str, None]] = mapped_column( String(1000), nullable=True, comment="Tryout description" ) # Scoring mode: ctt (Classical Test Theory), irt (Item Response Theory), hybrid scoring_mode: Mapped[Literal["ctt", "irt", "hybrid"]] = mapped_column( String(50), nullable=False, default="ctt", comment="Scoring mode" ) # Selection mode: fixed (slot order), adaptive (CAT), hybrid (mixed) selection_mode: Mapped[Literal["fixed", "adaptive", "hybrid"]] = mapped_column( String(50), nullable=False, default="fixed", comment="Item selection mode" ) # Normalization mode: static (hardcoded), dynamic (real-time), hybrid normalization_mode: Mapped[Literal["static", "dynamic", "hybrid"]] = mapped_column( String(50), nullable=False, default="static", comment="Normalization mode" ) # Normalization settings min_sample_for_dynamic: Mapped[int] = mapped_column( Integer, nullable=False, default=100, comment="Minimum sample size for dynamic normalization", ) static_rataan: Mapped[float] = mapped_column( Float, nullable=False, default=500.0, comment="Static mean value for manual normalization", ) static_sb: Mapped[float] = mapped_column( Float, nullable=False, default=100.0, comment="Static standard deviation for manual normalization", ) # AI generation settings ai_generation_enabled: Mapped[bool] = mapped_column( Boolean, nullable=False, default=False, comment="Enable/disable AI question generation", ) # Hybrid mode settings hybrid_transition_slot: Mapped[Union[int, None]] = mapped_column( Integer, nullable=True, comment="Slot number to transition from fixed to adaptive (hybrid mode)", ) # IRT settings min_calibration_sample: Mapped[int] = mapped_column( Integer, nullable=False, default=100, comment="Minimum responses needed for IRT calibration", ) theta_estimation_method: Mapped[Literal["mle", "map", "eap"]] = mapped_column( String(50), nullable=False, default="mle", comment="Method for estimating theta", ) fallback_to_ctt_on_error: Mapped[bool] = mapped_column( Boolean, nullable=False, default=True, comment="Fallback to CTT if IRT fails", ) # Timestamps created_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), nullable=False, server_default=func.now() ) updated_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), nullable=False, server_default=func.now(), onupdate=func.now(), ) # Relationships website: Mapped["Website"] = relationship( "Website", back_populates="tryouts", lazy="selectin" ) items: Mapped[list["Item"]] = relationship( "Item", back_populates="tryout", lazy="selectin", cascade="all, delete-orphan" ) sessions: Mapped[list["Session"]] = relationship( "Session", back_populates="tryout", lazy="selectin", cascade="all, delete-orphan", overlaps="user", ) stats: Mapped["TryoutStats"] = relationship( "TryoutStats", back_populates="tryout", lazy="selectin", uselist=False ) # Constraints and indexes __table_args__ = ( UniqueConstraint( "website_id", "tryout_id", name="uq_tryouts_website_id_tryout_id", ), CheckConstraint("min_sample_for_dynamic > 0", "ck_min_sample_positive"), CheckConstraint("static_rataan > 0", "ck_static_rataan_positive"), CheckConstraint("static_sb > 0", "ck_static_sb_positive"), CheckConstraint("min_calibration_sample > 0", "ck_min_calibration_positive"), ) def __repr__(self) -> str: return f""