c09710f4e5
Departments are now a customizable per-project list (standard + custom), matching the existing custom task type/status pattern, instead of a fixed 7-value enum. They're usable directly on tasks (new field, independent of assignee) and continue to drive team member department roles.
342 lines
13 KiB
Python
342 lines
13 KiB
Python
from pydantic import BaseModel, Field, validator
|
|
from typing import Optional, List, Dict
|
|
from datetime import date, datetime
|
|
from enum import Enum
|
|
import re
|
|
|
|
from models.project import ProjectStatus, ProjectType
|
|
|
|
|
|
# Technical Specifications Schemas
|
|
class DeliveryMovieSpec(BaseModel):
|
|
"""Delivery movie specifications for a department"""
|
|
resolution: str = Field(..., description="Movie resolution (e.g., '1920x1080', '2048x1080')")
|
|
format: str = Field(..., description="Movie format (e.g., 'mov', 'mp4', 'exr')")
|
|
codec: Optional[str] = Field(None, description="Video codec (e.g., 'h264', 'prores', 'dnxhd')")
|
|
quality: Optional[str] = Field(None, description="Quality setting (e.g., 'high', 'medium', 'low')")
|
|
|
|
@validator('resolution')
|
|
def validate_resolution(cls, v):
|
|
if not re.match(r'^\d+x\d+$', v):
|
|
raise ValueError('Resolution must be in format "WIDTHxHEIGHT" (e.g., "1920x1080")')
|
|
return v
|
|
|
|
@validator('format')
|
|
def validate_format(cls, v):
|
|
allowed_formats = ['mov', 'mp4', 'exr', 'dpx', 'tiff']
|
|
if v.lower() not in allowed_formats:
|
|
raise ValueError(f'Format must be one of: {", ".join(allowed_formats)}')
|
|
return v.lower()
|
|
|
|
@validator('codec')
|
|
def validate_codec(cls, v):
|
|
if v is None:
|
|
return v
|
|
allowed_codecs = ['h264', 'h265', 'prores', 'dnxhd', 'dnxhr', 'uncompressed']
|
|
if v.lower() not in allowed_codecs:
|
|
raise ValueError(f'Codec must be one of: {", ".join(allowed_codecs)}')
|
|
return v.lower()
|
|
|
|
@validator('quality')
|
|
def validate_quality(cls, v):
|
|
if v is None:
|
|
return v
|
|
allowed_qualities = ['low', 'medium', 'high', 'lossless']
|
|
if v.lower() not in allowed_qualities:
|
|
raise ValueError(f'Quality must be one of: {", ".join(allowed_qualities)}')
|
|
return v.lower()
|
|
|
|
|
|
class ProjectTechnicalSpecs(BaseModel):
|
|
"""Technical specifications for a project"""
|
|
frame_rate: Optional[float] = Field(None, ge=1.0, le=120.0, description="Frames per second (1-120 fps)")
|
|
data_drive_path: Optional[str] = Field(None, description="Physical path for project data storage")
|
|
publish_storage_path: Optional[str] = Field(None, description="Path for approved work delivery")
|
|
delivery_image_resolution: Optional[str] = Field(None, description="Required image resolution (e.g., '1920x1080', '4096x2160')")
|
|
delivery_movie_specs_by_department: Optional[Dict[str, DeliveryMovieSpec]] = Field(
|
|
default_factory=dict,
|
|
description="Delivery movie resolution and format specifications per department"
|
|
)
|
|
|
|
@validator('delivery_image_resolution')
|
|
def validate_delivery_image_resolution(cls, v):
|
|
if v is None:
|
|
return v
|
|
if not re.match(r'^\d+x\d+$', v):
|
|
raise ValueError('Delivery image resolution must be in format "WIDTHxHEIGHT" (e.g., "1920x1080")')
|
|
return v
|
|
|
|
|
|
# Default delivery movie specifications per department
|
|
DEFAULT_DELIVERY_MOVIE_SPECS = {
|
|
"layout": DeliveryMovieSpec(
|
|
resolution="1920x1080",
|
|
format="mov",
|
|
codec="h264",
|
|
quality="medium"
|
|
),
|
|
"animation": DeliveryMovieSpec(
|
|
resolution="1920x1080",
|
|
format="mov",
|
|
codec="h264",
|
|
quality="high"
|
|
),
|
|
"lighting": DeliveryMovieSpec(
|
|
resolution="2048x1080",
|
|
format="exr",
|
|
codec=None,
|
|
quality="high"
|
|
),
|
|
"composite": DeliveryMovieSpec(
|
|
resolution="2048x1080",
|
|
format="mov",
|
|
codec="prores",
|
|
quality="high"
|
|
)
|
|
}
|
|
|
|
|
|
class ProjectBase(BaseModel):
|
|
name: str = Field(..., min_length=1, max_length=255)
|
|
code_name: str = Field(..., min_length=1, max_length=50, description="Unique project code identifier")
|
|
client_name: str = Field(..., min_length=1, max_length=255, description="Client or studio name")
|
|
project_type: ProjectType = Field(..., description="Project type: TV, Cinema, or Game")
|
|
description: Optional[str] = None
|
|
status: ProjectStatus = ProjectStatus.PLANNING
|
|
start_date: Optional[date] = None
|
|
end_date: Optional[date] = None
|
|
|
|
# Technical specifications
|
|
frame_rate: Optional[float] = Field(None, ge=1.0, le=120.0, description="Frames per second (1-120 fps)")
|
|
data_drive_path: Optional[str] = Field(None, description="Physical path for project data storage")
|
|
publish_storage_path: Optional[str] = Field(None, description="Path for approved work delivery")
|
|
delivery_image_resolution: Optional[str] = Field(None, description="Required image resolution")
|
|
delivery_movie_specs_by_department: Optional[Dict[str, DeliveryMovieSpec]] = Field(
|
|
default_factory=dict,
|
|
description="Delivery movie specifications per department"
|
|
)
|
|
|
|
@validator('delivery_image_resolution')
|
|
def validate_delivery_image_resolution(cls, v):
|
|
if v is None:
|
|
return v
|
|
if not re.match(r'^\d+x\d+$', v):
|
|
raise ValueError('Delivery image resolution must be in format "WIDTHxHEIGHT" (e.g., "1920x1080")')
|
|
return v
|
|
|
|
class ProjectCreate(ProjectBase):
|
|
pass
|
|
|
|
|
|
class ProjectUpdate(BaseModel):
|
|
name: Optional[str] = Field(None, min_length=1, max_length=255)
|
|
code_name: Optional[str] = Field(None, min_length=1, max_length=50, description="Unique project code identifier")
|
|
client_name: Optional[str] = Field(None, min_length=1, max_length=255, description="Client or studio name")
|
|
project_type: Optional[ProjectType] = Field(None, description="Project type: TV, Cinema, or Game")
|
|
description: Optional[str] = None
|
|
status: Optional[ProjectStatus] = None
|
|
start_date: Optional[date] = None
|
|
end_date: Optional[date] = None
|
|
|
|
# Technical specifications
|
|
frame_rate: Optional[float] = Field(None, ge=1.0, le=120.0, description="Frames per second (1-120 fps)")
|
|
data_drive_path: Optional[str] = Field(None, description="Physical path for project data storage")
|
|
publish_storage_path: Optional[str] = Field(None, description="Path for approved work delivery")
|
|
delivery_image_resolution: Optional[str] = Field(None, description="Required image resolution")
|
|
delivery_movie_specs_by_department: Optional[Dict[str, DeliveryMovieSpec]] = Field(
|
|
None,
|
|
description="Delivery movie specifications per department"
|
|
)
|
|
|
|
@validator('delivery_image_resolution')
|
|
def validate_delivery_image_resolution(cls, v):
|
|
if v is None:
|
|
return v
|
|
if not re.match(r'^\d+x\d+$', v):
|
|
raise ValueError('Delivery image resolution must be in format "WIDTHxHEIGHT" (e.g., "1920x1080")')
|
|
return v
|
|
|
|
class ProjectMemberBase(BaseModel):
|
|
user_id: int
|
|
department_role: Optional[str] = None
|
|
|
|
|
|
class ProjectMemberCreate(ProjectMemberBase):
|
|
pass
|
|
|
|
|
|
class ProjectMemberUpdate(BaseModel):
|
|
department_role: Optional[str] = None
|
|
|
|
|
|
class ProjectMemberResponse(ProjectMemberBase):
|
|
id: int
|
|
project_id: int
|
|
joined_at: datetime
|
|
|
|
# User information
|
|
user_email: str
|
|
user_first_name: str
|
|
user_last_name: str
|
|
user_avatar_url: Optional[str] = None
|
|
|
|
class Config:
|
|
from_attributes = True
|
|
|
|
|
|
class ProjectResponse(ProjectBase):
|
|
id: int
|
|
created_at: datetime
|
|
updated_at: datetime
|
|
thumbnail_url: Optional[str] = None
|
|
|
|
# Optional member list
|
|
project_members: Optional[List[ProjectMemberResponse]] = None
|
|
|
|
class Config:
|
|
from_attributes = True
|
|
|
|
|
|
class ProjectListResponse(BaseModel):
|
|
id: int
|
|
name: str
|
|
code_name: str
|
|
client_name: str
|
|
project_type: ProjectType
|
|
description: Optional[str] = None
|
|
status: ProjectStatus
|
|
start_date: Optional[date] = None
|
|
end_date: Optional[date] = None
|
|
created_at: datetime
|
|
updated_at: datetime
|
|
thumbnail_url: Optional[str] = None
|
|
|
|
# Summary information
|
|
member_count: int = 0
|
|
episode_count: int = 0
|
|
asset_count: int = 0
|
|
|
|
class Config:
|
|
from_attributes = True
|
|
|
|
|
|
# Default asset tasks by category
|
|
DEFAULT_ASSET_TASKS = {
|
|
"characters": ["modeling", "surfacing", "rigging"],
|
|
"props": ["modeling", "surfacing"],
|
|
"sets": ["modeling", "surfacing"],
|
|
"vehicles": ["modeling", "surfacing", "rigging"]
|
|
}
|
|
|
|
# Default shot tasks
|
|
DEFAULT_SHOT_TASKS = ["layout", "animation", "simulation", "lighting", "compositing"]
|
|
|
|
|
|
class ProjectSettings(BaseModel):
|
|
"""Project-specific settings for upload location and task templates"""
|
|
upload_data_location: Optional[str] = Field(None, description="Custom upload storage path for project files")
|
|
asset_task_templates: Optional[Dict[str, List[str]]] = Field(
|
|
default_factory=lambda: DEFAULT_ASSET_TASKS.copy(),
|
|
description="Custom default tasks per asset category"
|
|
)
|
|
shot_task_templates: Optional[List[str]] = Field(
|
|
default_factory=lambda: DEFAULT_SHOT_TASKS.copy(),
|
|
description="Custom default tasks for shots"
|
|
)
|
|
enabled_asset_tasks: Optional[Dict[str, List[str]]] = Field(
|
|
default_factory=dict,
|
|
description="Enabled/disabled status for asset tasks per category"
|
|
)
|
|
enabled_shot_tasks: Optional[List[str]] = Field(
|
|
default_factory=lambda: DEFAULT_SHOT_TASKS.copy(),
|
|
description="Enabled/disabled shot tasks"
|
|
)
|
|
|
|
@validator('asset_task_templates')
|
|
def validate_asset_task_templates(cls, v):
|
|
if v is None:
|
|
return DEFAULT_ASSET_TASKS.copy()
|
|
allowed_categories = ['characters', 'props', 'sets', 'vehicles']
|
|
for category in v.keys():
|
|
if category not in allowed_categories:
|
|
raise ValueError(f'Asset category must be one of: {", ".join(allowed_categories)}')
|
|
if not isinstance(v[category], list):
|
|
raise ValueError(f'Task templates for {category} must be a list')
|
|
return v
|
|
|
|
@validator('shot_task_templates')
|
|
def validate_shot_task_templates(cls, v):
|
|
if v is None:
|
|
return DEFAULT_SHOT_TASKS.copy()
|
|
if not isinstance(v, list):
|
|
raise ValueError('Shot task templates must be a list')
|
|
return v
|
|
|
|
@validator('enabled_asset_tasks')
|
|
def validate_enabled_asset_tasks(cls, v):
|
|
if v is None:
|
|
return {}
|
|
allowed_categories = ['characters', 'props', 'sets', 'vehicles']
|
|
for category in v.keys():
|
|
if category not in allowed_categories:
|
|
raise ValueError(f'Asset category must be one of: {", ".join(allowed_categories)}')
|
|
if not isinstance(v[category], list):
|
|
raise ValueError(f'Enabled tasks for {category} must be a list')
|
|
return v
|
|
|
|
@validator('enabled_shot_tasks')
|
|
def validate_enabled_shot_tasks(cls, v):
|
|
if v is None:
|
|
return DEFAULT_SHOT_TASKS.copy()
|
|
if not isinstance(v, list):
|
|
raise ValueError('Enabled shot tasks must be a list')
|
|
return v
|
|
|
|
|
|
class ProjectSettingsUpdate(BaseModel):
|
|
"""Update project settings"""
|
|
upload_data_location: Optional[str] = Field(None, description="Custom upload storage path for project files")
|
|
asset_task_templates: Optional[Dict[str, List[str]]] = Field(None, description="Custom default tasks per asset category")
|
|
shot_task_templates: Optional[List[str]] = Field(None, description="Custom default tasks for shots")
|
|
enabled_asset_tasks: Optional[Dict[str, List[str]]] = Field(None, description="Enabled/disabled asset tasks per category")
|
|
enabled_shot_tasks: Optional[List[str]] = Field(None, description="Enabled/disabled shot tasks")
|
|
|
|
@validator('asset_task_templates')
|
|
def validate_asset_task_templates(cls, v):
|
|
if v is None:
|
|
return v
|
|
allowed_categories = ['characters', 'props', 'sets', 'vehicles']
|
|
for category in v.keys():
|
|
if category not in allowed_categories:
|
|
raise ValueError(f'Asset category must be one of: {", ".join(allowed_categories)}')
|
|
if not isinstance(v[category], list):
|
|
raise ValueError(f'Task templates for {category} must be a list')
|
|
return v
|
|
|
|
@validator('shot_task_templates')
|
|
def validate_shot_task_templates(cls, v):
|
|
if v is None:
|
|
return v
|
|
if not isinstance(v, list):
|
|
raise ValueError('Shot task templates must be a list')
|
|
return v
|
|
|
|
@validator('enabled_asset_tasks')
|
|
def validate_enabled_asset_tasks(cls, v):
|
|
if v is None:
|
|
return v
|
|
allowed_categories = ['characters', 'props', 'sets', 'vehicles']
|
|
for category in v.keys():
|
|
if category not in allowed_categories:
|
|
raise ValueError(f'Asset category must be one of: {", ".join(allowed_categories)}')
|
|
if not isinstance(v[category], list):
|
|
raise ValueError(f'Enabled tasks for {category} must be a list')
|
|
return v
|
|
|
|
@validator('enabled_shot_tasks')
|
|
def validate_enabled_shot_tasks(cls, v):
|
|
if v is None:
|
|
return v
|
|
if not isinstance(v, list):
|
|
raise ValueError('Enabled shot tasks must be a list')
|
|
return v |