LinkDesk/backend/schemas/project.py

374 lines
14 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
from models.user import DepartmentRole
# 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
@validator('delivery_movie_specs_by_department')
def validate_delivery_movie_specs_by_department(cls, v):
if v is None:
return {}
allowed_departments = ['layout', 'animation', 'lighting', 'composite', 'modeling', 'rigging', 'surfacing']
for dept in v.keys():
if dept not in allowed_departments:
raise ValueError(f'Department must be one of: {", ".join(allowed_departments)}')
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
@validator('delivery_movie_specs_by_department')
def validate_delivery_movie_specs_by_department(cls, v):
if v is None:
return {}
allowed_departments = ['layout', 'animation', 'lighting', 'composite', 'modeling', 'rigging', 'surfacing']
for dept in v.keys():
if dept not in allowed_departments:
raise ValueError(f'Department must be one of: {", ".join(allowed_departments)}')
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
@validator('delivery_movie_specs_by_department')
def validate_delivery_movie_specs_by_department(cls, v):
if v is None:
return v
allowed_departments = ['layout', 'animation', 'lighting', 'composite', 'modeling', 'rigging', 'surfacing']
for dept in v.keys():
if dept not in allowed_departments:
raise ValueError(f'Department must be one of: {", ".join(allowed_departments)}')
return v
class ProjectMemberBase(BaseModel):
user_id: int
department_role: Optional[DepartmentRole] = None
class ProjectMemberCreate(ProjectMemberBase):
pass
class ProjectMemberUpdate(BaseModel):
department_role: Optional[DepartmentRole] = 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
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