refactor: consolidate 10+ model files into 2 modules
- models.py: All application models (users, decks, sessions, groups, imports, etc.) - mtg_models.py: All MTG card data models (sets, cards, mirrors, links) - Removed: mirror_models.py, user_data.py, user_deck.py, card_import_batch.py, user_card_import.py, user_card_import_record.py, relationships.py - Added __backup__/ directory with original files for reference - DecklistFile.card_links cross-model relationship resolved via late import
This commit is contained in:
@@ -1,56 +1,30 @@
|
|||||||
"""Models package initialization."""
|
"""Models package initialization.
|
||||||
from app.models.models import User, DecklistFile, DecklistFolder, Room, RoomGameType, Ban, GameLog, AuditLog
|
|
||||||
from app.models.mtg_models import MtgSet, MtgCard
|
|
||||||
from app.models.mirror_models import MtgCardMirror, DeckCardLink
|
|
||||||
from app.models.user_data import (
|
|
||||||
UserSession, DeckVersion, GameReplay, ReplayPlayer,
|
|
||||||
GameOutcome, UserStatistics, UserCardCollection, CardWishlist,
|
|
||||||
UserGroup, GroupMember, GroupChatMessage, UserNetwork,
|
|
||||||
NetworkMember, UserPreference, UserActivityLog
|
|
||||||
)
|
|
||||||
from app.models.user_deck import UserDeck, UserDeckCard, DeckPrecedent, DeckPrecedentCard, CardSuggestion
|
|
||||||
from app.models.card_import_batch import CardImportBatch
|
|
||||||
from app.models.user_card_import_record import UserCardImportRecord
|
|
||||||
from app.models.user_card_import import UserCardImport
|
|
||||||
|
|
||||||
# Import relationships LAST to avoid circular imports
|
Consolidated from 10+ fragmented model files into 2 main modules:
|
||||||
# This defines cross-model references (e.g., DecklistFile.card_links)
|
- models.py: Application models (users, decks, sessions, groups, etc.)
|
||||||
from app.models import relationships
|
- mtg_models.py: MTG card data models (sets, cards, mirrors, links)
|
||||||
|
"""
|
||||||
|
from app.models.models import (
|
||||||
|
User, DecklistFile, DecklistFolder, Room, RoomGameType, Ban, GameLog, AuditLog,
|
||||||
|
MtgonlineCard,
|
||||||
|
UserDeck, UserDeckCard, DeckPrecedent, DeckPrecedentCard, CardSuggestion,
|
||||||
|
UserSession, DeckVersion, GameReplay, ReplayPlayer, GameOutcome,
|
||||||
|
UserStatistics, UserCardCollection, CardWishlist,
|
||||||
|
UserGroup, GroupMember, GroupChatMessage,
|
||||||
|
UserNetwork, NetworkMember,
|
||||||
|
UserPreference, UserActivityLog,
|
||||||
|
CardImportBatch, UserCardImportRecord, UserCardImport,
|
||||||
|
)
|
||||||
|
from app.models.mtg_models import MtgSet, MtgCard, MtgCardMirror, DeckCardLink
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"User",
|
"User", "DecklistFile", "DecklistFolder", "Room", "RoomGameType",
|
||||||
"DecklistFile",
|
"Ban", "GameLog", "AuditLog", "MtgSet", "MtgCard", "MtgCardMirror",
|
||||||
"DecklistFolder",
|
"DeckCardLink", "MtgonlineCard", "UserSession", "DeckVersion",
|
||||||
"Room",
|
"GameReplay", "ReplayPlayer", "GameOutcome", "UserStatistics",
|
||||||
"RoomGameType",
|
"UserCardCollection", "CardWishlist", "UserGroup", "GroupMember",
|
||||||
"Ban",
|
"GroupChatMessage", "UserNetwork", "NetworkMember", "UserPreference",
|
||||||
"GameLog",
|
"UserActivityLog", "UserDeck", "UserDeckCard", "DeckPrecedent",
|
||||||
"AuditLog",
|
"DeckPrecedentCard", "CardSuggestion", "CardImportBatch",
|
||||||
"MtgSet",
|
"UserCardImportRecord", "UserCardImport",
|
||||||
"MtgCard",
|
|
||||||
"MtgCardMirror",
|
|
||||||
"DeckCardLink",
|
|
||||||
"UserSession",
|
|
||||||
"DeckVersion",
|
|
||||||
"GameReplay",
|
|
||||||
"ReplayPlayer",
|
|
||||||
"GameOutcome",
|
|
||||||
"UserStatistics",
|
|
||||||
"UserCardCollection",
|
|
||||||
"CardWishlist",
|
|
||||||
"UserGroup",
|
|
||||||
"GroupMember",
|
|
||||||
"GroupChatMessage",
|
|
||||||
"UserNetwork",
|
|
||||||
"NetworkMember",
|
|
||||||
"UserPreference",
|
|
||||||
"UserActivityLog",
|
|
||||||
"UserDeck",
|
|
||||||
"UserDeckCard",
|
|
||||||
"DeckPrecedent",
|
|
||||||
"DeckPrecedentCard",
|
|
||||||
"CardSuggestion",
|
|
||||||
"CardImportBatch",
|
|
||||||
"UserCardImportRecord",
|
|
||||||
"UserCardImport",
|
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -1,74 +0,0 @@
|
|||||||
"""
|
|
||||||
SQLAlchemy ORM models for card import tracking.
|
|
||||||
|
|
||||||
This module provides models for tracking card import batches
|
|
||||||
and individual card import records.
|
|
||||||
"""
|
|
||||||
from sqlalchemy import (
|
|
||||||
Column, Integer, String, BigInteger, Boolean, DateTime, Text,
|
|
||||||
ForeignKey, UniqueConstraint
|
|
||||||
)
|
|
||||||
from sqlalchemy.orm import relationship
|
|
||||||
from sqlalchemy.sql import func
|
|
||||||
from app.core.database import Base
|
|
||||||
|
|
||||||
|
|
||||||
class CardImportBatch(Base):
|
|
||||||
"""
|
|
||||||
Card import batch tracking.
|
|
||||||
|
|
||||||
Tracks a batch of card imports from a file, including
|
|
||||||
matching results and error information.
|
|
||||||
"""
|
|
||||||
__tablename__ = "card_import_batches"
|
|
||||||
|
|
||||||
id = Column(BigInteger, primary_key=True, autoincrement=True)
|
|
||||||
user_id = Column(Integer, ForeignKey("mtgonline_users.id", ondelete="CASCADE"), nullable=False)
|
|
||||||
filename = Column(String(255), nullable=False)
|
|
||||||
file_type = Column(String(50), nullable=True) # 'mtgjson', 'csv', etc.
|
|
||||||
file_size = Column(BigInteger, nullable=True) # Size in bytes
|
|
||||||
status = Column(String(20), default="PENDING") # PENDING, PROCESSING, COMPLETED, FAILED
|
|
||||||
total_cards = Column(Integer, default=0)
|
|
||||||
matched_cards = Column(Integer, default=0)
|
|
||||||
unmatched_cards = Column(Integer, default=0)
|
|
||||||
match_results = Column(Text, nullable=True) # JSON of match details
|
|
||||||
error_message = Column(Text, nullable=True)
|
|
||||||
created_at = Column(DateTime, server_default=func.now())
|
|
||||||
updated_at = Column(DateTime, server_default=func.now(), onupdate=func.now())
|
|
||||||
|
|
||||||
# Relationships
|
|
||||||
user = relationship("User", backref="card_import_batches")
|
|
||||||
records = relationship(
|
|
||||||
"UserCardImportRecord",
|
|
||||||
back_populates="batch",
|
|
||||||
cascade="all, delete-orphan"
|
|
||||||
)
|
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
|
||||||
return f"<CardImportBatch {self.filename} (status={self.status})>"
|
|
||||||
|
|
||||||
|
|
||||||
class UserCardImportRecord(Base):
|
|
||||||
"""
|
|
||||||
Individual card import record within a batch.
|
|
||||||
|
|
||||||
Tracks whether each card in an import batch was confirmed
|
|
||||||
and when the confirmation happened.
|
|
||||||
"""
|
|
||||||
__tablename__ = "user_card_imports_confirmed"
|
|
||||||
|
|
||||||
id = Column(BigInteger, primary_key=True, autoincrement=True)
|
|
||||||
user_id = Column(Integer, ForeignKey("mtgonline_users.id", ondelete="CASCADE"), nullable=False)
|
|
||||||
batch_id = Column(BigInteger, ForeignKey("card_import_batches.id", ondelete="CASCADE"), nullable=False)
|
|
||||||
is_confirmed = Column(Boolean, default=False)
|
|
||||||
confirmed_at = Column(DateTime, nullable=True)
|
|
||||||
|
|
||||||
# Relationships
|
|
||||||
batch = relationship("CardImportBatch", back_populates="records")
|
|
||||||
|
|
||||||
__table_args__ = (
|
|
||||||
UniqueConstraint('user_id', 'batch_id', name='uq_user_card_import_batch'),
|
|
||||||
)
|
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
|
||||||
return f"<UserCardImportRecord user={self.user_id} batch={self.batch_id} confirmed={self.is_confirmed}>"
|
|
||||||
@@ -1,97 +0,0 @@
|
|||||||
"""
|
|
||||||
SQLAlchemy ORM models for card mirrors and deck-card links.
|
|
||||||
|
|
||||||
Mirrored card data lives in mtgo_platform for fast deckbuilding queries.
|
|
||||||
The MTGJSON database remains the canonical source of truth.
|
|
||||||
"""
|
|
||||||
from sqlalchemy import (
|
|
||||||
Column, Integer, String, Text, DateTime, ForeignKey, Index,
|
|
||||||
UniqueConstraint, Boolean
|
|
||||||
)
|
|
||||||
from sqlalchemy.orm import relationship
|
|
||||||
from sqlalchemy.sql import func
|
|
||||||
from app.core.database import Base
|
|
||||||
|
|
||||||
|
|
||||||
class MtgCardMirror(Base):
|
|
||||||
"""
|
|
||||||
Mirrored card data for user decks.
|
|
||||||
|
|
||||||
Contains all relevant card fields copied from mtg_cards (mtg_data)
|
|
||||||
to avoid cross-DB joins during deckbuilding. Back-references source_id
|
|
||||||
to the canonical mtg_cards.id for sync tracking.
|
|
||||||
"""
|
|
||||||
__tablename__ = "mtg_cards_mirror"
|
|
||||||
|
|
||||||
id = Column(Integer, primary_key=True, index=True)
|
|
||||||
source_id = Column(
|
|
||||||
Integer,
|
|
||||||
nullable=True,
|
|
||||||
index=True,
|
|
||||||
comment="Logical reference to mtg_cards.id in mtg_data database (cross-DB reference, no FK constraint)"
|
|
||||||
)
|
|
||||||
name = Column(String(255), nullable=False, index=True)
|
|
||||||
mana_cost = Column(String(255), nullable=True)
|
|
||||||
type_line = Column(String(255), nullable=True)
|
|
||||||
oracle_text = Column(Text, nullable=True)
|
|
||||||
power = Column(String(50), nullable=True)
|
|
||||||
toughness = Column(String(50), nullable=True)
|
|
||||||
rarity = Column(String(50), nullable=True)
|
|
||||||
layout = Column(String(50), nullable=True)
|
|
||||||
artist = Column(String(255), nullable=True)
|
|
||||||
flavor_text = Column(Text, nullable=True)
|
|
||||||
numbers = Column(String(100), nullable=True)
|
|
||||||
identifiers = Column(Text, nullable=True) # JSON string of all identifiers
|
|
||||||
images = Column(Text, nullable=True) # JSON string of image URLs
|
|
||||||
image = Column(Text, nullable=True) # Card image URL from MTGJSON
|
|
||||||
card_parts = Column(Text, nullable=True) # Comma-separated list of face names
|
|
||||||
keywords = Column(Text, nullable=True) # Comma-separated keywords
|
|
||||||
legalities = Column(Text, nullable=True) # JSON of format legality
|
|
||||||
set_code = Column(String(10), nullable=True, index=True)
|
|
||||||
set_name = Column(String(255), nullable=True)
|
|
||||||
# Sync tracking
|
|
||||||
synced_at = Column(DateTime, server_default=func.now(), onupdate=func.now())
|
|
||||||
created_at = Column(DateTime, server_default=func.now())
|
|
||||||
|
|
||||||
# Relationships
|
|
||||||
deck_links = relationship(
|
|
||||||
"DeckCardLink",
|
|
||||||
back_populates="card",
|
|
||||||
cascade="all, delete-orphan"
|
|
||||||
)
|
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
|
||||||
return f"<MtgCardMirror {self.name} (ID: {self.id}, source: {self.source_id})>"
|
|
||||||
|
|
||||||
|
|
||||||
class DeckCardLink(Base):
|
|
||||||
"""
|
|
||||||
Junction table linking user decks to mirrored cards.
|
|
||||||
|
|
||||||
Represents a specific quantity of a mirrored card in a specific zone
|
|
||||||
(main deck or sideboard) of a user's deck.
|
|
||||||
"""
|
|
||||||
__tablename__ = "deck_card_links"
|
|
||||||
|
|
||||||
id = Column(Integer, primary_key=True, index=True)
|
|
||||||
deck_id = Column(Integer, ForeignKey("mtgonline_decklist_files.id", ondelete="CASCADE"), nullable=False)
|
|
||||||
card_id = Column(Integer, ForeignKey("mtg_cards_mirror.id"), nullable=False)
|
|
||||||
quantity = Column(Integer, nullable=False, default=1)
|
|
||||||
zone = Column(String(20), nullable=False, default="main") # 'main' or 'sideboard'
|
|
||||||
|
|
||||||
# Composite unique: a card can only appear once per zone in a deck
|
|
||||||
__table_args__ = (
|
|
||||||
UniqueConstraint('deck_id', 'card_id', 'zone', name='uq_deck_card_link'),
|
|
||||||
Index('idx_deck_card_deck', 'deck_id'),
|
|
||||||
Index('idx_deck_card_card', 'card_id'),
|
|
||||||
)
|
|
||||||
|
|
||||||
# Relationships
|
|
||||||
deck = relationship("DecklistFile", back_populates="card_links")
|
|
||||||
card = relationship("MtgCardMirror", back_populates="deck_links")
|
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
|
||||||
return (
|
|
||||||
f"<DeckCardLink deck={self.deck_id} card={self.card_id} "
|
|
||||||
f"qty={self.quantity} zone={self.zone}>"
|
|
||||||
)
|
|
||||||
+805
-65
@@ -1,14 +1,35 @@
|
|||||||
"""
|
"""
|
||||||
SQLAlchemy ORM models for the MTG Online database.
|
SQLAlchemy ORM models for the MTG Online application.
|
||||||
|
|
||||||
Mirrors the original MySQL schema with modern PostgreSQL features.
|
Consolidated from 9 fragmented model files (models.py, user_deck.py,
|
||||||
|
user_data.py, card_import_batch.py, user_card_import.py, relationships.py,
|
||||||
|
and others) into a single module for clarity and maintainability.
|
||||||
|
|
||||||
|
This file contains all application-level models:
|
||||||
|
- Core models: User, DecklistFile, DecklistFolder, Room, RoomGameType, Ban, GameLog, AuditLog
|
||||||
|
- Card mirror: MtgonlineCard (local copy of mtg_cards for deckbuilding)
|
||||||
|
- User decks: UserDeck, UserDeckCard, DeckPrecedent, DeckPrecedentCard, CardSuggestion
|
||||||
|
- User data: UserSession, DeckVersion, GameReplay, ReplayPlayer, GameOutcome,
|
||||||
|
UserStatistics, UserCardCollection, CardWishlist, UserGroup, GroupMember,
|
||||||
|
GroupChatMessage, UserNetwork, NetworkMember, UserPreference, UserActivityLog
|
||||||
|
- Card imports: CardImportBatch, UserCardImportRecord, UserCardImport
|
||||||
|
|
||||||
|
Cross-model relationships (e.g., DecklistFile.card_links → DeckCardLink)
|
||||||
|
are resolved at the bottom of this file after all classes are defined.
|
||||||
"""
|
"""
|
||||||
from sqlalchemy import Column, Integer, String, BigInteger, Boolean, DateTime, Text, ForeignKey, Index
|
from sqlalchemy import (
|
||||||
|
Column, Integer, String, BigInteger, Boolean, DateTime, Text,
|
||||||
|
ForeignKey, Index, UniqueConstraint, Float, JSON,
|
||||||
|
)
|
||||||
from sqlalchemy.orm import relationship
|
from sqlalchemy.orm import relationship
|
||||||
from sqlalchemy.sql import func
|
from sqlalchemy.sql import func
|
||||||
from app.core.database import Base
|
from app.core.database import Base
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Core Models
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
class User(Base):
|
class User(Base):
|
||||||
"""User account model."""
|
"""User account model."""
|
||||||
__tablename__ = "mtgonline_users"
|
__tablename__ = "mtgonline_users"
|
||||||
@@ -38,65 +59,17 @@ class User(Base):
|
|||||||
last_login = Column(DateTime, nullable=True)
|
last_login = Column(DateTime, nullable=True)
|
||||||
|
|
||||||
# Relationships
|
# Relationships
|
||||||
decklist_files = relationship("DecklistFile", back_populates="owner", cascade="all, delete-orphan")
|
decklist_files = relationship(
|
||||||
decklist_folders = relationship("DecklistFolder", back_populates="owner", cascade="all, delete-orphan")
|
"DecklistFile", back_populates="owner", cascade="all, delete-orphan"
|
||||||
|
)
|
||||||
|
decklist_folders = relationship(
|
||||||
|
"DecklistFolder", back_populates="owner", cascade="all, delete-orphan"
|
||||||
|
)
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
return f"<User {self.username} (ID: {self.id})>"
|
return f"<User {self.username} (ID: {self.id})>"
|
||||||
|
|
||||||
|
|
||||||
class MtgonlineCard(Base):
|
|
||||||
"""
|
|
||||||
Local card data mirror for the mtgonline database.
|
|
||||||
|
|
||||||
Mirrors data from mtg_cards (mtgdata database) to avoid cross-database
|
|
||||||
joins during deckbuilding. Maintains source_id to reference the canonical
|
|
||||||
mtg_cards.id for sync tracking.
|
|
||||||
"""
|
|
||||||
__tablename__ = "mtgonline_cards"
|
|
||||||
|
|
||||||
id = Column(Integer, primary_key=True, index=True)
|
|
||||||
source_id = Column(Integer, nullable=True, index=True) # References mtg_cards.id in mtgdata
|
|
||||||
name = Column(String(255), nullable=False, index=True)
|
|
||||||
mana_cost = Column(String(255), nullable=True)
|
|
||||||
type_line = Column(String(255), nullable=True)
|
|
||||||
oracle_text = Column(Text, nullable=True)
|
|
||||||
power = Column(String(50), nullable=True)
|
|
||||||
toughness = Column(String(50), nullable=True)
|
|
||||||
rarity = Column(String(50), nullable=True)
|
|
||||||
layout = Column(String(50), nullable=True)
|
|
||||||
artist = Column(String(255), nullable=True)
|
|
||||||
flavor_text = Column(Text, nullable=True)
|
|
||||||
numbers = Column(String(100), nullable=True)
|
|
||||||
identifiers = Column(Text, nullable=True) # JSON string
|
|
||||||
images = Column(Text, nullable=True) # JSON string
|
|
||||||
image = Column(Text, nullable=True) # Card image URL
|
|
||||||
set_code = Column(String(10), nullable=True, index=True)
|
|
||||||
set_name = Column(String(255), nullable=True)
|
|
||||||
card_parts = Column(Text, nullable=True) # Comma-separated face names
|
|
||||||
keywords = Column(Text, nullable=True) # Comma-separated keywords
|
|
||||||
legalities = Column(Text, nullable=True) # JSON of format legality
|
|
||||||
# Sync tracking
|
|
||||||
synced_at = Column(DateTime, server_default=func.now(), onupdate=func.now())
|
|
||||||
created_at = Column(DateTime, server_default=func.now())
|
|
||||||
|
|
||||||
# Relationships
|
|
||||||
deck_cards = relationship(
|
|
||||||
"UserDeckCard",
|
|
||||||
back_populates="card",
|
|
||||||
cascade="all, delete-orphan"
|
|
||||||
)
|
|
||||||
|
|
||||||
# Indexes
|
|
||||||
__table_args__ = (
|
|
||||||
Index('idx_mtgonline_cards_name', 'name'),
|
|
||||||
Index('idx_mtgonline_cards_set', 'set_code'),
|
|
||||||
)
|
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
|
||||||
return f"<MtgonlineCard {self.name} (ID: {self.id}, source: {self.source_id})>"
|
|
||||||
|
|
||||||
|
|
||||||
class DecklistFolder(Base):
|
class DecklistFolder(Base):
|
||||||
"""User deck folder."""
|
"""User deck folder."""
|
||||||
__tablename__ = "mtgonline_decklist_folders"
|
__tablename__ = "mtgonline_decklist_folders"
|
||||||
@@ -109,13 +82,17 @@ class DecklistFolder(Base):
|
|||||||
|
|
||||||
# Relationships
|
# Relationships
|
||||||
owner = relationship("User", back_populates="decklist_folders")
|
owner = relationship("User", back_populates="decklist_folders")
|
||||||
children = relationship("DecklistFolder", back_populates="parent", cascade="all, delete-orphan")
|
children = relationship(
|
||||||
parent = relationship("DecklistFolder", back_populates="children", remote_side=[id])
|
"DecklistFolder", back_populates="parent", cascade="all, delete-orphan"
|
||||||
files = relationship("DecklistFile", back_populates="folder", cascade="all, delete-orphan")
|
)
|
||||||
|
parent = relationship(
|
||||||
|
"DecklistFolder", back_populates="children", remote_side=[id]
|
||||||
|
)
|
||||||
|
files = relationship(
|
||||||
|
"DecklistFile", back_populates="folder", cascade="all, delete-orphan"
|
||||||
|
)
|
||||||
user_decks = relationship(
|
user_decks = relationship(
|
||||||
"UserDeck",
|
"UserDeck", back_populates="folder", lazy="select"
|
||||||
back_populates="folder",
|
|
||||||
lazy="select"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -132,7 +109,7 @@ class DecklistFile(Base):
|
|||||||
status = Column(String(20), default="DRAUGHT") # 'DRAUGHT' or 'FINAL'
|
status = Column(String(20), default="DRAUGHT") # 'DRAUGHT' or 'FINAL'
|
||||||
creation_date = Column(DateTime, server_default=func.now())
|
creation_date = Column(DateTime, server_default=func.now())
|
||||||
|
|
||||||
# Relationships
|
# Relationships (card_links is added below after DeckCardLink is defined)
|
||||||
folder = relationship("DecklistFolder", back_populates="files")
|
folder = relationship("DecklistFolder", back_populates="files")
|
||||||
owner = relationship("User", back_populates="decklist_files")
|
owner = relationship("User", back_populates="decklist_files")
|
||||||
|
|
||||||
@@ -152,7 +129,9 @@ class Room(Base):
|
|||||||
creation_date = Column(DateTime, server_default=func.now())
|
creation_date = Column(DateTime, server_default=func.now())
|
||||||
|
|
||||||
# Game types available in this room
|
# Game types available in this room
|
||||||
game_types = relationship("RoomGameType", back_populates="room", cascade="all, delete-orphan")
|
game_types = relationship(
|
||||||
|
"RoomGameType", back_populates="room", cascade="all, delete-orphan"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class RoomGameType(Base):
|
class RoomGameType(Base):
|
||||||
@@ -221,7 +200,768 @@ class AuditLog(Base):
|
|||||||
target_user = relationship("User", foreign_keys=[target_user_id])
|
target_user = relationship("User", foreign_keys=[target_user_id])
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Card Mirror (local copy for deckbuilding)
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
class MtgonlineCard(Base):
|
||||||
|
"""
|
||||||
|
Local card data mirror for the mtgonline database.
|
||||||
|
|
||||||
|
Mirrors data from mtg_cards (mtgdata database) to avoid cross-database
|
||||||
|
joins during deckbuilding. Maintains source_id to reference the canonical
|
||||||
|
mtg_cards.id for sync tracking.
|
||||||
|
"""
|
||||||
|
__tablename__ = "mtgonline_cards"
|
||||||
|
|
||||||
|
id = Column(Integer, primary_key=True, index=True)
|
||||||
|
source_id = Column(Integer, nullable=True, index=True) # References mtg_cards.id in mtgdata
|
||||||
|
name = Column(String(255), nullable=False, index=True)
|
||||||
|
mana_cost = Column(String(255), nullable=True)
|
||||||
|
type_line = Column(String(255), nullable=True)
|
||||||
|
oracle_text = Column(Text, nullable=True)
|
||||||
|
power = Column(String(50), nullable=True)
|
||||||
|
toughness = Column(String(50), nullable=True)
|
||||||
|
rarity = Column(String(50), nullable=True)
|
||||||
|
layout = Column(String(50), nullable=True)
|
||||||
|
artist = Column(String(255), nullable=True)
|
||||||
|
flavor_text = Column(Text, nullable=True)
|
||||||
|
numbers = Column(String(100), nullable=True)
|
||||||
|
identifiers = Column(Text, nullable=True) # JSON string
|
||||||
|
images = Column(Text, nullable=True) # JSON string
|
||||||
|
image = Column(Text, nullable=True) # Card image URL
|
||||||
|
set_code = Column(String(10), nullable=True, index=True)
|
||||||
|
set_name = Column(String(255), nullable=True)
|
||||||
|
card_parts = Column(Text, nullable=True) # Comma-separated face names
|
||||||
|
keywords = Column(Text, nullable=True) # Comma-separated keywords
|
||||||
|
legalities = Column(Text, nullable=True) # JSON of format legality
|
||||||
|
# Sync tracking
|
||||||
|
synced_at = Column(DateTime, server_default=func.now(), onupdate=func.now())
|
||||||
|
created_at = Column(DateTime, server_default=func.now())
|
||||||
|
|
||||||
|
# Relationships
|
||||||
|
deck_cards = relationship(
|
||||||
|
"UserDeckCard",
|
||||||
|
back_populates="card",
|
||||||
|
cascade="all, delete-orphan",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Indexes
|
||||||
|
__table_args__ = (
|
||||||
|
Index("idx_mtgonline_cards_name", "name"),
|
||||||
|
Index("idx_mtgonline_cards_set", "set_code"),
|
||||||
|
)
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
return f"<MtgonlineCard {self.name} (ID: {self.id}, source: {self.source_id})>"
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# User Deck Models
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
class UserDeck(Base):
|
||||||
|
"""Per-user deck storage in the primary mtgonline database."""
|
||||||
|
__tablename__ = "user_decks"
|
||||||
|
|
||||||
|
id = Column(BigInteger, primary_key=True, autoincrement=True)
|
||||||
|
user_id = Column(
|
||||||
|
Integer, ForeignKey("mtgonline_users.id", ondelete="CASCADE"),
|
||||||
|
nullable=False, index=True,
|
||||||
|
)
|
||||||
|
name = Column(String(255), nullable=False, index=True)
|
||||||
|
status = Column(
|
||||||
|
String(20), nullable=False, default="DRAFT", index=True,
|
||||||
|
) # DRAFT or FINAL
|
||||||
|
folder_id = Column(
|
||||||
|
Integer, ForeignKey("mtgonline_decklist_folders.id"), nullable=True,
|
||||||
|
)
|
||||||
|
format = Column(
|
||||||
|
String(50), nullable=True, default="standard",
|
||||||
|
) # Standard, Modern, etc.
|
||||||
|
notes = Column(Text, nullable=True)
|
||||||
|
is_precedent = Column(Boolean, default=False, index=True) # Mark as deck precedent/template
|
||||||
|
precedent_name = Column(String(255), nullable=True) # Name for precedent (if applicable)
|
||||||
|
created_at = Column(DateTime, server_default=func.now())
|
||||||
|
updated_at = Column(DateTime, server_default=func.now(), onupdate=func.now())
|
||||||
|
|
||||||
|
# Relationships
|
||||||
|
user = relationship("User", backref="user_decks")
|
||||||
|
folder = relationship("DecklistFolder", backref="user_decks")
|
||||||
|
cards = relationship(
|
||||||
|
"UserDeckCard",
|
||||||
|
back_populates="deck",
|
||||||
|
cascade="all, delete-orphan",
|
||||||
|
order_by="UserDeckCard.id",
|
||||||
|
)
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
return f"<UserDeck {self.name} (user={self.user_id}, status={self.status})>"
|
||||||
|
|
||||||
|
|
||||||
|
class UserDeckCard(Base):
|
||||||
|
"""
|
||||||
|
Junction table for user deck cards.
|
||||||
|
|
||||||
|
Stores individual card entries in a user's deck with quantity and zone.
|
||||||
|
Card references point to mtg_cards.id in the mtgdata database.
|
||||||
|
"""
|
||||||
|
__tablename__ = "user_deck_cards"
|
||||||
|
|
||||||
|
id = Column(BigInteger, primary_key=True, autoincrement=True)
|
||||||
|
deck_id = Column(
|
||||||
|
BigInteger, ForeignKey("user_decks.id", ondelete="CASCADE"),
|
||||||
|
nullable=False, index=True,
|
||||||
|
)
|
||||||
|
card_id = Column(
|
||||||
|
Integer, ForeignKey("mtgonline_cards.id"), nullable=False, index=True,
|
||||||
|
)
|
||||||
|
quantity = Column(Integer, nullable=False, default=1)
|
||||||
|
zone = Column(String(20), nullable=False, default="main") # 'main' or 'sideboard'
|
||||||
|
position = Column(Integer, nullable=True) # Optional ordering within zone
|
||||||
|
|
||||||
|
__table_args__ = (
|
||||||
|
UniqueConstraint("deck_id", "card_id", "zone", name="uq_deck_card_unique"),
|
||||||
|
Index("idx_deck_cards_deck", "deck_id"),
|
||||||
|
Index("idx_deck_cards_card", "card_id"),
|
||||||
|
)
|
||||||
|
|
||||||
|
# Relationships
|
||||||
|
deck = relationship("UserDeck", back_populates="cards")
|
||||||
|
card = relationship("MtgonlineCard", back_populates="deck_cards")
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
return f"<UserDeckCard deck={self.deck_id} card={self.card_id} qty={self.quantity}>"
|
||||||
|
|
||||||
|
|
||||||
|
class DeckPrecedent(Base):
|
||||||
|
"""
|
||||||
|
Deck precedent (template) storage.
|
||||||
|
|
||||||
|
Precedents are pre-built deck templates that users can clone
|
||||||
|
to start building their own decks.
|
||||||
|
"""
|
||||||
|
__tablename__ = "deck_precedents"
|
||||||
|
|
||||||
|
id = Column(BigInteger, primary_key=True, autoincrement=True)
|
||||||
|
name = Column(String(255), nullable=False, index=True)
|
||||||
|
description = Column(Text, nullable=True)
|
||||||
|
format = Column(String(50), nullable=True, default="standard")
|
||||||
|
is_public = Column(Boolean, default=True, index=True)
|
||||||
|
created_by = Column(
|
||||||
|
Integer, ForeignKey("mtgonline_users.id"), nullable=True,
|
||||||
|
) # None = system
|
||||||
|
created_at = Column(DateTime, server_default=func.now())
|
||||||
|
updated_at = Column(DateTime, server_default=func.now(), onupdate=func.now())
|
||||||
|
|
||||||
|
# Relationships
|
||||||
|
creator = relationship("User", foreign_keys=[created_by])
|
||||||
|
cards = relationship(
|
||||||
|
"DeckPrecedentCard",
|
||||||
|
back_populates="precedent",
|
||||||
|
cascade="all, delete-orphan",
|
||||||
|
)
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
return f"<DeckPrecedent {self.name} (format={self.format})>"
|
||||||
|
|
||||||
|
|
||||||
|
class DeckPrecedentCard(Base):
|
||||||
|
"""Junction table for deck precedent cards."""
|
||||||
|
__tablename__ = "deck_precedent_cards"
|
||||||
|
|
||||||
|
id = Column(BigInteger, primary_key=True, autoincrement=True)
|
||||||
|
precedent_id = Column(
|
||||||
|
BigInteger, ForeignKey("deck_precedents.id", ondelete="CASCADE"),
|
||||||
|
nullable=False, index=True,
|
||||||
|
)
|
||||||
|
card_id = Column(Integer, nullable=False, index=True)
|
||||||
|
quantity = Column(Integer, nullable=False, default=1)
|
||||||
|
zone = Column(String(20), nullable=False, default="main")
|
||||||
|
|
||||||
|
__table_args__ = (
|
||||||
|
UniqueConstraint(
|
||||||
|
"precedent_id", "card_id", "zone",
|
||||||
|
name="uq_precedent_card_unique",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
precedent = relationship("DeckPrecedent", back_populates="cards")
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
return f"<DeckPrecedentCard precedent={self.precedent_id} card={self.card_id}>"
|
||||||
|
|
||||||
|
|
||||||
|
class CardSuggestion(Base):
|
||||||
|
"""
|
||||||
|
Card suggestion storage.
|
||||||
|
|
||||||
|
Stores suggested cards for a deck based on similarity,
|
||||||
|
pairing patterns, or manual curation.
|
||||||
|
"""
|
||||||
|
__tablename__ = "card_suggestions"
|
||||||
|
|
||||||
|
id = Column(BigInteger, primary_key=True, autoincrement=True)
|
||||||
|
deck_id = Column(
|
||||||
|
BigInteger, ForeignKey("user_decks.id", ondelete="CASCADE"),
|
||||||
|
nullable=False, index=True,
|
||||||
|
)
|
||||||
|
card_id = Column(Integer, nullable=False, index=True) # Suggested card
|
||||||
|
source_card_id = Column(Integer, nullable=True) # Card that triggered the suggestion
|
||||||
|
suggestion_type = Column(
|
||||||
|
String(50), nullable=False, default="SIMILAR",
|
||||||
|
) # SIMILAR, PAIRING, ALTERNATIVE
|
||||||
|
confidence = Column(Float, nullable=True) # 0.0 to 1.0
|
||||||
|
notes = Column(Text, nullable=True)
|
||||||
|
created_at = Column(DateTime, server_default=func.now())
|
||||||
|
|
||||||
|
deck = relationship("UserDeck", backref="suggestions")
|
||||||
|
|
||||||
|
__table_args__ = (
|
||||||
|
UniqueConstraint(
|
||||||
|
"deck_id", "card_id", "source_card_id",
|
||||||
|
name="uq_suggestion_unique",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
return (
|
||||||
|
f"<CardSuggestion deck={self.deck_id} card={self.card_id} "
|
||||||
|
f"type={self.suggestion_type}>"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# User Data Models
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
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} "
|
||||||
|
f"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} "
|
||||||
|
f"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} "
|
||||||
|
f"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} "
|
||||||
|
f"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}>"
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Card Import Models
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
class CardImportBatch(Base):
|
||||||
|
"""
|
||||||
|
Card import batch tracking.
|
||||||
|
|
||||||
|
Tracks a batch of card imports from a file, including
|
||||||
|
matching results and error information.
|
||||||
|
"""
|
||||||
|
__tablename__ = "card_import_batches"
|
||||||
|
|
||||||
|
id = Column(BigInteger, primary_key=True, autoincrement=True)
|
||||||
|
user_id = Column(
|
||||||
|
Integer, ForeignKey("mtgonline_users.id", ondelete="CASCADE"),
|
||||||
|
nullable=False,
|
||||||
|
)
|
||||||
|
filename = Column(String(255), nullable=False)
|
||||||
|
file_type = Column(String(50), nullable=True) # 'mtgjson', 'csv', etc.
|
||||||
|
file_size = Column(BigInteger, nullable=True) # Size in bytes
|
||||||
|
status = Column(
|
||||||
|
String(20), default="PENDING",
|
||||||
|
) # PENDING, PROCESSING, COMPLETED, FAILED
|
||||||
|
total_cards = Column(Integer, default=0)
|
||||||
|
matched_cards = Column(Integer, default=0)
|
||||||
|
unmatched_cards = Column(Integer, default=0)
|
||||||
|
match_results = Column(Text, nullable=True) # JSON of match details
|
||||||
|
error_message = Column(Text, nullable=True)
|
||||||
|
created_at = Column(DateTime, server_default=func.now())
|
||||||
|
updated_at = Column(DateTime, server_default=func.now(), onupdate=func.now())
|
||||||
|
|
||||||
|
# Relationships
|
||||||
|
user = relationship("User", backref="card_import_batches")
|
||||||
|
records = relationship(
|
||||||
|
"UserCardImportRecord",
|
||||||
|
back_populates="batch",
|
||||||
|
cascade="all, delete-orphan",
|
||||||
|
)
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
return f"<CardImportBatch {self.filename} (status={self.status})>"
|
||||||
|
|
||||||
|
|
||||||
|
class UserCardImportRecord(Base):
|
||||||
|
"""
|
||||||
|
Individual card import record within a batch.
|
||||||
|
|
||||||
|
Tracks whether each card in an import batch was confirmed
|
||||||
|
and when the confirmation happened.
|
||||||
|
"""
|
||||||
|
__tablename__ = "user_card_imports_confirmed"
|
||||||
|
|
||||||
|
id = Column(BigInteger, primary_key=True, autoincrement=True)
|
||||||
|
user_id = Column(
|
||||||
|
Integer, ForeignKey("mtgonline_users.id", ondelete="CASCADE"),
|
||||||
|
nullable=False,
|
||||||
|
)
|
||||||
|
batch_id = Column(
|
||||||
|
BigInteger, ForeignKey("card_import_batches.id", ondelete="CASCADE"),
|
||||||
|
nullable=False,
|
||||||
|
)
|
||||||
|
is_confirmed = Column(Boolean, default=False)
|
||||||
|
confirmed_at = Column(DateTime, nullable=True)
|
||||||
|
|
||||||
|
# Relationships
|
||||||
|
batch = relationship("CardImportBatch", back_populates="records")
|
||||||
|
|
||||||
|
__table_args__ = (
|
||||||
|
UniqueConstraint(
|
||||||
|
"user_id", "batch_id", name="uq_user_card_import_batch",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
return (
|
||||||
|
f"<UserCardImportRecord user={self.user_id} batch={self.batch_id} "
|
||||||
|
f"confirmed={self.is_confirmed}>"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class UserCardImport(Base):
|
||||||
|
"""
|
||||||
|
User card import record.
|
||||||
|
|
||||||
|
Stores a user's imported card collection as a JSON array of card names.
|
||||||
|
This table was created in migration 004 and may be superseded by
|
||||||
|
CardImportBatch and UserCardImportRecord in migration 005.
|
||||||
|
"""
|
||||||
|
__tablename__ = "user_card_imports"
|
||||||
|
|
||||||
|
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||||
|
user_id = Column(
|
||||||
|
Integer, ForeignKey("mtgonline_users.id", ondelete="CASCADE"),
|
||||||
|
nullable=False,
|
||||||
|
)
|
||||||
|
card_names_json = Column(Text, nullable=False) # JSON array of card names
|
||||||
|
created_at = Column(DateTime, server_default=func.now())
|
||||||
|
updated_at = Column(DateTime, server_default=func.now(), onupdate=func.now())
|
||||||
|
|
||||||
|
# Relationships
|
||||||
|
user = relationship("User", backref="card_imports")
|
||||||
|
|
||||||
|
__table_args__ = (
|
||||||
|
UniqueConstraint(
|
||||||
|
"user_id", name="uq_user_card_imports_user_id",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
return f"<UserCardImport user={self.user_id}>"
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Cross-Model Relationships
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
# Add the back-reference from DecklistFile to DeckCardLink.
|
||||||
|
# DeckCardLink is defined in mtg_models.py; we import it here after all
|
||||||
|
# classes in this file are defined to avoid circular imports.
|
||||||
|
from app.models.mtg_models import DeckCardLink # noqa: E402
|
||||||
|
|
||||||
|
DecklistFile.card_links = relationship(
|
||||||
|
"DeckCardLink",
|
||||||
|
back_populates="deck",
|
||||||
|
cascade="all, delete-orphan",
|
||||||
|
order_by="DeckCardLink.id",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
# Indexes for performance
|
# Indexes for performance
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
Index("idx_decks_owner", DecklistFile.owner_id)
|
Index("idx_decks_owner", DecklistFile.owner_id)
|
||||||
Index("idx_decks_folder", DecklistFile.folder_id)
|
Index("idx_decks_folder", DecklistFile.folder_id)
|
||||||
Index("idx_bans_active", Ban.active)
|
Index("idx_bans_active", Ban.active)
|
||||||
|
|||||||
@@ -1,14 +1,29 @@
|
|||||||
"""
|
"""
|
||||||
SQLAlchemy ORM models for the MTG database (mtgjson.com data).
|
SQLAlchemy ORM models for MTG card data and mirrors.
|
||||||
|
|
||||||
Models mirror the mtg_cards and mtg_sets tables in the MTG PostgreSQL database.
|
Consolidated from mtg_models.py and mirror_models.py into a single module.
|
||||||
|
|
||||||
|
This file contains all MTG card data models:
|
||||||
|
- MtgSet, MtgCard: Canonical card/set data from mtgjson.com (mtg_data DB)
|
||||||
|
- MtgCardMirror: Mirrored card data copied to mtgonline DB for fast deckbuilding queries
|
||||||
|
- DeckCardLink: Junction table linking user decks to mirrored cards
|
||||||
|
|
||||||
|
Cross-model references to application models (e.g., DeckCardLink.deck → DecklistFile)
|
||||||
|
use string-based relationship() calls to avoid circular imports.
|
||||||
"""
|
"""
|
||||||
from sqlalchemy import Column, Integer, String, Text, DateTime, ForeignKey, Index
|
from sqlalchemy import (
|
||||||
|
Column, Integer, String, Text, DateTime, ForeignKey, Index,
|
||||||
|
UniqueConstraint,
|
||||||
|
)
|
||||||
from sqlalchemy.orm import relationship
|
from sqlalchemy.orm import relationship
|
||||||
from sqlalchemy.sql import func
|
from sqlalchemy.sql import func
|
||||||
from app.core.database import Base
|
from app.core.database import Base
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# MTG Set and Card (canonical data from mtgjson.com)
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
class MtgSet(Base):
|
class MtgSet(Base):
|
||||||
"""MTG Set model."""
|
"""MTG Set model."""
|
||||||
__tablename__ = "mtg_sets"
|
__tablename__ = "mtg_sets"
|
||||||
@@ -65,7 +80,105 @@ class MtgCard(Base):
|
|||||||
return f"<MtgCard {self.name} ({self.set_id})>"
|
return f"<MtgCard {self.name} ({self.set_id})>"
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Card Mirror and Deck Links (mtgonline DB)
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
class MtgCardMirror(Base):
|
||||||
|
"""
|
||||||
|
Mirrored card data for user decks.
|
||||||
|
|
||||||
|
Contains all relevant card fields copied from mtg_cards (mtg_data)
|
||||||
|
to avoid cross-DB joins during deckbuilding. Back-references source_id
|
||||||
|
to the canonical mtg_cards.id for sync tracking.
|
||||||
|
"""
|
||||||
|
__tablename__ = "mtg_cards_mirror"
|
||||||
|
|
||||||
|
id = Column(Integer, primary_key=True, index=True)
|
||||||
|
source_id = Column(
|
||||||
|
Integer,
|
||||||
|
nullable=True,
|
||||||
|
index=True,
|
||||||
|
comment="Logical reference to mtg_cards.id in mtg_data database "
|
||||||
|
"(cross-DB reference, no FK constraint)",
|
||||||
|
)
|
||||||
|
name = Column(String(255), nullable=False, index=True)
|
||||||
|
mana_cost = Column(String(255), nullable=True)
|
||||||
|
type_line = Column(String(255), nullable=True)
|
||||||
|
oracle_text = Column(Text, nullable=True)
|
||||||
|
power = Column(String(50), nullable=True)
|
||||||
|
toughness = Column(String(50), nullable=True)
|
||||||
|
rarity = Column(String(50), nullable=True)
|
||||||
|
layout = Column(String(50), nullable=True)
|
||||||
|
artist = Column(String(255), nullable=True)
|
||||||
|
flavor_text = Column(Text, nullable=True)
|
||||||
|
numbers = Column(String(100), nullable=True)
|
||||||
|
identifiers = Column(Text, nullable=True) # JSON string of all identifiers
|
||||||
|
images = Column(Text, nullable=True) # JSON string of image URLs
|
||||||
|
image = Column(Text, nullable=True) # Card image URL from MTGJSON
|
||||||
|
card_parts = Column(Text, nullable=True) # Comma-separated list of face names
|
||||||
|
keywords = Column(Text, nullable=True) # Comma-separated keywords
|
||||||
|
legalities = Column(Text, nullable=True) # JSON of format legality
|
||||||
|
set_code = Column(String(10), nullable=True, index=True)
|
||||||
|
set_name = Column(String(255), nullable=True)
|
||||||
|
# Sync tracking
|
||||||
|
synced_at = Column(DateTime, server_default=func.now(), onupdate=func.now())
|
||||||
|
created_at = Column(DateTime, server_default=func.now())
|
||||||
|
|
||||||
|
# Relationships
|
||||||
|
deck_links = relationship(
|
||||||
|
"DeckCardLink",
|
||||||
|
back_populates="card",
|
||||||
|
cascade="all, delete-orphan",
|
||||||
|
)
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
return f"<MtgCardMirror {self.name} (ID: {self.id}, source: {self.source_id})>"
|
||||||
|
|
||||||
|
|
||||||
|
class DeckCardLink(Base):
|
||||||
|
"""
|
||||||
|
Junction table linking user decks to mirrored cards.
|
||||||
|
|
||||||
|
Represents a specific quantity of a mirrored card in a specific zone
|
||||||
|
(main deck or sideboard) of a user's deck.
|
||||||
|
"""
|
||||||
|
__tablename__ = "deck_card_links"
|
||||||
|
|
||||||
|
id = Column(Integer, primary_key=True, index=True)
|
||||||
|
deck_id = Column(
|
||||||
|
Integer, ForeignKey("mtgonline_decklist_files.id", ondelete="CASCADE"),
|
||||||
|
nullable=False,
|
||||||
|
)
|
||||||
|
card_id = Column(
|
||||||
|
Integer, ForeignKey("mtg_cards_mirror.id"), nullable=False,
|
||||||
|
)
|
||||||
|
quantity = Column(Integer, nullable=False, default=1)
|
||||||
|
zone = Column(String(20), nullable=False, default="main") # 'main' or 'sideboard'
|
||||||
|
|
||||||
|
# Composite unique: a card can only appear once per zone in a deck
|
||||||
|
__table_args__ = (
|
||||||
|
UniqueConstraint("deck_id", "card_id", "zone", name="uq_deck_card_link"),
|
||||||
|
Index("idx_deck_card_deck", "deck_id"),
|
||||||
|
Index("idx_deck_card_card", "card_id"),
|
||||||
|
)
|
||||||
|
|
||||||
|
# Relationships
|
||||||
|
# String reference to DecklistFile in models.py to avoid circular imports
|
||||||
|
deck = relationship("DecklistFile", back_populates="card_links")
|
||||||
|
card = relationship("MtgCardMirror", back_populates="deck_links")
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
return (
|
||||||
|
f"<DeckCardLink deck={self.deck_id} card={self.card_id} "
|
||||||
|
f"qty={self.quantity} zone={self.zone}>"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
# Indexes for performance
|
# Indexes for performance
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
Index("idx_mtg_cards_name_set", MtgCard.name, MtgCard.set_id)
|
Index("idx_mtg_cards_name_set", MtgCard.name, MtgCard.set_id)
|
||||||
Index("idx_mtg_cards_type", MtgCard.type_line)
|
Index("idx_mtg_cards_type", MtgCard.type_line)
|
||||||
Index("idx_mtg_cards_rarity", MtgCard.rarity)
|
Index("idx_mtg_cards_rarity", MtgCard.rarity)
|
||||||
|
|||||||
@@ -1,24 +0,0 @@
|
|||||||
"""
|
|
||||||
Cross-model relationships.
|
|
||||||
|
|
||||||
This module defines relationships that reference models from other
|
|
||||||
model files (e.g., DecklistFile.card_links → DeckCardLink). These
|
|
||||||
must be defined AFTER all model classes have been imported to avoid
|
|
||||||
circular import issues.
|
|
||||||
|
|
||||||
Import this module LAST in app/models/__init__.py.
|
|
||||||
"""
|
|
||||||
from sqlalchemy.orm import relationship
|
|
||||||
from app.models.models import DecklistFile
|
|
||||||
from app.models.mirror_models import DeckCardLink
|
|
||||||
|
|
||||||
|
|
||||||
# Add the back-reference from DecklistFile to DeckCardLink.
|
|
||||||
# This was previously defined dynamically at the bottom of mirror_models.py,
|
|
||||||
# but that caused circular imports. Now it lives here and is imported last.
|
|
||||||
DecklistFile.card_links = relationship(
|
|
||||||
"DeckCardLink",
|
|
||||||
back_populates="deck",
|
|
||||||
cascade="all, delete-orphan",
|
|
||||||
order_by="DeckCardLink.id"
|
|
||||||
)
|
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
"""
|
|
||||||
SQLAlchemy ORM model for user card imports.
|
|
||||||
|
|
||||||
This model corresponds to the user_card_imports table created in migration 004.
|
|
||||||
It stores a user's imported card collection as a JSON array of card names.
|
|
||||||
"""
|
|
||||||
from sqlalchemy import (
|
|
||||||
Column, Integer, String, BigInteger, DateTime, Text,
|
|
||||||
ForeignKey, UniqueConstraint
|
|
||||||
)
|
|
||||||
from sqlalchemy.orm import relationship
|
|
||||||
from sqlalchemy.sql import func
|
|
||||||
from app.core.database import Base
|
|
||||||
|
|
||||||
|
|
||||||
class UserCardImport(Base):
|
|
||||||
"""
|
|
||||||
User card import record.
|
|
||||||
|
|
||||||
Stores a user's imported card collection as a JSON array of card names.
|
|
||||||
This table was created in migration 004 and may be superseded by
|
|
||||||
CardImportBatch and UserCardImportRecord in migration 005.
|
|
||||||
"""
|
|
||||||
__tablename__ = "user_card_imports"
|
|
||||||
|
|
||||||
id = Column(Integer, primary_key=True, autoincrement=True)
|
|
||||||
user_id = Column(Integer, ForeignKey("mtgonline_users.id", ondelete="CASCADE"), nullable=False)
|
|
||||||
card_names_json = Column(Text, nullable=False) # JSON array of card names
|
|
||||||
created_at = Column(DateTime, server_default=func.now())
|
|
||||||
updated_at = Column(DateTime, server_default=func.now(), onupdate=func.now())
|
|
||||||
|
|
||||||
# Relationships
|
|
||||||
user = relationship("User", backref="card_imports")
|
|
||||||
|
|
||||||
__table_args__ = (
|
|
||||||
UniqueConstraint('user_id', name='uq_user_card_imports_user_id'),
|
|
||||||
)
|
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
|
||||||
return f"<UserCardImport user={self.user_id}>"
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
"""
|
|
||||||
SQLAlchemy ORM models for user card import records.
|
|
||||||
|
|
||||||
This module is deprecated. UserCardImportRecord is now defined in
|
|
||||||
card_import_batch.py along with CardImportBatch.
|
|
||||||
"""
|
|
||||||
# This file is kept for backward compatibility but the actual model
|
|
||||||
# is now in card_import_batch.py
|
|
||||||
from app.models.card_import_batch import UserCardImportRecord as UserCardImportRecord
|
|
||||||
|
|
||||||
__all__ = ["UserCardImportRecord"]
|
|
||||||
@@ -1,325 +0,0 @@
|
|||||||
"""
|
|
||||||
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}>"
|
|
||||||
@@ -1,153 +0,0 @@
|
|||||||
"""
|
|
||||||
SQLAlchemy ORM models for per-user deck building.
|
|
||||||
|
|
||||||
These models live in the primary mtgonline database and support
|
|
||||||
the deckbuilding feature with DRAFT/FINAL status, card management,
|
|
||||||
deck precedents, and card suggestions.
|
|
||||||
"""
|
|
||||||
from sqlalchemy import (
|
|
||||||
Column, Integer, String, BigInteger, Boolean, DateTime, Text,
|
|
||||||
ForeignKey, UniqueConstraint, Index, Float
|
|
||||||
)
|
|
||||||
from sqlalchemy.orm import relationship
|
|
||||||
from sqlalchemy.sql import func
|
|
||||||
from app.core.database import Base
|
|
||||||
from app.models.models import MtgonlineCard
|
|
||||||
|
|
||||||
|
|
||||||
class UserDeck(Base):
|
|
||||||
"""Per-user deck storage in the primary mtgonline database."""
|
|
||||||
__tablename__ = "user_decks"
|
|
||||||
|
|
||||||
id = Column(BigInteger, primary_key=True, autoincrement=True)
|
|
||||||
user_id = Column(Integer, ForeignKey("mtgonline_users.id", ondelete="CASCADE"), nullable=False, index=True)
|
|
||||||
name = Column(String(255), nullable=False, index=True)
|
|
||||||
status = Column(String(20), nullable=False, default="DRAFT", index=True) # DRAFT or FINAL
|
|
||||||
folder_id = Column(Integer, ForeignKey("mtgonline_decklist_folders.id"), nullable=True)
|
|
||||||
format = Column(String(50), nullable=True, default="standard") # Standard, Modern, etc.
|
|
||||||
notes = Column(Text, nullable=True)
|
|
||||||
is_precedent = Column(Boolean, default=False, index=True) # Mark as deck precedent/template
|
|
||||||
precedent_name = Column(String(255), nullable=True) # Name for precedent (if applicable)
|
|
||||||
created_at = Column(DateTime, server_default=func.now())
|
|
||||||
updated_at = Column(DateTime, server_default=func.now(), onupdate=func.now())
|
|
||||||
|
|
||||||
# Relationships
|
|
||||||
user = relationship("User", backref="user_decks")
|
|
||||||
folder = relationship("DecklistFolder", backref="user_decks")
|
|
||||||
cards = relationship(
|
|
||||||
"UserDeckCard",
|
|
||||||
back_populates="deck",
|
|
||||||
cascade="all, delete-orphan",
|
|
||||||
order_by="UserDeckCard.id"
|
|
||||||
)
|
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
|
||||||
return f"<UserDeck {self.name} (user={self.user_id}, status={self.status})>"
|
|
||||||
|
|
||||||
|
|
||||||
class UserDeckCard(Base):
|
|
||||||
"""
|
|
||||||
Junction table for user deck cards.
|
|
||||||
|
|
||||||
Stores individual card entries in a user's deck with quantity and zone.
|
|
||||||
Card references point to mtg_cards.id in the mtgdata database.
|
|
||||||
"""
|
|
||||||
__tablename__ = "user_deck_cards"
|
|
||||||
|
|
||||||
id = Column(BigInteger, primary_key=True, autoincrement=True)
|
|
||||||
deck_id = Column(BigInteger, ForeignKey("user_decks.id", ondelete="CASCADE"), nullable=False, index=True)
|
|
||||||
card_id = Column(Integer, ForeignKey("mtgonline_cards.id"), nullable=False, index=True)
|
|
||||||
quantity = Column(Integer, nullable=False, default=1)
|
|
||||||
zone = Column(String(20), nullable=False, default="main") # 'main' or 'sideboard'
|
|
||||||
position = Column(Integer, nullable=True) # Optional ordering within zone
|
|
||||||
|
|
||||||
__table_args__ = (
|
|
||||||
UniqueConstraint('deck_id', 'card_id', 'zone', name='uq_deck_card_unique'),
|
|
||||||
Index('idx_deck_cards_deck', 'deck_id'),
|
|
||||||
Index('idx_deck_cards_card', 'card_id'),
|
|
||||||
)
|
|
||||||
|
|
||||||
# Relationships
|
|
||||||
deck = relationship("UserDeck", back_populates="cards")
|
|
||||||
card = relationship("MtgonlineCard", back_populates="deck_cards")
|
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
|
||||||
return f"<UserDeckCard deck={self.deck_id} card={self.card_id} qty={self.quantity}>"
|
|
||||||
|
|
||||||
|
|
||||||
class DeckPrecedent(Base):
|
|
||||||
"""
|
|
||||||
Deck precedent (template) storage.
|
|
||||||
|
|
||||||
Precedents are pre-built deck templates that users can clone
|
|
||||||
to start building their own decks.
|
|
||||||
"""
|
|
||||||
__tablename__ = "deck_precedents"
|
|
||||||
|
|
||||||
id = Column(BigInteger, primary_key=True, autoincrement=True)
|
|
||||||
name = Column(String(255), nullable=False, index=True)
|
|
||||||
description = Column(Text, nullable=True)
|
|
||||||
format = Column(String(50), nullable=True, default="standard")
|
|
||||||
is_public = Column(Boolean, default=True, index=True)
|
|
||||||
created_by = Column(Integer, ForeignKey("mtgonline_users.id"), nullable=True) # None = system
|
|
||||||
created_at = Column(DateTime, server_default=func.now())
|
|
||||||
updated_at = Column(DateTime, server_default=func.now(), onupdate=func.now())
|
|
||||||
|
|
||||||
# Relationships
|
|
||||||
creator = relationship("User", foreign_keys=[created_by])
|
|
||||||
cards = relationship(
|
|
||||||
"DeckPrecedentCard",
|
|
||||||
back_populates="precedent",
|
|
||||||
cascade="all, delete-orphan"
|
|
||||||
)
|
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
|
||||||
return f"<DeckPrecedent {self.name} (format={self.format})>"
|
|
||||||
|
|
||||||
|
|
||||||
class DeckPrecedentCard(Base):
|
|
||||||
"""Junction table for deck precedent cards."""
|
|
||||||
__tablename__ = "deck_precedent_cards"
|
|
||||||
|
|
||||||
id = Column(BigInteger, primary_key=True, autoincrement=True)
|
|
||||||
precedent_id = Column(BigInteger, ForeignKey("deck_precedents.id", ondelete="CASCADE"), nullable=False, index=True)
|
|
||||||
card_id = Column(Integer, nullable=False, index=True)
|
|
||||||
quantity = Column(Integer, nullable=False, default=1)
|
|
||||||
zone = Column(String(20), nullable=False, default="main")
|
|
||||||
|
|
||||||
__table_args__ = (
|
|
||||||
UniqueConstraint('precedent_id', 'card_id', 'zone', name='uq_precedent_card_unique'),
|
|
||||||
)
|
|
||||||
|
|
||||||
precedent = relationship("DeckPrecedent", back_populates="cards")
|
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
|
||||||
return f"<DeckPrecedentCard precedent={self.precedent_id} card={self.card_id}>"
|
|
||||||
|
|
||||||
|
|
||||||
class CardSuggestion(Base):
|
|
||||||
"""
|
|
||||||
Card suggestion storage.
|
|
||||||
|
|
||||||
Stores suggested cards for a deck based on similarity,
|
|
||||||
pairing patterns, or manual curation.
|
|
||||||
"""
|
|
||||||
__tablename__ = "card_suggestions"
|
|
||||||
|
|
||||||
id = Column(BigInteger, primary_key=True, autoincrement=True)
|
|
||||||
deck_id = Column(BigInteger, ForeignKey("user_decks.id", ondelete="CASCADE"), nullable=False, index=True)
|
|
||||||
card_id = Column(Integer, nullable=False, index=True) # Suggested card
|
|
||||||
source_card_id = Column(Integer, nullable=True) # Card that triggered the suggestion
|
|
||||||
suggestion_type = Column(String(50), nullable=False, default="SIMILAR") # SIMILAR, PAIRING, ALTERNATIVE
|
|
||||||
confidence = Column(Float, nullable=True) # 0.0 to 1.0
|
|
||||||
notes = Column(Text, nullable=True)
|
|
||||||
created_at = Column(DateTime, server_default=func.now())
|
|
||||||
|
|
||||||
deck = relationship("UserDeck", backref="suggestions")
|
|
||||||
|
|
||||||
__table_args__ = (
|
|
||||||
UniqueConstraint('deck_id', 'card_id', 'source_card_id', name='uq_suggestion_unique'),
|
|
||||||
)
|
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
|
||||||
return f"<CardSuggestion deck={self.deck_id} card={self.card_id} type={self.suggestion_type}>"
|
|
||||||
Reference in New Issue
Block a user