diff --git a/models/__init__.py b/models/__init__.py new file mode 100644 index 0000000..fa2b68a --- /dev/null +++ b/models/__init__.py @@ -0,0 +1,5 @@ +from sqlalchemy.orm import DeclarativeBase + + +class Base(DeclarativeBase): + pass diff --git a/models/category.py b/models/category.py new file mode 100644 index 0000000..e811af2 --- /dev/null +++ b/models/category.py @@ -0,0 +1,24 @@ +from sqlalchemy import Column, ForeignKey, Integer, String, UniqueConstraint +from sqlalchemy.orm import relationship + +from models import Base + + +class Category(Base): + __tablename__ = "categories" + + id = Column(Integer, primary_key=True, autoincrement=True) + name = Column(String(50), unique=True, nullable=False) + + words = relationship("CategoryWord", back_populates="category") + + +class CategoryWord(Base): + __tablename__ = "category_words" + __table_args__ = (UniqueConstraint("category_id", "word"),) + + id = Column(Integer, primary_key=True, autoincrement=True) + category_id = Column(Integer, ForeignKey("categories.id"), nullable=False) + word = Column(String(20), nullable=False) + + category = relationship("Category", back_populates="words") diff --git a/models/game.py b/models/game.py new file mode 100644 index 0000000..3262fe8 --- /dev/null +++ b/models/game.py @@ -0,0 +1,47 @@ +import uuid +from datetime import datetime, timezone + +from sqlalchemy import ( + Boolean, + Column, + DateTime, + ForeignKey, + Integer, + String, +) +from sqlalchemy.orm import relationship +from sqlalchemy.types import JSON + +from models import Base + + +class Game(Base): + __tablename__ = "games" + + id = Column(String(36), primary_key=True, default=lambda: str(uuid.uuid4())) + category_id = Column(Integer, ForeignKey("categories.id"), nullable=False) + board_size = Column(Integer, nullable=False, default=10) + grid = Column(JSON, nullable=False) + status = Column(String(20), nullable=False, default="in_progress") + started_at = Column( + DateTime, nullable=False, default=lambda: datetime.now(timezone.utc) + ) + completed_at = Column(DateTime, nullable=True) + + category = relationship("Category") + words = relationship("Word", back_populates="game") + + +class Word(Base): + __tablename__ = "words" + + id = Column(Integer, primary_key=True, autoincrement=True) + game_id = Column(String(36), ForeignKey("games.id"), nullable=False) + text = Column(String(20), nullable=False) + start_row = Column(Integer, nullable=False) + start_col = Column(Integer, nullable=False) + direction = Column(String(2), nullable=False) + found = Column(Boolean, nullable=False, default=False) + found_at = Column(DateTime, nullable=True) + + game = relationship("Game", back_populates="words") diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..f6b8ae7 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,16 @@ +import pytest +from sqlalchemy import create_engine +from sqlalchemy.orm import sessionmaker + +from models import Base + + +@pytest.fixture +def db_session(): + engine = create_engine("sqlite:///:memory:") + Base.metadata.create_all(engine) + Session = sessionmaker(bind=engine) + session = Session() + yield session + session.close() + engine.dispose() diff --git a/tests/unit/test_models.py b/tests/unit/test_models.py new file mode 100644 index 0000000..492938f --- /dev/null +++ b/tests/unit/test_models.py @@ -0,0 +1,74 @@ +from models.category import Category, CategoryWord +from models.game import Game, Word + + +def test_create_category(db_session): + cat = Category(name="animals") + db_session.add(cat) + db_session.commit() + assert cat.id is not None + assert cat.name == "animals" + + +def test_create_category_word(db_session): + cat = Category(name="animals") + db_session.add(cat) + db_session.flush() + word = CategoryWord(category_id=cat.id, word="TIGER") + db_session.add(word) + db_session.commit() + assert word.category_id == cat.id + assert word.word == "TIGER" + + +def test_create_game(db_session): + cat = Category(name="animals") + db_session.add(cat) + db_session.flush() + game = Game( + category_id=cat.id, + board_size=10, + grid=[["A"] * 10 for _ in range(10)], + ) + db_session.add(game) + db_session.commit() + assert game.id is not None + assert len(game.id) == 36 # UUID + assert game.status == "in_progress" + assert game.started_at is not None + assert game.completed_at is None + + +def test_create_word(db_session): + cat = Category(name="animals") + db_session.add(cat) + db_session.flush() + game = Game(category_id=cat.id, board_size=10, grid=[]) + db_session.add(game) + db_session.flush() + word = Word( + game_id=game.id, + text="TIGER", + start_row=0, + start_col=0, + direction="E", + ) + db_session.add(word) + db_session.commit() + assert word.found is False + assert word.found_at is None + + +def test_game_words_relationship(db_session): + cat = Category(name="animals") + db_session.add(cat) + db_session.flush() + game = Game(category_id=cat.id, board_size=10, grid=[]) + db_session.add(game) + db_session.flush() + w1 = Word(game_id=game.id, text="TIGER", start_row=0, start_col=0, direction="E") + w2 = Word(game_id=game.id, text="LION", start_row=1, start_col=0, direction="S") + db_session.add_all([w1, w2]) + db_session.commit() + db_session.refresh(game) + assert len(game.words) == 2