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

138 lines
4.4 KiB
Python

"""
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"<UserAnswer(id={self.id}, session_id={self.session_id}, item_id={self.item_id})>"