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)
|
||||
|
||||
Reference in New Issue
Block a user