from __future__ import annotations from datetime import datetime from typing import Optional from sqlalchemy import Boolean, DateTime, ForeignKey, Integer, String, Text, UniqueConstraint from sqlalchemy.orm import Mapped, mapped_column, relationship from app.database import Base class TimestampMixin: created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow) updated_at: Mapped[datetime] = mapped_column( DateTime, default=datetime.utcnow, onupdate=datetime.utcnow ) class Admin(TimestampMixin, Base): __tablename__ = "admins" id: Mapped[int] = mapped_column(primary_key=True) username: Mapped[str] = mapped_column(String(80), unique=True, index=True) display_name: Mapped[str] = mapped_column(String(120)) password_hash: Mapped[str] = mapped_column(String(255)) role: Mapped[str] = mapped_column(String(32), default="admin") is_active: Mapped[bool] = mapped_column(Boolean, default=True) last_seen_at: Mapped[Optional[datetime]] = mapped_column(DateTime, nullable=True) created_activities: Mapped[list["Activity"]] = relationship(back_populates="created_by") reviewed_submissions: Mapped[list["Submission"]] = relationship( back_populates="reviewed_by", foreign_keys="Submission.reviewed_by_id", ) assigned_submissions: Mapped[list["Submission"]] = relationship( back_populates="assigned_admin", foreign_keys="Submission.assigned_admin_id", ) class Group(TimestampMixin, Base): __tablename__ = "groups" id: Mapped[int] = mapped_column(primary_key=True) name: Mapped[str] = mapped_column(String(120), unique=True) max_members: Mapped[int] = mapped_column(Integer, default=6) members: Mapped[list["User"]] = relationship(back_populates="group") submissions: Mapped[list["Submission"]] = relationship(back_populates="group") class User(TimestampMixin, Base): __tablename__ = "users" id: Mapped[int] = mapped_column(primary_key=True) student_id: Mapped[str] = mapped_column(String(64), unique=True, index=True) full_name: Mapped[str] = mapped_column(String(120)) password_hash: Mapped[str] = mapped_column(String(255)) is_active: Mapped[bool] = mapped_column(Boolean, default=True) group_id: Mapped[Optional[int]] = mapped_column(ForeignKey("groups.id"), nullable=True) last_seen_at: Mapped[Optional[datetime]] = mapped_column(DateTime, nullable=True) group: Mapped[Optional["Group"]] = relationship(back_populates="members") submissions: Mapped[list["Submission"]] = relationship(back_populates="user") class Activity(TimestampMixin, Base): __tablename__ = "activities" id: Mapped[int] = mapped_column(primary_key=True) title: Mapped[str] = mapped_column(String(160)) description: Mapped[str] = mapped_column(Text, default="") start_at: Mapped[datetime] = mapped_column(DateTime) deadline_at: Mapped[datetime] = mapped_column(DateTime) is_visible: Mapped[bool] = mapped_column(Boolean, default=True) leaderboard_visible: Mapped[bool] = mapped_column(Boolean, default=True) clue_interval_minutes: Mapped[Optional[int]] = mapped_column(Integer, nullable=True) created_by_id: Mapped[int] = mapped_column(ForeignKey("admins.id")) created_by: Mapped["Admin"] = relationship(back_populates="created_activities") tasks: Mapped[list["Task"]] = relationship( back_populates="activity", order_by="Task.display_order", cascade="all, delete-orphan", ) class Task(TimestampMixin, Base): __tablename__ = "tasks" id: Mapped[int] = mapped_column(primary_key=True) activity_id: Mapped[int] = mapped_column(ForeignKey("activities.id")) title: Mapped[str] = mapped_column(String(160)) description: Mapped[str] = mapped_column(Text, default="") display_order: Mapped[int] = mapped_column(Integer, default=1) image_url: Mapped[Optional[str]] = mapped_column(String(1000), nullable=True) image_path: Mapped[Optional[str]] = mapped_column(String(600), nullable=True) image_mime: Mapped[str] = mapped_column(String(120), default="image/jpeg") image_filename: Mapped[str] = mapped_column(String(255), default="task.jpg") clue_image_url: Mapped[Optional[str]] = mapped_column(String(1000), nullable=True) clue_image_path: Mapped[Optional[str]] = mapped_column(String(600), nullable=True) clue_image_mime: Mapped[Optional[str]] = mapped_column(String(120), nullable=True) clue_image_filename: Mapped[Optional[str]] = mapped_column(String(255), nullable=True) clue_release_at: Mapped[Optional[datetime]] = mapped_column(DateTime, nullable=True) activity: Mapped["Activity"] = relationship(back_populates="tasks") submissions: Mapped[list["Submission"]] = relationship( back_populates="task", cascade="all, delete-orphan" ) class Submission(TimestampMixin, Base): __tablename__ = "submissions" __table_args__ = ( UniqueConstraint("group_id", "task_id", name="uq_group_task_submission"), ) id: Mapped[int] = mapped_column(primary_key=True) task_id: Mapped[int] = mapped_column(ForeignKey("tasks.id")) user_id: Mapped[int] = mapped_column(ForeignKey("users.id")) group_id: Mapped[Optional[int]] = mapped_column(ForeignKey("groups.id"), nullable=True) stored_filename: Mapped[str] = mapped_column(String(255)) original_filename: Mapped[str] = mapped_column(String(255)) file_path: Mapped[str] = mapped_column(String(600)) mime_type: Mapped[str] = mapped_column(String(120), default="image/jpeg") file_size: Mapped[int] = mapped_column(Integer) status: Mapped[str] = mapped_column(String(32), default="pending") feedback: Mapped[Optional[str]] = mapped_column(Text, nullable=True) reviewed_by_id: Mapped[Optional[int]] = mapped_column( ForeignKey("admins.id"), nullable=True ) assigned_admin_id: Mapped[Optional[int]] = mapped_column( ForeignKey("admins.id"), nullable=True ) assigned_at: Mapped[Optional[datetime]] = mapped_column(DateTime, nullable=True) reviewed_at: Mapped[Optional[datetime]] = mapped_column(DateTime, nullable=True) approved_at: Mapped[Optional[datetime]] = mapped_column(DateTime, nullable=True) task: Mapped["Task"] = relationship(back_populates="submissions") user: Mapped["User"] = relationship(back_populates="submissions") group: Mapped[Optional["Group"]] = relationship(back_populates="submissions") reviewed_by: Mapped[Optional["Admin"]] = relationship( back_populates="reviewed_submissions", foreign_keys=[reviewed_by_id], ) assigned_admin: Mapped[Optional["Admin"]] = relationship( back_populates="assigned_submissions", foreign_keys=[assigned_admin_id], )