import uuid from datetime import datetime, timezone from sqlalchemy import ( Boolean, DateTime, ForeignKey, Integer, String, Text ) from sqlalchemy.orm import Mapped, mapped_column, relationship from app.core.database import Base def new_uuid() -> str: return str(uuid.uuid4()) def now_utc() -> datetime: return datetime.now(timezone.utc) # ── User ────────────────────────────────────────────────────────────────────── class User(Base): __tablename__ = "users" id: Mapped[str] = mapped_column(String(36), primary_key=True, default=new_uuid) username: Mapped[str] = mapped_column(String(64), unique=True, nullable=False) email: Mapped[str] = mapped_column(String(255), unique=True, nullable=False) password_hash: Mapped[str] = mapped_column(String(255), nullable=False) created_at: Mapped[datetime] = mapped_column(DateTime, default=now_utc) projects: Mapped[list["Project"]] = relationship("Project", back_populates="owner") memberships: Mapped[list["ProjectMember"]] = relationship("ProjectMember", back_populates="user") songs: Mapped[list["Song"]] = relationship("Song", back_populates="owner") alternatives: Mapped[list["DanceAlternative"]] = relationship("DanceAlternative", foreign_keys="DanceAlternative.created_by", back_populates="creator") alt_ratings: Mapped[list["DanceAlternativeRating"]] = relationship("DanceAlternativeRating", foreign_keys="DanceAlternativeRating.user_id", back_populates="user") # ── Project ─────────────────────────────────────────────────────────────────── class Project(Base): __tablename__ = "projects" id: Mapped[str] = mapped_column(String(36), primary_key=True, default=new_uuid) owner_id: Mapped[str] = mapped_column(String(36), ForeignKey("users.id"), nullable=False) name: Mapped[str] = mapped_column(String(128), nullable=False) description: Mapped[str] = mapped_column(Text, default="") is_public: Mapped[bool] = mapped_column(Boolean, default=False) updated_at: Mapped[datetime] = mapped_column(DateTime, default=now_utc, onupdate=now_utc) owner: Mapped["User"] = relationship("User", back_populates="projects") members: Mapped[list["ProjectMember"]] = relationship("ProjectMember", back_populates="project", cascade="all, delete-orphan") project_songs: Mapped[list["ProjectSong"]] = relationship("ProjectSong", back_populates="project", order_by="ProjectSong.position", cascade="all, delete-orphan") class ProjectMember(Base): __tablename__ = "project_members" id: Mapped[str] = mapped_column(String(36), primary_key=True, default=new_uuid) project_id: Mapped[str] = mapped_column(String(36), ForeignKey("projects.id"), nullable=False) user_id: Mapped[str] = mapped_column(String(36), ForeignKey("users.id"), nullable=False) role: Mapped[str] = mapped_column(String(16), default="viewer") # owner | editor | viewer status: Mapped[str] = mapped_column(String(16), default="pending") # pending | accepted invited_at: Mapped[datetime] = mapped_column(DateTime, default=now_utc) project: Mapped["Project"] = relationship("Project", back_populates="members") user: Mapped["User"] = relationship("User", back_populates="memberships") # ── Song ────────────────────────────────────────────────────────────────────── class Song(Base): __tablename__ = "songs" id: Mapped[str] = mapped_column(String(36), primary_key=True, default=new_uuid) owner_id: Mapped[str] = mapped_column(String(36), ForeignKey("users.id"), nullable=False) title: Mapped[str] = mapped_column(String(255), nullable=False) artist: Mapped[str] = mapped_column(String(255), default="") local_path: Mapped[str] = mapped_column(String(512), default="") # kun relevant på PC bpm: Mapped[int] = mapped_column(Integer, default=0) duration_sec: Mapped[int] = mapped_column(Integer, default=0) synced_at: Mapped[datetime] = mapped_column(DateTime, default=now_utc) owner: Mapped["User"] = relationship("User", back_populates="songs") dances: Mapped[list["SongDance"]] = relationship("SongDance", back_populates="song", order_by="SongDance.dance_order", cascade="all, delete-orphan") project_songs: Mapped[list["ProjectSong"]] = relationship("ProjectSong", back_populates="song") class ProjectSong(Base): __tablename__ = "project_songs" id: Mapped[str] = mapped_column(String(36), primary_key=True, default=new_uuid) project_id: Mapped[str] = mapped_column(String(36), ForeignKey("projects.id"), nullable=False) song_id: Mapped[str] = mapped_column(String(36), ForeignKey("songs.id"), nullable=False) position: Mapped[int] = mapped_column(Integer, nullable=False) status: Mapped[str] = mapped_column(String(16), default="pending") # pending | playing | played | skipped project: Mapped["Project"] = relationship("Project", back_populates="project_songs") song: Mapped["Song"] = relationship("Song", back_populates="project_songs") # ── Dance ───────────────────────────────────────────────────────────────────── class SongDance(Base): __tablename__ = "song_dances" id: Mapped[str] = mapped_column(String(36), primary_key=True, default=new_uuid) song_id: Mapped[str] = mapped_column(String(36), ForeignKey("songs.id"), nullable=False) dance_name: Mapped[str] = mapped_column(String(128), nullable=False) dance_order: Mapped[int] = mapped_column(Integer, default=1) song: Mapped["Song"] = relationship("Song", back_populates="dances") alternatives: Mapped[list["DanceAlternative"]] = relationship("DanceAlternative", foreign_keys="DanceAlternative.song_dance_id", back_populates="song_dance", cascade="all, delete-orphan") class DanceAlternative(Base): __tablename__ = "dance_alternatives" id: Mapped[str] = mapped_column(String(36), primary_key=True, default=new_uuid) song_dance_id: Mapped[str] = mapped_column(String(36), ForeignKey("song_dances.id"), nullable=False) alt_song_dance_id: Mapped[str] = mapped_column(String(36), ForeignKey("song_dances.id"), nullable=False) created_by: Mapped[str] = mapped_column(String(36), ForeignKey("users.id"), nullable=False) note: Mapped[str] = mapped_column(Text, default="") bayesian_score: Mapped[float] = mapped_column(default=0.0) # genberegnes ved hver rating song_dance: Mapped["SongDance"] = relationship("SongDance", foreign_keys=[song_dance_id], back_populates="alternatives") alt_song_dance: Mapped["SongDance"] = relationship("SongDance", foreign_keys=[alt_song_dance_id]) creator: Mapped["User"] = relationship("User", foreign_keys=[created_by]) ratings: Mapped[list["DanceAlternativeRating"]] = relationship("DanceAlternativeRating", back_populates="alternative", cascade="all, delete-orphan") class DanceAlternativeRating(Base): __tablename__ = "dance_alternative_ratings" id: Mapped[str] = mapped_column(String(36), primary_key=True, default=new_uuid) alternative_id: Mapped[str] = mapped_column(String(36), ForeignKey("dance_alternatives.id"), nullable=False) user_id: Mapped[str] = mapped_column(String(36), ForeignKey("users.id"), nullable=False) score: Mapped[int] = mapped_column(Integer, nullable=False) # 1-5 alternative: Mapped["DanceAlternative"] = relationship("DanceAlternative", back_populates="ratings") user: Mapped["User"] = relationship("User", foreign_keys=[user_id])