Start
This commit is contained in:
180
linedance-api/app/models/__init__.py
Normal file
180
linedance-api/app/models/__init__.py
Normal file
@@ -0,0 +1,180 @@
|
||||
import uuid
|
||||
from datetime import datetime, timezone
|
||||
from sqlalchemy import Boolean, DateTime, ForeignKey, Integer, String, Text, Float, UniqueConstraint
|
||||
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")
|
||||
alt_ratings: Mapped[list["DanceAltRating"]] = relationship("DanceAltRating", back_populates="user")
|
||||
|
||||
|
||||
# ── 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="")
|
||||
album: Mapped[str] = mapped_column(String(255), default="")
|
||||
bpm: Mapped[int] = mapped_column(Integer, default=0)
|
||||
duration_sec: Mapped[int] = mapped_column(Integer, default=0)
|
||||
file_format: Mapped[str] = mapped_column(String(8), default="")
|
||||
mbid: Mapped[str|None] = mapped_column(String(36), nullable=True) # MusicBrainz ID
|
||||
acoustid: Mapped[str|None] = mapped_column(String(64), nullable=True) # AcoustID fingerprint
|
||||
synced_at: Mapped[datetime] = mapped_column(DateTime, default=now_utc)
|
||||
|
||||
owner: Mapped["User"] = relationship("User", back_populates="songs")
|
||||
project_songs: Mapped[list["ProjectSong"]] = relationship("ProjectSong", back_populates="song")
|
||||
|
||||
|
||||
# ── Dans-entitet ──────────────────────────────────────────────────────────────
|
||||
|
||||
class DanceLevel(Base):
|
||||
__tablename__ = "dance_levels"
|
||||
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
|
||||
sort_order: Mapped[int] = mapped_column(Integer, nullable=False)
|
||||
name: Mapped[str] = mapped_column(String(64), unique=True, nullable=False)
|
||||
description: Mapped[str] = mapped_column(String(255), default="")
|
||||
|
||||
|
||||
class Dance(Base):
|
||||
"""Dans-entitet: navn + niveau er unik kombination."""
|
||||
__tablename__ = "dances"
|
||||
__table_args__ = (UniqueConstraint("name", "level_id", name="uq_dance_name_level"),)
|
||||
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
|
||||
name: Mapped[str] = mapped_column(String(128), nullable=False)
|
||||
level_id: Mapped[int|None] = mapped_column(Integer, ForeignKey("dance_levels.id"), nullable=True)
|
||||
use_count: Mapped[int] = mapped_column(Integer, default=1)
|
||||
source: Mapped[str] = mapped_column(String(16), default="local") # local | community
|
||||
synced_at: Mapped[datetime|None] = mapped_column(DateTime, nullable=True)
|
||||
|
||||
level: Mapped["DanceLevel|None"] = relationship("DanceLevel")
|
||||
|
||||
|
||||
# ── Project / Playlist ────────────────────────────────────────────────────────
|
||||
|
||||
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")
|
||||
|
||||
|
||||
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|played|skipped
|
||||
|
||||
project: Mapped["Project"] = relationship("Project", back_populates="project_songs")
|
||||
song: Mapped["Song"] = relationship("Song", back_populates="project_songs")
|
||||
|
||||
|
||||
# ── Community dans-tags ───────────────────────────────────────────────────────
|
||||
|
||||
class CommunityDance(Base):
|
||||
"""Fællesskabets dans-tags på sange — identificeret ved mbid eller titel+artist."""
|
||||
__tablename__ = "community_dances"
|
||||
__table_args__ = (UniqueConstraint("song_mbid", "dance_id", name="uq_comm_dance"),)
|
||||
|
||||
id: Mapped[str] = mapped_column(String(36), primary_key=True, default=new_uuid)
|
||||
song_mbid: Mapped[str|None] = mapped_column(String(36), nullable=True)
|
||||
song_title: Mapped[str] = mapped_column(String(255), default="")
|
||||
song_artist: Mapped[str] = mapped_column(String(255), default="")
|
||||
dance_id: Mapped[int] = mapped_column(Integer, ForeignKey("dances.id"), nullable=False)
|
||||
submitted_by: Mapped[str] = mapped_column(String(36), ForeignKey("users.id"), nullable=False)
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime, default=now_utc)
|
||||
|
||||
dance: Mapped["Dance"] = relationship("Dance")
|
||||
|
||||
|
||||
# ── Community alternativ-dans + rating ────────────────────────────────────────
|
||||
|
||||
class CommunityDanceAlt(Base):
|
||||
"""Fællesskabets alternativ-danse til en sang — uafhængigt af sangens hoveddanse."""
|
||||
__tablename__ = "community_dance_alts"
|
||||
__table_args__ = (
|
||||
UniqueConstraint("song_mbid", "song_title", "song_artist",
|
||||
"alt_dance_id", name="uq_comm_alt"),
|
||||
)
|
||||
|
||||
id: Mapped[str] = mapped_column(String(36), primary_key=True, default=new_uuid)
|
||||
song_mbid: Mapped[str|None] = mapped_column(String(36), nullable=True)
|
||||
song_title: Mapped[str] = mapped_column(String(255), default="")
|
||||
song_artist: Mapped[str] = mapped_column(String(255), default="")
|
||||
alt_dance_id: Mapped[int] = mapped_column(Integer, ForeignKey("dances.id"), nullable=False)
|
||||
note: Mapped[str] = mapped_column(Text, default="")
|
||||
submitted_by: Mapped[str] = mapped_column(String(36), ForeignKey("users.id"), nullable=False)
|
||||
avg_rating: Mapped[float] = mapped_column(Float, default=0.0)
|
||||
rating_count: Mapped[int] = mapped_column(Integer, default=0)
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime, default=now_utc)
|
||||
|
||||
alt_dance: Mapped["Dance"] = relationship("Dance", foreign_keys=[alt_dance_id])
|
||||
ratings: Mapped[list["DanceAltRating"]] = relationship("DanceAltRating", back_populates="alternative", cascade="all, delete-orphan")
|
||||
|
||||
|
||||
class DanceAltRating(Base):
|
||||
"""En brugers 1-5 stjerne rating — knyttet til sang + dans + alternativ + bruger."""
|
||||
__tablename__ = "dance_alt_ratings"
|
||||
__table_args__ = (
|
||||
UniqueConstraint("alternative_id", "user_id", name="uq_rating"),
|
||||
)
|
||||
|
||||
id: Mapped[str] = mapped_column(String(36), primary_key=True, default=new_uuid)
|
||||
alternative_id: Mapped[str] = mapped_column(String(36), ForeignKey("community_dance_alts.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
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime, default=now_utc)
|
||||
|
||||
alternative: Mapped["CommunityDanceAlt"] = relationship("CommunityDanceAlt", back_populates="ratings")
|
||||
user: Mapped["User"] = relationship("User", back_populates="alt_ratings")
|
||||
Reference in New Issue
Block a user