Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
eb366e7
Unlink team from a project for allowing team deletion
prabinoid Jul 25, 2025
59b88be
Add CustomMenu component with active item highlighting
suzit-10 Jul 29, 2025
12cf5a1
Add TeamLinkedProjectCard component with placeholder support
suzit-10 Jul 29, 2025
efb4128
Create `TeamLinkedProjects` to display a team's linked projects group…
suzit-10 Jul 29, 2025
feb1ebf
Replaced the generic `Projects` component with the new `TeamLinkedPro…
suzit-10 Jul 29, 2025
ec108f4
Add source strings of new component `TeamLinkedProjects` on messages.js
suzit-10 Jul 29, 2025
300582b
refactor: Minor code cleanup and accessibility improvements
suzit-10 Jul 29, 2025
01bddff
Check team permissions and eligibility to unlink from projects and bu…
prabinoid Aug 22, 2025
b0720e9
Unlink a team from all the project after permission validation
prabinoid Aug 27, 2025
1efd8fe
Update individual project unlink api endpoint
suzit-10 Aug 25, 2025
89848e6
add i18n messages for Unlink buttons
suzit-10 Sep 2, 2025
e7dcedd
Add project selection checkbox on project list and visible to users h…
suzit-10 Sep 2, 2025
8146dbf
Enable unlink project by project selection/inidividually or in bulk(A…
suzit-10 Sep 2, 2025
6d8b879
Team unlink check modified with updated mapping and validation permis…
prabinoid Sep 8, 2025
d9e4a94
Show inline error message for individual project unlink failure
suzit-10 Sep 8, 2025
46616ea
Display persistent success/error message insted of toast on project u…
suzit-10 Sep 8, 2025
d3a14c1
Add confirmation dialog for bulk actions(unlink all/unlink selected) …
suzit-10 Sep 8, 2025
bb27c41
Add i18n messages for confirmation dialog for bulk project unlink
suzit-10 Sep 8, 2025
5e48531
Add project manager permission check for team unlinking
prabinoid Sep 16, 2025
8865849
Update error message for bulk unlinking failure by restricted permiss…
suzit-10 Oct 6, 2025
a0ef91f
Comment density text check on test case
suzit-10 Oct 9, 2025
b3568d5
Subcode refactored on team deletion permission checks
prabinoid Oct 9, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
212 changes: 212 additions & 0 deletions backend/api/teams/actions.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from typing import List
from backend.models.dtos.team_dto import ProjectTeamPairDTOList
from databases import Database
from fastapi import APIRouter, BackgroundTasks, Body, Depends, Request
from fastapi.responses import JSONResponse
Expand Down Expand Up @@ -407,3 +409,213 @@ async def message_team(
)
except ValueError as e:
return JSONResponse(content={"Error": str(e)}, status_code=400)


@router.delete("/projects/{project_id}/teams/{team_id}/")
async def remove_team_from_project(
project_id: int,
team_id: int,
request: Request,
user: AuthUserDTO = Depends(login_required),
db: Database = Depends(get_db),
):
"""
Unlink a Team from a Project.
"""
permitted = await TeamService.is_user_team_manager(team_id, user.id, db)
if not permitted:
return JSONResponse(
{
"Error": "User is not a manager of the team",
"SubCode": "UserPermissionError",
},
status_code=403,
)

deny_resp = await TeamService.ensure_unlink_allowed(project_id, team_id, db)
if deny_resp:
return deny_resp

try:
deleted = await TeamService.unlink_team(project_id, team_id, db)
if not deleted:
return JSONResponse(
{"Error": "No such team linked to project", "SubCode": "NotFoundError"},
status_code=404,
)
return JSONResponse({"Success": True}, status_code=200)
except Exception as e:
return JSONResponse(
{"Error": "Internal server error", "Details": str(e)},
status_code=500,
)


@router.delete("/projects/teams/{team_id}/unlink")
async def remove_team_from_all_projects(
team_id: int,
request: Request,
user: AuthUserDTO = Depends(login_required),
db: Database = Depends(get_db),
):
"""
Unlink the given team from all projects it is assigned to.

Steps:
- ensure caller is a manager of the team
- fetch all project_ids for the team from project_teams
- run ensure_unlink_allowed(project_id, team_id, db) for every project
- if all checks pass, unlink each (inside one DB transaction)
"""
permitted = await TeamService.is_user_team_manager(team_id, user.id, db)
if not permitted:
return JSONResponse(
{
"Error": (
f"Cannot unlink team with team id-{team_id}: "
f"user {user.id} is not a manager of the team"
),
"SubCode": "UserPermissionError",
},
status_code=403,
)

rows = await db.fetch_all(
"SELECT project_id FROM project_teams WHERE team_id = :tid",
{"tid": team_id},
)
project_ids: List[int] = [r["project_id"] for r in rows] if rows else []

if not project_ids:
return JSONResponse(
{
"Error": (
f"Cannot unlink team with team id-{team_id}: "
"team is not linked to any projects"
),
"SubCode": "NotFoundError",
},
status_code=404,
)

for pid in project_ids:
deny_resp = await TeamService.ensure_unlink_allowed(pid, team_id, db)
if deny_resp:
return deny_resp

try:
async with db.transaction():
for pid in project_ids:
deleted = await TeamService.unlink_team(pid, team_id, db)
if not deleted:
raise RuntimeError(f"NOT_FOUND:{pid}:{team_id}")

projects_str = ", ".join(str(p) for p in project_ids)
return JSONResponse(
{
"Success": True,
"Message": (
f"Team id-{team_id} unlinked from projects: {projects_str}"
),
},
status_code=200,
)

except Exception as e:
return JSONResponse(
{
"Error": (
f"Cannot unlink team with team id-{team_id}: internal server error - {str(e)}"
),
"SubCode": "InternalServerError",
},
status_code=500,
)


@router.delete("/projects/unlink")
async def remove_teams_from_projects(
payload: ProjectTeamPairDTOList,
request: Request,
user: AuthUserDTO = Depends(login_required),
db: Database = Depends(get_db),
):
"""
Bulk unlink teams from projects.

Body:
{
"items": [
{"project_id": 1442, "team_id": 43},
{"project_id": 2000, "team_id": 55}
]
}

First: run all checks for all items (manager check + ensure_unlink_allowed).
If any check fails, return error immediately and do NOT modify DB.
If all checks pass, perform unlink operations inside a single DB transaction.
"""
items = payload.items or []
if not items:
return JSONResponse(
{"Error": "No project/team pairs provided", "SubCode": "InvalidRequest"},
status_code=400,
)

seen = set()
pairs = []
for it in items:
key = (it.project_id, it.team_id)
if key in seen:
continue
seen.add(key)
pairs.append(it)

for it in pairs:
pid = it.project_id
tid = it.team_id

permitted = await TeamService.is_user_team_manager(tid, user.id, db)
if not permitted:
return JSONResponse(
{
"Error": (
f"Cannot unlink team with team id-{tid}: user {user.id} is not a manager of the team"
),
"SubCode": "UserPermissionError",
},
status_code=403,
)

deny_resp = await TeamService.ensure_unlink_allowed(pid, tid, db)
if deny_resp:
return deny_resp

try:
async with db.transaction():
for it in pairs:
pid = it.project_id
tid = it.team_id

deleted = await TeamService.unlink_team(pid, tid, db)
if not deleted:
raise RuntimeError(f"NOT_FOUND:{pid}:{tid}")

pairs_str = ", ".join(
[f"(project {p.project_id}, team {p.team_id})" for p in pairs]
)
return JSONResponse(
{
"Success": True,
"Message": f"Unlinked teams: {pairs_str}",
},
status_code=200,
)
except Exception as e:
return JSONResponse(
{
"Error": f"Cannot unlink teams: internal server error - {str(e)}",
"SubCode": "InternalServerError",
},
status_code=500,
)
9 changes: 9 additions & 0 deletions backend/models/dtos/team_dto.py
Original file line number Diff line number Diff line change
Expand Up @@ -247,3 +247,12 @@ class TeamSearchDTO(BaseModel):

class Config:
populate_by_name = True


class ProjectTeamPairDTO(BaseModel):
project_id: int
team_id: int


class ProjectTeamPairDTOList(BaseModel):
items: List[ProjectTeamPairDTO]
Loading
Loading