diff --git a/backend/schemas/submission_config.py b/backend/schemas/submission_config.py index 8f83451..c5c0ed9 100644 --- a/backend/schemas/submission_config.py +++ b/backend/schemas/submission_config.py @@ -7,8 +7,8 @@ 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)\}') +# 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_\-\.]*$') @@ -26,7 +26,12 @@ class SubmissionTypeConfig(BaseModel): 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") + 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") @@ -51,15 +56,21 @@ class SubmissionTypeConfig(BaseModel): return normalized @validator('naming_pattern') - def validate_naming_pattern(cls, v): + def validate_naming_pattern(cls, v, values): if v is None or v == '': return None - # Strip out known tokens, then everything left must be safe literal characters + 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}, {version}' + 'and the tokens {name}, {task_type}, {task_name}, {project_name}, {project_code}, {version}' ) return v diff --git a/frontend/src/components/project/SubmissionConfigManager.vue b/frontend/src/components/project/SubmissionConfigManager.vue index 39de198..3f32540 100644 --- a/frontend/src/components/project/SubmissionConfigManager.vue +++ b/frontend/src/components/project/SubmissionConfigManager.vue @@ -11,132 +11,36 @@ Loading submission configuration... -
- - -
- {{ formatTaskType(taskType) }} -
- - -
-
-
- -
- -
- -
-

Leave all unchecked to accept any supported file type

-
+ + + Shot Tasks + Asset Tasks + -
-
- -
- - -
-
- -

- Tokens: {name} (shot/asset name), {task_type}, {version} (auto 3-digit) -

-
+ + +
+ No shot task types configured for this project yet. +
+
-
-
- -
- - -
-
-
-
- -
-
- -
-
- -
-
- -
-
-

Checks resolution/format/codec/frame rate for video submissions only, in-browser before upload

-
-
-
- -
- No task types configured for this project yet. -
-
+ + +
+ No asset task types configured for this project yet. +
+
+
- Naming: {{ submissionConfig.naming_pattern }} + {{ submissionConfig.naming_pattern_is_regex ? 'Naming (regex):' : 'Naming:' }} + {{ submissionConfig.naming_pattern }}
Movie spec: @@ -155,6 +156,8 @@ const props = defineProps<{ taskType?: string projectId?: number name?: string + taskName?: string + projectName?: string }>() const emit = defineEmits<{ @@ -170,13 +173,19 @@ const viewerOpen = ref(false) const selectedSubmission = ref(null) const mediaBlobUrl = ref(null) const submissionConfig = ref(null) +const projectCode = ref(null) async function loadSubmissionConfig() { submissionConfig.value = null + projectCode.value = null if (!props.projectId || !props.taskType) return try { const config = await projectService.getProjectSubmissionConfig(props.projectId) submissionConfig.value = config.config_by_task_type[props.taskType] || null + if (submissionConfig.value?.naming_pattern?.includes('{project_code}')) { + const project = await projectService.getProject(props.projectId) + projectCode.value = project.code_name + } } catch (error) { console.error('Failed to load submission configuration:', error) } @@ -273,26 +282,49 @@ async function findSubmissionViolation(file: File): Promise { } if (config.check_naming && config.naming_pattern) { - if (config.naming_pattern.includes('{name}') && !props.name) { - return null // can't resolve {name} client-side - } - const escapeRegex = (s: string) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') - const pattern = config.naming_pattern - .split(/(\{name\}|\{task_type\}|\{version\})/) - .map(part => { - if (part === '{name}') return escapeRegex(props.name || '') - if (part === '{task_type}') return escapeRegex(props.taskType || '') - if (part === '{version}') return '\\d{3}' - return escapeRegex(part) - }) - .join('') - const regex = new RegExp(`^${pattern}$`) - if (!regex.test(stem)) { - const example = config.naming_pattern - .replace('{name}', props.name || 'name') - .replace('{task_type}', props.taskType || '') - .replace('{version}', '001') - return `Filename does not match the required naming convention '${config.naming_pattern}' (e.g. '${example}${extension}')` + if (config.naming_pattern_is_regex) { + try { + const regex = new RegExp(config.naming_pattern) + if (!regex.test(stem)) { + return `Filename does not match the required pattern '${config.naming_pattern}'` + } + } catch (error) { + console.error('Invalid naming pattern regex:', error) + } + } else { + const tokenValues: Record = { + name: props.name, + task_type: props.taskType, + task_name: props.taskName, + project_name: props.projectName, + project_code: projectCode.value || undefined + } + const usedTokens = config.naming_pattern.match(/\{(name|task_name|project_name|project_code)\}/g) || [] + const unresolvable = usedTokens.some(token => !tokenValues[token.slice(1, -1)]) + if (!unresolvable) { + const escapeRegex = (s: string) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + const pattern = config.naming_pattern + .split(/(\{name\}|\{task_type\}|\{task_name\}|\{project_name\}|\{project_code\}|\{version\})/) + .map(part => { + if (part === '{version}') return '\\d{3}' + const key = part.startsWith('{') && part.endsWith('}') ? part.slice(1, -1) : null + if (key && key in tokenValues) return escapeRegex(tokenValues[key] || '') + return escapeRegex(part) + }) + .join('') + const regex = new RegExp(`^${pattern}$`) + if (!regex.test(stem)) { + const example = config.naming_pattern + .replace('{name}', props.name || 'name') + .replace('{task_name}', props.taskName || 'taskname') + .replace('{project_name}', props.projectName || 'project') + .replace('{project_code}', projectCode.value || 'CODE') + .replace('{task_type}', props.taskType || '') + .replace('{version}', '001') + return `Filename does not match the required naming convention '${config.naming_pattern}' (e.g. '${example}${extension}')` + } + } + // If a token couldn't be resolved client-side, don't false-block } } diff --git a/frontend/src/services/project.ts b/frontend/src/services/project.ts index a3ebdd5..29bce11 100644 --- a/frontend/src/services/project.ts +++ b/frontend/src/services/project.ts @@ -97,6 +97,7 @@ export interface ProjectSettings { export interface SubmissionTypeConfig { allowed_extensions: string[] + naming_pattern_is_regex: boolean naming_pattern?: string | null check_naming: boolean required: boolean