fix: harden admin access, repair ORM joins, and add migration/tests

This commit is contained in:
dwindown
2026-04-01 14:59:54 +07:00
parent de592d140e
commit 16ab13e911
21 changed files with 1275 additions and 368 deletions

View File

@@ -14,11 +14,13 @@ from sqlalchemy import (
DateTime,
Float,
ForeignKey,
ForeignKeyConstraint,
Index,
Integer,
JSON,
String,
Text,
func,
)
from sqlalchemy.orm import Mapped, mapped_column, relationship
@@ -156,13 +158,13 @@ class Item(Base):
# Timestamps
created_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), nullable=False, server_default="NOW()"
DateTime(timezone=True), nullable=False, server_default=func.now()
)
updated_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True),
nullable=False,
server_default="NOW()",
onupdate="NOW()",
server_default=func.now(),
onupdate=func.now(),
)
# Relationships
@@ -188,6 +190,13 @@ class Item(Base):
# Constraints and indexes
__table_args__ = (
ForeignKeyConstraint(
["website_id", "tryout_id"],
["tryouts.website_id", "tryouts.tryout_id"],
name="fk_items_tryout",
ondelete="CASCADE",
onupdate="CASCADE",
),
Index(
"ix_items_tryout_id_website_id_slot",
"tryout_id",

View File

@@ -13,9 +13,11 @@ from sqlalchemy import (
DateTime,
Float,
ForeignKey,
ForeignKeyConstraint,
Index,
Integer,
String,
func,
)
from sqlalchemy.orm import Mapped, mapped_column, relationship
@@ -82,7 +84,7 @@ class Session(Base):
# Timestamps
start_time: Mapped[datetime] = mapped_column(
DateTime(timezone=True), nullable=False, server_default="NOW()"
DateTime(timezone=True), nullable=False, server_default=func.now()
)
end_time: Mapped[Union[datetime, None]] = mapped_column(
DateTime(timezone=True), nullable=True, comment="Session end timestamp"
@@ -144,21 +146,27 @@ class Session(Base):
# Timestamps
created_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), nullable=False, server_default="NOW()"
DateTime(timezone=True), nullable=False, server_default=func.now()
)
updated_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True),
nullable=False,
server_default="NOW()",
onupdate="NOW()",
server_default=func.now(),
onupdate=func.now(),
)
# Relationships
user: Mapped["User"] = relationship(
"User", back_populates="sessions", lazy="selectin"
"User",
back_populates="sessions",
lazy="selectin",
overlaps="tryout,sessions",
)
tryout: Mapped["Tryout"] = relationship(
"Tryout", back_populates="sessions", lazy="selectin"
"Tryout",
back_populates="sessions",
lazy="selectin",
overlaps="user",
)
user_answers: Mapped[list["UserAnswer"]] = relationship(
"UserAnswer", back_populates="session", lazy="selectin", cascade="all, delete-orphan"
@@ -166,6 +174,20 @@ class Session(Base):
# Constraints and indexes
__table_args__ = (
ForeignKeyConstraint(
["website_id", "tryout_id"],
["tryouts.website_id", "tryouts.tryout_id"],
name="fk_sessions_tryout",
ondelete="CASCADE",
onupdate="CASCADE",
),
ForeignKeyConstraint(
["wp_user_id", "website_id"],
["users.wp_user_id", "users.website_id"],
name="fk_sessions_user",
ondelete="CASCADE",
onupdate="CASCADE",
),
Index("ix_sessions_wp_user_id", "wp_user_id"),
Index("ix_sessions_website_id", "website_id"),
Index("ix_sessions_tryout_id", "tryout_id"),

View File

@@ -7,7 +7,17 @@ Represents tryout exams with configurable scoring, selection, and normalization
from datetime import datetime
from typing import Literal, Union
from sqlalchemy import Boolean, CheckConstraint, DateTime, Float, ForeignKey, Index, Integer, String
from sqlalchemy import (
Boolean,
CheckConstraint,
DateTime,
Float,
ForeignKey,
Integer,
String,
UniqueConstraint,
func,
)
from sqlalchemy.orm import Mapped, mapped_column, relationship
from app.database import Base
@@ -146,13 +156,13 @@ class Tryout(Base):
# Timestamps
created_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), nullable=False, server_default="NOW()"
DateTime(timezone=True), nullable=False, server_default=func.now()
)
updated_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True),
nullable=False,
server_default="NOW()",
onupdate="NOW()",
server_default=func.now(),
onupdate=func.now(),
)
# Relationships
@@ -163,7 +173,11 @@ class Tryout(Base):
"Item", back_populates="tryout", lazy="selectin", cascade="all, delete-orphan"
)
sessions: Mapped[list["Session"]] = relationship(
"Session", back_populates="tryout", lazy="selectin", cascade="all, delete-orphan"
"Session",
back_populates="tryout",
lazy="selectin",
cascade="all, delete-orphan",
overlaps="user",
)
stats: Mapped["TryoutStats"] = relationship(
"TryoutStats", back_populates="tryout", lazy="selectin", uselist=False
@@ -171,8 +185,10 @@ class Tryout(Base):
# Constraints and indexes
__table_args__ = (
Index(
"ix_tryouts_website_id_tryout_id", "website_id", "tryout_id", unique=True
UniqueConstraint(
"website_id",
"tryout_id",
name="uq_tryouts_website_id_tryout_id",
),
CheckConstraint("min_sample_for_dynamic > 0", "ck_min_sample_positive"),
CheckConstraint("static_rataan > 0", "ck_static_rataan_positive"),

View File

@@ -7,7 +7,17 @@ Maintains running statistics for dynamic normalization and reporting.
from datetime import datetime
from typing import Union
from sqlalchemy import CheckConstraint, DateTime, Float, ForeignKey, Index, Integer, String
from sqlalchemy import (
CheckConstraint,
DateTime,
Float,
ForeignKey,
ForeignKeyConstraint,
Index,
Integer,
String,
func,
)
from sqlalchemy.orm import Mapped, mapped_column, relationship
from app.database import Base
@@ -107,13 +117,13 @@ class TryoutStats(Base):
comment="Timestamp of last statistics update",
)
created_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), nullable=False, server_default="NOW()"
DateTime(timezone=True), nullable=False, server_default=func.now()
)
updated_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True),
nullable=False,
server_default="NOW()",
onupdate="NOW()",
server_default=func.now(),
onupdate=func.now(),
)
# Relationships
@@ -123,6 +133,13 @@ class TryoutStats(Base):
# Constraints and indexes
__table_args__ = (
ForeignKeyConstraint(
["website_id", "tryout_id"],
["tryouts.website_id", "tryouts.tryout_id"],
name="fk_tryout_stats_tryout",
ondelete="CASCADE",
onupdate="CASCADE",
),
Index(
"ix_tryout_stats_website_id_tryout_id",
"website_id",

View File

@@ -6,7 +6,7 @@ Represents users from WordPress that can take tryouts.
from datetime import datetime
from sqlalchemy import DateTime, ForeignKey, Index, String
from sqlalchemy import DateTime, ForeignKey, Index, String, UniqueConstraint, func
from sqlalchemy.orm import Mapped, mapped_column, relationship
from app.database import Base
@@ -31,7 +31,7 @@ class User(Base):
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
# WordPress user ID (unique within website context)
wp_user_id: Mapped[int] = mapped_column(
wp_user_id: Mapped[str] = mapped_column(
String(255), nullable=False, index=True, comment="WordPress user ID"
)
@@ -44,13 +44,13 @@ class User(Base):
# Timestamps
created_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), nullable=False, server_default="NOW()"
DateTime(timezone=True), nullable=False, server_default=func.now()
)
updated_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True),
nullable=False,
server_default="NOW()",
onupdate="NOW()",
server_default=func.now(),
onupdate=func.now(),
)
# Relationships
@@ -58,12 +58,20 @@ class User(Base):
"Website", back_populates="users", lazy="selectin"
)
sessions: Mapped[list["Session"]] = relationship(
"Session", back_populates="user", lazy="selectin", cascade="all, delete-orphan"
"Session",
back_populates="user",
lazy="selectin",
cascade="all, delete-orphan",
overlaps="sessions,tryout",
)
# Indexes
__table_args__ = (
Index("ix_users_wp_user_id_website_id", "wp_user_id", "website_id", unique=True),
UniqueConstraint(
"wp_user_id",
"website_id",
name="uq_users_wp_user_id_website_id",
),
Index("ix_users_website_id", "website_id"),
)

View File

@@ -7,7 +7,7 @@ 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 import Boolean, CheckConstraint, DateTime, Float, ForeignKey, Index, Integer, String, func
from sqlalchemy.orm import Mapped, mapped_column, relationship
from app.database import Base
@@ -94,13 +94,13 @@ class UserAnswer(Base):
# Timestamps
created_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), nullable=False, server_default="NOW()"
DateTime(timezone=True), nullable=False, server_default=func.now()
)
updated_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True),
nullable=False,
server_default="NOW()",
onupdate="NOW()",
server_default=func.now(),
onupdate=func.now(),
)
# Relationships

View File

@@ -6,7 +6,7 @@ Represents WordPress websites that use the IRT Bank Soal system.
from datetime import datetime
from sqlalchemy import DateTime, String
from sqlalchemy import DateTime, String, func
from sqlalchemy.orm import Mapped, mapped_column, relationship
from app.database import Base
@@ -48,13 +48,13 @@ class Website(Base):
# Timestamps
created_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), nullable=False, server_default="NOW()"
DateTime(timezone=True), nullable=False, server_default=func.now()
)
updated_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True),
nullable=False,
server_default="NOW()",
onupdate="NOW()",
server_default=func.now(),
onupdate=func.now(),
)
# Relationships