""" 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, Index, Integer, String 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="NOW()" ) updated_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), nullable=False, server_default="NOW()", onupdate="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" ) stats: Mapped["TryoutStats"] = relationship( "TryoutStats", back_populates="tryout", lazy="selectin", uselist=False ) # Constraints and indexes __table_args__ = ( Index( "ix_tryouts_website_id_tryout_id", "website_id", "tryout_id", unique=True ), 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""