675 lines
24 KiB
Python
675 lines
24 KiB
Python
|
|
"""Project CRUD endpoints including sub-projects, function groups, and functions."""
|
||
|
|
|
||
|
|
from fastapi import APIRouter, Depends, HTTPException, status, Query
|
||
|
|
from sqlalchemy import select, func, or_
|
||
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||
|
|
from sqlalchemy.orm import selectinload
|
||
|
|
|
||
|
|
from app.api.deps import get_current_user, require_permission
|
||
|
|
from app.db.session import get_async_session
|
||
|
|
from app.models import User, Project, SubProject, ProjectFunctionGroup, ProjectFunction
|
||
|
|
from app.schemas.project import (
|
||
|
|
ProjectCreateRequest,
|
||
|
|
ProjectUpdateRequest,
|
||
|
|
ProjectResponse,
|
||
|
|
ProjectListResponse,
|
||
|
|
SubProjectCreateRequest,
|
||
|
|
SubProjectUpdateRequest,
|
||
|
|
SubProjectResponse,
|
||
|
|
SubProjectListResponse,
|
||
|
|
ProjectFunctionGroupCreateRequest,
|
||
|
|
ProjectFunctionGroupUpdateRequest,
|
||
|
|
ProjectFunctionGroupResponse,
|
||
|
|
ProjectFunctionGroupListResponse,
|
||
|
|
ProjectFunctionCreateRequest,
|
||
|
|
ProjectFunctionUpdateRequest,
|
||
|
|
ProjectFunctionResponse,
|
||
|
|
ProjectFunctionListResponse,
|
||
|
|
)
|
||
|
|
|
||
|
|
router = APIRouter(prefix="/projects", tags=["projects"])
|
||
|
|
|
||
|
|
|
||
|
|
# ===== Project Endpoints =====
|
||
|
|
|
||
|
|
|
||
|
|
@router.get("", response_model=ProjectListResponse)
|
||
|
|
async def list_projects(
|
||
|
|
page: int = Query(1, ge=1),
|
||
|
|
size: int = Query(20, ge=1, le=100),
|
||
|
|
search: str | None = Query(None, description="Search in project name"),
|
||
|
|
status: str | None = Query(None, description="Filter by status (draft, confirmed, in_progress, completed, cancelled)"),
|
||
|
|
current_user: User = Depends(require_permission("projects:read")),
|
||
|
|
session: AsyncSession = Depends(get_async_session),
|
||
|
|
):
|
||
|
|
"""List projects with search, filters, pagination."""
|
||
|
|
account_id = current_user.account_id
|
||
|
|
|
||
|
|
base_q = select(Project).where(Project.account_id == account_id)
|
||
|
|
|
||
|
|
if search:
|
||
|
|
pattern = f"%{search}%"
|
||
|
|
base_q = base_q.where(Project.name.ilike(pattern))
|
||
|
|
|
||
|
|
if status:
|
||
|
|
base_q = base_q.where(Project.status == status)
|
||
|
|
|
||
|
|
count_q = select(func.count()).select_from(base_q.subquery())
|
||
|
|
total = (await session.execute(count_q)).scalar() or 0
|
||
|
|
|
||
|
|
q = (
|
||
|
|
base_q
|
||
|
|
.order_by(Project.updated_at.desc())
|
||
|
|
.offset((page - 1) * size)
|
||
|
|
.limit(size)
|
||
|
|
)
|
||
|
|
result = await session.execute(q)
|
||
|
|
items = result.scalars().all()
|
||
|
|
|
||
|
|
return ProjectListResponse(
|
||
|
|
items=[ProjectResponse.model_validate(i) for i in items],
|
||
|
|
total=total,
|
||
|
|
page=page,
|
||
|
|
size=size,
|
||
|
|
)
|
||
|
|
|
||
|
|
|
||
|
|
@router.post("", response_model=ProjectResponse, status_code=status.HTTP_201_CREATED)
|
||
|
|
async def create_project(
|
||
|
|
body: ProjectCreateRequest,
|
||
|
|
current_user: User = Depends(require_permission("projects:write")),
|
||
|
|
session: AsyncSession = Depends(get_async_session),
|
||
|
|
):
|
||
|
|
"""Create a new project."""
|
||
|
|
account_id = current_user.account_id
|
||
|
|
project = Project(account_id=account_id, **body.model_dump())
|
||
|
|
session.add(project)
|
||
|
|
await session.commit()
|
||
|
|
await session.refresh(project)
|
||
|
|
return ProjectResponse.model_validate(project)
|
||
|
|
|
||
|
|
|
||
|
|
@router.get("/{project_id}", response_model=ProjectResponse)
|
||
|
|
async def get_project(
|
||
|
|
project_id: str,
|
||
|
|
current_user: User = Depends(require_permission("projects:read")),
|
||
|
|
session: AsyncSession = Depends(get_async_session),
|
||
|
|
):
|
||
|
|
"""Get a specific project."""
|
||
|
|
account_id = current_user.account_id
|
||
|
|
result = await session.execute(
|
||
|
|
select(Project).where(Project.id == project_id, Project.account_id == account_id)
|
||
|
|
)
|
||
|
|
project = result.scalars().first()
|
||
|
|
if not project:
|
||
|
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Project not found")
|
||
|
|
return ProjectResponse.model_validate(project)
|
||
|
|
|
||
|
|
|
||
|
|
@router.put("/{project_id}", response_model=ProjectResponse)
|
||
|
|
async def update_project(
|
||
|
|
project_id: str,
|
||
|
|
body: ProjectUpdateRequest,
|
||
|
|
current_user: User = Depends(require_permission("projects:write")),
|
||
|
|
session: AsyncSession = Depends(get_async_session),
|
||
|
|
):
|
||
|
|
"""Update an existing project."""
|
||
|
|
account_id = current_user.account_id
|
||
|
|
result = await session.execute(
|
||
|
|
select(Project).where(Project.id == project_id, Project.account_id == account_id)
|
||
|
|
)
|
||
|
|
project = result.scalars().first()
|
||
|
|
if not project:
|
||
|
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Project not found")
|
||
|
|
|
||
|
|
update_data = body.model_dump(exclude_unset=True)
|
||
|
|
for field, value in update_data.items():
|
||
|
|
setattr(project, field, value)
|
||
|
|
|
||
|
|
await session.commit()
|
||
|
|
await session.refresh(project)
|
||
|
|
return ProjectResponse.model_validate(project)
|
||
|
|
|
||
|
|
|
||
|
|
@router.delete("/{project_id}", status_code=status.HTTP_204_NO_CONTENT)
|
||
|
|
async def delete_project(
|
||
|
|
project_id: str,
|
||
|
|
current_user: User = Depends(require_permission("projects:delete")),
|
||
|
|
session: AsyncSession = Depends(get_async_session),
|
||
|
|
):
|
||
|
|
"""Delete a project."""
|
||
|
|
account_id = current_user.account_id
|
||
|
|
result = await session.execute(
|
||
|
|
select(Project).where(Project.id == project_id, Project.account_id == account_id)
|
||
|
|
)
|
||
|
|
project = result.scalars().first()
|
||
|
|
if not project:
|
||
|
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Project not found")
|
||
|
|
|
||
|
|
await session.delete(project)
|
||
|
|
await session.commit()
|
||
|
|
return None
|
||
|
|
|
||
|
|
|
||
|
|
# ===== SubProject Endpoints =====
|
||
|
|
|
||
|
|
|
||
|
|
@router.get("/{project_id}/subprojects", response_model=SubProjectListResponse)
|
||
|
|
async def list_subprojects(
|
||
|
|
project_id: str,
|
||
|
|
page: int = Query(1, ge=1),
|
||
|
|
size: int = Query(50, ge=1, le=200),
|
||
|
|
current_user: User = Depends(require_permission("projects:read")),
|
||
|
|
session: AsyncSession = Depends(get_async_session),
|
||
|
|
):
|
||
|
|
"""List subprojects for a project."""
|
||
|
|
account_id = current_user.account_id
|
||
|
|
|
||
|
|
# Verify project ownership
|
||
|
|
proj_result = await session.execute(
|
||
|
|
select(Project.id).where(Project.id == project_id, Project.account_id == account_id)
|
||
|
|
)
|
||
|
|
if not proj_result.scalars().first():
|
||
|
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Project not found")
|
||
|
|
|
||
|
|
base_q = select(SubProject).where(SubProject.project_id == project_id)
|
||
|
|
count_q = select(func.count()).select_from(base_q.subquery())
|
||
|
|
total = (await session.execute(count_q)).scalar() or 0
|
||
|
|
|
||
|
|
q = base_q.order_by(SubProject.sort_order).offset((page - 1) * size).limit(size)
|
||
|
|
result = await session.execute(q)
|
||
|
|
items = result.scalars().all()
|
||
|
|
|
||
|
|
return SubProjectListResponse(
|
||
|
|
items=[SubProjectResponse.model_validate(i) for i in items],
|
||
|
|
total=total,
|
||
|
|
page=page,
|
||
|
|
size=size,
|
||
|
|
)
|
||
|
|
|
||
|
|
|
||
|
|
@router.post("/{project_id}/subprojects", response_model=SubProjectResponse, status_code=status.HTTP_201_CREATED)
|
||
|
|
async def create_subproject(
|
||
|
|
project_id: str,
|
||
|
|
body: SubProjectCreateRequest,
|
||
|
|
current_user: User = Depends(require_permission("projects:write")),
|
||
|
|
session: AsyncSession = Depends(get_async_session),
|
||
|
|
):
|
||
|
|
"""Create a subproject within a project."""
|
||
|
|
account_id = current_user.account_id
|
||
|
|
|
||
|
|
# Verify project ownership
|
||
|
|
proj_result = await session.execute(
|
||
|
|
select(Project.id).where(Project.id == project_id, Project.account_id == account_id)
|
||
|
|
)
|
||
|
|
if not proj_result.scalars().first():
|
||
|
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Project not found")
|
||
|
|
|
||
|
|
# Verify parent subproject if provided
|
||
|
|
if body.parent_id:
|
||
|
|
parent_result = await session.execute(
|
||
|
|
select(SubProject.id).where(
|
||
|
|
SubProject.id == body.parent_id,
|
||
|
|
SubProject.project_id == project_id,
|
||
|
|
)
|
||
|
|
)
|
||
|
|
if not parent_result.scalars().first():
|
||
|
|
raise HTTPException(
|
||
|
|
status_code=status.HTTP_404_NOT_FOUND,
|
||
|
|
detail="Parent subproject not found",
|
||
|
|
)
|
||
|
|
|
||
|
|
sub = SubProject(project_id=project_id, **body.model_dump())
|
||
|
|
session.add(sub)
|
||
|
|
await session.commit()
|
||
|
|
await session.refresh(sub)
|
||
|
|
return SubProjectResponse.model_validate(sub)
|
||
|
|
|
||
|
|
|
||
|
|
@router.get("/{project_id}/subprojects/{sub_id}", response_model=SubProjectResponse)
|
||
|
|
async def get_subproject(
|
||
|
|
project_id: str,
|
||
|
|
sub_id: str,
|
||
|
|
current_user: User = Depends(require_permission("projects:read")),
|
||
|
|
session: AsyncSession = Depends(get_async_session),
|
||
|
|
):
|
||
|
|
"""Get a specific subproject."""
|
||
|
|
account_id = current_user.account_id
|
||
|
|
|
||
|
|
# Verify project ownership
|
||
|
|
proj_result = await session.execute(
|
||
|
|
select(Project.id).where(Project.id == project_id, Project.account_id == account_id)
|
||
|
|
)
|
||
|
|
if not proj_result.scalars().first():
|
||
|
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Project not found")
|
||
|
|
|
||
|
|
result = await session.execute(
|
||
|
|
select(SubProject).where(SubProject.id == sub_id, SubProject.project_id == project_id)
|
||
|
|
)
|
||
|
|
sub = result.scalars().first()
|
||
|
|
if not sub:
|
||
|
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="SubProject not found")
|
||
|
|
return SubProjectResponse.model_validate(sub)
|
||
|
|
|
||
|
|
|
||
|
|
@router.put("/{project_id}/subprojects/{sub_id}", response_model=SubProjectResponse)
|
||
|
|
async def update_subproject(
|
||
|
|
project_id: str,
|
||
|
|
sub_id: str,
|
||
|
|
body: SubProjectUpdateRequest,
|
||
|
|
current_user: User = Depends(require_permission("projects:write")),
|
||
|
|
session: AsyncSession = Depends(get_async_session),
|
||
|
|
):
|
||
|
|
"""Update a subproject."""
|
||
|
|
account_id = current_user.account_id
|
||
|
|
|
||
|
|
# Verify project ownership
|
||
|
|
proj_result = await session.execute(
|
||
|
|
select(Project.id).where(Project.id == project_id, Project.account_id == account_id)
|
||
|
|
)
|
||
|
|
if not proj_result.scalars().first():
|
||
|
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Project not found")
|
||
|
|
|
||
|
|
result = await session.execute(
|
||
|
|
select(SubProject).where(SubProject.id == sub_id, SubProject.project_id == project_id)
|
||
|
|
)
|
||
|
|
sub = result.scalars().first()
|
||
|
|
if not sub:
|
||
|
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="SubProject not found")
|
||
|
|
|
||
|
|
update_data = body.model_dump(exclude_unset=True)
|
||
|
|
for field, value in update_data.items():
|
||
|
|
setattr(sub, field, value)
|
||
|
|
|
||
|
|
await session.commit()
|
||
|
|
await session.refresh(sub)
|
||
|
|
return SubProjectResponse.model_validate(sub)
|
||
|
|
|
||
|
|
|
||
|
|
@router.delete("/{project_id}/subprojects/{sub_id}", status_code=status.HTTP_204_NO_CONTENT)
|
||
|
|
async def delete_subproject(
|
||
|
|
project_id: str,
|
||
|
|
sub_id: str,
|
||
|
|
current_user: User = Depends(require_permission("projects:delete")),
|
||
|
|
session: AsyncSession = Depends(get_async_session),
|
||
|
|
):
|
||
|
|
"""Delete a subproject."""
|
||
|
|
account_id = current_user.account_id
|
||
|
|
|
||
|
|
# Verify project ownership
|
||
|
|
proj_result = await session.execute(
|
||
|
|
select(Project.id).where(Project.id == project_id, Project.account_id == account_id)
|
||
|
|
)
|
||
|
|
if not proj_result.scalars().first():
|
||
|
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Project not found")
|
||
|
|
|
||
|
|
result = await session.execute(
|
||
|
|
select(SubProject).where(SubProject.id == sub_id, SubProject.project_id == project_id)
|
||
|
|
)
|
||
|
|
sub = result.scalars().first()
|
||
|
|
if not sub:
|
||
|
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="SubProject not found")
|
||
|
|
|
||
|
|
await session.delete(sub)
|
||
|
|
await session.commit()
|
||
|
|
return None
|
||
|
|
|
||
|
|
|
||
|
|
# ===== ProjectFunctionGroup Endpoints =====
|
||
|
|
|
||
|
|
|
||
|
|
@router.get("/{project_id}/function-groups", response_model=ProjectFunctionGroupListResponse)
|
||
|
|
async def list_function_groups(
|
||
|
|
project_id: str,
|
||
|
|
page: int = Query(1, ge=1),
|
||
|
|
size: int = Query(50, ge=1, le=200),
|
||
|
|
current_user: User = Depends(require_permission("projects:read")),
|
||
|
|
session: AsyncSession = Depends(get_async_session),
|
||
|
|
):
|
||
|
|
"""List function groups for a project."""
|
||
|
|
account_id = current_user.account_id
|
||
|
|
|
||
|
|
proj_result = await session.execute(
|
||
|
|
select(Project.id).where(Project.id == project_id, Project.account_id == account_id)
|
||
|
|
)
|
||
|
|
if not proj_result.scalars().first():
|
||
|
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Project not found")
|
||
|
|
|
||
|
|
base_q = select(ProjectFunctionGroup).where(ProjectFunctionGroup.project_id == project_id)
|
||
|
|
count_q = select(func.count()).select_from(base_q.subquery())
|
||
|
|
total = (await session.execute(count_q)).scalar() or 0
|
||
|
|
|
||
|
|
q = base_q.order_by(ProjectFunctionGroup.sort_order).offset((page - 1) * size).limit(size)
|
||
|
|
result = await session.execute(q)
|
||
|
|
items = result.scalars().all()
|
||
|
|
|
||
|
|
return ProjectFunctionGroupListResponse(
|
||
|
|
items=[ProjectFunctionGroupResponse.model_validate(i) for i in items],
|
||
|
|
total=total,
|
||
|
|
page=page,
|
||
|
|
size=size,
|
||
|
|
)
|
||
|
|
|
||
|
|
|
||
|
|
@router.post("/{project_id}/function-groups", response_model=ProjectFunctionGroupResponse, status_code=status.HTTP_201_CREATED)
|
||
|
|
async def create_function_group(
|
||
|
|
project_id: str,
|
||
|
|
body: ProjectFunctionGroupCreateRequest,
|
||
|
|
current_user: User = Depends(require_permission("projects:write")),
|
||
|
|
session: AsyncSession = Depends(get_async_session),
|
||
|
|
):
|
||
|
|
"""Create a function group within a project."""
|
||
|
|
account_id = current_user.account_id
|
||
|
|
|
||
|
|
proj_result = await session.execute(
|
||
|
|
select(Project.id).where(Project.id == project_id, Project.account_id == account_id)
|
||
|
|
)
|
||
|
|
if not proj_result.scalars().first():
|
||
|
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Project not found")
|
||
|
|
|
||
|
|
fg = ProjectFunctionGroup(project_id=project_id, **body.model_dump())
|
||
|
|
session.add(fg)
|
||
|
|
await session.commit()
|
||
|
|
await session.refresh(fg)
|
||
|
|
return ProjectFunctionGroupResponse.model_validate(fg)
|
||
|
|
|
||
|
|
|
||
|
|
@router.get("/{project_id}/function-groups/{fg_id}", response_model=ProjectFunctionGroupResponse)
|
||
|
|
async def get_function_group(
|
||
|
|
project_id: str,
|
||
|
|
fg_id: str,
|
||
|
|
current_user: User = Depends(require_permission("projects:read")),
|
||
|
|
session: AsyncSession = Depends(get_async_session),
|
||
|
|
):
|
||
|
|
"""Get a specific function group."""
|
||
|
|
account_id = current_user.account_id
|
||
|
|
|
||
|
|
proj_result = await session.execute(
|
||
|
|
select(Project.id).where(Project.id == project_id, Project.account_id == account_id)
|
||
|
|
)
|
||
|
|
if not proj_result.scalars().first():
|
||
|
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Project not found")
|
||
|
|
|
||
|
|
result = await session.execute(
|
||
|
|
select(ProjectFunctionGroup).where(
|
||
|
|
ProjectFunctionGroup.id == fg_id,
|
||
|
|
ProjectFunctionGroup.project_id == project_id,
|
||
|
|
)
|
||
|
|
)
|
||
|
|
fg = result.scalars().first()
|
||
|
|
if not fg:
|
||
|
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Function group not found")
|
||
|
|
return ProjectFunctionGroupResponse.model_validate(fg)
|
||
|
|
|
||
|
|
|
||
|
|
@router.put("/{project_id}/function-groups/{fg_id}", response_model=ProjectFunctionGroupResponse)
|
||
|
|
async def update_function_group(
|
||
|
|
project_id: str,
|
||
|
|
fg_id: str,
|
||
|
|
body: ProjectFunctionGroupUpdateRequest,
|
||
|
|
current_user: User = Depends(require_permission("projects:write")),
|
||
|
|
session: AsyncSession = Depends(get_async_session),
|
||
|
|
):
|
||
|
|
"""Update a function group."""
|
||
|
|
account_id = current_user.account_id
|
||
|
|
|
||
|
|
proj_result = await session.execute(
|
||
|
|
select(Project.id).where(Project.id == project_id, Project.account_id == account_id)
|
||
|
|
)
|
||
|
|
if not proj_result.scalars().first():
|
||
|
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Project not found")
|
||
|
|
|
||
|
|
result = await session.execute(
|
||
|
|
select(ProjectFunctionGroup).where(
|
||
|
|
ProjectFunctionGroup.id == fg_id,
|
||
|
|
ProjectFunctionGroup.project_id == project_id,
|
||
|
|
)
|
||
|
|
)
|
||
|
|
fg = result.scalars().first()
|
||
|
|
if not fg:
|
||
|
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Function group not found")
|
||
|
|
|
||
|
|
update_data = body.model_dump(exclude_unset=True)
|
||
|
|
for field, value in update_data.items():
|
||
|
|
setattr(fg, field, value)
|
||
|
|
|
||
|
|
await session.commit()
|
||
|
|
await session.refresh(fg)
|
||
|
|
return ProjectFunctionGroupResponse.model_validate(fg)
|
||
|
|
|
||
|
|
|
||
|
|
@router.delete("/{project_id}/function-groups/{fg_id}", status_code=status.HTTP_204_NO_CONTENT)
|
||
|
|
async def delete_function_group(
|
||
|
|
project_id: str,
|
||
|
|
fg_id: str,
|
||
|
|
current_user: User = Depends(require_permission("projects:delete")),
|
||
|
|
session: AsyncSession = Depends(get_async_session),
|
||
|
|
):
|
||
|
|
"""Delete a function group."""
|
||
|
|
account_id = current_user.account_id
|
||
|
|
|
||
|
|
proj_result = await session.execute(
|
||
|
|
select(Project.id).where(Project.id == project_id, Project.account_id == account_id)
|
||
|
|
)
|
||
|
|
if not proj_result.scalars().first():
|
||
|
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Project not found")
|
||
|
|
|
||
|
|
result = await session.execute(
|
||
|
|
select(ProjectFunctionGroup).where(
|
||
|
|
ProjectFunctionGroup.id == fg_id,
|
||
|
|
ProjectFunctionGroup.project_id == project_id,
|
||
|
|
)
|
||
|
|
)
|
||
|
|
fg = result.scalars().first()
|
||
|
|
if not fg:
|
||
|
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Function group not found")
|
||
|
|
|
||
|
|
await session.delete(fg)
|
||
|
|
await session.commit()
|
||
|
|
return None
|
||
|
|
|
||
|
|
|
||
|
|
# ===== ProjectFunction Endpoints =====
|
||
|
|
|
||
|
|
|
||
|
|
@router.get("/{project_id}/function-groups/{fg_id}/functions", response_model=ProjectFunctionListResponse)
|
||
|
|
async def list_project_functions(
|
||
|
|
project_id: str,
|
||
|
|
fg_id: str,
|
||
|
|
page: int = Query(1, ge=1),
|
||
|
|
size: int = Query(50, ge=1, le=200),
|
||
|
|
current_user: User = Depends(require_permission("projects:read")),
|
||
|
|
session: AsyncSession = Depends(get_async_session),
|
||
|
|
):
|
||
|
|
"""List functions within a function group."""
|
||
|
|
account_id = current_user.account_id
|
||
|
|
|
||
|
|
proj_result = await session.execute(
|
||
|
|
select(Project.id).where(Project.id == project_id, Project.account_id == account_id)
|
||
|
|
)
|
||
|
|
if not proj_result.scalars().first():
|
||
|
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Project not found")
|
||
|
|
|
||
|
|
# Verify function group belongs to project
|
||
|
|
fg_result = await session.execute(
|
||
|
|
select(ProjectFunctionGroup.id).where(
|
||
|
|
ProjectFunctionGroup.id == fg_id,
|
||
|
|
ProjectFunctionGroup.project_id == project_id,
|
||
|
|
)
|
||
|
|
)
|
||
|
|
if not fg_result.scalars().first():
|
||
|
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Function group not found")
|
||
|
|
|
||
|
|
base_q = select(ProjectFunction).where(ProjectFunction.function_group_id == fg_id)
|
||
|
|
count_q = select(func.count()).select_from(base_q.subquery())
|
||
|
|
total = (await session.execute(count_q)).scalar() or 0
|
||
|
|
|
||
|
|
q = base_q.order_by(ProjectFunction.sort_order).offset((page - 1) * size).limit(size)
|
||
|
|
result = await session.execute(q)
|
||
|
|
items = result.scalars().all()
|
||
|
|
|
||
|
|
return ProjectFunctionListResponse(
|
||
|
|
items=[ProjectFunctionResponse.model_validate(i) for i in items],
|
||
|
|
total=total,
|
||
|
|
page=page,
|
||
|
|
size=size,
|
||
|
|
)
|
||
|
|
|
||
|
|
|
||
|
|
@router.post("/{project_id}/function-groups/{fg_id}/functions", response_model=ProjectFunctionResponse, status_code=status.HTTP_201_CREATED)
|
||
|
|
async def create_project_function(
|
||
|
|
project_id: str,
|
||
|
|
fg_id: str,
|
||
|
|
body: ProjectFunctionCreateRequest,
|
||
|
|
current_user: User = Depends(require_permission("projects:write")),
|
||
|
|
session: AsyncSession = Depends(get_async_session),
|
||
|
|
):
|
||
|
|
"""Create a function within a function group."""
|
||
|
|
account_id = current_user.account_id
|
||
|
|
|
||
|
|
proj_result = await session.execute(
|
||
|
|
select(Project.id).where(Project.id == project_id, Project.account_id == account_id)
|
||
|
|
)
|
||
|
|
if not proj_result.scalars().first():
|
||
|
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Project not found")
|
||
|
|
|
||
|
|
fg_result = await session.execute(
|
||
|
|
select(ProjectFunctionGroup.id).where(
|
||
|
|
ProjectFunctionGroup.id == fg_id,
|
||
|
|
ProjectFunctionGroup.project_id == project_id,
|
||
|
|
)
|
||
|
|
)
|
||
|
|
if not fg_result.scalars().first():
|
||
|
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Function group not found")
|
||
|
|
|
||
|
|
func_obj = ProjectFunction(function_group_id=fg_id, **body.model_dump())
|
||
|
|
session.add(func_obj)
|
||
|
|
await session.commit()
|
||
|
|
await session.refresh(func_obj)
|
||
|
|
return ProjectFunctionResponse.model_validate(func_obj)
|
||
|
|
|
||
|
|
|
||
|
|
@router.get("/{project_id}/function-groups/{fg_id}/functions/{func_id}", response_model=ProjectFunctionResponse)
|
||
|
|
async def get_project_function(
|
||
|
|
project_id: str,
|
||
|
|
fg_id: str,
|
||
|
|
func_id: str,
|
||
|
|
current_user: User = Depends(require_permission("projects:read")),
|
||
|
|
session: AsyncSession = Depends(get_async_session),
|
||
|
|
):
|
||
|
|
"""Get a specific project function."""
|
||
|
|
account_id = current_user.account_id
|
||
|
|
|
||
|
|
proj_result = await session.execute(
|
||
|
|
select(Project.id).where(Project.id == project_id, Project.account_id == account_id)
|
||
|
|
)
|
||
|
|
if not proj_result.scalars().first():
|
||
|
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Project not found")
|
||
|
|
|
||
|
|
fg_result = await session.execute(
|
||
|
|
select(ProjectFunctionGroup.id).where(
|
||
|
|
ProjectFunctionGroup.id == fg_id,
|
||
|
|
ProjectFunctionGroup.project_id == project_id,
|
||
|
|
)
|
||
|
|
)
|
||
|
|
if not fg_result.scalars().first():
|
||
|
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Function group not found")
|
||
|
|
|
||
|
|
result = await session.execute(
|
||
|
|
select(ProjectFunction).where(
|
||
|
|
ProjectFunction.id == func_id,
|
||
|
|
ProjectFunction.function_group_id == fg_id,
|
||
|
|
)
|
||
|
|
)
|
||
|
|
func_obj = result.scalars().first()
|
||
|
|
if not func_obj:
|
||
|
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Function not found")
|
||
|
|
return ProjectFunctionResponse.model_validate(func_obj)
|
||
|
|
|
||
|
|
|
||
|
|
@router.put("/{project_id}/function-groups/{fg_id}/functions/{func_id}", response_model=ProjectFunctionResponse)
|
||
|
|
async def update_project_function(
|
||
|
|
project_id: str,
|
||
|
|
fg_id: str,
|
||
|
|
func_id: str,
|
||
|
|
body: ProjectFunctionUpdateRequest,
|
||
|
|
current_user: User = Depends(require_permission("projects:write")),
|
||
|
|
session: AsyncSession = Depends(get_async_session),
|
||
|
|
):
|
||
|
|
"""Update a project function."""
|
||
|
|
account_id = current_user.account_id
|
||
|
|
|
||
|
|
proj_result = await session.execute(
|
||
|
|
select(Project.id).where(Project.id == project_id, Project.account_id == account_id)
|
||
|
|
)
|
||
|
|
if not proj_result.scalars().first():
|
||
|
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Project not found")
|
||
|
|
|
||
|
|
fg_result = await session.execute(
|
||
|
|
select(ProjectFunctionGroup.id).where(
|
||
|
|
ProjectFunctionGroup.id == fg_id,
|
||
|
|
ProjectFunctionGroup.project_id == project_id,
|
||
|
|
)
|
||
|
|
)
|
||
|
|
if not fg_result.scalars().first():
|
||
|
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Function group not found")
|
||
|
|
|
||
|
|
result = await session.execute(
|
||
|
|
select(ProjectFunction).where(
|
||
|
|
ProjectFunction.id == func_id,
|
||
|
|
ProjectFunction.function_group_id == fg_id,
|
||
|
|
)
|
||
|
|
)
|
||
|
|
func_obj = result.scalars().first()
|
||
|
|
if not func_obj:
|
||
|
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Function not found")
|
||
|
|
|
||
|
|
update_data = body.model_dump(exclude_unset=True)
|
||
|
|
for field, value in update_data.items():
|
||
|
|
setattr(func_obj, field, value)
|
||
|
|
|
||
|
|
await session.commit()
|
||
|
|
await session.refresh(func_obj)
|
||
|
|
return ProjectFunctionResponse.model_validate(func_obj)
|
||
|
|
|
||
|
|
|
||
|
|
@router.delete("/{project_id}/function-groups/{fg_id}/functions/{func_id}", status_code=status.HTTP_204_NO_CONTENT)
|
||
|
|
async def delete_project_function(
|
||
|
|
project_id: str,
|
||
|
|
fg_id: str,
|
||
|
|
func_id: str,
|
||
|
|
current_user: User = Depends(require_permission("projects:delete")),
|
||
|
|
session: AsyncSession = Depends(get_async_session),
|
||
|
|
):
|
||
|
|
"""Delete a project function."""
|
||
|
|
account_id = current_user.account_id
|
||
|
|
|
||
|
|
proj_result = await session.execute(
|
||
|
|
select(Project.id).where(Project.id == project_id, Project.account_id == account_id)
|
||
|
|
)
|
||
|
|
if not proj_result.scalars().first():
|
||
|
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Project not found")
|
||
|
|
|
||
|
|
fg_result = await session.execute(
|
||
|
|
select(ProjectFunctionGroup.id).where(
|
||
|
|
ProjectFunctionGroup.id == fg_id,
|
||
|
|
ProjectFunctionGroup.project_id == project_id,
|
||
|
|
)
|
||
|
|
)
|
||
|
|
if not fg_result.scalars().first():
|
||
|
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Function group not found")
|
||
|
|
|
||
|
|
result = await session.execute(
|
||
|
|
select(ProjectFunction).where(
|
||
|
|
ProjectFunction.id == func_id,
|
||
|
|
ProjectFunction.function_group_id == fg_id,
|
||
|
|
)
|
||
|
|
)
|
||
|
|
func_obj = result.scalars().first()
|
||
|
|
if not func_obj:
|
||
|
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Function not found")
|
||
|
|
|
||
|
|
await session.delete(func_obj)
|
||
|
|
await session.commit()
|
||
|
|
return None
|