""" UserAnswer model for tracking individual question responses. Represents a student's response to a single question with scoring metadata. """ 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 UserAnswer(Base): """ UserAnswer model representing a student's response to a question. Tracks response, correctness, scoring, and timing information. Attributes: id: Primary key session_id: Session identifier wp_user_id: WordPress user ID website_id: Website identifier tryout_id: Tryout identifier item_id: Item identifier response: User's answer (A, B, C, D) is_correct: Whether answer is correct time_spent: Time spent on this question (seconds) scoring_mode_used: Scoring mode used bobot_earned: Weight earned for this answer created_at: Record creation timestamp updated_at: Record update timestamp session: Session relationship item: Item relationship """ __tablename__ = "user_answers" # Primary key id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True) # Foreign keys session_id: Mapped[str] = mapped_column( ForeignKey("sessions.session_id", ondelete="CASCADE", onupdate="CASCADE"), nullable=False, index=True, comment="Session identifier", ) wp_user_id: Mapped[str] = mapped_column( String(255), nullable=False, index=True, comment="WordPress user ID" ) website_id: Mapped[int] = mapped_column( ForeignKey("websites.id", ondelete="CASCADE", onupdate="CASCADE"), nullable=False, index=True, comment="Website identifier", ) tryout_id: Mapped[str] = mapped_column( String(255), nullable=False, index=True, comment="Tryout identifier" ) item_id: Mapped[int] = mapped_column( ForeignKey("items.id", ondelete="CASCADE", onupdate="CASCADE"), nullable=False, index=True, comment="Item identifier", ) # Response information response: Mapped[str] = mapped_column( String(10), nullable=False, comment="User's answer (A, B, C, D)" ) is_correct: Mapped[bool] = mapped_column( Boolean, nullable=False, comment="Whether answer is correct" ) time_spent: Mapped[int] = mapped_column( Integer, nullable=False, default=0, comment="Time spent on this question (seconds)", ) # Scoring metadata scoring_mode_used: Mapped[Literal["ctt", "irt", "hybrid"]] = mapped_column( String(50), nullable=False, comment="Scoring mode used", ) bobot_earned: Mapped[float] = mapped_column( Float, nullable=False, default=0.0, comment="Weight earned for this answer", ) # 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 session: Mapped["Session"] = relationship( "Session", back_populates="user_answers", lazy="selectin" ) item: Mapped["Item"] = relationship( "Item", back_populates="user_answers", lazy="selectin" ) # Constraints and indexes __table_args__ = ( Index("ix_user_answers_session_id", "session_id"), Index("ix_user_answers_wp_user_id", "wp_user_id"), Index("ix_user_answers_website_id", "website_id"), Index("ix_user_answers_tryout_id", "tryout_id"), Index("ix_user_answers_item_id", "item_id"), Index( "ix_user_answers_session_id_item_id", "session_id", "item_id", unique=True, ), # Time spent must be non-negative CheckConstraint("time_spent >= 0", "ck_time_spent_non_negative"), # Bobot earned must be non-negative CheckConstraint("bobot_earned >= 0", "ck_bobot_earned_non_negative"), ) def __repr__(self) -> str: return f""