23740af816
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).
104 lines
4.5 KiB
Python
104 lines
4.5 KiB
Python
"""
|
|
Pydantic schemas for per-task-type submission configuration
|
|
"""
|
|
from pydantic import BaseModel, Field, validator
|
|
from typing import Optional, Dict, List
|
|
import re
|
|
|
|
from utils.file_handler import file_handler
|
|
|
|
# Tokens supported in a naming_pattern: {name} (shot/asset name), {task_type}, {version}
|
|
NAMING_PATTERN_TOKEN_RE = re.compile(r'\{(name|task_type|version)\}')
|
|
NAMING_PATTERN_ALLOWED_CHARS_RE = re.compile(r'^[A-Za-z0-9_\-\.]*$')
|
|
|
|
|
|
MOVIE_FORMATS = {ext.lstrip('.') for ext in file_handler.MOVIE_EXTENSIONS}
|
|
MOVIE_CODECS = {'h264', 'h265', 'mjpeg', 'dnxhd', 'dnxhr', 'prores', 'uncompressed', 'avid', 'cineform'}
|
|
MOVIE_FRAME_RATES = {23.976, 24, 30, 48, 60}
|
|
RESOLUTION_RE = re.compile(r'^\d+x\d+$')
|
|
|
|
|
|
class SubmissionTypeConfig(BaseModel):
|
|
"""Submission rules for a single task type.
|
|
|
|
All checking against these rules happens client-side (in TaskSubmissions.vue) when an
|
|
artist picks a file to submit - this schema only validates and stores the configuration
|
|
itself, it is never used to inspect an uploaded file server-side.
|
|
"""
|
|
allowed_extensions: List[str] = Field(default_factory=list, description="Accepted file extensions, e.g. ['.mov', '.exr']")
|
|
naming_pattern: Optional[str] = Field(None, description="Filename pattern using {name}, {task_type}, {version} tokens")
|
|
check_naming: bool = Field(True, description="Whether the naming pattern is actively checked")
|
|
required: bool = Field(False, description="Whether a submission is expected for this task type (informational only)")
|
|
check_movie_spec: bool = Field(False, description="Whether to check video submissions against movie_resolution/movie_format/movie_codec/movie_frame_rate")
|
|
movie_resolution: Optional[str] = Field(None, description="Required video resolution, e.g. '1920x1080'")
|
|
movie_format: Optional[str] = Field(None, description="Required video format, e.g. 'mov'")
|
|
movie_codec: Optional[str] = Field(None, description="Required video codec, e.g. 'h264'")
|
|
movie_frame_rate: Optional[float] = Field(None, description="Required video frame rate, e.g. 23.976")
|
|
|
|
@validator('allowed_extensions')
|
|
def validate_allowed_extensions(cls, v):
|
|
normalized = []
|
|
for ext in v:
|
|
ext = ext.lower()
|
|
if not ext.startswith('.'):
|
|
ext = f'.{ext}'
|
|
if ext not in file_handler.SUPPORTED_FORMATS:
|
|
raise ValueError(
|
|
f"Extension '{ext}' is not a supported file format. "
|
|
f"Supported formats: {', '.join(sorted(file_handler.SUPPORTED_FORMATS))}"
|
|
)
|
|
normalized.append(ext)
|
|
return normalized
|
|
|
|
@validator('naming_pattern')
|
|
def validate_naming_pattern(cls, v):
|
|
if v is None or v == '':
|
|
return None
|
|
# Strip out known tokens, then everything left must be safe literal characters
|
|
stripped = NAMING_PATTERN_TOKEN_RE.sub('', v)
|
|
if not NAMING_PATTERN_ALLOWED_CHARS_RE.match(stripped):
|
|
raise ValueError(
|
|
'Naming pattern may only contain letters, numbers, "_", "-", "." '
|
|
'and the tokens {name}, {task_type}, {version}'
|
|
)
|
|
return v
|
|
|
|
@validator('movie_resolution')
|
|
def validate_movie_resolution(cls, v):
|
|
if v is None or v == '':
|
|
return None
|
|
if not RESOLUTION_RE.match(v):
|
|
raise ValueError('Movie resolution must be in format "WIDTHxHEIGHT" (e.g., "1920x1080")')
|
|
return v
|
|
|
|
@validator('movie_format')
|
|
def validate_movie_format(cls, v):
|
|
if v is None or v == '':
|
|
return None
|
|
v = v.lower().lstrip('.')
|
|
if v not in MOVIE_FORMATS:
|
|
raise ValueError(f'Movie format must be one of: {", ".join(sorted(MOVIE_FORMATS))}')
|
|
return v
|
|
|
|
@validator('movie_codec')
|
|
def validate_movie_codec(cls, v):
|
|
if v is None or v == '':
|
|
return None
|
|
v = v.lower()
|
|
if v not in MOVIE_CODECS:
|
|
raise ValueError(f'Movie codec must be one of: {", ".join(sorted(MOVIE_CODECS))}')
|
|
return v
|
|
|
|
@validator('movie_frame_rate')
|
|
def validate_movie_frame_rate(cls, v):
|
|
if v is None:
|
|
return None
|
|
if not any(abs(v - rate) < 0.001 for rate in MOVIE_FRAME_RATES):
|
|
raise ValueError(f'Movie frame rate must be one of: {", ".join(str(r) for r in sorted(MOVIE_FRAME_RATES))}')
|
|
return v
|
|
|
|
|
|
class ProjectSubmissionConfig(BaseModel):
|
|
"""Submission configuration for a project, keyed by task type"""
|
|
config_by_task_type: Dict[str, SubmissionTypeConfig] = Field(default_factory=dict)
|