phase 3.1

This commit is contained in:
Danilo Reyes
2025-11-01 23:33:52 -06:00
parent da4892cc30
commit a95a4c091a
25 changed files with 1214 additions and 27 deletions

View File

@@ -1,5 +1,18 @@
"""Database models."""
from app.database.models.user import User
from app.database.models.board import Board
from app.database.models.image import Image
from app.database.models.board_image import BoardImage
from app.database.models.group import Group
from app.database.models.share_link import ShareLink
from app.database.models.comment import Comment
# Import all models here for Alembic autogenerate
# Models will be created in separate phases
__all__ = [
"User",
"Board",
"Image",
"BoardImage",
"Group",
"ShareLink",
"Comment",
]

View File

@@ -0,0 +1,38 @@
"""Board model for reference boards."""
import uuid
from datetime import datetime
from sqlalchemy import Boolean, Column, DateTime, ForeignKey, String, Text
from sqlalchemy.dialects.postgresql import JSONB, UUID
from sqlalchemy.orm import relationship
from app.database.base import Base
class Board(Base):
"""Board model representing a reference board."""
__tablename__ = "boards"
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
user_id = Column(UUID(as_uuid=True), ForeignKey("users.id", ondelete="CASCADE"), nullable=False, index=True)
title = Column(String(255), nullable=False)
description = Column(Text, nullable=True)
viewport_state = Column(
JSONB,
nullable=False,
default={"x": 0, "y": 0, "zoom": 1.0, "rotation": 0}
)
created_at = Column(DateTime, nullable=False, default=datetime.utcnow)
updated_at = Column(DateTime, nullable=False, default=datetime.utcnow, onupdate=datetime.utcnow)
is_deleted = Column(Boolean, nullable=False, default=False)
# Relationships
user = relationship("User", back_populates="boards")
board_images = relationship("BoardImage", back_populates="board", cascade="all, delete-orphan")
groups = relationship("Group", back_populates="board", cascade="all, delete-orphan")
share_links = relationship("ShareLink", back_populates="board", cascade="all, delete-orphan")
comments = relationship("Comment", back_populates="board", cascade="all, delete-orphan")
def __repr__(self) -> str:
return f"<Board(id={self.id}, title={self.title})>"

View File

@@ -0,0 +1,48 @@
"""BoardImage junction model."""
import uuid
from datetime import datetime
from sqlalchemy import Column, DateTime, ForeignKey, Integer, UniqueConstraint
from sqlalchemy.dialects.postgresql import JSONB, UUID
from sqlalchemy.orm import relationship
from app.database.base import Base
class BoardImage(Base):
"""Junction table connecting boards and images with position/transformation data."""
__tablename__ = "board_images"
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
board_id = Column(UUID(as_uuid=True), ForeignKey("boards.id", ondelete="CASCADE"), nullable=False, index=True)
image_id = Column(UUID(as_uuid=True), ForeignKey("images.id", ondelete="CASCADE"), nullable=False, index=True)
position = Column(JSONB, nullable=False)
transformations = Column(
JSONB,
nullable=False,
default={
"scale": 1.0,
"rotation": 0,
"opacity": 1.0,
"flipped_h": False,
"flipped_v": False,
"greyscale": False
}
)
z_order = Column(Integer, nullable=False, default=0, index=True)
group_id = Column(UUID(as_uuid=True), ForeignKey("groups.id", ondelete="SET NULL"), nullable=True, index=True)
created_at = Column(DateTime, nullable=False, default=datetime.utcnow)
updated_at = Column(DateTime, nullable=False, default=datetime.utcnow, onupdate=datetime.utcnow)
__table_args__ = (
UniqueConstraint("board_id", "image_id", name="uq_board_image"),
)
# Relationships
board = relationship("Board", back_populates="board_images")
image = relationship("Image", back_populates="board_images")
group = relationship("Group", back_populates="board_images")
def __repr__(self) -> str:
return f"<BoardImage(board_id={self.board_id}, image_id={self.image_id})>"

View File

@@ -0,0 +1,31 @@
"""Comment model for board comments."""
import uuid
from datetime import datetime
from sqlalchemy import Boolean, Column, DateTime, ForeignKey, String, Text
from sqlalchemy.dialects.postgresql import JSONB, UUID
from sqlalchemy.orm import relationship
from app.database.base import Base
class Comment(Base):
"""Comment model for viewer comments on shared boards."""
__tablename__ = "comments"
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
board_id = Column(UUID(as_uuid=True), ForeignKey("boards.id", ondelete="CASCADE"), nullable=False, index=True)
share_link_id = Column(UUID(as_uuid=True), ForeignKey("share_links.id", ondelete="SET NULL"), nullable=True, index=True)
author_name = Column(String(100), nullable=False)
content = Column(Text, nullable=False)
position = Column(JSONB, nullable=True) # Optional canvas position
created_at = Column(DateTime, nullable=False, default=datetime.utcnow)
is_deleted = Column(Boolean, nullable=False, default=False)
# Relationships
board = relationship("Board", back_populates="comments")
share_link = relationship("ShareLink", back_populates="comments")
def __repr__(self) -> str:
return f"<Comment(id={self.id}, author={self.author_name})>"

