Files
LinkDesk/backend/models/project.py
T
indigo 23740af816 Add per-task-type submission configuration with client-side movie spec checks
Coordinators can configure, per task type, accepted file extensions, a
naming convention, and (for video files) required resolution/format/
codec/frame rate. All checking happens entirely client-side in the
browser before upload - the backend only stores and validates the
configuration itself, never inspects submitted files.

- backend: submission_config_by_task_type JSON column on Project,
  schemas/submission_config.py for validation, GET/PUT
  /projects/{id}/submission-config endpoints. Added .mxf/.ma/.usd(a/c)
  to the supported file formats.
- frontend: new Submissions tab in Project Settings
  (SubmissionConfigManager.vue) to configure rules per task type.
  TaskSubmissions.vue enforces them before upload: extension/naming
  checks via regex, and real video resolution/codec/frame rate
  detection via mediainfo.js (parses container metadata directly,
  so it works for formats browsers can't play natively like MXF,
  ProRes, or DNxHD).
2026-07-19 10:38:29 +08:00

91 lines
4.1 KiB
Python

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
# Submission configuration per task type
submission_config_by_task_type = Column(JSON, nullable=True) # Allowed file types, naming pattern, required flag per task type
# 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}')>"