981808b901
- Movie spec checks now also cover codec (h264/h265/mjpeg/dnxhd/dnxhr/
prores/uncompressed/avid/cineform) and frame rate (23.976/24/30/48/60),
detected in-browser via mediainfo.js container parsing - works even
for formats the native <video> element can't decode (MXF, ProRes,
DNxHD). Added .mxf to the supported/allowed format lists.
- Naming pattern supports a raw regex mode (validated by compiling
server-side at save time) as an alternative to the token pattern,
and gained three more tokens: {task_name}, {project_name},
{project_code} (fetched lazily only when referenced).
- Submission Configuration settings page now splits Shot Tasks /
Asset Tasks into separate tabs instead of one merged list; the
per-task-type card was extracted into SubmissionTypeConfigCard.vue
to avoid duplicating it across both tabs.
115 lines
5.1 KiB
Python
115 lines
5.1 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 token-mode naming_pattern
|
|
NAMING_PATTERN_TOKEN_RE = re.compile(r'\{(name|task_type|task_name|project_name|project_code|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_is_regex: bool = Field(False, description="If true, naming_pattern is a raw regular expression instead of a token pattern")
|
|
naming_pattern: Optional[str] = Field(
|
|
None,
|
|
description="Either a token pattern (using {name}, {task_type}, {task_name}, {project_name}, "
|
|
"{project_code}, {version}) or, if naming_pattern_is_regex is set, a raw regular expression"
|
|
)
|
|
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, values):
|
|
if v is None or v == '':
|
|
return None
|
|
if values.get('naming_pattern_is_regex'):
|
|
try:
|
|
re.compile(v)
|
|
except re.error as e:
|
|
raise ValueError(f'Invalid regular expression: {e}')
|
|
return v
|
|
# Token mode: 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}, {task_name}, {project_name}, {project_code}, {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)
|