from sqlalchemy import Column, Integer, String, DateTime, Date, ForeignKey, Enum, Text from sqlalchemy.orm import relationship, Query from sqlalchemy.sql import func from database import Base import enum class TaskType(str, enum.Enum): # Shot tasks LAYOUT = "layout" ANIMATION = "animation" SIMULATION = "simulation" LIGHTING = "lighting" COMPOSITING = "compositing" # Asset tasks MODELING = "modeling" SURFACING = "surfacing" RIGGING = "rigging" class TaskStatus(str, enum.Enum): NOT_STARTED = "not_started" IN_PROGRESS = "in_progress" SUBMITTED = "submitted" APPROVED = "approved" RETAKE = "retake" class ReviewDecision(str, enum.Enum): APPROVED = "approved" RETAKE = "retake" class AttachmentType(str, enum.Enum): REFERENCE = "reference" WORK_FILE = "work_file" PREVIEW = "preview" DOCUMENTATION = "documentation" class Task(Base): __tablename__ = "tasks" id = Column(Integer, primary_key=True, index=True) project_id = Column(Integer, ForeignKey("projects.id"), nullable=False) episode_id = Column(Integer, ForeignKey("episodes.id"), nullable=True) shot_id = Column(Integer, ForeignKey("shots.id"), nullable=True) asset_id = Column(Integer, ForeignKey("assets.id"), nullable=True) assigned_user_id = Column(Integer, ForeignKey("users.id"), nullable=True) task_type = Column(String, nullable=False) # Changed from Enum to String to support custom task types name = Column(String, nullable=False, index=True) description = Column(Text) status = Column(String, nullable=False, default="not_started") # Changed from Enum to String to support custom statuses deadline = Column(Date) created_at = Column(DateTime(timezone=True), server_default=func.now()) updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now()) # Soft deletion columns deleted_at = Column(DateTime(timezone=True), nullable=True) deleted_by = Column(Integer, ForeignKey("users.id"), nullable=True) # Relationships project = relationship("Project", back_populates="tasks") episode = relationship("Episode", back_populates="tasks") shot = relationship("Shot", back_populates="tasks") asset = relationship("Asset", back_populates="tasks") assigned_user = relationship("User", foreign_keys=[assigned_user_id], back_populates="assigned_tasks") submissions = relationship("Submission", back_populates="task", cascade="all, delete-orphan") production_notes = relationship("ProductionNote", back_populates="task", cascade="all, delete-orphan") attachments = relationship("TaskAttachment", back_populates="task", cascade="all, delete-orphan") deleted_by_user = relationship("User", foreign_keys=[deleted_by]) @property def is_deleted(self) -> bool: """Check if the task is soft deleted.""" return self.deleted_at is not None @classmethod def query_active(cls, query: Query) -> Query: """Filter query to exclude soft deleted tasks.""" return query.filter(cls.deleted_at.is_(None)) @classmethod def query_deleted(cls, query: Query) -> Query: """Filter query to include only soft deleted tasks.""" return query.filter(cls.deleted_at.isnot(None)) @classmethod def query_all_including_deleted(cls, query: Query) -> Query: """Return query without soft deletion filtering (for admin use).""" return query def __repr__(self): return f"" class Submission(Base): __tablename__ = "submissions" id = Column(Integer, primary_key=True, index=True) task_id = Column(Integer, ForeignKey("tasks.id"), nullable=False) user_id = Column(Integer, ForeignKey("users.id"), nullable=False) file_path = Column(String, nullable=False) file_name = Column(String, nullable=False) version_number = Column(Integer, nullable=False, default=1) notes = Column(Text) submitted_at = Column(DateTime(timezone=True), server_default=func.now()) # Soft deletion columns deleted_at = Column(DateTime(timezone=True), nullable=True) deleted_by = Column(Integer, ForeignKey("users.id"), nullable=True) # Relationships task = relationship("Task", back_populates="submissions") user = relationship("User", foreign_keys=[user_id], back_populates="submissions") reviews = relationship("Review", back_populates="submission", cascade="all, delete-orphan") deleted_by_user = relationship("User", foreign_keys=[deleted_by]) @property def is_deleted(self) -> bool: """Check if the submission is soft deleted.""" return self.deleted_at is not None @classmethod def query_active(cls, query: Query) -> Query: """Filter query to exclude soft deleted submissions.""" return query.filter(cls.deleted_at.is_(None)) @classmethod def query_deleted(cls, query: Query) -> Query: """Filter query to include only soft deleted submissions.""" return query.filter(cls.deleted_at.isnot(None)) @classmethod def query_all_including_deleted(cls, query: Query) -> Query: """Return query without soft deletion filtering (for admin use).""" return query def __repr__(self): return f"" class Review(Base): __tablename__ = "reviews" id = Column(Integer, primary_key=True, index=True) submission_id = Column(Integer, ForeignKey("submissions.id"), nullable=False) reviewer_id = Column(Integer, ForeignKey("users.id"), nullable=False) decision = Column(Enum(ReviewDecision), nullable=False) feedback = Column(Text) reviewed_at = Column(DateTime(timezone=True), server_default=func.now()) # Soft deletion columns deleted_at = Column(DateTime(timezone=True), nullable=True) deleted_by = Column(Integer, ForeignKey("users.id"), nullable=True) # Relationships submission = relationship("Submission", back_populates="reviews") reviewer = relationship("User", foreign_keys=[reviewer_id], back_populates="reviews") deleted_by_user = relationship("User", foreign_keys=[deleted_by]) @property def is_deleted(self) -> bool: """Check if the review is soft deleted.""" return self.deleted_at is not None @classmethod def query_active(cls, query: Query) -> Query: """Filter query to exclude soft deleted reviews.""" return query.filter(cls.deleted_at.is_(None)) @classmethod def query_deleted(cls, query: Query) -> Query: """Filter query to include only soft deleted reviews.""" return query.filter(cls.deleted_at.isnot(None)) @classmethod def query_all_including_deleted(cls, query: Query) -> Query: """Return query without soft deletion filtering (for admin use).""" return query def __repr__(self): return f"" class ProductionNote(Base): __tablename__ = "production_notes" id = Column(Integer, primary_key=True, index=True) task_id = Column(Integer, ForeignKey("tasks.id"), nullable=False) user_id = Column(Integer, ForeignKey("users.id"), nullable=False) content = Column(Text, nullable=False) parent_note_id = Column(Integer, ForeignKey("production_notes.id"), nullable=True) created_at = Column(DateTime(timezone=True), server_default=func.now()) updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now()) # Soft deletion columns deleted_at = Column(DateTime(timezone=True), nullable=True) deleted_by = Column(Integer, ForeignKey("users.id"), nullable=True) # Relationships task = relationship("Task", back_populates="production_notes") user = relationship("User", foreign_keys=[user_id], back_populates="production_notes") parent_note = relationship("ProductionNote", remote_side=[id], back_populates="child_notes") child_notes = relationship("ProductionNote", back_populates="parent_note", cascade="all, delete-orphan") deleted_by_user = relationship("User", foreign_keys=[deleted_by]) @property def is_deleted(self) -> bool: """Check if the production note is soft deleted.""" return self.deleted_at is not None @classmethod def query_active(cls, query: Query) -> Query: """Filter query to exclude soft deleted production notes.""" return query.filter(cls.deleted_at.is_(None)) @classmethod def query_deleted(cls, query: Query) -> Query: """Filter query to include only soft deleted production notes.""" return query.filter(cls.deleted_at.isnot(None)) @classmethod def query_all_including_deleted(cls, query: Query) -> Query: """Return query without soft deletion filtering (for admin use).""" return query def __repr__(self): return f"" class TaskAttachment(Base): __tablename__ = "task_attachments" id = Column(Integer, primary_key=True, index=True) task_id = Column(Integer, ForeignKey("tasks.id"), nullable=False) user_id = Column(Integer, ForeignKey("users.id"), nullable=False) file_name = Column(String, nullable=False) file_path = Column(String, nullable=False) file_type = Column(String, nullable=False) file_size = Column(Integer, nullable=False) attachment_type = Column(Enum(AttachmentType), nullable=False, default=AttachmentType.REFERENCE) description = Column(Text) uploaded_at = Column(DateTime(timezone=True), server_default=func.now()) # Soft deletion columns deleted_at = Column(DateTime(timezone=True), nullable=True) deleted_by = Column(Integer, ForeignKey("users.id"), nullable=True) # Relationships task = relationship("Task", back_populates="attachments") user = relationship("User", foreign_keys=[user_id], back_populates="task_attachments") deleted_by_user = relationship("User", foreign_keys=[deleted_by]) @property def is_deleted(self) -> bool: """Check if the task attachment is soft deleted.""" return self.deleted_at is not None @classmethod def query_active(cls, query: Query) -> Query: """Filter query to exclude soft deleted task attachments.""" return query.filter(cls.deleted_at.is_(None)) @classmethod def query_deleted(cls, query: Query) -> Query: """Filter query to include only soft deleted task attachments.""" return query.filter(cls.deleted_at.isnot(None)) @classmethod def query_all_including_deleted(cls, query: Query) -> Query: """Return query without soft deletion filtering (for admin use).""" return query def __repr__(self): return f""