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

223 lines
6.7 KiB
Python

"""
Item model for questions with CTT and IRT parameters.
Represents individual questions with both classical test theory (CTT)
and item response theory (IRT) parameters.
"""
from datetime import datetime
from typing import Literal, Union
from sqlalchemy import (
Boolean,
CheckConstraint,
DateTime,
Float,
ForeignKey,
Index,
Integer,
JSON,
String,
Text,
)
from sqlalchemy.orm import Mapped, mapped_column, relationship
from app.database import Base
class Item(Base):
"""
Item model representing individual questions.
Supports both CTT (p, bobot, category) and IRT (b, se) parameters.
Tracks AI generation metadata and calibration status.
Attributes:
id: Primary key
tryout_id: Tryout identifier
website_id: Website identifier
slot: Question position in tryout
level: Difficulty level (mudah, sedang, sulit)
stem: Question text
options: JSON array of answer options
correct_answer: Correct option (A, B, C, D)
explanation: Answer explanation
ctt_p: CTT difficulty (proportion correct)
ctt_bobot: CTT weight (1 - p)
ctt_category: CTT difficulty category
irt_b: IRT difficulty parameter [-3, +3]
irt_se: IRT standard error
calibrated: Calibration status
calibration_sample_size: Sample size for calibration
generated_by: Generation source (manual, ai)
ai_model: AI model used (if generated by AI)
basis_item_id: Original item ID (for AI variants)
created_at: Record creation timestamp
updated_at: Record update timestamp
tryout: Tryout relationship
user_answers: User responses to this item
"""
__tablename__ = "items"
# Primary key
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
# Foreign keys
tryout_id: Mapped[str] = mapped_column(
String(255), nullable=False, index=True, comment="Tryout identifier"
)
website_id: Mapped[int] = mapped_column(
ForeignKey("websites.id", ondelete="CASCADE", onupdate="CASCADE"),
nullable=False,
index=True,
comment="Website identifier",
)
# Position and difficulty
slot: Mapped[int] = mapped_column(
Integer, nullable=False, comment="Question position in tryout"
)
level: Mapped[Literal["mudah", "sedang", "sulit"]] = mapped_column(
String(50), nullable=False, comment="Difficulty level"
)
# Question content
stem: Mapped[str] = mapped_column(Text, nullable=False, comment="Question text")
options: Mapped[dict] = mapped_column(
JSON,
nullable=False,
comment="JSON object with options (e.g., {\"A\": \"option1\", \"B\": \"option2\"})",
)
correct_answer: Mapped[str] = mapped_column(
String(10), nullable=False, comment="Correct option (A, B, C, D)"
)
explanation: Mapped[Union[str, None]] = mapped_column(
Text, nullable=True, comment="Answer explanation"
)
# CTT parameters
ctt_p: Mapped[Union[float, None]] = mapped_column(
Float,
nullable=True,
comment="CTT difficulty (proportion correct)",
)
ctt_bobot: Mapped[Union[float, None]] = mapped_column(
Float,
nullable=True,
comment="CTT weight (1 - p)",
)
ctt_category: Mapped[Union[Literal["mudah", "sedang", "sulit"], None]] = mapped_column(
String(50),
nullable=True,
comment="CTT difficulty category",
)
# IRT parameters (1PL Rasch model)
irt_b: Mapped[Union[float, None]] = mapped_column(
Float,
nullable=True,
comment="IRT difficulty parameter [-3, +3]",
)
irt_se: Mapped[Union[float, None]] = mapped_column(
Float,
nullable=True,
comment="IRT standard error",
)
# Calibration status
calibrated: Mapped[bool] = mapped_column(
Boolean, nullable=False, default=False, comment="Calibration status"
)
calibration_sample_size: Mapped[int] = mapped_column(
Integer,
nullable=False,
default=0,
comment="Sample size for calibration",
)
# AI generation metadata
generated_by: Mapped[Literal["manual", "ai"]] = mapped_column(
String(50),
nullable=False,
default="manual",
comment="Generation source",
)
ai_model: Mapped[Union[str, None]] = mapped_column(
String(255),
nullable=True,
comment="AI model used (if generated by AI)",
)
basis_item_id: Mapped[Union[int, None]] = mapped_column(
ForeignKey("items.id", ondelete="SET NULL", onupdate="CASCADE"),
nullable=True,
index=True,
comment="Original item ID (for AI variants)",
)
# 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
tryout: Mapped["Tryout"] = relationship(
"Tryout", back_populates="items", lazy="selectin"
)
user_answers: Mapped[list["UserAnswer"]] = relationship(
"UserAnswer", back_populates="item", lazy="selectin", cascade="all, delete-orphan"
)
basis_item: Mapped[Union["Item", None]] = relationship(
"Item",
remote_side=[id],
back_populates="variants",
lazy="selectin",
single_parent=True,
)
variants: Mapped[list["Item"]] = relationship(
"Item",
back_populates="basis_item",
lazy="selectin",
cascade="all, delete-orphan",
)
# Constraints and indexes
__table_args__ = (
Index(
"ix_items_tryout_id_website_id_slot",
"tryout_id",
"website_id",
"slot",
"level",
unique=True,
),
Index("ix_items_calibrated", "calibrated"),
Index("ix_items_basis_item_id", "basis_item_id"),
# IRT b parameter constraint [-3, +3]
CheckConstraint(
"irt_b IS NULL OR (irt_b >= -3 AND irt_b <= 3)",
"ck_irt_b_range",
),
# CTT p constraint [0, 1]
CheckConstraint(
"ctt_p IS NULL OR (ctt_p >= 0 AND ctt_p <= 1)",
"ck_ctt_p_range",
),
# CTT bobot constraint [0, 1]
CheckConstraint(
"ctt_bobot IS NULL OR (ctt_bobot >= 0 AND ctt_bobot <= 1)",
"ck_ctt_bobot_range",
),
# Slot must be positive
CheckConstraint("slot > 0", "ck_slot_positive"),
)
def __repr__(self) -> str:
return f"<Item(id={self.id}, slot={self.slot}, level={self.level})>"