218 lines
7.6 KiB
Python
218 lines
7.6 KiB
Python
from __future__ import annotations
|
|
|
|
from .shared import *
|
|
from .helpers import _cleanup_project_dependencies, _fetch_project_member_user_ids, _get_project_or_404, _serialize_project_row
|
|
from .schemas import ProjectCreateRequest, ProjectMembersAssignRequest, ProjectUpdateRequest
|
|
|
|
@router.post("/")
|
|
async def create_project(
|
|
body: ProjectCreateRequest,
|
|
db: AsyncSession = Depends(get_db),
|
|
current_user: User = Depends(require_roles("BOSS", "ADMIN")),
|
|
):
|
|
tenant_id = int(current_user.tenant_id)
|
|
title = compact_title(body.title, max_len=24)
|
|
if not title:
|
|
raise HTTPException(status_code=400, detail="title required")
|
|
try:
|
|
member_user_ids = await resolve_project_member_ids(
|
|
db,
|
|
tenant_id=tenant_id,
|
|
owner_id=int(current_user.id),
|
|
assigned_role=body.assigned_role,
|
|
requested_member_user_ids=body.member_user_ids,
|
|
require_requested_members=False,
|
|
include_owner=True,
|
|
)
|
|
except ProjectMemberAssignmentError as exc:
|
|
await notify_member_assignment_failure(
|
|
db,
|
|
tenant_id=tenant_id,
|
|
actor_user_id=int(current_user.id),
|
|
project_title=title,
|
|
reason=str(exc),
|
|
interface_path="/projects/",
|
|
commit=True,
|
|
)
|
|
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
|
|
|
await ensure_project_id_sequence(db)
|
|
project = Project(
|
|
tenant_id=tenant_id,
|
|
name=title,
|
|
title=title,
|
|
description=body.description,
|
|
status=body.status or "active",
|
|
domain_id=body.domain_id,
|
|
assigned_role=body.assigned_role,
|
|
owner_id=int(current_user.id),
|
|
signal_count=0,
|
|
)
|
|
db.add(project)
|
|
try:
|
|
await db.flush()
|
|
await replace_project_members(
|
|
db,
|
|
tenant_id=tenant_id,
|
|
project_id=int(project.id),
|
|
member_user_ids=member_user_ids,
|
|
)
|
|
await log_operation(
|
|
db,
|
|
tenant_id=tenant_id,
|
|
user_id=int(current_user.id),
|
|
project_id=int(project.id),
|
|
entity_type="project",
|
|
entity_id=str(project.id),
|
|
action="project.create",
|
|
before_data=None,
|
|
after_data={
|
|
"title": project.title,
|
|
"status": project.status,
|
|
"member_user_ids": member_user_ids,
|
|
},
|
|
interface_path="/projects/",
|
|
caller="user",
|
|
)
|
|
await db.commit()
|
|
except Exception:
|
|
await db.rollback()
|
|
raise
|
|
await db.refresh(project)
|
|
await context_cache_service.invalidate_system_context(tenant_id=tenant_id)
|
|
await AIPMOKernelService.invalidate_world_model_cache(tenant_id=tenant_id, project_id=int(project.id))
|
|
return _serialize_project_row(project)
|
|
|
|
@router.put("/{project_id}/members")
|
|
async def assign_project_members(
|
|
project_id: int,
|
|
body: ProjectMembersAssignRequest,
|
|
db: AsyncSession = Depends(get_db),
|
|
current_user: User = Depends(require_roles("BOSS", "ADMIN")),
|
|
):
|
|
tenant_id = int(current_user.tenant_id)
|
|
project = await _get_project_or_404(db, tenant_id=tenant_id, project_id=project_id)
|
|
before_member_user_ids = await _fetch_project_member_user_ids(
|
|
db,
|
|
tenant_id=tenant_id,
|
|
project_id=int(project.id),
|
|
)
|
|
|
|
try:
|
|
member_user_ids = await resolve_project_member_ids(
|
|
db,
|
|
tenant_id=tenant_id,
|
|
owner_id=None,
|
|
assigned_role=project.assigned_role,
|
|
requested_member_user_ids=body.member_user_ids,
|
|
require_requested_members=True,
|
|
include_owner=False,
|
|
)
|
|
await replace_project_members(
|
|
db,
|
|
tenant_id=tenant_id,
|
|
project_id=int(project.id),
|
|
member_user_ids=member_user_ids,
|
|
)
|
|
await log_operation(
|
|
db,
|
|
tenant_id=tenant_id,
|
|
user_id=int(current_user.id),
|
|
project_id=int(project.id),
|
|
entity_type="project",
|
|
entity_id=str(project.id),
|
|
action="project.members.assign",
|
|
before_data={"member_user_ids": before_member_user_ids},
|
|
after_data={"member_user_ids": member_user_ids},
|
|
interface_path=f"/projects/{project_id}/members",
|
|
caller="user",
|
|
)
|
|
await db.commit()
|
|
except ProjectMemberAssignmentError as exc:
|
|
await db.rollback()
|
|
await notify_member_assignment_failure(
|
|
db,
|
|
tenant_id=tenant_id,
|
|
actor_user_id=int(current_user.id),
|
|
project_id=int(project.id),
|
|
project_title=project.title or project.name,
|
|
reason=str(exc),
|
|
interface_path=f"/projects/{project_id}/members",
|
|
commit=True,
|
|
)
|
|
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
|
except Exception:
|
|
await db.rollback()
|
|
raise
|
|
|
|
await context_cache_service.invalidate_system_context(tenant_id=tenant_id)
|
|
await AIPMOKernelService.invalidate_world_model_cache(tenant_id=tenant_id, project_id=int(project.id))
|
|
return {
|
|
"project_id": str(project.id),
|
|
"member_user_ids": [str(user_id) for user_id in member_user_ids],
|
|
}
|
|
|
|
@router.patch("/{project_id}")
|
|
async def update_project(
|
|
project_id: int,
|
|
body: ProjectUpdateRequest,
|
|
db: AsyncSession = Depends(get_db),
|
|
current_user: User = Depends(require_roles("BOSS", "ADMIN")),
|
|
):
|
|
tenant_id = int(current_user.tenant_id)
|
|
project = await _get_project_or_404(db, tenant_id=tenant_id, project_id=project_id)
|
|
if body.title is not None:
|
|
title = compact_title(body.title, max_len=24)
|
|
if not title:
|
|
raise HTTPException(status_code=400, detail="title cannot be empty")
|
|
project.title = title
|
|
project.name = title
|
|
if body.description is not None:
|
|
project.description = body.description
|
|
if body.status is not None:
|
|
project.status = body.status
|
|
if body.domain_id is not None:
|
|
project.domain_id = body.domain_id
|
|
if body.assigned_role is not None:
|
|
project.assigned_role = body.assigned_role
|
|
await db.commit()
|
|
await db.refresh(project)
|
|
await context_cache_service.invalidate_system_context(tenant_id=tenant_id)
|
|
await AIPMOKernelService.invalidate_world_model_cache(tenant_id=tenant_id, project_id=int(project.id))
|
|
return _serialize_project_row(project)
|
|
|
|
@router.delete("/{project_id}")
|
|
async def delete_project(
|
|
project_id: int,
|
|
db: AsyncSession = Depends(get_db),
|
|
current_user: User = Depends(require_roles("BOSS", "ADMIN")),
|
|
):
|
|
tenant_id = int(current_user.tenant_id)
|
|
_ = current_user
|
|
try:
|
|
await _get_project_or_404(db, tenant_id=tenant_id, project_id=project_id)
|
|
await _cleanup_project_dependencies(
|
|
db,
|
|
tenant_id=tenant_id,
|
|
project_id=project_id,
|
|
)
|
|
await db.execute(
|
|
delete(Project).where(
|
|
and_(
|
|
Project.tenant_id == tenant_id,
|
|
Project.id == project_id,
|
|
)
|
|
)
|
|
)
|
|
await db.commit()
|
|
except IntegrityError as exc:
|
|
await db.rollback()
|
|
raise HTTPException(status_code=409, detail="Project has related records and cannot be deleted") from exc
|
|
except Exception:
|
|
await db.rollback()
|
|
raise
|
|
await context_cache_service.invalidate_system_context(tenant_id=tenant_id)
|
|
await AIPMOKernelService.invalidate_world_model_cache(tenant_id=tenant_id, project_id=project_id)
|
|
return {"message": "Project deleted"}
|
|
|