Add per-project Department management
Departments are now a customizable per-project list (standard + custom), matching the existing custom task type/status pattern, instead of a fixed 7-value enum. They're usable directly on tasks (new field, independent of assignee) and continue to drive team member department roles.
This commit is contained in:
@@ -0,0 +1,111 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Migration script to add the custom_departments column to the projects table.
|
||||
|
||||
Usage:
|
||||
python migrate_project_custom_departments.py
|
||||
"""
|
||||
|
||||
import sqlite3
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def get_database_path():
|
||||
"""Get the database path, trying multiple possible locations."""
|
||||
possible_paths = [
|
||||
"vfx_project_management.db", # Primary database
|
||||
"database.db",
|
||||
"../vfx_project_management.db"
|
||||
]
|
||||
|
||||
for path in possible_paths:
|
||||
if Path(path).exists():
|
||||
return path
|
||||
|
||||
return "vfx_project_management.db"
|
||||
|
||||
|
||||
def check_column_exists(cursor, table_name, column_name):
|
||||
"""Check if a column exists in a table."""
|
||||
cursor.execute(f"PRAGMA table_info({table_name})")
|
||||
columns = [column[1] for column in cursor.fetchall()]
|
||||
return column_name in columns
|
||||
|
||||
|
||||
def migrate_database():
|
||||
"""Add custom_departments column to the projects table."""
|
||||
db_path = get_database_path()
|
||||
print(f"Using database: {db_path}")
|
||||
|
||||
conn = None
|
||||
try:
|
||||
conn = sqlite3.connect(db_path)
|
||||
cursor = conn.cursor()
|
||||
|
||||
cursor.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='projects'")
|
||||
if not cursor.fetchone():
|
||||
print("Projects table not found. Creating new database schema...")
|
||||
conn.close()
|
||||
return
|
||||
|
||||
if check_column_exists(cursor, "projects", "custom_departments"):
|
||||
print("Column custom_departments already exists, skipping...")
|
||||
else:
|
||||
print("Adding column: custom_departments")
|
||||
cursor.execute("ALTER TABLE projects ADD COLUMN custom_departments TEXT")
|
||||
cursor.execute("UPDATE projects SET custom_departments = '[]' WHERE custom_departments IS NULL")
|
||||
|
||||
# project_members.department_role was previously backed by SQLAlchemy's
|
||||
# Enum(DepartmentRole) type, which stores the enum MEMBER NAME (e.g. "LAYOUT"),
|
||||
# not its value ("layout"). The old Optional[DepartmentRole] schema silently
|
||||
# normalized this back to lowercase on read. Now that the column is a plain
|
||||
# string, normalize any existing uppercase values so they match the standard
|
||||
# department strings used everywhere else.
|
||||
cursor.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='project_members'")
|
||||
if cursor.fetchone():
|
||||
department_name_to_value = {
|
||||
'LAYOUT': 'layout',
|
||||
'ANIMATION': 'animation',
|
||||
'LIGHTING': 'lighting',
|
||||
'COMPOSITE': 'composite',
|
||||
'MODELING': 'modeling',
|
||||
'RIGGING': 'rigging',
|
||||
'SURFACING': 'surfacing',
|
||||
}
|
||||
for old_value, new_value in department_name_to_value.items():
|
||||
cursor.execute(
|
||||
"UPDATE project_members SET department_role = ? WHERE department_role = ?",
|
||||
(new_value, old_value)
|
||||
)
|
||||
if cursor.rowcount > 0:
|
||||
print(f"Normalized {cursor.rowcount} project members from '{old_value}' to '{new_value}'")
|
||||
|
||||
conn.commit()
|
||||
|
||||
cursor.execute("SELECT COUNT(*) FROM projects")
|
||||
project_count = cursor.fetchone()[0]
|
||||
print(f"Migration completed successfully! {project_count} projects unaffected (column defaults to '[]').")
|
||||
|
||||
except sqlite3.Error as e:
|
||||
print(f"Database error: {e}")
|
||||
if conn:
|
||||
conn.rollback()
|
||||
sys.exit(1)
|
||||
except Exception as e:
|
||||
print(f"Unexpected error: {e}")
|
||||
if conn:
|
||||
conn.rollback()
|
||||
sys.exit(1)
|
||||
finally:
|
||||
if conn:
|
||||
conn.close()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("VFX Project Management - Project Custom Departments Migration")
|
||||
print("=" * 60)
|
||||
|
||||
migrate_database()
|
||||
|
||||
print("\nMigration completed successfully!")
|
||||
@@ -0,0 +1,85 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Migration script to add the department column to the tasks table.
|
||||
|
||||
Usage:
|
||||
python migrate_task_department.py
|
||||
"""
|
||||
|
||||
import sqlite3
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def get_database_path():
|
||||
"""Get the database path, trying multiple possible locations."""
|
||||
possible_paths = [
|
||||
"vfx_project_management.db", # Primary database
|
||||
"database.db",
|
||||
"../vfx_project_management.db"
|
||||
]
|
||||
|
||||
for path in possible_paths:
|
||||
if Path(path).exists():
|
||||
return path
|
||||
|
||||
return "vfx_project_management.db"
|
||||
|
||||
|
||||
def check_column_exists(cursor, table_name, column_name):
|
||||
"""Check if a column exists in a table."""
|
||||
cursor.execute(f"PRAGMA table_info({table_name})")
|
||||
columns = [column[1] for column in cursor.fetchall()]
|
||||
return column_name in columns
|
||||
|
||||
|
||||
def migrate_database():
|
||||
"""Add department column to the tasks table."""
|
||||
db_path = get_database_path()
|
||||
print(f"Using database: {db_path}")
|
||||
|
||||
conn = None
|
||||
try:
|
||||
conn = sqlite3.connect(db_path)
|
||||
cursor = conn.cursor()
|
||||
|
||||
cursor.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='tasks'")
|
||||
if not cursor.fetchone():
|
||||
print("Tasks table not found. Creating new database schema...")
|
||||
conn.close()
|
||||
return
|
||||
|
||||
if check_column_exists(cursor, "tasks", "department"):
|
||||
print("Column department already exists, skipping...")
|
||||
else:
|
||||
print("Adding column: department")
|
||||
cursor.execute("ALTER TABLE tasks ADD COLUMN department VARCHAR")
|
||||
|
||||
conn.commit()
|
||||
|
||||
cursor.execute("SELECT COUNT(*) FROM tasks")
|
||||
task_count = cursor.fetchone()[0]
|
||||
print(f"Migration completed successfully! {task_count} tasks unaffected (column defaults to NULL).")
|
||||
|
||||
except sqlite3.Error as e:
|
||||
print(f"Database error: {e}")
|
||||
if conn:
|
||||
conn.rollback()
|
||||
sys.exit(1)
|
||||
except Exception as e:
|
||||
print(f"Unexpected error: {e}")
|
||||
if conn:
|
||||
conn.rollback()
|
||||
sys.exit(1)
|
||||
finally:
|
||||
if conn:
|
||||
conn.close()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("VFX Project Management - Task Department Migration")
|
||||
print("=" * 60)
|
||||
|
||||
migrate_database()
|
||||
|
||||
print("\nMigration completed successfully!")
|
||||
@@ -1,5 +1,5 @@
|
||||
# Models package
|
||||
from .user import User, UserRole, DepartmentRole
|
||||
from .user import User, UserRole
|
||||
from .project import Project, ProjectMember, ProjectStatus
|
||||
from .episode import Episode, EpisodeStatus
|
||||
from .asset import Asset, AssetCategory, AssetStatus
|
||||
@@ -17,7 +17,7 @@ from .role import Role, Permission, role_permissions, user_roles
|
||||
|
||||
__all__ = [
|
||||
# User models
|
||||
"User", "UserRole", "DepartmentRole",
|
||||
"User", "UserRole",
|
||||
# Project models
|
||||
"Project", "ProjectMember", "ProjectStatus",
|
||||
# Episode models
|
||||
|
||||
@@ -2,7 +2,6 @@ from sqlalchemy import Column, Integer, String, DateTime, Date, Enum, ForeignKey
|
||||
from sqlalchemy.orm import relationship
|
||||
from sqlalchemy.sql import func
|
||||
from database import Base
|
||||
from .user import DepartmentRole
|
||||
import enum
|
||||
|
||||
|
||||
@@ -54,6 +53,9 @@ class Project(Base):
|
||||
# Custom task statuses
|
||||
custom_task_statuses = Column(JSON, nullable=True) # Custom task statuses for project
|
||||
|
||||
# Custom departments
|
||||
custom_departments = Column(JSON, nullable=True) # Custom departments for project (in addition to standard ones)
|
||||
|
||||
# Submission configuration per task type
|
||||
submission_config_by_task_type = Column(JSON, nullable=True) # Allowed file types, naming pattern, required flag per task type
|
||||
|
||||
@@ -80,7 +82,7 @@ class ProjectMember(Base):
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
user_id = Column(Integer, ForeignKey("users.id"), nullable=False)
|
||||
project_id = Column(Integer, ForeignKey("projects.id"), nullable=False)
|
||||
department_role = Column(Enum(DepartmentRole), nullable=True)
|
||||
department_role = Column(String, nullable=True) # Free-form: standard department or a project's custom department
|
||||
joined_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||
|
||||
# Relationships
|
||||
|
||||
@@ -56,6 +56,7 @@ class Task(Base):
|
||||
name = Column(String, nullable=False, index=True)
|
||||
description = Column(Text)
|
||||
status = Column(String, nullable=False, default="not_started") # Changed from Enum to String to support custom statuses
|
||||
department = Column(String, nullable=True) # Standard or project-custom department, independent of assignee
|
||||
start_date = Column(Date)
|
||||
deadline = Column(Date)
|
||||
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||
|
||||
@@ -12,16 +12,6 @@ class UserRole(str, enum.Enum):
|
||||
DEVELOPER = "developer"
|
||||
|
||||
|
||||
class DepartmentRole(str, enum.Enum):
|
||||
LAYOUT = "layout"
|
||||
ANIMATION = "animation"
|
||||
LIGHTING = "lighting"
|
||||
COMPOSITE = "composite"
|
||||
MODELING = "modeling"
|
||||
RIGGING = "rigging"
|
||||
SURFACING = "surfacing"
|
||||
|
||||
|
||||
class User(Base):
|
||||
__tablename__ = "users"
|
||||
|
||||
|
||||
@@ -1078,6 +1078,264 @@ async def delete_custom_task_type(
|
||||
return _build_all_task_types_response(db_project)
|
||||
|
||||
|
||||
# Department Management Endpoints
|
||||
|
||||
# Standard departments (read-only)
|
||||
STANDARD_DEPARTMENTS = ["layout", "animation", "lighting", "composite", "modeling", "rigging", "surfacing"]
|
||||
|
||||
|
||||
def _build_all_departments_response(db_project: Project):
|
||||
"""Helper function to build AllDepartmentsResponse"""
|
||||
from schemas.department import AllDepartmentsResponse
|
||||
|
||||
custom_departments = db_project.custom_departments or []
|
||||
all_departments = STANDARD_DEPARTMENTS + custom_departments
|
||||
|
||||
return AllDepartmentsResponse(
|
||||
departments=all_departments,
|
||||
standard_departments=STANDARD_DEPARTMENTS,
|
||||
custom_departments=custom_departments
|
||||
)
|
||||
|
||||
|
||||
@router.get("/{project_id}/departments")
|
||||
async def get_all_departments(
|
||||
project_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user_with_db)
|
||||
):
|
||||
"""Get all departments (standard + custom) for a project"""
|
||||
db_project = db.query(Project).filter(Project.id == project_id).first()
|
||||
|
||||
if not db_project:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Project not found"
|
||||
)
|
||||
|
||||
return _build_all_departments_response(db_project)
|
||||
|
||||
|
||||
@router.post("/{project_id}/departments", status_code=status.HTTP_201_CREATED)
|
||||
async def add_department(
|
||||
project_id: int,
|
||||
department_data: dict,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(require_coordinator_or_admin)
|
||||
):
|
||||
"""Add a new custom department to a project"""
|
||||
from schemas.department import CustomDepartmentCreate
|
||||
|
||||
try:
|
||||
department_create = CustomDepartmentCreate(**department_data)
|
||||
except Exception as e:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
|
||||
detail=str(e)
|
||||
)
|
||||
|
||||
db_project = db.query(Project).filter(Project.id == project_id).first()
|
||||
|
||||
if not db_project:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Project not found"
|
||||
)
|
||||
|
||||
custom_departments = db_project.custom_departments or []
|
||||
|
||||
if department_create.department in STANDARD_DEPARTMENTS:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_409_CONFLICT,
|
||||
detail=f"Department '{department_create.department}' is a standard department and cannot be added as custom"
|
||||
)
|
||||
|
||||
if department_create.department in custom_departments:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_409_CONFLICT,
|
||||
detail=f"Department '{department_create.department}' already exists"
|
||||
)
|
||||
|
||||
custom_departments.append(department_create.department)
|
||||
|
||||
db_project.custom_departments = custom_departments
|
||||
flag_modified(db_project, 'custom_departments')
|
||||
|
||||
try:
|
||||
db.commit()
|
||||
db.refresh(db_project)
|
||||
except Exception as e:
|
||||
db.rollback()
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="Failed to add department"
|
||||
)
|
||||
|
||||
return _build_all_departments_response(db_project)
|
||||
|
||||
|
||||
@router.put("/{project_id}/departments/{department}")
|
||||
async def update_department(
|
||||
project_id: int,
|
||||
department: str,
|
||||
update_data: dict,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(require_coordinator_or_admin)
|
||||
):
|
||||
"""Update a custom department name, cascading the rename to members and tasks using it"""
|
||||
from schemas.department import CustomDepartmentUpdate
|
||||
from models.task import Task
|
||||
|
||||
try:
|
||||
department_update = CustomDepartmentUpdate(**update_data)
|
||||
except Exception as e:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
|
||||
detail=str(e)
|
||||
)
|
||||
|
||||
if department != department_update.old_name:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Department in URL does not match old_name in request body"
|
||||
)
|
||||
|
||||
db_project = db.query(Project).filter(Project.id == project_id).first()
|
||||
|
||||
if not db_project:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Project not found"
|
||||
)
|
||||
|
||||
custom_departments = db_project.custom_departments or []
|
||||
|
||||
if department_update.old_name not in custom_departments:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=f"Custom department '{department_update.old_name}' not found"
|
||||
)
|
||||
|
||||
if department_update.new_name in STANDARD_DEPARTMENTS:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_409_CONFLICT,
|
||||
detail=f"Department '{department_update.new_name}' is a standard department"
|
||||
)
|
||||
|
||||
if department_update.new_name in custom_departments and department_update.new_name != department_update.old_name:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_409_CONFLICT,
|
||||
detail=f"Department '{department_update.new_name}' already exists"
|
||||
)
|
||||
|
||||
custom_departments = [
|
||||
department_update.new_name if d == department_update.old_name else d
|
||||
for d in custom_departments
|
||||
]
|
||||
|
||||
db_project.custom_departments = custom_departments
|
||||
flag_modified(db_project, 'custom_departments')
|
||||
|
||||
# Cascade rename to project members using this department
|
||||
members_to_update = db.query(ProjectMember).filter(
|
||||
ProjectMember.project_id == project_id,
|
||||
ProjectMember.department_role == department_update.old_name
|
||||
).all()
|
||||
for member in members_to_update:
|
||||
member.department_role = department_update.new_name
|
||||
|
||||
# Cascade rename to tasks using this department
|
||||
tasks_to_update = db.query(Task).filter(
|
||||
Task.project_id == project_id,
|
||||
Task.department == department_update.old_name
|
||||
).all()
|
||||
for task in tasks_to_update:
|
||||
task.department = department_update.new_name
|
||||
|
||||
try:
|
||||
db.commit()
|
||||
db.refresh(db_project)
|
||||
except Exception as e:
|
||||
db.rollback()
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="Failed to update department"
|
||||
)
|
||||
|
||||
return _build_all_departments_response(db_project)
|
||||
|
||||
|
||||
@router.delete("/{project_id}/departments/{department}")
|
||||
async def delete_department(
|
||||
project_id: int,
|
||||
department: str,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(require_coordinator_or_admin)
|
||||
):
|
||||
"""Delete a custom department (blocked if any member or task is currently using it)"""
|
||||
from models.task import Task
|
||||
|
||||
if department in STANDARD_DEPARTMENTS:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Standard departments cannot be deleted"
|
||||
)
|
||||
|
||||
db_project = db.query(Project).filter(Project.id == project_id).first()
|
||||
|
||||
if not db_project:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Project not found"
|
||||
)
|
||||
|
||||
custom_departments = db_project.custom_departments or []
|
||||
|
||||
if department not in custom_departments:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=f"Custom department '{department}' not found"
|
||||
)
|
||||
|
||||
members_using_department = db.query(ProjectMember).filter(
|
||||
ProjectMember.project_id == project_id,
|
||||
ProjectMember.department_role == department
|
||||
).all()
|
||||
|
||||
tasks_using_department = db.query(Task).filter(
|
||||
Task.project_id == project_id,
|
||||
Task.department == department
|
||||
).all()
|
||||
|
||||
if members_using_department or tasks_using_department:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
|
||||
detail={
|
||||
"error": f"Cannot delete department '{department}' because it is currently in use",
|
||||
"department": department,
|
||||
"member_count": len(members_using_department),
|
||||
"task_count": len(tasks_using_department)
|
||||
}
|
||||
)
|
||||
|
||||
custom_departments.remove(department)
|
||||
|
||||
db_project.custom_departments = custom_departments
|
||||
flag_modified(db_project, 'custom_departments')
|
||||
|
||||
try:
|
||||
db.commit()
|
||||
db.refresh(db_project)
|
||||
except Exception as e:
|
||||
db.rollback()
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="Failed to delete department"
|
||||
)
|
||||
|
||||
return _build_all_departments_response(db_project)
|
||||
|
||||
|
||||
# Submission Configuration Endpoints
|
||||
|
||||
@router.get("/{project_id}/submission-config", response_model=ProjectSubmissionConfig)
|
||||
|
||||
@@ -9,7 +9,7 @@ from datetime import datetime
|
||||
|
||||
from database import get_db
|
||||
from models.task import Task, ProductionNote, TaskAttachment, Submission, Review, NoteType
|
||||
from models.user import User, UserRole, DepartmentRole
|
||||
from models.user import User, UserRole
|
||||
from models.project import Project, ProjectMember
|
||||
from models.asset import Asset
|
||||
from models.shot import Shot
|
||||
@@ -248,6 +248,7 @@ async def get_tasks(
|
||||
"name": task.name,
|
||||
"task_type": task.task_type,
|
||||
"status": task.status,
|
||||
"department": task.department,
|
||||
"start_date": task.start_date,
|
||||
"deadline": task.deadline,
|
||||
"project_id": task.project_id,
|
||||
@@ -330,6 +331,7 @@ async def get_my_tasks(
|
||||
name=task.name,
|
||||
task_type=task.task_type,
|
||||
status=task.status,
|
||||
department=task.department,
|
||||
start_date=task.start_date,
|
||||
deadline=task.deadline,
|
||||
project_id=task.project_id,
|
||||
@@ -740,6 +742,7 @@ async def get_task(
|
||||
"description": task.description,
|
||||
"task_type": task.task_type,
|
||||
"status": task.status,
|
||||
"department": task.department,
|
||||
"start_date": task.start_date,
|
||||
"deadline": task.deadline,
|
||||
"project_id": task.project_id,
|
||||
@@ -866,6 +869,7 @@ async def update_task(
|
||||
"description": task.description,
|
||||
"task_type": task.task_type,
|
||||
"status": task.status,
|
||||
"department": task.department,
|
||||
"start_date": task.start_date,
|
||||
"deadline": task.deadline,
|
||||
"project_id": task.project_id,
|
||||
@@ -958,6 +962,7 @@ async def update_task_status(
|
||||
"description": task.description,
|
||||
"task_type": task.task_type,
|
||||
"status": task.status,
|
||||
"department": task.department,
|
||||
"start_date": task.start_date,
|
||||
"deadline": task.deadline,
|
||||
"project_id": task.project_id,
|
||||
@@ -1024,13 +1029,13 @@ async def assign_task(
|
||||
|
||||
# Check if user's department role matches task type (optional validation)
|
||||
task_to_department_mapping = {
|
||||
"layout": DepartmentRole.LAYOUT,
|
||||
"animation": DepartmentRole.ANIMATION,
|
||||
"lighting": DepartmentRole.LIGHTING,
|
||||
"compositing": DepartmentRole.COMPOSITE,
|
||||
"modeling": DepartmentRole.MODELING,
|
||||
"rigging": DepartmentRole.RIGGING,
|
||||
"surfacing": DepartmentRole.SURFACING,
|
||||
"layout": "layout",
|
||||
"animation": "animation",
|
||||
"lighting": "lighting",
|
||||
"compositing": "composite",
|
||||
"modeling": "modeling",
|
||||
"rigging": "rigging",
|
||||
"surfacing": "surfacing",
|
||||
"simulation": None # Simulation can be handled by multiple departments
|
||||
}
|
||||
|
||||
@@ -1064,6 +1069,7 @@ async def assign_task(
|
||||
"description": task.description,
|
||||
"task_type": task.task_type,
|
||||
"status": task.status,
|
||||
"department": task.department,
|
||||
"start_date": task.start_date,
|
||||
"deadline": task.deadline,
|
||||
"project_id": task.project_id,
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
"""
|
||||
Pydantic schemas for department management
|
||||
"""
|
||||
from pydantic import BaseModel, Field, validator
|
||||
from typing import List
|
||||
import re
|
||||
|
||||
|
||||
class CustomDepartmentCreate(BaseModel):
|
||||
"""Schema for creating a new custom department"""
|
||||
department: str = Field(..., min_length=2, max_length=50, description="Department name")
|
||||
|
||||
@validator('department')
|
||||
def validate_department_name(cls, v):
|
||||
"""Validate department name format"""
|
||||
if not re.match(r'^[a-z0-9_]{2,50}$', v):
|
||||
raise ValueError(
|
||||
'Department name must be 2-50 characters, lowercase alphanumeric with underscores only'
|
||||
)
|
||||
return v
|
||||
|
||||
|
||||
class CustomDepartmentUpdate(BaseModel):
|
||||
"""Schema for updating a custom department name"""
|
||||
old_name: str = Field(..., description="Current department name")
|
||||
new_name: str = Field(..., min_length=2, max_length=50, description="New department name")
|
||||
|
||||
@validator('new_name')
|
||||
def validate_department_name(cls, v):
|
||||
"""Validate department name format"""
|
||||
if not re.match(r'^[a-z0-9_]{2,50}$', v):
|
||||
raise ValueError(
|
||||
'Department name must be 2-50 characters, lowercase alphanumeric with underscores only'
|
||||
)
|
||||
return v
|
||||
|
||||
|
||||
class AllDepartmentsResponse(BaseModel):
|
||||
"""Schema for response containing all departments (standard + custom)"""
|
||||
departments: List[str] = Field(..., description="All departments")
|
||||
standard_departments: List[str] = Field(..., description="Standard departments (read-only)")
|
||||
custom_departments: List[str] = Field(..., description="Custom departments")
|
||||
|
||||
|
||||
class DepartmentInUseError(BaseModel):
|
||||
"""Schema for error when trying to delete a department in use"""
|
||||
error: str = Field(..., description="Error message")
|
||||
department: str = Field(..., description="Department that is in use")
|
||||
member_count: int = Field(..., description="Number of project members using this department")
|
||||
task_count: int = Field(..., description="Number of tasks using this department")
|
||||
@@ -5,7 +5,6 @@ from enum import Enum
|
||||
import re
|
||||
|
||||
from models.project import ProjectStatus, ProjectType
|
||||
from models.user import DepartmentRole
|
||||
|
||||
|
||||
# Technical Specifications Schemas
|
||||
@@ -67,16 +66,6 @@ class ProjectTechnicalSpecs(BaseModel):
|
||||
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 = {
|
||||
@@ -135,17 +124,6 @@ class ProjectBase(BaseModel):
|
||||
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
|
||||
|
||||
@@ -178,20 +156,9 @@ class ProjectUpdate(BaseModel):
|
||||
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
|
||||
department_role: Optional[str] = None
|
||||
|
||||
|
||||
class ProjectMemberCreate(ProjectMemberBase):
|
||||
@@ -199,7 +166,7 @@ class ProjectMemberCreate(ProjectMemberBase):
|
||||
|
||||
|
||||
class ProjectMemberUpdate(BaseModel):
|
||||
department_role: Optional[DepartmentRole] = None
|
||||
department_role: Optional[str] = None
|
||||
|
||||
|
||||
class ProjectMemberResponse(ProjectMemberBase):
|
||||
|
||||
@@ -4,7 +4,6 @@ from datetime import date, datetime
|
||||
from enum import Enum
|
||||
|
||||
from models.task import TaskType, TaskStatus, ReviewDecision, AttachmentType, NoteType
|
||||
from models.user import DepartmentRole
|
||||
|
||||
|
||||
class TaskBase(BaseModel):
|
||||
@@ -14,6 +13,7 @@ class TaskBase(BaseModel):
|
||||
start_date: Optional[date] = None
|
||||
deadline: Optional[date] = None
|
||||
status: str = "not_started" # Changed from TaskStatus enum to str to support custom statuses
|
||||
department: Optional[str] = None # Standard or project-custom department, independent of assignee
|
||||
|
||||
|
||||
class TaskCreate(TaskBase):
|
||||
@@ -31,6 +31,7 @@ class TaskUpdate(BaseModel):
|
||||
start_date: Optional[date] = None
|
||||
deadline: Optional[date] = None
|
||||
status: Optional[str] = None # Changed from TaskStatus enum to str to support custom statuses
|
||||
department: Optional[str] = None # Standard or project-custom department, independent of assignee
|
||||
assigned_user_id: Optional[int] = None
|
||||
|
||||
|
||||
@@ -70,6 +71,7 @@ class TaskListResponse(BaseModel):
|
||||
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
|
||||
department: Optional[str] = None # Standard or project-custom department, independent of assignee
|
||||
start_date: Optional[date] = None
|
||||
deadline: Optional[date] = None
|
||||
project_id: int
|
||||
|
||||
Reference in New Issue
Block a user