194 lines
6.0 KiB
Python
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})>"
|