diff --git a/backend/api/projects/resources.py b/backend/api/projects/resources.py index 212ddf8b56..a05238bcc3 100644 --- a/backend/api/projects/resources.py +++ b/backend/api/projects/resources.py @@ -512,10 +512,10 @@ async def delete( }, status_code=403, ) - try: - await ProjectAdminService.delete_project(project_id, user.id, db) - return JSONResponse(content={"Success": "Project deleted"}, status_code=200) + async with db.transaction(): + await ProjectAdminService.delete_project(project_id, user.id, db) + return JSONResponse(content={"Success": "Project deleted"}, status_code=200) except ProjectAdminServiceError as e: return JSONResponse( content={"Error": str(e).split("-")[1], "SubCode": str(e).split("-")[0]}, diff --git a/backend/models/postgis/project.py b/backend/models/postgis/project.py index 21c53c2181..2adfb59c5c 100644 --- a/backend/models/postgis/project.py +++ b/backend/models/postgis/project.py @@ -627,11 +627,8 @@ async def update(self, project_dto: ProjectDTO, db: Database): self.allowed_users.append(user) # Update teams and projects relationship. - self.teams = [] + await db.execute(delete(ProjectTeams).where(ProjectTeams.project_id == self.id)) if hasattr(project_dto, "project_teams") and project_dto.project_teams: - await db.execute( - delete(ProjectTeams).where(ProjectTeams.project_id == self.id) - ) for team_dto in project_dto.project_teams: team = await Team.get(team_dto.team_id, db) if team is None: @@ -814,8 +811,35 @@ async def update(self, project_dto: ProjectDTO, db: Database): ) async def delete(self, db: Database): - """Deletes the current model from the DB""" - await db.execute(delete(Project.__table__).where(Project.id == self.id)) + """Deletes the current project and related records from the database using raw SQL.""" + # List of tables to delete from, in the order required to satisfy foreign key constraints + related_tables = [ + "project_favorites", + "project_custom_editors", + "project_interests", + "project_priority_areas", + "project_allowed_users", + "project_teams", + "task_invalidation_history", + "task_history", + "tasks", + "project_info", + "project_chat", + ] + + # Start a transaction to ensure atomic deletion + async with db.transaction(): + # Loop through each table and execute the delete query + for table in related_tables: + await db.execute( + f"DELETE FROM {table} WHERE project_id = :project_id", + {"project_id": self.id}, + ) + + # Finally, delete the project itself + await db.execute( + "DELETE FROM projects WHERE id = :project_id", {"project_id": self.id} + ) @staticmethod async def exists(project_id: int, db: Database) -> bool: diff --git a/backend/models/postgis/team.py b/backend/models/postgis/team.py index 8eeb463cb5..dff93b9c5b 100644 --- a/backend/models/postgis/team.py +++ b/backend/models/postgis/team.py @@ -7,7 +7,6 @@ ForeignKey, String, insert, - delete, ) from sqlalchemy.orm import relationship, backref from backend.exceptions import NotFound @@ -230,12 +229,26 @@ async def update(team, team_dto: TeamDTO, db: Database): await Team.update_team_members(team, team_dto, db) async def delete(self, db: Database): - """Deletes the current model from the DB""" - await db.execute(delete(Team.__table__).where(Team.id == self.id)) + """Deletes the current team and its members from the DB""" + + # Delete team members associated with this team + delete_team_members_query = """ + DELETE FROM team_members WHERE team_id = :team_id + """ + await db.execute(delete_team_members_query, values={"team_id": self.id}) - def can_be_deleted(self) -> bool: - """A Team can be deleted if it doesn't have any projects""" - return len(self.projects) == 0 + # Delete the team + delete_team_query = """ + DELETE FROM teams WHERE id = :team_id + """ + await db.execute(delete_team_query, values={"team_id": self.id}) + + @staticmethod + async def can_be_deleted(team_id: int, db: Database) -> bool: + """Check if a Team can be deleted by querying for associated projects""" + query = "SELECT COUNT(*) FROM project_teams WHERE team_id = :team_id" + result = await db.fetch_one(query, {"team_id": team_id}) + return result[0] == 0 async def get(team_id: int, db: Database): """ diff --git a/backend/services/organisation_service.py b/backend/services/organisation_service.py index 94e236516b..d646c8b0b7 100644 --- a/backend/services/organisation_service.py +++ b/backend/services/organisation_service.py @@ -309,9 +309,8 @@ async def delete_organisation(organisation_id: int, db: Database): except Exception as e: raise HTTPException(status_code=500, detail="Deletion failed") from e else: - raise HTTPException( - status_code=400, - detail="Organisation has projects or teams, cannot be deleted", + raise OrganisationServiceError( + "Organisation has projects, cannot be deleted" ) @staticmethod diff --git a/backend/services/project_admin_service.py b/backend/services/project_admin_service.py index a956bce3f9..d74489eabb 100644 --- a/backend/services/project_admin_service.py +++ b/backend/services/project_admin_service.py @@ -138,7 +138,9 @@ async def update_project( ) if project_dto.license_id: - ProjectAdminService._validate_imagery_licence(project_dto.license_id) + await ProjectAdminService._validate_imagery_licence( + project_dto.license_id, db + ) # To be handled before reaching this function if await ProjectAdminService.is_user_action_permitted_on_project( @@ -155,10 +157,10 @@ async def update_project( return project @staticmethod - def _validate_imagery_licence(license_id: int): + async def _validate_imagery_licence(license_id: int, db: Database): """Ensures that the suppliced license Id actually exists""" try: - LicenseService.get_license_as_dto(license_id) + await LicenseService.get_license_as_dto(license_id, db) except NotFound: raise ProjectAdminServiceError( f"RequireLicenseId- LicenseId {license_id} not found" diff --git a/backend/services/team_service.py b/backend/services/team_service.py index 18a1d02497..b9503a61dc 100644 --- a/backend/services/team_service.py +++ b/backend/services/team_service.py @@ -747,8 +747,7 @@ async def is_user_team_manager(team_id: int, user_id: int, db: Database) -> bool async def delete_team(team_id: int, db: Database): """Deletes a team""" team = await TeamService.get_team_by_id(team_id, db) - - if Team.can_be_deleted(team): + if await Team.can_be_deleted(team_id, db): await Team.delete(team, db) return JSONResponse(content={"Success": "Team deleted"}, status_code=200) else: