Files
yellow-bank-soal/app/models/session.py
Dwindi Ramadhana cf193d7ea0 first commit
2026-03-21 23:32:59 +07:00

194 lines
6.0 KiB
Python

"""
Session model for tryout attempt tracking.
Represents a student's attempt at a tryout with scoring information.
"""
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 Session(Base):
"""
Session model representing a student's tryout attempt.
Tracks session metadata, scoring results, and IRT estimates.
Attributes:
id: Primary key
session_id: Unique session identifier
wp_user_id: WordPress user ID
website_id: Website identifier
tryout_id: Tryout identifier
start_time: Session start timestamp
end_time: Session end timestamp
is_completed: Completion status
scoring_mode_used: Scoring mode used for this session
total_benar: Total correct answers
total_bobot_earned: Total weight earned
NM: Nilai Mentah (raw score) [0, 1000]
NN: Nilai Nasional (normalized score) [0, 1000]
theta: IRT ability estimate [-3, +3]
theta_se: IRT standard error
rataan_used: Mean value used for normalization
sb_used: Standard deviation used for normalization
created_at: Record creation timestamp
updated_at: Record update timestamp
user: User relationship
tryout: Tryout relationship
user_answers: User's responses in this session
"""
__tablename__ = "sessions"
# Primary key
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
# Session identifier (globally unique)
session_id: Mapped[str] = mapped_column(
String(255),
nullable=False,
unique=True,
index=True,
comment="Unique session identifier",
)
# Foreign keys
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"
)
# Timestamps
start_time: Mapped[datetime] = mapped_column(
DateTime(timezone=True), nullable=False, server_default="NOW()"
)
end_time: Mapped[Union[datetime, None]] = mapped_column(
DateTime(timezone=True), nullable=True, comment="Session end timestamp"
)
is_completed: Mapped[bool] = mapped_column(
Boolean, nullable=False, default=False, comment="Completion status"
)
# Scoring metadata
scoring_mode_used: Mapped[Literal["ctt", "irt", "hybrid"]] = mapped_column(
String(50),
nullable=False,
comment="Scoring mode used for this session",
)
# CTT scoring results
total_benar: Mapped[int] = mapped_column(
Integer, nullable=False, default=0, comment="Total correct answers"
)
total_bobot_earned: Mapped[float] = mapped_column(
Float, nullable=False, default=0.0, comment="Total weight earned"
)
NM: Mapped[Union[int, None]] = mapped_column(
Integer,
nullable=True,
comment="Nilai Mentah (raw score) [0, 1000]",
)
NN: Mapped[Union[int, None]] = mapped_column(
Integer,
nullable=True,
comment="Nilai Nasional (normalized score) [0, 1000]",
)
# IRT scoring results
theta: Mapped[Union[float, None]] = mapped_column(
Float,
nullable=True,
comment="IRT ability estimate [-3, +3]",
)
theta_se: Mapped[Union[float, None]] = mapped_column(
Float,
nullable=True,
comment="IRT standard error",
)
# Normalization metadata
rataan_used: Mapped[Union[float, None]] = mapped_column(
Float,
nullable=True,
comment="Mean value used for normalization",
)
sb_used: Mapped[Union[float, None]] = mapped_column(
Float,
nullable=True,
comment="Standard deviation used for normalization",
)
# 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
user: Mapped["User"] = relationship(
"User", back_populates="sessions", lazy="selectin"
)
tryout: Mapped["Tryout"] = relationship(
"Tryout", back_populates="sessions", lazy="selectin"
)
user_answers: Mapped[list["UserAnswer"]] = relationship(
"UserAnswer", back_populates="session", lazy="selectin", cascade="all, delete-orphan"
)
# Constraints and indexes
__table_args__ = (
Index("ix_sessions_wp_user_id", "wp_user_id"),
Index("ix_sessions_website_id", "website_id"),
Index("ix_sessions_tryout_id", "tryout_id"),
Index("ix_sessions_is_completed", "is_completed"),
# Score constraints [0, 1000]
CheckConstraint(
"NM IS NULL OR (NM >= 0 AND NM <= 1000)",
"ck_nm_range",
),
CheckConstraint(
"NN IS NULL OR (NN >= 0 AND NN <= 1000)",
"ck_nn_range",
),
# IRT theta constraint [-3, +3]
CheckConstraint(
"theta IS NULL OR (theta >= -3 AND theta <= 3)",
"ck_theta_range",
),
# Total correct must be non-negative
CheckConstraint("total_benar >= 0", "ck_total_benar_non_negative"),
# Total bobot must be non-negative
CheckConstraint("total_bobot_earned >= 0", "ck_total_bobot_non_negative"),
)
def __repr__(self) -> str:
return f"<Session(session_id={self.session_id}, tryout_id={self.tryout_id})>"