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:
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user