- Add Alembic migration setup with async configuration - Create 16 user data models (users, decks, cards, replays, etc.) - Implement comprehensive API endpoints with JWT auth - Add replay, card collection, group, network, preferences, and activity log routers - Include API documentation and migration test plan - Update Dockerfile to run migrations on startup
326 lines
13 KiB
Python
326 lines
13 KiB
Python
"""
|
|
SQLAlchemy ORM models for user data features.
|
|
|
|
Includes sessions, decks, replays, cards, groups, and networks.
|
|
"""
|
|
from sqlalchemy import (
|
|
Column, Integer, String, BigInteger, Boolean, DateTime, Text,
|
|
ForeignKey, Index, UniqueConstraint, Float, JSON
|
|
)
|
|
from sqlalchemy.orm import relationship
|
|
from sqlalchemy.sql import func
|
|
from app.core.database import Base
|
|
|
|
|
|
class UserSession(Base):
|
|
"""User authentication session."""
|
|
__tablename__ = "user_sessions"
|
|
|
|
id = Column(BigInteger, primary_key=True)
|
|
user_id = Column(Integer, ForeignKey("mtgonline_users.id", ondelete="CASCADE"), nullable=False, index=True)
|
|
session_token_hash = Column(String(255), unique=True, nullable=False, index=True)
|
|
ip_address = Column(String(45), nullable=True)
|
|
user_agent = Column(Text, nullable=True)
|
|
created_at = Column(DateTime, server_default=func.now())
|
|
expires_at = Column(DateTime, nullable=False)
|
|
is_active = Column(Boolean, default=True)
|
|
|
|
user = relationship("User", backref="sessions")
|
|
|
|
def __repr__(self) -> str:
|
|
return f"<UserSession user={self.user_id} expires={self.expires_at}>"
|
|
|
|
|
|
class DeckVersion(Base):
|
|
"""Deck version history."""
|
|
__tablename__ = "deck_versions"
|
|
|
|
id = Column(BigInteger, primary_key=True)
|
|
deck_id = Column(Integer, ForeignKey("mtgonline_decklist_files.id", ondelete="CASCADE"), nullable=False, index=True)
|
|
version_number = Column(Integer, nullable=False)
|
|
content = Column(Text, nullable=False)
|
|
status = Column(String(20), server_default="DRAFT")
|
|
comment = Column(Text, nullable=True)
|
|
created_at = Column(DateTime, server_default=func.now())
|
|
|
|
deck = relationship("DecklistFile", backref="versions")
|
|
|
|
def __repr__(self) -> str:
|
|
return f"<DeckVersion deck={self.deck_id} v={self.version_number}>"
|
|
|
|
|
|
class GameReplay(Base):
|
|
"""Game replay recording."""
|
|
__tablename__ = "game_replays"
|
|
|
|
id = Column(BigInteger, primary_key=True)
|
|
game_uuid = Column(String(36), unique=True, nullable=False)
|
|
room_id = Column(Integer, ForeignKey("mtgonline_rooms.id"), nullable=True, index=True)
|
|
game_type = Column(String(50), nullable=True)
|
|
format = Column(String(50), nullable=True)
|
|
duration_seconds = Column(Integer, nullable=True)
|
|
start_time = Column(DateTime, nullable=False)
|
|
end_time = Column(DateTime, nullable=True)
|
|
status = Column(String(20), server_default="IN_PROGRESS")
|
|
replay_data = Column(JSON, nullable=True)
|
|
created_at = Column(DateTime, server_default=func.now())
|
|
updated_at = Column(DateTime, server_default=func.now(), onupdate=func.now())
|
|
|
|
players = relationship("ReplayPlayer", back_populates="replay", cascade="all, delete-orphan")
|
|
outcomes = relationship("GameOutcome", back_populates="replay", cascade="all, delete-orphan")
|
|
|
|
def __repr__(self) -> str:
|
|
return f"<GameReplay {self.game_uuid} status={self.status}>"
|
|
|
|
|
|
class ReplayPlayer(Base):
|
|
"""Player in a game replay."""
|
|
__tablename__ = "replay_players"
|
|
|
|
id = Column(BigInteger, primary_key=True)
|
|
replay_id = Column(BigInteger, ForeignKey("game_replays.id", ondelete="CASCADE"), nullable=False, index=True)
|
|
user_id = Column(Integer, ForeignKey("mtgonline_users.id"), nullable=False, index=True)
|
|
position = Column(Integer, nullable=True)
|
|
deck_id = Column(Integer, ForeignKey("mtgonline_decklist_files.id"), nullable=True)
|
|
won = Column(Boolean, nullable=True)
|
|
lost = Column(Boolean, nullable=True)
|
|
concession = Column(Boolean, default=False)
|
|
turn_one = Column(Boolean, default=False)
|
|
created_at = Column(DateTime, server_default=func.now())
|
|
|
|
replay = relationship("GameReplay", back_populates="players")
|
|
user = relationship("User")
|
|
deck = relationship("DecklistFile")
|
|
|
|
def __repr__(self) -> str:
|
|
return f"<ReplayPlayer replay={self.replay_id} user={self.user_id}>"
|
|
|
|
|
|
class GameOutcome(Base):
|
|
"""Game outcome record."""
|
|
__tablename__ = "game_outcomes"
|
|
|
|
id = Column(BigInteger, primary_key=True)
|
|
user_id = Column(Integer, ForeignKey("mtgonline_users.id"), nullable=False, index=True)
|
|
game_uuid = Column(String(36), ForeignKey("game_replays.game_uuid"), nullable=False, index=True)
|
|
outcome = Column(String(20), nullable=False, index=True)
|
|
opponent_id = Column(Integer, ForeignKey("mtgonline_users.id"), nullable=True)
|
|
format = Column(String(50), nullable=True)
|
|
rating_before = Column(Integer, nullable=True)
|
|
rating_after = Column(Integer, nullable=True)
|
|
rating_change = Column(Integer, nullable=True)
|
|
created_at = Column(DateTime, server_default=func.now())
|
|
|
|
replay = relationship("GameReplay", back_populates="outcomes")
|
|
user = relationship("User", foreign_keys=[user_id])
|
|
opponent = relationship("User", foreign_keys=[opponent_id])
|
|
|
|
def __repr__(self) -> str:
|
|
return f"<GameOutcome user={self.user_id} {self.outcome}>"
|
|
|
|
|
|
class UserStatistics(Base):
|
|
"""User game statistics summary."""
|
|
__tablename__ = "user_statistics"
|
|
|
|
user_id = Column(Integer, ForeignKey("mtgonline_users.id"), primary_key=True)
|
|
total_games = Column(Integer, default=0)
|
|
total_wins = Column(Integer, default=0)
|
|
total_losses = Column(Integer, default=0)
|
|
total_concessions = Column(Integer, default=0)
|
|
win_rate = Column(Float, default=0.0)
|
|
current_streak = Column(Integer, default=0)
|
|
best_streak = Column(Integer, default=0)
|
|
average_rating = Column(Float, default=0.0)
|
|
last_game_date = Column(DateTime, nullable=True)
|
|
updated_at = Column(DateTime, server_default=func.now(), onupdate=func.now())
|
|
|
|
user = relationship("User")
|
|
|
|
def __repr__(self) -> str:
|
|
return f"<UserStatistics user={self.user_id} wins={self.total_wins} losses={self.total_losses}>"
|
|
|
|
|
|
class UserCardCollection(Base):
|
|
"""User card collection."""
|
|
__tablename__ = "user_card_collection"
|
|
|
|
id = Column(BigInteger, primary_key=True)
|
|
user_id = Column(Integer, ForeignKey("mtgonline_users.id", ondelete="CASCADE"), nullable=False, index=True)
|
|
card_id = Column(Integer, nullable=False, index=True)
|
|
quantity = Column(Integer, default=1)
|
|
condition = Column(String(20), server_default="NEAR_MINT")
|
|
language = Column(String(5), server_default="EN")
|
|
is_foil = Column(Boolean, default=False)
|
|
is_alt_art = Column(Boolean, default=False)
|
|
acquired_date = Column(DateTime, server_default=func.now())
|
|
acquisition_method = Column(String(50), nullable=True)
|
|
notes = Column(Text, nullable=True)
|
|
created_at = Column(DateTime, server_default=func.now())
|
|
updated_at = Column(DateTime, server_default=func.now(), onupdate=func.now())
|
|
|
|
__table_args__ = (
|
|
UniqueConstraint('user_id', 'card_id', 'is_foil', 'is_alt_art', name='uq_collection_unique'),
|
|
Index('idx_collection_user_card', 'user_id', 'card_id'),
|
|
)
|
|
|
|
user = relationship("User", backref="card_collection")
|
|
|
|
def __repr__(self) -> str:
|
|
return f"<UserCard user={self.user_id} card={self.card_id} qty={self.quantity}>"
|
|
|
|
|
|
class CardWishlist(Base):
|
|
"""User card wishlist."""
|
|
__tablename__ = "card_wishlist"
|
|
|
|
id = Column(BigInteger, primary_key=True)
|
|
user_id = Column(Integer, ForeignKey("mtgonline_users.id", ondelete="CASCADE"), nullable=False)
|
|
card_id = Column(Integer, nullable=False)
|
|
max_price = Column(Float, nullable=True)
|
|
notes = Column(Text, nullable=True)
|
|
created_at = Column(DateTime, server_default=func.now())
|
|
|
|
__table_args__ = (
|
|
UniqueConstraint('user_id', 'card_id', name='uq_wishlist_user_card'),
|
|
)
|
|
|
|
user = relationship("User", backref="wishlist")
|
|
|
|
def __repr__(self) -> str:
|
|
return f"<CardWishlist user={self.user_id} card={self.card_id}>"
|
|
|
|
|
|
class UserGroup(Base):
|
|
"""User group."""
|
|
__tablename__ = "user_groups"
|
|
|
|
id = Column(BigInteger, primary_key=True)
|
|
name = Column(String(100), nullable=False)
|
|
description = Column(Text, nullable=True)
|
|
owner_id = Column(Integer, ForeignKey("mtgonline_users.id"), nullable=False, index=True)
|
|
is_public = Column(Boolean, default=True)
|
|
max_members = Column(Integer, default=50)
|
|
created_at = Column(DateTime, server_default=func.now())
|
|
updated_at = Column(DateTime, server_default=func.now(), onupdate=func.now())
|
|
|
|
owner = relationship("User", foreign_keys=[owner_id])
|
|
members = relationship("GroupMember", back_populates="group", cascade="all, delete-orphan")
|
|
messages = relationship("GroupChatMessage", back_populates="group", cascade="all, delete-orphan")
|
|
|
|
def __repr__(self) -> str:
|
|
return f"<UserGroup {self.name} owner={self.owner_id}>"
|
|
|
|
|
|
class GroupMember(Base):
|
|
"""Group member."""
|
|
__tablename__ = "group_members"
|
|
|
|
id = Column(BigInteger, primary_key=True)
|
|
group_id = Column(BigInteger, ForeignKey("user_groups.id", ondelete="CASCADE"), nullable=False, index=True)
|
|
user_id = Column(Integer, ForeignKey("mtgonline_users.id"), nullable=False, index=True)
|
|
role = Column(String(20), server_default="MEMBER")
|
|
joined_at = Column(DateTime, server_default=func.now())
|
|
|
|
group = relationship("UserGroup", back_populates="members")
|
|
user = relationship("User")
|
|
|
|
__table_args__ = (
|
|
UniqueConstraint('group_id', 'user_id', name='uq_group_member'),
|
|
)
|
|
|
|
def __repr__(self) -> str:
|
|
return f"<GroupMember group={self.group_id} user={self.user_id} role={self.role}>"
|
|
|
|
|
|
class GroupChatMessage(Base):
|
|
"""Group chat message."""
|
|
__tablename__ = "group_chat_messages"
|
|
|
|
id = Column(BigInteger, primary_key=True)
|
|
group_id = Column(BigInteger, ForeignKey("user_groups.id", ondelete="CASCADE"), nullable=False, index=True)
|
|
sender_id = Column(Integer, ForeignKey("mtgonline_users.id"), nullable=False, index=True)
|
|
message = Column(Text, nullable=False)
|
|
created_at = Column(DateTime, server_default=func.now(), index=True)
|
|
|
|
group = relationship("UserGroup", back_populates="messages")
|
|
sender = relationship("User", foreign_keys=[sender_id])
|
|
|
|
def __repr__(self) -> str:
|
|
return f"<GroupChatMessage group={self.group_id} sender={self.sender_id}>"
|
|
|
|
|
|
class UserNetwork(Base):
|
|
"""User network (extended social connection)."""
|
|
__tablename__ = "user_networks"
|
|
|
|
id = Column(BigInteger, primary_key=True)
|
|
name = Column(String(100), nullable=False)
|
|
description = Column(Text, nullable=True)
|
|
creator_id = Column(Integer, ForeignKey("mtgonline_users.id"), nullable=False)
|
|
is_public = Column(Boolean, default=True)
|
|
created_at = Column(DateTime, server_default=func.now())
|
|
|
|
creator = relationship("User", foreign_keys=[creator_id])
|
|
members = relationship("NetworkMember", back_populates="network", cascade="all, delete-orphan")
|
|
|
|
def __repr__(self) -> str:
|
|
return f"<UserNetwork {self.name} creator={self.creator_id}>"
|
|
|
|
|
|
class NetworkMember(Base):
|
|
"""Network member."""
|
|
__tablename__ = "network_members"
|
|
|
|
id = Column(BigInteger, primary_key=True)
|
|
network_id = Column(BigInteger, ForeignKey("user_networks.id", ondelete="CASCADE"), nullable=False, index=True)
|
|
user_id = Column(Integer, ForeignKey("mtgonline_users.id"), nullable=False, index=True)
|
|
role = Column(String(20), server_default="MEMBER")
|
|
joined_at = Column(DateTime, server_default=func.now())
|
|
|
|
network = relationship("UserNetwork", back_populates="members")
|
|
user = relationship("User")
|
|
|
|
__table_args__ = (
|
|
UniqueConstraint('network_id', 'user_id', name='uq_network_member'),
|
|
)
|
|
|
|
def __repr__(self) -> str:
|
|
return f"<NetworkMember network={self.network_id} user={self.user_id} role={self.role}>"
|
|
|
|
|
|
class UserPreference(Base):
|
|
"""User preferences and settings."""
|
|
__tablename__ = "user_preferences"
|
|
|
|
user_id = Column(Integer, ForeignKey("mtgonline_users.id"), primary_key=True)
|
|
theme = Column(String(20), server_default="light")
|
|
notifications_enabled = Column(Boolean, default=True)
|
|
email_notifications = Column(Boolean, default=True)
|
|
auto_save_decks = Column(Boolean, default=True)
|
|
default_format = Column(String(50), server_default="standard")
|
|
language = Column(String(5), server_default="EN")
|
|
updated_at = Column(DateTime, server_default=func.now(), onupdate=func.now())
|
|
|
|
user = relationship("User")
|
|
|
|
def __repr__(self) -> str:
|
|
return f"<UserPreference user={self.user_id} theme={self.theme}>"
|
|
|
|
|
|
class UserActivityLog(Base):
|
|
"""User activity log."""
|
|
__tablename__ = "user_activity_log"
|
|
|
|
id = Column(BigInteger, primary_key=True)
|
|
user_id = Column(Integer, ForeignKey("mtgonline_users.id"), nullable=False, index=True)
|
|
activity_type = Column(String(50), nullable=False, index=True)
|
|
activity_data = Column(JSON, nullable=True)
|
|
ip_address = Column(String(45), nullable=True)
|
|
created_at = Column(DateTime, server_default=func.now(), index=True)
|
|
|
|
user = relationship("User", backref="activity_logs")
|
|
|
|
def __repr__(self) -> str:
|
|
return f"<UserActivity user={self.user_id} type={self.activity_type}>"
|