Files
mtgonline/backend/app/models/models.py
T

171 lines
6.6 KiB
Python

"""
SQLAlchemy ORM models for the Cockatrice 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__ = "cockatrice_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")
decks = relationship("Deck", 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__ = "cockatrice_decklist_folders"
id = Column(Integer, primary_key=True, index=True)
owner_id = Column(Integer, ForeignKey("cockatrice_users.id"), nullable=False)
name = Column(String(255), nullable=False)
parent_id = Column(Integer, ForeignKey("cockatrice_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__ = "cockatrice_decklist_files"
id = Column(Integer, primary_key=True, index=True)
folder_id = Column(Integer, ForeignKey("cockatrice_decklist_folders.id"), nullable=False)
owner_id = Column(Integer, ForeignKey("cockatrice_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'
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__ = "cockatrice_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__ = "cockatrice_rooms_gametypes"
id = Column(Integer, primary_key=True, index=True)
room_id = Column(Integer, ForeignKey("cockatrice_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__ = "cockatrice_bans"
id = Column(Integer, primary_key=True, index=True)
user_id = Column(Integer, ForeignKey("cockatrice_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__ = "cockatrice_log"
id = Column(Integer, primary_key=True, index=True)
room_id = Column(Integer, ForeignKey("cockatrice_rooms.id"), nullable=True)
player_id = Column(Integer, ForeignKey("cockatrice_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__ = "cockatrice_audit"
id = Column(Integer, primary_key=True, index=True)
admin_id = Column(Integer, ForeignKey("cockatrice_users.id"), nullable=False)
action_type = Column(String(50), nullable=False) # 'ban', 'unban', 'warn', etc.
target_user_id = Column(Integer, ForeignKey("cockatrice_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)