Init Repo

This commit is contained in:
2026-02-28 03:22:04 +08:00
commit de59b57ee7
883 changed files with 156857 additions and 0 deletions
+39
View File
@@ -0,0 +1,39 @@
# Models package
from .user import User, UserRole, DepartmentRole
from .project import Project, ProjectMember, ProjectStatus
from .episode import Episode, EpisodeStatus
from .asset import Asset, AssetCategory, AssetStatus
from .shot import Shot, ShotStatus
from .task import (
Task, Submission, Review, ProductionNote, TaskAttachment,
TaskType, TaskStatus, ReviewDecision, AttachmentType
)
from .api_key import APIKey, APIKeyScope
from .api_key_usage import APIKeyUsage
from .global_settings import GlobalSettings
from .notification import Notification, UserNotificationPreference, NotificationType
from .activity import Activity, ActivityType
__all__ = [
# User models
"User", "UserRole", "DepartmentRole",
# Project models
"Project", "ProjectMember", "ProjectStatus",
# Episode models
"Episode", "EpisodeStatus",
# Asset models
"Asset", "AssetCategory", "AssetStatus",
# Shot models
"Shot", "ShotStatus",
# Task models
"Task", "Submission", "Review", "ProductionNote", "TaskAttachment",
"TaskType", "TaskStatus", "ReviewDecision", "AttachmentType",
# API Key models
"APIKey", "APIKeyScope", "APIKeyUsage",
# Global Settings models
"GlobalSettings",
# Notification models
"Notification", "UserNotificationPreference", "NotificationType",
# Activity models
"Activity", "ActivityType"
]
+55
View File
@@ -0,0 +1,55 @@
from sqlalchemy import Column, Integer, String, DateTime, ForeignKey, Text, Enum as SQLEnum, JSON
from sqlalchemy.orm import relationship
from database import Base
from datetime import datetime
import enum
class ActivityType(str, enum.Enum):
TASK_CREATED = "task_created"
TASK_UPDATED = "task_updated"
TASK_ASSIGNED = "task_assigned"
TASK_STATUS_CHANGED = "task_status_changed"
SUBMISSION_CREATED = "submission_created"
SUBMISSION_REVIEWED = "submission_reviewed"
COMMENT_ADDED = "comment_added"
ASSET_CREATED = "asset_created"
ASSET_UPDATED = "asset_updated"
SHOT_CREATED = "shot_created"
SHOT_UPDATED = "shot_updated"
PROJECT_CREATED = "project_created"
PROJECT_UPDATED = "project_updated"
USER_JOINED_PROJECT = "user_joined_project"
SHOT_DELETED = "shot_deleted"
ASSET_DELETED = "asset_deleted"
SHOT_RECOVERED = "shot_recovered"
ASSET_RECOVERED = "asset_recovered"
class Activity(Base):
__tablename__ = "activities"
id = Column(Integer, primary_key=True, index=True)
type = Column(SQLEnum(ActivityType), nullable=False, index=True)
user_id = Column(Integer, ForeignKey("users.id"), nullable=False, index=True)
# Optional references to related entities
project_id = Column(Integer, ForeignKey("projects.id"), nullable=True, index=True)
task_id = Column(Integer, ForeignKey("tasks.id"), nullable=True)
asset_id = Column(Integer, ForeignKey("assets.id"), nullable=True)
shot_id = Column(Integer, ForeignKey("shots.id"), nullable=True)
submission_id = Column(Integer, ForeignKey("submissions.id"), nullable=True)
# Activity details
description = Column(Text, nullable=False)
activity_metadata = Column(JSON, nullable=True) # Additional context data
created_at = Column(DateTime, default=datetime.utcnow, nullable=False, index=True)
# Relationships
user = relationship("User", foreign_keys=[user_id])
project = relationship("Project", foreign_keys=[project_id])
task = relationship("Task", foreign_keys=[task_id])
asset = relationship("Asset", foreign_keys=[asset_id])
shot = relationship("Shot", foreign_keys=[shot_id])
submission = relationship("Submission", foreign_keys=[submission_id])
+36
View File
@@ -0,0 +1,36 @@
from sqlalchemy import Column, Integer, String, Boolean, DateTime, ForeignKey, Text
from sqlalchemy.orm import relationship
from sqlalchemy.sql import func
from database import Base
import enum
class APIKeyScope(str, enum.Enum):
READ_PROJECTS = "read:projects"
READ_TASKS = "read:tasks"
READ_SUBMISSIONS = "read:submissions"
READ_USERS = "read:users"
WRITE_TASKS = "write:tasks"
WRITE_SUBMISSIONS = "write:submissions"
ADMIN_USERS = "admin:users"
FULL_ACCESS = "full:access"
class APIKey(Base):
__tablename__ = "api_keys"
id = Column(Integer, primary_key=True, index=True)
user_id = Column(Integer, ForeignKey("users.id"), nullable=False)
key_hash = Column(String, unique=True, index=True, nullable=False)
name = Column(String, nullable=False)
scopes = Column(Text, nullable=False) # JSON string of scopes
is_active = Column(Boolean, default=True, nullable=False)
expires_at = Column(DateTime(timezone=True), nullable=True)
last_used_at = Column(DateTime(timezone=True), nullable=True)
created_at = Column(DateTime(timezone=True), server_default=func.now())
# Relationships
user = relationship("User", back_populates="api_keys")
def __repr__(self):
return f"<APIKey(id={self.id}, name='{self.name}', user_id={self.user_id})>"
+22
View File
@@ -0,0 +1,22 @@
from sqlalchemy import Column, Integer, String, DateTime, ForeignKey
from sqlalchemy.orm import relationship
from sqlalchemy.sql import func
from database import Base
class APIKeyUsage(Base):
__tablename__ = "api_key_usage"
id = Column(Integer, primary_key=True, index=True)
api_key_id = Column(Integer, ForeignKey("api_keys.id"), nullable=False)
endpoint = Column(String, nullable=False)
method = Column(String, nullable=False)
timestamp = Column(DateTime(timezone=True), server_default=func.now())
ip_address = Column(String, nullable=True)
user_agent = Column(String, nullable=True)
# Relationships
api_key = relationship("APIKey")
def __repr__(self):
return f"<APIKeyUsage(id={self.id}, api_key_id={self.api_key_id}, endpoint='{self.endpoint}')>"
+65
View File
@@ -0,0 +1,65 @@
from sqlalchemy import Column, Integer, String, DateTime, ForeignKey, Enum
from sqlalchemy.orm import relationship, Query
from sqlalchemy.sql import func
from database import Base
import enum
class AssetCategory(str, enum.Enum):
CHARACTERS = "characters"
PROPS = "props"
SETS = "sets"
VEHICLES = "vehicles"
class AssetStatus(str, enum.Enum):
NOT_STARTED = "not_started"
IN_PROGRESS = "in_progress"
ON_HOLD = "on_hold"
COMPLETED = "completed"
APPROVED = "approved"
class Asset(Base):
__tablename__ = "assets"
id = Column(Integer, primary_key=True, index=True)
project_id = Column(Integer, ForeignKey("projects.id"), nullable=False)
name = Column(String, nullable=False, index=True)
description = Column(String)
category = Column(Enum(AssetCategory), nullable=False)
status = Column(Enum(AssetStatus), nullable=False, default=AssetStatus.NOT_STARTED)
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="assets")
tasks = relationship("Task", back_populates="asset", cascade="all, delete-orphan")
deleted_by_user = relationship("User", foreign_keys=[deleted_by])
@property
def is_deleted(self) -> bool:
"""Check if the asset is soft deleted."""
return self.deleted_at is not None
@classmethod
def query_active(cls, query: Query) -> Query:
"""Filter query to exclude soft deleted assets."""
return query.filter(cls.deleted_at.is_(None))
@classmethod
def query_deleted(cls, query: Query) -> Query:
"""Filter query to include only soft deleted assets."""
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"<Asset(id={self.id}, name='{self.name}', category='{self.category}')>"
+34
View File
@@ -0,0 +1,34 @@
from sqlalchemy import Column, Integer, String, DateTime, ForeignKey, Enum
from sqlalchemy.orm import relationship
from sqlalchemy.sql import func
from database import Base
import enum
class EpisodeStatus(str, enum.Enum):
PLANNING = "planning"
IN_PROGRESS = "in_progress"
ON_HOLD = "on_hold"
COMPLETED = "completed"
CANCELLED = "cancelled"
class Episode(Base):
__tablename__ = "episodes"
id = Column(Integer, primary_key=True, index=True)
project_id = Column(Integer, ForeignKey("projects.id"), nullable=False)
name = Column(String, nullable=False, index=True)
description = Column(String)
episode_number = Column(Integer, nullable=False)
status = Column(Enum(EpisodeStatus), nullable=False, default=EpisodeStatus.PLANNING)
created_at = Column(DateTime(timezone=True), server_default=func.now())
updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now())
# Relationships
project = relationship("Project", back_populates="episodes")
shots = relationship("Shot", back_populates="episode", cascade="all, delete-orphan")
tasks = relationship("Task", back_populates="episode", cascade="all, delete-orphan")
def __repr__(self):
return f"<Episode(id={self.id}, name='{self.name}', episode_number={self.episode_number})>"
+14
View File
@@ -0,0 +1,14 @@
from sqlalchemy import Column, Integer, String, DateTime, Text
from sqlalchemy.sql import func
from database import Base
class GlobalSettings(Base):
__tablename__ = "global_settings"
id = Column(Integer, primary_key=True, index=True)
setting_key = Column(String(100), unique=True, index=True, nullable=False)
setting_value = Column(Text, nullable=False)
description = Column(Text)
created_at = Column(DateTime(timezone=True), server_default=func.now())
updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now())
+89
View File
@@ -0,0 +1,89 @@
from sqlalchemy import Column, Integer, String, Boolean, DateTime, ForeignKey, Text, Enum as SQLEnum
from sqlalchemy.orm import relationship
from database import Base
from datetime import datetime
import enum
class NotificationType(str, enum.Enum):
TASK_ASSIGNED = "task_assigned"
TASK_STATUS_CHANGED = "task_status_changed"
SUBMISSION_REVIEWED = "submission_reviewed"
WORK_SUBMITTED = "work_submitted"
DEADLINE_APPROACHING = "deadline_approaching"
PROJECT_UPDATE = "project_update"
COMMENT_ADDED = "comment_added"
class NotificationPriority(str, enum.Enum):
LOW = "low"
NORMAL = "normal"
HIGH = "high"
URGENT = "urgent"
class Notification(Base):
__tablename__ = "notifications"
id = Column(Integer, primary_key=True, index=True)
user_id = Column(Integer, ForeignKey("users.id"), nullable=False, index=True)
type = Column(SQLEnum(NotificationType), nullable=False)
priority = Column(SQLEnum(NotificationPriority), default=NotificationPriority.NORMAL)
title = Column(String(255), nullable=False)
message = Column(Text, nullable=False)
read = Column(Boolean, default=False, index=True)
# Optional references to related entities
project_id = Column(Integer, ForeignKey("projects.id"), nullable=True)
task_id = Column(Integer, ForeignKey("tasks.id"), nullable=True)
submission_id = Column(Integer, ForeignKey("submissions.id"), nullable=True)
# Email notification tracking
email_sent = Column(Boolean, default=False)
email_sent_at = Column(DateTime, nullable=True)
created_at = Column(DateTime, default=datetime.utcnow, nullable=False)
read_at = Column(DateTime, nullable=True)
# Relationships
user = relationship("User", back_populates="notifications")
project = relationship("Project", foreign_keys=[project_id])
task = relationship("Task", foreign_keys=[task_id])
submission = relationship("Submission", foreign_keys=[submission_id])
class UserNotificationPreference(Base):
__tablename__ = "user_notification_preferences"
id = Column(Integer, primary_key=True, index=True)
user_id = Column(Integer, ForeignKey("users.id"), nullable=False, unique=True)
# Email notification preferences
email_enabled = Column(Boolean, default=True)
email_task_assigned = Column(Boolean, default=True)
email_task_status_changed = Column(Boolean, default=True)
email_submission_reviewed = Column(Boolean, default=True)
email_work_submitted = Column(Boolean, default=True)
email_deadline_approaching = Column(Boolean, default=True)
email_project_update = Column(Boolean, default=True)
email_comment_added = Column(Boolean, default=True)
# In-app notification preferences
inapp_enabled = Column(Boolean, default=True)
inapp_task_assigned = Column(Boolean, default=True)
inapp_task_status_changed = Column(Boolean, default=True)
inapp_submission_reviewed = Column(Boolean, default=True)
inapp_work_submitted = Column(Boolean, default=True)
inapp_deadline_approaching = Column(Boolean, default=True)
inapp_project_update = Column(Boolean, default=True)
inapp_comment_added = Column(Boolean, default=True)
# Digest settings
email_digest_enabled = Column(Boolean, default=False)
email_digest_frequency = Column(String(50), default="daily") # daily, weekly
created_at = Column(DateTime, default=datetime.utcnow)
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
# Relationships
user = relationship("User", back_populates="notification_preferences")
+88
View File
@@ -0,0 +1,88 @@
from sqlalchemy import Column, Integer, String, DateTime, Date, Enum, ForeignKey, Float, JSON
from sqlalchemy.orm import relationship
from sqlalchemy.sql import func
from database import Base
from .user import DepartmentRole
import enum
class ProjectStatus(str, enum.Enum):
PLANNING = "planning"
IN_PROGRESS = "in_progress"
ON_HOLD = "on_hold"
COMPLETED = "completed"
CANCELLED = "cancelled"
class ProjectType(str, enum.Enum):
TV = "tv"
CINEMA = "cinema"
GAME = "game"
class Project(Base):
__tablename__ = "projects"
id = Column(Integer, primary_key=True, index=True)
name = Column(String, nullable=False, index=True)
code_name = Column(String, nullable=False, unique=True, index=True) # Unique project code
client_name = Column(String, nullable=False, index=True) # Client/studio name
project_type = Column(Enum(ProjectType), nullable=False) # TV, Cinema, Game
description = Column(String)
status = Column(Enum(ProjectStatus), nullable=False, default=ProjectStatus.PLANNING)
start_date = Column(Date)
end_date = Column(Date)
# Technical specifications
frame_rate = Column(Float, nullable=True) # Frames per second (1-120 fps)
data_drive_path = Column(String, nullable=True) # Physical path for project data storage
publish_storage_path = Column(String, nullable=True) # Path for approved work delivery
delivery_image_resolution = Column(String, nullable=True) # Required image resolution
delivery_movie_specs_by_department = Column(JSON, nullable=True) # Delivery movie specs per department
# Project-specific settings
upload_data_location = Column(String, nullable=True) # Custom upload storage path
asset_task_templates = Column(JSON, nullable=True) # Default tasks per asset category
shot_task_templates = Column(JSON, nullable=True) # Default tasks for shots
enabled_asset_tasks = Column(JSON, nullable=True) # Enabled/disabled asset tasks per category
enabled_shot_tasks = Column(JSON, nullable=True) # Enabled/disabled shot tasks
# Custom task types
custom_asset_task_types = Column(JSON, nullable=True) # Custom task types for assets
custom_shot_task_types = Column(JSON, nullable=True) # Custom task types for shots
# Custom task statuses
custom_task_statuses = Column(JSON, nullable=True) # Custom task statuses for project
# Project thumbnail
thumbnail_path = Column(String, nullable=True) # Path to project thumbnail image
created_at = Column(DateTime(timezone=True), server_default=func.now())
updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now())
# Relationships
episodes = relationship("Episode", back_populates="project", cascade="all, delete-orphan")
shots = relationship("Shot", back_populates="project", cascade="all, delete-orphan")
assets = relationship("Asset", back_populates="project", cascade="all, delete-orphan")
project_members = relationship("ProjectMember", back_populates="project", cascade="all, delete-orphan")
tasks = relationship("Task", back_populates="project", cascade="all, delete-orphan")
def __repr__(self):
return f"<Project(id={self.id}, name='{self.name}', status='{self.status}')>"
class ProjectMember(Base):
__tablename__ = "project_members"
id = Column(Integer, primary_key=True, index=True)
user_id = Column(Integer, ForeignKey("users.id"), nullable=False)
project_id = Column(Integer, ForeignKey("projects.id"), nullable=False)
department_role = Column(Enum(DepartmentRole), nullable=True)
joined_at = Column(DateTime(timezone=True), server_default=func.now())
# Relationships
user = relationship("User", back_populates="project_memberships")
project = relationship("Project", back_populates="project_members")
def __repr__(self):
return f"<ProjectMember(user_id={self.user_id}, project_id={self.project_id}, role='{self.department_role}')>"
+66
View File
@@ -0,0 +1,66 @@
from sqlalchemy import Column, Integer, String, DateTime, ForeignKey, Enum, UniqueConstraint
from sqlalchemy.orm import relationship, Query
from sqlalchemy.sql import func
from database import Base
import enum
class ShotStatus(str, enum.Enum):
NOT_STARTED = "not_started"
IN_PROGRESS = "in_progress"
ON_HOLD = "on_hold"
COMPLETED = "completed"
APPROVED = "approved"
class Shot(Base):
__tablename__ = "shots"
id = Column(Integer, primary_key=True, index=True)
project_id = Column(Integer, ForeignKey("projects.id"), nullable=False, index=True)
episode_id = Column(Integer, ForeignKey("episodes.id"), nullable=False)
name = Column(String, nullable=False, index=True)
description = Column(String)
frame_start = Column(Integer, nullable=False, default=1001)
frame_end = Column(Integer, nullable=False, default=1001)
status = Column(Enum(ShotStatus), nullable=False, default=ShotStatus.NOT_STARTED)
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="shots")
episode = relationship("Episode", back_populates="shots")
tasks = relationship("Task", back_populates="shot", cascade="all, delete-orphan")
deleted_by_user = relationship("User", foreign_keys=[deleted_by])
# Constraints
__table_args__ = (
UniqueConstraint('project_id', 'name', name='uq_shot_project_name'),
)
@property
def is_deleted(self) -> bool:
"""Check if the shot is soft deleted."""
return self.deleted_at is not None
@classmethod
def query_active(cls, query: Query) -> Query:
"""Filter query to exclude soft deleted shots."""
return query.filter(cls.deleted_at.is_(None))
@classmethod
def query_deleted(cls, query: Query) -> Query:
"""Filter query to include only soft deleted shots."""
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"<Shot(id={self.id}, name='{self.name}', frames={self.frame_start}-{self.frame_end})>"
+276
View File
@@ -0,0 +1,276 @@
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"<Task(id={self.id}, name='{self.name}', type='{self.task_type}', status='{self.status}')>"
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"<Submission(id={self.id}, task_id={self.task_id}, version={self.version_number})>"
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"<Review(id={self.id}, decision='{self.decision}', submission_id={self.submission_id})>"
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"<ProductionNote(id={self.id}, task_id={self.task_id}, user_id={self.user_id})>"
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"<TaskAttachment(id={self.id}, file_name='{self.file_name}', type='{self.attachment_type}')>"
+52
View File
@@ -0,0 +1,52 @@
from sqlalchemy import Column, Integer, String, Boolean, DateTime, Enum
from sqlalchemy.orm import relationship
from sqlalchemy.sql import func
from database import Base
import enum
class UserRole(str, enum.Enum):
DIRECTOR = "director"
COORDINATOR = "coordinator"
ARTIST = "artist"
DEVELOPER = "developer"
class DepartmentRole(str, enum.Enum):
LAYOUT = "layout"
ANIMATION = "animation"
LIGHTING = "lighting"
COMPOSITE = "composite"
MODELING = "modeling"
RIGGING = "rigging"
SURFACING = "surfacing"
class User(Base):
__tablename__ = "users"
id = Column(Integer, primary_key=True, index=True)
email = Column(String, unique=True, index=True, nullable=False)
password_hash = Column(String, nullable=False)
first_name = Column(String, nullable=False)
last_name = Column(String, nullable=False)
role = Column(Enum(UserRole), nullable=False, default=UserRole.ARTIST)
is_admin = Column(Boolean, default=False, nullable=False)
is_approved = Column(Boolean, default=False, nullable=False)
avatar_url = Column(String, 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())
# Relationships
project_memberships = relationship("ProjectMember", back_populates="user", cascade="all, delete-orphan")
assigned_tasks = relationship("Task", foreign_keys="Task.assigned_user_id", back_populates="assigned_user")
submissions = relationship("Submission", foreign_keys="Submission.user_id", back_populates="user")
reviews = relationship("Review", foreign_keys="Review.reviewer_id", back_populates="reviewer")
production_notes = relationship("ProductionNote", foreign_keys="ProductionNote.user_id", back_populates="user")
task_attachments = relationship("TaskAttachment", foreign_keys="TaskAttachment.user_id", back_populates="user")
api_keys = relationship("APIKey", back_populates="user", cascade="all, delete-orphan")
notifications = relationship("Notification", back_populates="user", cascade="all, delete-orphan")
notification_preferences = relationship("UserNotificationPreference", back_populates="user", uselist=False, cascade="all, delete-orphan")
def __repr__(self):
return f"<User(id={self.id}, email='{self.email}', role='{self.role}')>"