- Created MtgCardMirror and DeckCardLink models in mirror_models.py - Created card_mirror_service.py with sync_mirrors functionality - Added sync_mirrors() method to mtgjson_manager.py - Added mirror_get_db() dependency to database.py - Added DecklistFile.status column (DRAUGHT/FINAL) - Updated DeckCreate schema with status field - Added DeckWithCardsResponse schema with card_count - Updated deck router to query card mirrors and return card counts - Added plain text deck content support
172 lines
6.6 KiB
Python
172 lines
6.6 KiB
Python
"""
|
|
SQLAlchemy ORM models for the MTG Online database.
|
|
|
|
Mirrors the original MySQL schema with modern PostgreSQL features.
|
|
"""
|
|
from sqlalchemy import Column, Integer, String, BigInteger, Boolean, DateTime, Text, ForeignKey, Index
|
|
from sqlalchemy.orm import relationship
|
|
from sqlalchemy.sql import func
|
|
from app.core.database import Base
|
|
|
|
|
|
class User(Base):
|
|
"""User account model."""
|
|
__tablename__ = "mtgonline_users"
|
|
|
|
id = Column(Integer, primary_key=True, index=True)
|
|
username = Column(String(64), unique=True, nullable=False, index=True)
|
|
password_hash = Column(String(128), nullable=False) # bcrypt hash
|
|
salt = Column(String(128), nullable=False) # password salt
|
|
email = Column(String(255), nullable=True, index=True)
|
|
country = Column(String(2), nullable=True)
|
|
real_name = Column(String(128), nullable=True)
|
|
avatar_bmp = Column(Text, nullable=True) # Base64 encoded avatar
|
|
privlevel = Column(String(50), nullable=True, default="User")
|
|
|
|
# Account status
|
|
is_active = Column(Boolean, default=True)
|
|
is_banned = Column(Boolean, default=False)
|
|
ban_reason = Column(Text, nullable=True)
|
|
ban_ends = Column(DateTime, nullable=True)
|
|
|
|
# VIP/Donator status
|
|
vip_status = Column(Integer, default=0) # 0=normal, 1=vip, 2=donator
|
|
vip_expiry = Column(DateTime, nullable=True)
|
|
|
|
# Timestamps
|
|
creation_date = Column(DateTime, server_default=func.now())
|
|
last_login = Column(DateTime, nullable=True)
|
|
|
|
# Relationships
|
|
decklist_files = relationship("DecklistFile", back_populates="owner", cascade="all, delete-orphan")
|
|
decklist_folders = relationship("DecklistFolder", back_populates="owner", cascade="all, delete-orphan")
|
|
|
|
def __repr__(self) -> str:
|
|
return f"<User {self.username} (ID: {self.id})>"
|
|
|
|
|
|
class DecklistFolder(Base):
|
|
"""User deck folder."""
|
|
__tablename__ = "mtgonline_decklist_folders"
|
|
|
|
id = Column(Integer, primary_key=True, index=True)
|
|
owner_id = Column(Integer, ForeignKey("mtgonline_users.id"), nullable=False)
|
|
name = Column(String(255), nullable=False)
|
|
parent_id = Column(Integer, ForeignKey("mtgonline_decklist_folders.id"), nullable=True)
|
|
creation_date = Column(DateTime, server_default=func.now())
|
|
|
|
# Relationships
|
|
owner = relationship("User", back_populates="decklist_folders")
|
|
children = relationship("DecklistFolder", back_populates="parent", cascade="all, delete-orphan")
|
|
parent = relationship("DecklistFolder", back_populates="children", remote_side=[id])
|
|
files = relationship("DecklistFile", back_populates="folder", cascade="all, delete-orphan")
|
|
|
|
|
|
class DecklistFile(Base):
|
|
"""Deck file stored in a folder."""
|
|
__tablename__ = "mtgonline_decklist_files"
|
|
|
|
id = Column(Integer, primary_key=True, index=True)
|
|
folder_id = Column(Integer, ForeignKey("mtgonline_decklist_folders.id"), nullable=True)
|
|
owner_id = Column(Integer, ForeignKey("mtgonline_users.id"), nullable=False)
|
|
name = Column(String(255), nullable=False)
|
|
content = Column(Text, nullable=False) # Native XML or plain text deck format
|
|
format = Column(String(50), default="native") # 'native' or 'plain'
|
|
status = Column(String(20), default="DRAUGHT") # 'DRAUGHT' or 'FINAL'
|
|
creation_date = Column(DateTime, server_default=func.now())
|
|
|
|
# Relationships
|
|
folder = relationship("DecklistFolder", back_populates="files")
|
|
owner = relationship("User", back_populates="decklist_files")
|
|
|
|
def __repr__(self) -> str:
|
|
return f"<DecklistFile {self.name} (ID: {self.id})>"
|
|
|
|
|
|
class Room(Base):
|
|
"""Chat room."""
|
|
__tablename__ = "mtgonline_rooms"
|
|
|
|
id = Column(Integer, primary_key=True, index=True)
|
|
name = Column(String(100), unique=True, nullable=False)
|
|
description = Column(Text, nullable=True)
|
|
is_password_protected = Column(Boolean, default=False)
|
|
password_hash = Column(String(128), nullable=True)
|
|
creation_date = Column(DateTime, server_default=func.now())
|
|
|
|
# Game types available in this room
|
|
game_types = relationship("RoomGameType", back_populates="room", cascade="all, delete-orphan")
|
|
|
|
|
|
class RoomGameType(Base):
|
|
"""Game type definition for a room."""
|
|
__tablename__ = "mtgonline_rooms_gametypes"
|
|
|
|
id = Column(Integer, primary_key=True, index=True)
|
|
room_id = Column(Integer, ForeignKey("mtgonline_rooms.id"), nullable=False)
|
|
name = Column(String(100), nullable=False)
|
|
description = Column(Text, nullable=True)
|
|
|
|
# Relationships
|
|
room = relationship("Room", back_populates="game_types")
|
|
|
|
|
|
class Ban(Base):
|
|
"""User ban record."""
|
|
__tablename__ = "mtgonline_bans"
|
|
|
|
id = Column(Integer, primary_key=True, index=True)
|
|
user_id = Column(Integer, ForeignKey("mtgonline_users.id"), nullable=False)
|
|
server_id = Column(Integer, nullable=True)
|
|
reason = Column(Text, nullable=False)
|
|
moderators = Column(String(255), nullable=True) # Admin usernames
|
|
ip_address = Column(String(45), nullable=True) # IPv4 or IPv6
|
|
expiration_time = Column(DateTime, nullable=True)
|
|
active = Column(Boolean, default=True)
|
|
creation_date = Column(DateTime, server_default=func.now())
|
|
|
|
# Relationships
|
|
user = relationship("User")
|
|
|
|
def __repr__(self) -> str:
|
|
return f"<Ban for User {self.user_id} (ID: {self.id})>"
|
|
|
|
|
|
class GameLog(Base):
|
|
"""Game log entry."""
|
|
__tablename__ = "mtgonline_log"
|
|
|
|
id = Column(Integer, primary_key=True, index=True)
|
|
room_id = Column(Integer, ForeignKey("mtgonline_rooms.id"), nullable=True)
|
|
player_id = Column(Integer, ForeignKey("mtgonline_users.id"), nullable=True)
|
|
message = Column(Text, nullable=False)
|
|
timestamp = Column(DateTime, server_default=func.now())
|
|
|
|
# Relationships
|
|
room = relationship("Room")
|
|
player = relationship("User")
|
|
|
|
|
|
class AuditLog(Base):
|
|
"""Audit trail for administrative actions."""
|
|
__tablename__ = "mtgonline_audit"
|
|
|
|
id = Column(Integer, primary_key=True, index=True)
|
|
admin_id = Column(Integer, ForeignKey("mtgonline_users.id"), nullable=False)
|
|
action_type = Column(String(50), nullable=False) # 'ban', 'unban', 'warn', etc.
|
|
target_user_id = Column(Integer, ForeignKey("mtgonline_users.id"), nullable=True)
|
|
details = Column(Text, nullable=True)
|
|
ip_address = Column(String(45), nullable=True)
|
|
timestamp = Column(DateTime, server_default=func.now())
|
|
|
|
# Relationships
|
|
admin = relationship("User", foreign_keys=[admin_id])
|
|
target_user = relationship("User", foreign_keys=[target_user_id])
|
|
|
|
|
|
# Indexes for performance
|
|
Index("idx_decks_owner", DecklistFile.owner_id)
|
|
Index("idx_decks_folder", DecklistFile.folder_id)
|
|
Index("idx_bans_active", Ban.active)
|
|
Index("idx_log_timestamp", GameLog.timestamp)
|