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
+14
View File
@@ -0,0 +1,14 @@
from .auth import UserLogin, UserRegister, Token, TokenData, RefreshToken
from .user import UserBase, UserCreate, UserUpdate, UserResponse, UserApproval, UserRoleUpdate
from .project import (
ProjectBase, ProjectCreate, ProjectUpdate, ProjectResponse, ProjectListResponse,
ProjectMemberBase, ProjectMemberCreate, ProjectMemberUpdate, ProjectMemberResponse
)
from .episode import EpisodeBase, EpisodeCreate, EpisodeUpdate, EpisodeResponse, EpisodeListResponse
from .task import (
TaskBase, TaskCreate, TaskUpdate, TaskResponse, TaskListResponse, TaskStatusUpdate, TaskAssignment,
ProductionNoteBase, ProductionNoteCreate, ProductionNoteUpdate, ProductionNoteResponse,
TaskAttachmentBase, TaskAttachmentCreate, TaskAttachmentResponse,
SubmissionBase, SubmissionCreate, SubmissionResponse,
ReviewBase, ReviewCreate, ReviewResponse
)
+65
View File
@@ -0,0 +1,65 @@
from pydantic import BaseModel
from datetime import datetime
from typing import Optional, Dict, Any
from enum import Enum
class ActivityType(str, 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 ActivityBase(BaseModel):
type: ActivityType
description: str
activity_metadata: Optional[Dict[str, Any]] = None
class ActivityCreate(ActivityBase):
user_id: int
project_id: Optional[int] = None
task_id: Optional[int] = None
asset_id: Optional[int] = None
shot_id: Optional[int] = None
submission_id: Optional[int] = None
class UserInfo(BaseModel):
id: int
first_name: str
last_name: str
email: str
class Config:
from_attributes = True
class ActivityResponse(ActivityBase):
id: int
user_id: int
user: UserInfo
project_id: Optional[int] = None
task_id: Optional[int] = None
asset_id: Optional[int] = None
shot_id: Optional[int] = None
submission_id: Optional[int] = None
created_at: datetime
class Config:
from_attributes = True
+50
View File
@@ -0,0 +1,50 @@
from pydantic import BaseModel
from typing import List, Optional
from datetime import datetime
from models.api_key import APIKeyScope
class APIKeyCreate(BaseModel):
name: str
scopes: List[APIKeyScope]
expires_at: Optional[datetime] = None
user_id: Optional[int] = None # Only admins can specify this
class APIKeyResponse(BaseModel):
id: int
user_id: int
name: str
scopes: List[str]
is_active: bool
expires_at: Optional[datetime]
last_used_at: Optional[datetime]
created_at: datetime
user_email: Optional[str] = None # Include user email for admin view
class Config:
from_attributes = True
class APIKeyWithToken(BaseModel):
api_key: APIKeyResponse
token: str # The actual API key token (only returned on creation)
class APIKeyUpdate(BaseModel):
name: Optional[str] = None
scopes: Optional[List[APIKeyScope]] = None
is_active: Optional[bool] = None
expires_at: Optional[datetime] = None
class APIKeyUsageLog(BaseModel):
api_key_id: int
endpoint: str
method: str
timestamp: datetime
ip_address: Optional[str] = None
user_agent: Optional[str] = None
class Config:
from_attributes = True
+66
View File
@@ -0,0 +1,66 @@
from pydantic import BaseModel, Field
from typing import Optional, List, Dict
from datetime import datetime
from models.asset import AssetCategory, AssetStatus
from models.task import TaskType, TaskStatus
class AssetBase(BaseModel):
name: str = Field(..., min_length=1, max_length=255)
description: Optional[str] = None
category: AssetCategory
status: AssetStatus = AssetStatus.NOT_STARTED
class AssetCreate(AssetBase):
create_default_tasks: bool = Field(default=True, description="Whether to create default tasks for this asset")
selected_task_types: Optional[List[str]] = Field(default=None, description="Specific task types to create (if None, uses category defaults)")
class AssetUpdate(BaseModel):
name: Optional[str] = Field(None, min_length=1, max_length=255)
description: Optional[str] = None
category: Optional[AssetCategory] = None
status: Optional[AssetStatus] = None
class AssetResponse(AssetBase):
id: int
project_id: int
created_at: datetime
updated_at: datetime
# Summary information
task_count: int = 0
class Config:
from_attributes = True
class TaskStatusInfo(BaseModel):
task_type: str # Changed from TaskType enum to str to support custom task types
status: str # Changed from TaskStatus enum to str to support custom statuses
task_id: Optional[int] = None
assigned_user_id: Optional[int] = None
class AssetListResponse(BaseModel):
id: int
name: str
description: Optional[str] = None
category: AssetCategory
status: AssetStatus
project_id: int
created_at: datetime
updated_at: datetime
# Summary information
task_count: int = 0
# Task status information for table display
task_status: Dict[str, Optional[str]] = Field(default_factory=dict, description="Task status by task type")
task_details: List[TaskStatusInfo] = Field(default_factory=list, description="Detailed task information")
class Config:
from_attributes = True
+30
View File
@@ -0,0 +1,30 @@
from pydantic import BaseModel, EmailStr
from typing import Optional
from models.user import UserRole
class UserLogin(BaseModel):
email: EmailStr
password: str
class UserRegister(BaseModel):
email: EmailStr
password: str
first_name: str
last_name: str
class Token(BaseModel):
access_token: str
refresh_token: str
token_type: str = "bearer"
class TokenData(BaseModel):
email: Optional[str] = None
user_id: Optional[int] = None
class RefreshToken(BaseModel):
refresh_token: str
+157
View File
@@ -0,0 +1,157 @@
from pydantic import BaseModel, Field, validator
from typing import List, Optional
import re
class CustomTaskStatus(BaseModel):
"""Schema for a custom task status"""
id: str = Field(..., description="Unique identifier for the status")
name: str = Field(..., min_length=1, max_length=50, description="Display name")
color: str = Field(..., pattern=r'^#[0-9A-Fa-f]{6}$', description="Hex color code")
order: int = Field(..., ge=0, description="Display order")
is_default: bool = Field(default=False, description="Whether this is the default status")
class Config:
from_attributes = True
class CustomTaskStatusCreate(BaseModel):
"""Schema for creating a new custom task status"""
name: str = Field(..., min_length=1, max_length=50, description="Status name")
color: Optional[str] = Field(None, description="Hex color code (e.g., #FF5733)")
@validator('name')
def validate_name(cls, v):
"""Validate and normalize status name"""
# Trim whitespace
v = v.strip()
if not v:
raise ValueError('Status name cannot be empty')
if len(v) > 50:
raise ValueError('Status name cannot exceed 50 characters')
return v
@validator('color')
def validate_color(cls, v):
"""Validate color format if provided"""
if v is None:
return v
# Trim whitespace
v = v.strip()
# Check hex color format
if not re.match(r'^#[0-9A-Fa-f]{6}$', v):
raise ValueError('Color must be a valid hex code (e.g., #FF5733)')
return v.upper() # Normalize to uppercase
class CustomTaskStatusUpdate(BaseModel):
"""Schema for updating a custom task status"""
name: Optional[str] = Field(None, min_length=1, max_length=50, description="Status name")
color: Optional[str] = Field(None, description="Hex color code (e.g., #FF5733)")
is_default: Optional[bool] = Field(None, description="Set as default status")
@validator('name')
def validate_name(cls, v):
"""Validate and normalize status name"""
if v is None:
return v
# Trim whitespace
v = v.strip()
if not v:
raise ValueError('Status name cannot be empty')
if len(v) > 50:
raise ValueError('Status name cannot exceed 50 characters')
return v
@validator('color')
def validate_color(cls, v):
"""Validate color format if provided"""
if v is None:
return v
# Trim whitespace
v = v.strip()
# Check hex color format
if not re.match(r'^#[0-9A-Fa-f]{6}$', v):
raise ValueError('Color must be a valid hex code (e.g., #FF5733)')
return v.upper() # Normalize to uppercase
class CustomTaskStatusReorder(BaseModel):
"""Schema for reordering statuses"""
status_ids: List[str] = Field(..., min_items=1, description="Ordered list of status IDs")
@validator('status_ids')
def validate_status_ids(cls, v):
"""Validate status IDs list"""
if not v:
raise ValueError('Status IDs list cannot be empty')
# Check for duplicates
if len(v) != len(set(v)):
raise ValueError('Status IDs list contains duplicates')
return v
class CustomTaskStatusDelete(BaseModel):
"""Schema for deleting a status with optional reassignment"""
reassign_to_status_id: Optional[str] = Field(
None,
description="Status ID to reassign tasks to (required if status is in use)"
)
class SystemTaskStatus(BaseModel):
"""Schema for system (built-in) task statuses"""
id: str = Field(..., description="System status identifier")
name: str = Field(..., description="Display name")
color: str = Field(..., description="Hex color code")
is_system: bool = Field(default=True, description="Indicates this is a system status")
class Config:
from_attributes = True
class AllTaskStatusesResponse(BaseModel):
"""Schema for response containing all task statuses (system + custom)"""
statuses: List[CustomTaskStatus] = Field(
default_factory=list,
description="Custom task statuses defined for the project"
)
system_statuses: List[SystemTaskStatus] = Field(
default_factory=list,
description="Built-in system task statuses"
)
default_status_id: str = Field(
...,
description="ID of the default status for new tasks"
)
class Config:
from_attributes = True
class TaskStatusInUseError(BaseModel):
"""Schema for error when trying to delete a status that is in use"""
error: str = Field(..., description="Error message")
status_id: str = Field(..., description="ID of the status that cannot be deleted")
status_name: str = Field(..., description="Name of the status that cannot be deleted")
task_count: int = Field(..., ge=0, description="Number of tasks using this status")
task_ids: List[int] = Field(
default_factory=list,
description="IDs of tasks using this status"
)
class Config:
from_attributes = True
class CustomTaskStatusResponse(BaseModel):
"""Schema for successful custom task status operation response"""
message: str = Field(..., description="Success message")
status: Optional[CustomTaskStatus] = Field(None, description="The created or updated status")
all_statuses: Optional[AllTaskStatusesResponse] = Field(
None,
description="All statuses after the operation"
)
class Config:
from_attributes = True
+62
View File
@@ -0,0 +1,62 @@
"""
Pydantic schemas for custom task type management
"""
from pydantic import BaseModel, Field, validator
from typing import List, Literal
import re
class CustomTaskTypeCreate(BaseModel):
"""Schema for creating a new custom task type"""
task_type: str = Field(..., min_length=3, max_length=50, description="Task type name")
category: Literal["asset", "shot"] = Field(..., description="Task category (asset or shot)")
@validator('task_type')
def validate_task_type_name(cls, v):
"""Validate task type name format"""
# Must be lowercase alphanumeric with underscores only
if not re.match(r'^[a-z0-9_]{3,50}$', v):
raise ValueError(
'Task type name must be 3-50 characters, lowercase alphanumeric with underscores only'
)
return v
class CustomTaskTypeUpdate(BaseModel):
"""Schema for updating a custom task type name"""
old_name: str = Field(..., description="Current task type name")
new_name: str = Field(..., min_length=3, max_length=50, description="New task type name")
category: Literal["asset", "shot"] = Field(..., description="Task category (asset or shot)")
@validator('new_name')
def validate_task_type_name(cls, v):
"""Validate task type name format"""
if not re.match(r'^[a-z0-9_]{3,50}$', v):
raise ValueError(
'Task type name must be 3-50 characters, lowercase alphanumeric with underscores only'
)
return v
class CustomTaskTypeDelete(BaseModel):
"""Schema for deleting a custom task type"""
task_type: str = Field(..., description="Task type name to delete")
category: Literal["asset", "shot"] = Field(..., description="Task category (asset or shot)")
class AllTaskTypesResponse(BaseModel):
"""Schema for response containing all task types (standard + custom)"""
asset_task_types: List[str] = Field(..., description="All asset task types")
shot_task_types: List[str] = Field(..., description="All shot task types")
standard_asset_types: List[str] = Field(..., description="Standard asset task types (read-only)")
standard_shot_types: List[str] = Field(..., description="Standard shot task types (read-only)")
custom_asset_types: List[str] = Field(..., description="Custom asset task types")
custom_shot_types: List[str] = Field(..., description="Custom shot task types")
class TaskTypeInUseError(BaseModel):
"""Schema for error when trying to delete a task type in use"""
error: str = Field(..., description="Error message")
task_type: str = Field(..., description="Task type that is in use")
task_count: int = Field(..., description="Number of tasks using this type")
task_ids: List[int] = Field(..., description="IDs of tasks using this type")
+54
View File
@@ -0,0 +1,54 @@
from pydantic import BaseModel, Field
from typing import Optional
from datetime import datetime
from enum import Enum
from models.episode import EpisodeStatus
class EpisodeBase(BaseModel):
name: str = Field(..., min_length=1, max_length=255)
description: Optional[str] = None
episode_number: int = Field(..., ge=1)
status: EpisodeStatus = EpisodeStatus.PLANNING
class EpisodeCreate(EpisodeBase):
pass
class EpisodeUpdate(BaseModel):
name: Optional[str] = Field(None, min_length=1, max_length=255)
description: Optional[str] = None
episode_number: Optional[int] = Field(None, ge=1)
status: Optional[EpisodeStatus] = None
class EpisodeResponse(EpisodeBase):
id: int
project_id: int
created_at: datetime
updated_at: datetime
# Summary information
shot_count: int = 0
class Config:
from_attributes = True
class EpisodeListResponse(BaseModel):
id: int
name: str
description: Optional[str] = None
episode_number: int
status: EpisodeStatus
project_id: int
created_at: datetime
updated_at: datetime
# Summary information
shot_count: int = 0
class Config:
from_attributes = True
+36
View File
@@ -0,0 +1,36 @@
from pydantic import BaseModel, Field
from typing import Optional
from datetime import datetime
class GlobalSettingBase(BaseModel):
setting_key: str = Field(..., max_length=100)
setting_value: str
description: Optional[str] = None
class GlobalSettingCreate(GlobalSettingBase):
pass
class GlobalSettingUpdate(BaseModel):
setting_value: str
description: Optional[str] = None
class GlobalSetting(GlobalSettingBase):
id: int
created_at: datetime
updated_at: datetime
class Config:
from_attributes = True
class UploadLimitResponse(BaseModel):
upload_limit_mb: int
description: str
class UploadLimitUpdate(BaseModel):
upload_limit_mb: int = Field(..., ge=1, le=10000, description="Upload limit in MB (1-10000)")
+98
View File
@@ -0,0 +1,98 @@
from pydantic import BaseModel, Field
from datetime import datetime
from typing import Optional
from enum import Enum
class NotificationType(str, 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):
LOW = "low"
NORMAL = "normal"
HIGH = "high"
URGENT = "urgent"
class NotificationBase(BaseModel):
type: NotificationType
priority: NotificationPriority = NotificationPriority.NORMAL
title: str = Field(..., max_length=255)
message: str
project_id: Optional[int] = None
task_id: Optional[int] = None
submission_id: Optional[int] = None
class NotificationCreate(NotificationBase):
user_id: int
class NotificationResponse(NotificationBase):
id: int
user_id: int
read: bool
email_sent: bool
email_sent_at: Optional[datetime] = None
created_at: datetime
read_at: Optional[datetime] = None
class Config:
from_attributes = True
class NotificationMarkRead(BaseModel):
notification_ids: list[int]
class NotificationPreferencesBase(BaseModel):
# Email notification preferences
email_enabled: bool = True
email_task_assigned: bool = True
email_task_status_changed: bool = True
email_submission_reviewed: bool = True
email_work_submitted: bool = True
email_deadline_approaching: bool = True
email_project_update: bool = True
email_comment_added: bool = True
# In-app notification preferences
inapp_enabled: bool = True
inapp_task_assigned: bool = True
inapp_task_status_changed: bool = True
inapp_submission_reviewed: bool = True
inapp_work_submitted: bool = True
inapp_deadline_approaching: bool = True
inapp_project_update: bool = True
inapp_comment_added: bool = True
# Digest settings
email_digest_enabled: bool = False
email_digest_frequency: str = "daily"
class NotificationPreferencesUpdate(NotificationPreferencesBase):
pass
class NotificationPreferencesResponse(NotificationPreferencesBase):
id: int
user_id: int
created_at: datetime
updated_at: datetime
class Config:
from_attributes = True
class NotificationStats(BaseModel):
total: int
unread: int
by_type: dict[str, int]
+374
View File
@@ -0,0 +1,374 @@
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
+99
View File
@@ -0,0 +1,99 @@
from pydantic import BaseModel, Field
from typing import Optional, List, Dict
from datetime import datetime
from models.shot import ShotStatus
from models.task import TaskType, TaskStatus
class ShotBase(BaseModel):
name: str = Field(..., min_length=1, max_length=255)
description: Optional[str] = None
frame_start: int = Field(default=1001, ge=1)
frame_end: int = Field(default=1001, ge=1)
status: ShotStatus = ShotStatus.NOT_STARTED
project_id: Optional[int] = Field(None, description="Project ID - auto-populated from episode if not provided")
class ShotCreate(ShotBase):
pass
class ShotUpdate(BaseModel):
name: Optional[str] = Field(None, min_length=1, max_length=255)
description: Optional[str] = None
frame_start: Optional[int] = Field(None, ge=1)
frame_end: Optional[int] = Field(None, ge=1)
status: Optional[ShotStatus] = None
project_id: Optional[int] = Field(None, description="Project ID - must match episode's project")
class ShotResponse(ShotBase):
id: int
project_id: int # Make required in response
episode_id: int
created_at: datetime
updated_at: datetime
# Summary information
task_count: int = 0
# Optional computed field for display
project_name: Optional[str] = Field(None, description="Project name for display purposes")
class Config:
from_attributes = True
class TaskStatusInfo(BaseModel):
"""Task status information for table display"""
task_type: str # String to support custom task types
status: str # Changed from TaskStatus enum to str to support custom statuses
task_id: Optional[int] = None
assigned_user_id: Optional[int] = None
class ShotListResponse(BaseModel):
id: int
name: str
description: Optional[str] = None
frame_start: int
frame_end: int
status: ShotStatus
project_id: int
episode_id: int
created_at: datetime
updated_at: datetime
# Summary information
task_count: int = 0
# Optional computed field for display
project_name: Optional[str] = Field(None, description="Project name for display purposes")
# Task status information for table display
task_status: Dict[str, Optional[str]] = Field(default_factory=dict, description="Task status by task type")
task_details: List[TaskStatusInfo] = Field(default_factory=list, description="Detailed task information")
class Config:
from_attributes = True
class BulkShotCreate(BaseModel):
"""Schema for bulk shot creation with naming patterns"""
name_prefix: str = Field(..., min_length=1, max_length=100)
shot_count: int = Field(..., ge=1, le=1000)
start_number: int = Field(default=10, ge=1)
number_padding: int = Field(default=3, ge=1, le=6)
frame_start: int = Field(default=1001, ge=1)
frame_end: int = Field(default=1001, ge=1)
description_template: Optional[str] = None
create_default_tasks: bool = Field(default=True)
task_types: Optional[List[str]] = None # If None, use default shot task types (changed from TaskType to str)
class BulkShotResponse(BaseModel):
"""Response for bulk shot creation"""
created_shots: List[ShotResponse]
created_tasks_count: int = 0
message: str
+230
View File
@@ -0,0 +1,230 @@
from pydantic import BaseModel, Field
from typing import Optional, List
from datetime import date, datetime
from enum import Enum
from models.task import TaskType, TaskStatus, ReviewDecision, AttachmentType
from models.user import DepartmentRole
class TaskBase(BaseModel):
name: str = Field(..., min_length=1, max_length=255)
description: Optional[str] = None
task_type: str # Changed from TaskType enum to str to support custom task types
deadline: Optional[date] = None
status: str = "not_started" # Changed from TaskStatus enum to str to support custom statuses
class TaskCreate(TaskBase):
project_id: int
episode_id: Optional[int] = None
shot_id: Optional[int] = None
asset_id: Optional[int] = None
assigned_user_id: Optional[int] = None
class TaskUpdate(BaseModel):
name: Optional[str] = Field(None, min_length=1, max_length=255)
description: Optional[str] = None
task_type: Optional[str] = None # Changed from TaskType enum to str to support custom task types
deadline: Optional[date] = None
status: Optional[str] = None # Changed from TaskStatus enum to str to support custom statuses
assigned_user_id: Optional[int] = None
class TaskStatusUpdate(BaseModel):
status: str # Changed from TaskStatus enum to str to support custom statuses
class TaskAssignment(BaseModel):
assigned_user_id: int
class TaskResponse(TaskBase):
id: int
project_id: int
episode_id: Optional[int] = None
shot_id: Optional[int] = None
asset_id: Optional[int] = None
assigned_user_id: Optional[int] = None
created_at: datetime
updated_at: datetime
# Related entity names for display
project_name: Optional[str] = None
episode_name: Optional[str] = None
shot_name: Optional[str] = None
asset_name: Optional[str] = None
assigned_user_name: Optional[str] = None
assigned_user_email: Optional[str] = None
class Config:
from_attributes = True
class TaskListResponse(BaseModel):
id: int
name: str
task_type: str # Changed from TaskType enum to str to support custom task types
status: str # Changed from TaskStatus enum to str to support custom statuses
deadline: Optional[date] = None
project_id: int
project_name: str
episode_id: Optional[int] = None
episode_name: Optional[str] = None
shot_id: Optional[int] = None
shot_name: Optional[str] = None
asset_id: Optional[int] = None
asset_name: Optional[str] = None
assigned_user_id: Optional[int] = None
assigned_user_name: Optional[str] = None
created_at: datetime
updated_at: datetime
class Config:
from_attributes = True
# Production Notes schemas
class ProductionNoteBase(BaseModel):
content: str = Field(..., min_length=1)
parent_note_id: Optional[int] = None
class ProductionNoteCreate(ProductionNoteBase):
pass
class ProductionNoteUpdate(BaseModel):
content: str = Field(..., min_length=1)
class ProductionNoteResponse(ProductionNoteBase):
id: int
task_id: int
user_id: int
created_at: datetime
updated_at: datetime
# User information
user_first_name: str
user_last_name: str
user_email: str
# Child notes for threading
child_notes: Optional[List['ProductionNoteResponse']] = None
class Config:
from_attributes = True
# Task Attachment schemas
class TaskAttachmentBase(BaseModel):
file_name: str
attachment_type: AttachmentType = AttachmentType.REFERENCE
description: Optional[str] = None
class TaskAttachmentCreate(TaskAttachmentBase):
pass
class TaskAttachmentResponse(TaskAttachmentBase):
id: int
task_id: int
user_id: int
file_path: str
file_type: str
file_size: int
uploaded_at: datetime
# User information
user_first_name: str
user_last_name: str
# File serving URLs (computed properties)
download_url: Optional[str] = None
thumbnail_url: Optional[str] = None
class Config:
from_attributes = True
# Submission schemas
class SubmissionBase(BaseModel):
notes: Optional[str] = None
class SubmissionCreate(SubmissionBase):
pass
class SubmissionResponse(SubmissionBase):
id: int
task_id: int
user_id: int
file_path: str
file_name: str
version_number: int
submitted_at: datetime
# User information
user_first_name: str
user_last_name: str
# Latest review if any
latest_review: Optional['ReviewResponse'] = None
# File serving URLs (computed properties)
download_url: Optional[str] = None
thumbnail_url: Optional[str] = None
stream_url: Optional[str] = None
class Config:
from_attributes = True
# Review schemas
class ReviewBase(BaseModel):
decision: ReviewDecision
feedback: Optional[str] = None
class ReviewCreate(ReviewBase):
pass
class ReviewResponse(ReviewBase):
id: int
submission_id: int
reviewer_id: int
reviewed_at: datetime
# Reviewer information
reviewer_first_name: str
reviewer_last_name: str
class Config:
from_attributes = True
# Bulk action schemas
class BulkStatusUpdate(BaseModel):
task_ids: List[int] = Field(..., min_length=1)
status: TaskStatus
class BulkAssignment(BaseModel):
task_ids: List[int] = Field(..., min_length=1)
assigned_user_id: int
class BulkActionResult(BaseModel):
success_count: int
failed_count: int
errors: Optional[List[dict]] = None
# Update forward references
ProductionNoteResponse.model_rebuild()
SubmissionResponse.model_rebuild()
+75
View File
@@ -0,0 +1,75 @@
from pydantic import BaseModel, EmailStr
from typing import Optional
from datetime import datetime
from models.user import UserRole
class UserBase(BaseModel):
email: EmailStr
first_name: str
last_name: str
class UserCreate(UserBase):
password: str
class UserUpdate(BaseModel):
first_name: Optional[str] = None
last_name: Optional[str] = None
role: Optional[UserRole] = None
is_admin: Optional[bool] = None
is_approved: Optional[bool] = None
class UserResponse(UserBase):
id: int
role: UserRole
is_admin: bool
is_approved: bool
avatar_url: Optional[str] = None
created_at: datetime
updated_at: datetime
class Config:
from_attributes = True
class UserApproval(BaseModel):
is_approved: bool
class UserRoleUpdate(BaseModel):
role: UserRole
class UserAdminUpdate(BaseModel):
is_admin: bool
class UserAdminCreate(BaseModel):
email: EmailStr
password: str
first_name: str
last_name: str
role: UserRole
is_approved: bool = True
is_admin: bool = False
class UserAdminEdit(BaseModel):
first_name: Optional[str] = None
last_name: Optional[str] = None
email: Optional[EmailStr] = None
role: Optional[UserRole] = None
is_approved: Optional[bool] = None
is_admin: Optional[bool] = None
class UserPasswordReset(BaseModel):
new_password: str
class UserPasswordChange(BaseModel):
current_password: str
new_password: str