View File

@@ -0,0 +1,30 @@
"""Group model for image grouping."""
import uuid
from datetime import datetime
from sqlalchemy import Column, DateTime, ForeignKey, String, Text
from sqlalchemy.dialects.postgresql import UUID
from sqlalchemy.orm import relationship
from app.database.base import Base
class Group(Base):
"""Group model for organizing images with annotations."""
__tablename__ = "groups"
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
board_id = Column(UUID(as_uuid=True), ForeignKey("boards.id", ondelete="CASCADE"), nullable=False, index=True)
name = Column(String(255), nullable=False)
color = Column(String(7), nullable=False) # Hex color #RRGGBB
annotation = Column(Text, nullable=True)
created_at = Column(DateTime, nullable=False, default=datetime.utcnow)
updated_at = Column(DateTime, nullable=False, default=datetime.utcnow, onupdate=datetime.utcnow)
# Relationships
board = relationship("Board", back_populates="groups")
board_images = relationship("BoardImage", back_populates="group")
def __repr__(self) -> str:
return f"<Group(id={self.id}, name={self.name})>"

View File

@@ -0,0 +1,34 @@
"""Image model for uploaded images."""
import uuid
from datetime import datetime
from sqlalchemy import BigInteger, Column, DateTime, ForeignKey, Integer, String
from sqlalchemy.dialects.postgresql import JSONB, UUID
from sqlalchemy.orm import relationship
from app.database.base import Base
class Image(Base):
"""Image model representing uploaded image files."""
__tablename__ = "images"
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
user_id = Column(UUID(as_uuid=True), ForeignKey("users.id", ondelete="CASCADE"), nullable=False, index=True)
filename = Column(String(255), nullable=False, index=True)
storage_path = Column(String(512), nullable=False)
file_size = Column(BigInteger, nullable=False)
mime_type = Column(String(100), nullable=False)
width = Column(Integer, nullable=False)
height = Column(Integer, nullable=False)
image_metadata = Column(JSONB, nullable=False)
created_at = Column(DateTime, nullable=False, default=datetime.utcnow)
reference_count = Column(Integer, nullable=False, default=0)
# Relationships
user = relationship("User", back_populates="images")
board_images = relationship("BoardImage", back_populates="image", cascade="all, delete-orphan")
def __repr__(self) -> str:
return f"<Image(id={self.id}, filename={self.filename})>"

View File

@@ -0,0 +1,32 @@
"""ShareLink model for board sharing."""
import uuid
from datetime import datetime
from sqlalchemy import Boolean, Column, DateTime, ForeignKey, Integer, String
from sqlalchemy.dialects.postgresql import UUID
from sqlalchemy.orm import relationship
from app.database.base import Base
class ShareLink(Base):
"""ShareLink model for sharing boards with permission control."""
__tablename__ = "share_links"
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
board_id = Column(UUID(as_uuid=True), ForeignKey("boards.id", ondelete="CASCADE"), nullable=False, index=True)
token = Column(String(64), unique=True, nullable=False, index=True)
permission_level = Column(String(20), nullable=False) # 'view-only' or 'view-comment'
created_at = Column(DateTime, nullable=False, default=datetime.utcnow)
expires_at = Column(DateTime, nullable=True)
last_accessed_at = Column(DateTime, nullable=True)
access_count = Column(Integer, nullable=False, default=0)
is_revoked = Column(Boolean, nullable=False, default=False, index=True)
# Relationships
board = relationship("Board", back_populates="share_links")
comments = relationship("Comment", back_populates="share_link")
def __repr__(self) -> str:
return f"<ShareLink(id={self.id}, token={self.token[:8]}...)>"

View File

@@ -0,0 +1,29 @@
"""User model for authentication and ownership."""
import uuid
from datetime import datetime
from sqlalchemy import Boolean, Column, DateTime, String
from sqlalchemy.dialects.postgresql import UUID
from sqlalchemy.orm import relationship
from app.database.base import Base
class User(Base):
"""User model representing registered users."""
__tablename__ = "users"
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
email = Column(String(255), unique=True, nullable=False, index=True)
password_hash = Column(String(255), nullable=False)
created_at = Column(DateTime, nullable=False, default=datetime.utcnow)
updated_at = Column(DateTime, nullable=False, default=datetime.utcnow, onupdate=datetime.utcnow)
is_active = Column(Boolean, nullable=False, default=True)
# Relationships
boards = relationship("Board", back_populates="user", cascade="all, delete-orphan")
images = relationship("Image", back_populates="user", cascade="all, delete-orphan")
def __repr__(self) -> str:
return f"<User(id={self.id}, email={self.email})>"