Files
mtgonline/backend/app/models/user_data.py
T
akadmin c42d7ca0e1 feat: implement user data schema and API endpoints
- 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
2026-07-23 03:23:54 +00:00

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}>"