diff --git a/backend/api/issues/resources.py b/backend/api/issues/resources.py index d5c0801540..b7f114445b 100644 --- a/backend/api/issues/resources.py +++ b/backend/api/issues/resources.py @@ -5,9 +5,7 @@ from backend.models.dtos.mapping_issues_dto import MappingIssueCategoryDTO from backend.services.mapping_issues_service import MappingIssueCategoryService -from backend.services.users.authentication_service import tm from backend.db import get_session -from starlette.authentication import requires from backend.db import get_db from backend.models.dtos.user_dto import AuthUserDTO from backend.services.users.authentication_service import login_required diff --git a/backend/api/projects/teams.py b/backend/api/projects/teams.py index d57e8ec06c..9fcebc3110 100644 --- a/backend/api/projects/teams.py +++ b/backend/api/projects/teams.py @@ -1,22 +1,19 @@ -# from flask_restful import Resource, request, current_app -# from schematics.exceptions import DataError +from databases import Database +from fastapi.responses import JSONResponse +from fastapi import APIRouter, Depends, Request, Body +from loguru import logger +from backend.db import get_db from backend.models.dtos.user_dto import AuthUserDTO -from backend.services.team_service import TeamService, TeamServiceError from backend.services.project_admin_service import ProjectAdminService from backend.services.project_service import ProjectService - -# from backend.services.users.authentication_service import token_auth +from backend.services.team_service import TeamService, TeamServiceError from backend.services.users.authentication_service import login_required -from fastapi import APIRouter, Depends, Request -from backend.db import get_session, get_db -from starlette.authentication import requires -from databases import Database router = APIRouter( prefix="/projects", tags=["projects"], - dependencies=[Depends(get_session)], + dependencies=[Depends(get_db)], responses={404: {"description": "Not found"}}, ) @@ -62,12 +59,16 @@ async def get( teams_dto = await TeamService.get_project_teams_as_dto(project_id, db) return teams_dto - # @token_auth.login_required - @router.post("/{project_id}/teams/{team_id}/") -@requires("authenticated") -async def post(request: Request, team_id, project_id): +async def post( + request: Request, + user: AuthUserDTO = Depends(login_required), + db: Database = Depends(get_db), + team_id: int = None, + project_id: int = None, + data: dict = Body(...), +): """Assign a team to a project --- tags: @@ -113,42 +114,56 @@ async def post(request: Request, team_id, project_id): 500: description: Internal Server Error """ - if not TeamService.is_user_team_manager(team_id, request.user.display_name): - return { - "Error": "User is not an admin or a manager for the team", - "SubCode": "UserPermissionError", - }, 401 + if not await TeamService.is_user_team_manager(team_id, user.id, db): + return JSONResponse( + content={ + "Error": "User is not an admin or a manager for the team", + "SubCode": "UserPermissionError", + }, + status_code=403, + ) try: - role = request.get_json(force=True)["role"] - except DataError as e: - current_app.logger.error(f"Error validating request: {str(e)}") - return {"Error": str(e), "SubCode": "InvalidData"}, 400 + role = data["role"] + except ValueError as e: + logger.error(f"Error validating request: {str(e)}") + return JSONResponse( + content={"Error": str(e), "SubCode": "InvalidData"}, status_code=400 + ) try: - if not ProjectAdminService.is_user_action_permitted_on_project( - token_auth.current_user, project_id + if not await ProjectAdminService.is_user_action_permitted_on_project( + user.id, project_id, db ): raise ValueError() - TeamService.add_team_project(team_id, project_id, role) - return ( - { + await TeamService.add_team_project(team_id, project_id, role, db) + return JSONResponse( + content={ "Success": "Team {} assigned to project {} with role {}".format( team_id, project_id, role ) }, - 201, + status_code=201, ) except ValueError: - return { - "Error": "User is not a manager of the project", - "SubCode": "UserPermissionError", - }, 403 + return JSONResponse( + content={ + "Error": "User is not a manager of the project", + "SubCode": "UserPermissionError", + }, + status_code=403, + ) -@router.patch("//projects//") -@requires("authenticated") -async def patch(request: Request, team_id: int, project_id: int): +@router.patch("/{team_id}/projects/{project_id}/") +async def patch( + request: Request, + user: AuthUserDTO = Depends(login_required), + db: Database = Depends(get_db), + team_id: int = None, + project_id: int = None, + data: dict = Body(...), +): """Update role of a team on a project --- tags: @@ -195,30 +210,40 @@ async def patch(request: Request, team_id: int, project_id: int): description: Internal Server Error """ try: - role = request.get_json(force=True)["role"] - except DataError as e: - current_app.logger.error(f"Error validating request: {str(e)}") + role = data["role"] + except ValueError as e: + logger.error(f"Error validating request: {str(e)}") return {"Error": str(e), "SubCode": "InvalidData"}, 400 try: - if not ProjectAdminService.is_user_action_permitted_on_project( - token_auth.current_user, project_id + if not await ProjectAdminService.is_user_action_permitted_on_project( + user.id, project_id, db ): raise ValueError() - TeamService.change_team_role(team_id, project_id, role) - return {"Status": "Team role updated successfully."}, 200 + await TeamService.change_team_role(team_id, project_id, role, db) + return JSONResponse( + content={"Status": "Team role updated successfully."}, status_code=201 + ) except ValueError: - return { - "Error": "User is not a manager of the project", - "SubCode": "UserPermissionError", - }, 403 + return JSONResponse( + content={ + "Error": "User is not a manager of the project", + "SubCode": "UserPermissionError", + }, + status_code=403, + ) except TeamServiceError as e: - return str(e), 402 + return JSONResponse(content={"Error": str(e)}, status_code=402) -@router.delete("//projects//") -@requires("authenticated") -async def delete(request: Request, team_id: int, project_id: int): +@router.delete("/{team_id}/projects/{project_id}/") +async def delete( + request: Request, + user: AuthUserDTO = Depends(login_required), + db: Database = Depends(get_db), + team_id: int = None, + project_id: int = None, +): """ Deletes the specified team project assignment --- @@ -252,14 +277,17 @@ async def delete(request: Request, team_id: int, project_id: int): description: Internal Server Error """ try: - if not ProjectAdminService.is_user_action_permitted_on_project( - request.user.display_name, project_id + if not await ProjectAdminService.is_user_action_permitted_on_project( + user.id, project_id, db ): raise ValueError() - TeamService.delete_team_project(team_id, project_id) - return {"Success": True}, 200 + await TeamService.delete_team_project(team_id, project_id, db) + return JSONResponse(content={"Success": True}, status_code=200) except ValueError: - return { - "Error": "User is not a manager of the project", - "SubCode": "UserPermissionError", - }, 403 + return JSONResponse( + content={ + "Error": "User is not a manager of the project", + "SubCode": "UserPermissionError", + }, + status_code=403, + ) diff --git a/backend/api/system/general.py b/backend/api/system/general.py index 68a3f6ea88..21c215b186 100644 --- a/backend/api/system/general.py +++ b/backend/api/system/general.py @@ -1,7 +1,7 @@ from databases import Database from datetime import datetime from fastapi import APIRouter, Depends, Request, Body -from fastapi.responses import JSONResponse, Response +from fastapi.responses import JSONResponse import requests from backend.db import get_db diff --git a/backend/api/teams/actions.py b/backend/api/teams/actions.py index d8ee129fab..7ddf9ac2c9 100644 --- a/backend/api/teams/actions.py +++ b/backend/api/teams/actions.py @@ -1,22 +1,23 @@ -import threading +from databases import Database +from fastapi import APIRouter, Depends, Request, Body, BackgroundTasks +from fastapi.responses import JSONResponse +from loguru import logger +from backend.db import get_db from backend.models.dtos.message_dto import MessageDTO from backend.services.team_service import ( TeamService, TeamJoinNotAllowed, TeamServiceError, ) -from backend.services.users.authentication_service import tm from backend.models.postgis.user import User -from fastapi import APIRouter, Depends, Request -from backend.db import get_session -from starlette.authentication import requires -from loguru import logger +from backend.services.users.authentication_service import login_required +from backend.models.dtos.user_dto import AuthUserDTO router = APIRouter( prefix="/teams", tags=["teams"], - dependencies=[Depends(get_session)], + dependencies=[Depends(get_db)], responses={404: {"description": "Not found"}}, ) @@ -24,8 +25,12 @@ @router.post("/{team_id}/actions/join/") -@requires("authenticated") -async def post(request: Request, team_id): +async def post( + request: Request, + user: AuthUserDTO = Depends(login_required), + db: Database = Depends(get_db), + team_id: int = None, +): """ Request to join a team --- @@ -56,18 +61,26 @@ async def post(request: Request, team_id): 500: description: Internal Server Error """ - authenticated_user_id = request.user.display_name try: - TeamService.request_to_join_team(team_id, authenticated_user_id) - return {"Success": "Join request successful"}, 200 + await TeamService.request_to_join_team(team_id, user.id, db) + return JSONResponse( + content={"Success": "Join request successful"}, status_code=200 + ) except TeamServiceError as e: - return {"Error": str(e), "SubCode": "InvalidRequest"}, 400 + return JSONResponse( + content={"Error": str(e), "SubCode": "InvalidRequest"}, status_code=400 + ) @router.patch("/{team_id}/actions/join/") -@requires("authenticated") -@tm.pm_only(False) -async def patch(request: Request, team_id): +# @tm.pm_only(False) +async def patch( + request: Request, + user: AuthUserDTO = Depends(login_required), + db: Database = Depends(get_db), + team_id: int = None, + data: dict = Body(...), +): """ Take action on a team invite --- @@ -120,43 +133,49 @@ async def patch(request: Request, team_id): description: Internal Server Error """ try: - json_data = request.json(force=True) - username = json_data["username"] - request_type = json_data.get("type", "join-response") - action = json_data["action"] - role = json_data.get("role", "member") + username = data["username"] + request_type = data.get("type", "join-response") + action = data["action"] + role = data.get("role", "member") except Exception as e: logger.error(f"error validating request: {str(e)}") - return { - "Error": str(e), - "SubCode": "InvalidData", - }, 400 + return JSONResponse( + content={ + "Error": str(e), + "SubCode": "InvalidData", + }, + status_code=400, + ) - authenticated_user_id = request.user.display_name if request_type == "join-response": - if TeamService.is_user_team_manager(team_id, authenticated_user_id): - TeamService.accept_reject_join_request( - team_id, authenticated_user_id, username, role, action + if await TeamService.is_user_team_manager(team_id, user.id, db): + await TeamService.accept_reject_join_request( + team_id, user.id, username, role, action, db ) - return {"Success": "True"}, 200 + return JSONResponse(content={"Success": "True"}, status_code=200) else: - return ( - { + return JSONResponse( + content={ "Error": "You don't have permissions to approve this join team request", "SubCode": "ApproveJoinError", }, - 403, + status_code=403, ) elif request_type == "invite-response": - TeamService.accept_reject_invitation_request( - team_id, authenticated_user_id, username, role, action + await TeamService.accept_reject_invitation_request( + team_id, user.id, username, role, action, db ) - return {"Success": "True"}, 200 + return JSONResponse(content={"Success": "True"}, status_code=200) @router.post("/{team_id}/actions/add/") -@requires("authenticated") -async def post(request: Request, team_id): +async def post( + request: Request, + user: AuthUserDTO = Depends(login_required), + db: Database = Depends(get_db), + team_id: int = None, + data: dict = Body(...), +): """ Add members to the team --- @@ -200,27 +219,38 @@ async def post(request: Request, team_id): description: Internal Server Error """ try: - post_data = await request.json(force=True) - username = post_data["username"] - role = post_data.get("role", None) + username = data["username"] + role = data.get("role", None) except (Exception, KeyError) as e: logger.error(f"error validating request: {str(e)}") - return { - "Error": str(e), - "SubCode": "InvalidData", - }, 400 + return JSONResponse( + content={ + "Error": str(e), + "SubCode": "InvalidData", + }, + status_code=400, + ) try: - authenticated_user_id = request.user.display_name - TeamService.add_user_to_team(team_id, authenticated_user_id, username, role) - return {"Success": "User added to the team"}, 200 + await TeamService.add_user_to_team(team_id, user.id, username, role, db) + return JSONResponse( + content={"Success": "User added to the team"}, status_code=200 + ) except TeamJoinNotAllowed as e: - return {"Error": str(e).split("-")[1], "SubCode": str(e).split("-")[0]}, 403 + return JSONResponse( + content={"Error": str(e).split("-")[1], "SubCode": str(e).split("-")[0]}, + status_code=403, + ) @router.post("/{team_id}/actions/leave/") -@requires("authenticated") -async def post(request: Request, team_id: int): +async def post( + request: Request, + user: AuthUserDTO = Depends(login_required), + db: Database = Depends(get_db), + team_id: int = None, + data: dict = Body(...), +): """ Removes a user from a team --- @@ -261,30 +291,37 @@ async def post(request: Request, team_id: int): 500: description: Internal Server Error """ - authenticated_user_id = request.user.display_name - username = request.get_json(force=True)["username"] - request_user = User.get_by_id(authenticated_user_id) + username = data["username"] + request_user = await User.get_by_id(user.id, db) if ( - TeamService.is_user_team_manager(team_id, authenticated_user_id) + await TeamService.is_user_team_manager(team_id, user.id, db) or request_user.username == username ): - TeamService.leave_team(team_id, username) - return {"Success": "User removed from the team"}, 200 + await TeamService.leave_team(team_id, username, db) + return JSONResponse( + content={"Success": "User removed from the team"}, status_code=200 + ) else: - return ( - { + return JSONResponse( + content={ "Error": "You don't have permissions to remove {} from this team.".format( username ), "SubCode": "RemoveUserError", }, - 403, + status_code=403, ) @router.post("/{team_id}/actions/message-members/") -@requires("authenticated") -async def post(request: Request, team_id: int): +async def post( + request: Request, + background_tasks: BackgroundTasks, + user: AuthUserDTO = Depends(login_required), + db: Database = Depends(get_db), + team_id: int = None, + message_dto: MessageDTO = Body(...), +): """ Message all team members --- @@ -330,38 +367,50 @@ async def post(request: Request, team_id: int): description: Internal Server Error """ try: - authenticated_user_id = request.user.display_name - message_dto = MessageDTO(request.json()) # Validate if team is present - team = TeamService.get_team_by_id(team_id) + team = await TeamService.get_team_by_id(team_id, db) - is_manager = TeamService.is_user_team_manager(team_id, authenticated_user_id) + is_manager = await TeamService.is_user_team_manager(team_id, user.id, db) if not is_manager: raise ValueError - message_dto.from_user_id = authenticated_user_id - message_dto.validate() if not message_dto.message.strip() or not message_dto.subject.strip(): raise Exception( {"Error": "Empty message not allowed", "SubCode": "EmptyMessage"} ) except Exception as e: logger.error(f"Error validating request: {str(e)}") - return { - "Error": "Request payload did not match validation", - "SubCode": "InvalidData", - }, 400 + return JSONResponse( + content={ + "Error": "Request payload did not match validation", + "SubCode": "InvalidData", + }, + status_code=400, + ) except ValueError: - return { - "Error": "Unauthorised to send message to team members", - "SubCode": "UserNotPermitted", - }, 403 + return JSONResponse( + content={ + "Error": "Unauthorised to send message to team members", + "SubCode": "UserNotPermitted", + }, + status_code=403, + ) try: - threading.Thread( - target=TeamService.send_message_to_all_team_members, - args=(team_id, team.name, message_dto), - ).start() + # threading.Thread( + # target=TeamService.send_message_to_all_team_members, + # args=(team_id, team.name, message_dto, db), + # ).start() + background_tasks.add_task( + TeamService.send_message_to_all_team_members, + team_id, + team.name, + message_dto, + user.id, + db, + ) - return {"Success": "Message sent successfully"}, 200 + return JSONResponse( + content={"Success": "Message sent successfully"}, status_code=200 + ) except ValueError as e: - return {"Error": str(e)}, 403 + return JSONResponse(content={"Error": str(e)}, status_code=400) diff --git a/backend/api/teams/resources.py b/backend/api/teams/resources.py index d4230b1cdc..7e741eb098 100644 --- a/backend/api/teams/resources.py +++ b/backend/api/teams/resources.py @@ -1,3 +1,10 @@ +from databases import Database +from distutils.util import strtobool +from fastapi import APIRouter, Depends, Request, Body +from fastapi.responses import JSONResponse +from loguru import logger + +from backend.db import get_db from backend.models.dtos.team_dto import ( NewTeamDTO, UpdateTeamDTO, @@ -8,28 +15,24 @@ from backend.services.users.authentication_service import login_required from backend.services.users.user_service import UserService from backend.models.dtos.user_dto import AuthUserDTO -from distutils.util import strtobool -from fastapi import APIRouter, Depends, Request -from backend.db import get_db, get_session -from starlette.authentication import requires -from loguru import logger -from databases import Database router = APIRouter( prefix="/teams", tags=["teams"], - dependencies=[Depends(get_session)], + dependencies=[Depends(get_db)], responses={404: {"description": "Not found"}}, ) -# class TeamsRestAPI(Resource): -# @token_auth.login_required - @router.patch("/{team_id}/") -@requires("authenticated") -async def patch(request: Request, team_id: int): +async def patch( + request: Request, + user: AuthUserDTO = Depends(login_required), + db: Database = Depends(get_db), + team_id: int = None, + team_dto: UpdateTeamDTO = Body(...), +): """ Updates a team --- @@ -89,34 +92,41 @@ async def patch(request: Request, team_id: int): description: Internal Server Error """ try: - team = TeamService.get_team_by_id(team_id) - team_dto = UpdateTeamDTO(request.json()) + team = await TeamService.get_team_by_id(team_id, db) team_dto.team_id = team_id - team_dto.validate() - authenticated_user_id = request.user.display_name - if not TeamService.is_user_team_manager( - team_id, authenticated_user_id - ) and not OrganisationService.can_user_manage_organisation( - team.organisation_id, authenticated_user_id + if not await TeamService.is_user_team_manager( + team_id, user.id, db + ) and not await OrganisationService.can_user_manage_organisation( + team.organisation_id, user.id, db ): - return { - "Error": "User is not a admin or a manager for the team", - "SubCode": "UserNotTeamManager", - }, 403 + return JSONResponse( + content={ + "Error": "User is not a admin or a manager for the team", + "SubCode": "UserNotTeamManager", + }, + status_code=403, + ) except Exception as e: logger.error(f"error validating request: {str(e)}") - return {"Error": str(e), "SubCode": "InvalidData"}, 400 + return JSONResponse( + content={"Error": str(e), "SubCode": "InvalidData"}, status_code=400 + ) try: - TeamService.update_team(team_dto) - return {"Status": "Updated"}, 200 + await TeamService.update_team(team_dto, db) + return JSONResponse(content={"Status": "Updated"}, status_code=200) except TeamServiceError as e: - return str(e), 402 + return JSONResponse(content={"Error": str(e)}, status_code=402) @router.get("/{team_id}/") -async def retrieve_team(request: Request, team_id: int, db: Database = Depends(get_db)): +async def retrieve_team( + request: Request, + team_id: int, + db: Database = Depends(get_db), + user: AuthUserDTO = Depends(login_required), +): """ Retrieves a Team --- @@ -146,7 +156,7 @@ async def retrieve_team(request: Request, team_id: int, db: Database = Depends(g 500: description: Internal Server Error """ - authenticated_user_id = request.user.display_name + authenticated_user_id = user.id omit_members = strtobool(request.query_params.get("omitMemberList", "false")) if authenticated_user_id is None: user_id = 0 @@ -159,8 +169,12 @@ async def retrieve_team(request: Request, team_id: int, db: Database = Depends(g @router.delete("/{team_id}/") -@requires("authenticated") -async def delete(request: Request, team_id: int): +async def delete( + request: Request, + user: AuthUserDTO = Depends(login_required), + db: Database = Depends(get_db), + team_id: int = None, +): """ Deletes a Team --- @@ -193,13 +207,16 @@ async def delete(request: Request, team_id: int): 500: description: Internal Server Error """ - if not TeamService.is_user_team_manager(team_id, request.user.display_name): - return { - "Error": "User is not a manager for the team", - "SubCode": "UserNotTeamManager", - }, 401 + if not await TeamService.is_user_team_manager(team_id, user.id, db): + return JSONResponse( + content={ + "Error": "User is not a manager for the team", + "SubCode": "UserNotTeamManager", + }, + status_code=403, + ) - return TeamService.delete_team(team_id) + return await TeamService.delete_team(team_id, db) @router.get("/") @@ -306,8 +323,8 @@ async def list_teams( request.query_params.get("fullMemberList", "true") ) search_dto.paginate = strtobool(request.query_params.get("paginate", "false")) - search_dto.page = request.query_params.get("page", 1) - search_dto.per_page = request.query_params.get("perPage", 10) + search_dto.page = int(request.query_params.get("page", 1)) + search_dto.per_page = int(request.query_params.get("perPage", 10)) search_dto.user_id = user.id teams = await TeamService.get_all_teams(search_dto, db) @@ -315,8 +332,12 @@ async def list_teams( @router.post("/") -@requires("authenticated") -async def post(request: Request): +async def post( + request: Request, + user: AuthUserDTO = Depends(login_required), + db: Database = Depends(get_db), + team_dto: NewTeamDTO = Body(...), +): """ Creates a new team --- @@ -368,28 +389,30 @@ async def post(request: Request): 500: description: Internal Server Error """ - user_id = request.user.display_name try: - team_dto = NewTeamDTO(request.json()) - team_dto.creator = user_id - team_dto.validate() + team_dto.creator = user.id except Exception as e: logger.error(f"error validating request: {str(e)}") - return {"Error": str(e), "SubCode": "InvalidData"}, 400 + return JSONResponse( + content={"Error": str(e), "SubCode": "InvalidData"}, status_code=400 + ) try: organisation_id = team_dto.organisation_id - is_org_manager = OrganisationService.is_user_an_org_manager( - organisation_id, user_id + is_org_manager = await OrganisationService.is_user_an_org_manager( + organisation_id, user.id, db ) - is_admin = UserService.is_user_an_admin(user_id) + is_admin = await UserService.is_user_an_admin(user.id, db) if is_admin or is_org_manager: - team_id = TeamService.create_team(team_dto) - return {"teamId": team_id}, 201 + team_id = await TeamService.create_team(team_dto, db) + return JSONResponse(content={"teamId": team_id}, status_code=201) else: error_msg = "User not permitted to create team for the Organisation" - return {"Error": error_msg, "SubCode": "CreateTeamNotPermitted"}, 403 + return JSONResponse( + content={"Error": error_msg, "SubCode": "CreateTeamNotPermitted"}, + status_code=403, + ) except TeamServiceError as e: - return str(e), 400 + return JSONResponse(content={"Error": str(e)}, status_code=400) diff --git a/backend/api/users/resources.py b/backend/api/users/resources.py index aef8002326..91f59ed9a3 100644 --- a/backend/api/users/resources.py +++ b/backend/api/users/resources.py @@ -12,7 +12,6 @@ from backend.models.dtos.user_dto import AuthUserDTO from fastapi import APIRouter, Depends, Request from backend.db import get_session -from starlette.authentication import requires from databases import Database from backend.db import get_db diff --git a/backend/api/users/tasks.py b/backend/api/users/tasks.py index b004168901..86b00b7a72 100644 --- a/backend/api/users/tasks.py +++ b/backend/api/users/tasks.py @@ -1,5 +1,4 @@ from databases import Database -from dateutil.parser import parse as date_parse from fastapi import APIRouter, Depends, Request from fastapi.responses import JSONResponse diff --git a/backend/models/dtos/banner_dto.py b/backend/models/dtos/banner_dto.py index 237c351f9c..5061795bd3 100644 --- a/backend/models/dtos/banner_dto.py +++ b/backend/models/dtos/banner_dto.py @@ -1,4 +1,4 @@ -from pydantic import BaseModel, Field, validator +from pydantic import BaseModel, Field from typing import Optional diff --git a/backend/models/dtos/message_dto.py b/backend/models/dtos/message_dto.py index 1feaa457a2..0fe1ad482f 100644 --- a/backend/models/dtos/message_dto.py +++ b/backend/models/dtos/message_dto.py @@ -7,16 +7,18 @@ class MessageDTO(BaseModel): """DTO used to define a message that will be sent to a user""" - message_id: int = Field(alias="messageId") - subject: str - message: str - from_username: Optional[str] = Field("", alias="fromUsername") - display_picture_url: Optional[str] = Field("", alias="displayPictureUrl") - project_id: Optional[int] = Field(None, alias="projectId") - project_title: Optional[str] = Field(None, alias="projectTitle") - task_id: Optional[int] = Field(None, alias="taskId") - message_type: Optional[str] = Field(alias="messageType") - sent_date: datetime = Field(None, alias="sentDate") + message_id: int = Field(None, serialization_alias="message_id") + subject: str = Field(None, serialization_alias="subject") + message: str = Field(None, serialization_alias="message") + from_username: Optional[str] = Field("", serialization_alias="fromUsername") + display_picture_url: Optional[str] = Field( + "", serialization_alias="displayPictureUrl" + ) + project_id: Optional[int] = Field(None, serialization_alias="projectId") + project_title: Optional[str] = Field(None, serialization_alias="projectTitle") + task_id: Optional[int] = Field(None, serialization_alias="taskId") + message_type: Optional[str] = Field(None, serialization_alias="message_type") + sent_date: datetime = Field(None, serialization_alias="sentDate") read: bool = False class Config: diff --git a/backend/models/dtos/team_dto.py b/backend/models/dtos/team_dto.py index 5f5d21402d..fd79d32baa 100644 --- a/backend/models/dtos/team_dto.py +++ b/backend/models/dtos/team_dto.py @@ -75,9 +75,9 @@ def validate_function(cls, value): class TeamProjectDTO(BaseModel): """Describes a JSON model to create a project team""" - project_name: str - project_id: int - role: str + project_name: str = Field(None) + project_id: int = Field(None) + role: str = Field(None) class ProjectTeamDTO(BaseModel): @@ -102,8 +102,8 @@ class TeamDetailsDTO(BaseModel): visibility: str is_org_admin: bool = Field(False) is_general_admin: bool = Field(False) - members: List[TeamMembersDTO] = Field(default_factory=list) - team_projects: List[TeamProjectDTO] = Field(default_factory=list) + members: List[TeamMembersDTO] = Field([], alias="team_members") + team_projects: List[TeamProjectDTO] = Field([], alias="team_projects") @field_validator("join_method") def validate_join_method(cls, value): @@ -118,13 +118,13 @@ class TeamDTO(BaseModel): """Describes JSON model for a team""" team_id: Optional[int] = Field(None, serialization_alias="teamId") - organisation_id: int = Field(..., serialization_alias="organisationId") - organisation: str - name: str + organisation_id: int = Field(None, serialization_alias="organisation_id") + organisation: str = Field(None, serialization_alias="organisation") + name: str = Field(None, serialization_alias="name") logo: Optional[str] = None description: Optional[str] = None - join_method: str = Field(..., serialization_alias="joinMethod") - visibility: str = Field(..., serialization_alias="visibility") + join_method: str = Field(None, serialization_alias="joinMethod") + visibility: str = Field(None, serialization_alias="visibility") members: Optional[List[TeamMembersDTO]] = None members_count: Optional[int] = Field(None, serialization_alias="membersCount") managers_count: Optional[int] = Field(None, serialization_alias="managersCount") @@ -163,35 +163,46 @@ def __init__(self): class NewTeamDTO(BaseModel): """Describes a JSON model to create a new team""" - creator: float - organisation_id: int - name: str - description: Optional[str] + creator: float = Field(None, alias="creator") + organisation_id: int = Field(..., alias="organisation_id") + name: str = Field(..., alias="name") + description: Optional[str] = Field(None, alias="description") join_method: str = Field( - required=True, - validators=[validate_team_join_method], + ..., alias="joinMethod", ) - visibility: str = Field( - required=True, validators=[validate_team_visibility], serialize_when_none=False - ) + visibility: str = Field(..., serialize_when_none=False) + + @field_validator("join_method") + def validate_join_method(cls, value): + return validate_team_join_method(value) + + @field_validator("visibility") + def validate_visibility(cls, value): + return validate_team_visibility(value) class UpdateTeamDTO(BaseModel): """Describes a JSON model to update a team""" - creator: float - team_id: int - organisation: str - organisation_id: int - name: str - logo: str - description: str - join_method: str = Field(validators=[validate_team_join_method], alias="joinMethod") - visibility: str = Field( - validators=[validate_team_visibility], serialize_when_none=False - ) - # members = ListType(ModelType(TeamMembersDTO), serialize_when_none=False) + creator: float = Field(None, alias="creator") + team_id: int = Field(None, alias="team_id") + organisation: str = Field(None, alias="organisation") + organisation_id: int = Field(None, alias="organisation_id") + name: str = Field(None, alias="name") + logo: str = Field(None, alias="logo") + description: str = Field(None, alias="description") + join_method: str = Field(None, alias="joinMethod") + visibility: str = Field(None, serialize_when_none=False) + members: List[TeamMembersDTO] = Field([], serialize_when_none=False) + + @field_validator("join_method") + def validate_join_method(cls, value): + return validate_team_join_method(value) + + @field_validator("visibility") + def validate_visibility(cls, value): + return validate_team_visibility(value) class TeamSearchDTO(BaseModel): diff --git a/backend/models/postgis/message.py b/backend/models/postgis/message.py index 95f0bc1f99..08072700f5 100644 --- a/backend/models/postgis/message.py +++ b/backend/models/postgis/message.py @@ -112,15 +112,24 @@ def as_dto(self) -> MessageDTO: return dto - def add_message(self): + async def add_message(self, db: Database): """Add message into current transaction - DO NOT COMMIT HERE AS MESSAGES ARE PART OF LARGER TRANSACTIONS""" logger.debug("Adding message to session") session.add(self) - def save(self): + async def save(self, db: Database): """Save""" - session.add(self) - session.commit() + await db.execute( + Message.__table__.insert().values( + subject=self.subject, + message=self.message, + from_user_id=self.from_user_id, + to_user_id=self.to_user_id, + project_id=self.project_id, + task_id=self.task_id, + message_type=self.message_type, + ) + ) @staticmethod def get_all_contributors(project_id: int): diff --git a/backend/models/postgis/organisation.py b/backend/models/postgis/organisation.py index 468dc49a1c..a041a7c759 100644 --- a/backend/models/postgis/organisation.py +++ b/backend/models/postgis/organisation.py @@ -195,13 +195,16 @@ async def can_be_deleted(organisation_id: int, db) -> bool: return projects_count == 0 and teams_count == 0 @staticmethod - def get(organisation_id: int, session): + async def get(organisation_id: int, db: Database): """ Gets specified organisation by id :param organisation_id: organisation ID in scope :return: Organisation if found otherwise None """ - return session.get(Organisation, organisation_id) + organization = await db.fetch_one( + "SELECT * FROM organisations WHERE id = :id", values={"id": organisation_id} + ) + return organization["id"] if organization else None @staticmethod async def get_organisation_by_name(organisation_name: str, db: Database): diff --git a/backend/models/postgis/project.py b/backend/models/postgis/project.py index 39e760191b..846e94f0f6 100644 --- a/backend/models/postgis/project.py +++ b/backend/models/postgis/project.py @@ -106,10 +106,13 @@ class ProjectTeams(Base): ) team = relationship(Team, backref=backref("projects", cascade="all, delete-orphan")) - def create(self): + async def create(self, db: Database): """Creates and saves the current model to the DB""" - session.add(self) - session.commit() + await db.execute( + self.__table__.insert().values( + team_id=self.team_id, project_id=self.project_id, role=self.role + ) + ) def save(self): """Save changes to db""" @@ -1365,19 +1368,24 @@ async def get_project_and_base_dto(project_id: int, db: Database) -> ProjectDTO: author=record.author, active_mappers=active_mappers, task_creation_mode=TaskCreationMode(record.task_creation_mode).name, - mapping_types=[ - MappingTypes(mapping_type).name for mapping_type in record.mapping_types - ] - if record.mapping_types is not None - else [], - mapping_editors=[Editors(editor).name for editor in record.mapping_editors] - if record.mapping_editors - else [], - validation_editors=[ - Editors(editor).name for editor in record.validation_editors - ] - if record.validation_editors - else [], + mapping_types=( + [ + MappingTypes(mapping_type).name + for mapping_type in record.mapping_types + ] + if record.mapping_types is not None + else [] + ), + mapping_editors=( + [Editors(editor).name for editor in record.mapping_editors] + if record.mapping_editors + else [] + ), + validation_editors=( + [Editors(editor).name for editor in record.validation_editors] + if record.validation_editors + else [] + ), percent_mapped=percent_mapped, percent_validated=percent_validated, percent_bad_imagery=percent_bad_imagery, diff --git a/backend/models/postgis/team.py b/backend/models/postgis/team.py index f9d4b4ae8c..726d1cb94a 100644 --- a/backend/models/postgis/team.py +++ b/backend/models/postgis/team.py @@ -1,5 +1,14 @@ from databases import Database -from sqlalchemy import Column, Integer, BigInteger, Boolean, ForeignKey, String +from sqlalchemy import ( + Column, + Integer, + BigInteger, + Boolean, + ForeignKey, + String, + insert, + delete, +) from sqlalchemy.orm import relationship, backref from backend.exceptions import NotFound from backend.models.dtos.team_dto import ( @@ -40,24 +49,55 @@ class TeamMembers(Base): "Team", backref=backref("members", cascade="all, delete-orphan", lazy="joined") ) - def create(self): + async def create(self, db: Database): """Creates and saves the current model to the DB""" - session.add(self) - session.commit() + team_member = await db.execute( + insert(TeamMembers.__table__).values( + team_id=self.team_id, + user_id=self.user_id, + function=self.function, + active=self.active, + join_request_notifications=self.join_request_notifications, + ) + ) + return team_member def delete(self): """Deletes the current model from the DB""" session.delete(self) session.commit() - def update(self): + async def update(self, db: Database): """Updates the current model in the DB""" - session.commit() + await db.execute( + TeamMembers.__table__.update() + .where(TeamMembers.team_id == self.team_id) + .where(TeamMembers.user_id == self.user_id) + .values( + function=self.function, + active=self.active, + join_request_notifications=self.join_request_notifications, + ) + ) @staticmethod - def get(team_id: int, user_id: int): - """Returns a team member by team_id and user_id""" - return TeamMembers.query.filter_by(team_id=team_id, user_id=user_id).first() + async def get(team_id: int, user_id: int, db: Database): + """ + Returns a team member by team_id and user_id + :param team_id: ID of the team + :param user_id: ID of the user + :param db: async database connection + :return: Team member if found, otherwise None + """ + query = """ + SELECT * FROM team_members + WHERE team_id = :team_id AND user_id = :user_id + """ + member = await db.fetch_one( + query, values={"team_id": team_id, "user_id": user_id} + ) + + return member # Returns the team member if found, otherwise None class Team(Base): @@ -81,13 +121,22 @@ class Team(Base): # organisation = relationship(Organisation, backref="teams", lazy="joined") organisation = relationship(Organisation, backref="teams") - def create(self): + async def create(self, db: Database): """Creates and saves the current model to the DB""" - session.add(self) - session.commit() + team = await db.execute( + insert(Team.__table__).values( + organisation_id=self.organisation_id, + name=self.name, + logo=self.logo, + description=self.description, + join_method=self.join_method, + visibility=self.visibility, + ) + ) + return team if team else None @classmethod - def create_from_dto(cls, new_team_dto: NewTeamDTO): + async def create_from_dto(cls, new_team_dto: NewTeamDTO, db: Database): """Creates a new team from a dto""" new_team = cls() @@ -96,8 +145,8 @@ def create_from_dto(cls, new_team_dto: NewTeamDTO): new_team.join_method = TeamJoinMethod[new_team_dto.join_method].value new_team.visibility = TeamVisibility[new_team_dto.visibility].value - org = Organisation.get(new_team_dto.organisation_id) - new_team.organisation = org + org = await Organisation.get(new_team_dto.organisation_id, db) + new_team.organisation_id = org # Create team member with creator as a manager new_member = TeamMembers() @@ -108,17 +157,19 @@ def create_from_dto(cls, new_team_dto: NewTeamDTO): new_team.members.append(new_member) - new_team.create() - return new_team + team = await Team.create(new_team, db) + return team - def update(self, team_dto: TeamDTO): + async def update(self, team_dto: TeamDTO, db: Database): """Updates Team from DTO""" if team_dto.organisation: - self.organisation = Organisation().get_organisation_by_name( - team_dto.organisation + self.organisation = Organisation.get_organisation_by_name( + team_dto.organisation, db ) - for attr, value in team_dto.items(): + # Build the update query for the team attributes + update_fields = {} + for attr, value in team_dto.dict().items(): if attr == "visibility" and value is not None: value = TeamVisibility[team_dto.visibility].value if attr == "join_method" and value is not None: @@ -127,73 +178,123 @@ def update(self, team_dto: TeamDTO): if attr in ("members", "organisation"): continue - try: - is_field_nullable = self.__table__.columns[attr].nullable - if is_field_nullable and value is not None: - setattr(self, attr, value) - elif value is not None: - setattr(self, attr, value) - except KeyError: - continue + if attr in Team.__table__.columns: + update_fields[attr] = value - if team_dto.members != self._get_team_members() and team_dto.members: - for member in self.members: - member_name = User.get_by_id(member.user_id).username - if member_name not in [i["username"] for i in team_dto.members]: - member.delete() + # Update the team in the database + if update_fields: + update_query = ( + "UPDATE teams SET " + + ", ".join([f"{k} = :{k}" for k in update_fields.keys()]) + + " WHERE id = :id" + ) + await db.execute(update_query, {**update_fields, "id": self.id}) + + # Update team members if they have changed + if ( + team_dto.members != await Team._get_team_members(self, db) + and team_dto.members + ): + # Get existing members from the team + existing_members = await db.fetch_all( + "SELECT user_id FROM team_members WHERE team_id = :team_id", + {"team_id": self.id}, + ) + + # Remove members who are not in the new member list + new_member_usernames = [member["username"] for member in team_dto.members] + for member in existing_members: + username = await db.fetch_val( + "SELECT username FROM users WHERE id = :id", + {"id": member["user_id"]}, + ) + if username not in new_member_usernames: + await db.execute( + "DELETE FROM team_members WHERE team_id = :team_id AND user_id = :user_id", + {"team_id": self.id, "user_id": member["user_id"]}, + ) + + # Add or update members from the new member list for member in team_dto.members: - user = User.get_by_username(member["username"]) - if user is None: + user = await db.fetch_one( + "SELECT id FROM users WHERE username = :username", + {"username": member["username"]}, + ) + if not user: raise NotFound( sub_code="USER_NOT_FOUND", username=member["username"] ) - team_member = TeamMembers.get(self.id, user.id) + + # Check if the user is already a member of the team + team_member = await db.fetch_one( + "SELECT * FROM team_members WHERE team_id = :team_id AND user_id = :user_id", + {"team_id": self.id, "user_id": user["id"]}, + ) + if team_member: - team_member.join_request_notifications = member[ - "join_request_notifications" - ] + # Update member's join_request_notifications + await db.execute( + "UPDATE team_members SET join_request_notifications = :join_request_notifications WHERE team_id = :team_id AND user_id = :user_id", + { + "join_request_notifications": member[ + "join_request_notifications" + ], + "team_id": self.id, + "user_id": user["id"], + }, + ) else: - new_team_member = TeamMembers() - new_team_member.team = self - new_team_member.member = user - new_team_member.function = TeamMemberFunctions[ - member["function"] - ].value - - session.commit() + # Add a new member to the team + await db.execute( + "INSERT INTO team_members (team_id, user_id, function, join_request_notifications) VALUES (:team_id, :user_id, :function, :join_request_notifications)", + { + "team_id": self.id, + "user_id": user["id"], + "function": TeamMemberFunctions[member["function"]].value, + "join_request_notifications": member[ + "join_request_notifications" + ], + }, + ) - def delete(self): + async def delete(self, db: Database): """Deletes the current model from the DB""" - session.delete(self) - session.commit() + await db.execute(delete(Team.__table__).where(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 - def get(team_id: int): + async def get(team_id: int, db: Database): """ Gets specified team by id :param team_id: team ID in scope :return: Team if found otherwise None """ - return session.get(Team, team_id) + return db.fetch_one(Team.__table__, Team.id == team_id) - def get_team_by_name(team_name: str): + async def get_team_by_name(team_name: str, db: Database): """ Gets specified team by name :param team_name: team name in scope - :return: Team if found otherwise None + :param db: async database connection + :return: Team if found, otherwise None """ - return Team.query.filter_by(name=team_name).one_or_none() + query = """ + SELECT * FROM teams + WHERE name = :team_name + """ + team = await db.fetch_one(query, values={"team_name": team_name}) + + return team # Returns the team if found, otherwise None - def as_dto(self): + async def as_dto(self, db: Database): """Returns a dto for the team""" team_dto = TeamDTO() team_dto.team_id = self.id team_dto.description = self.description team_dto.join_method = TeamJoinMethod(self.join_method).name - team_dto.members = self._get_team_members() + team_dto.members = self._get_team_members(db) team_dto.name = self.name team_dto.organisation = self.organisation.name team_dto.organisation_id = self.organisation.id @@ -201,7 +302,7 @@ def as_dto(self): team_dto.visibility = TeamVisibility(self.visibility).name return team_dto - async def as_dto_inside_org(self, session): + async def as_dto_inside_org(self, db: Database): """Returns a dto for the team""" team_dto = OrganisationTeamsDTO() @@ -209,7 +310,7 @@ async def as_dto_inside_org(self, session): team_dto.name = self.name team_dto.description = self.description team_dto.join_method = TeamJoinMethod(self.join_method).name - team_dto.members = await self._get_team_members(session) + team_dto.members = self._get_team_members(db) team_dto.visibility = TeamVisibility(self.visibility).name return team_dto @@ -250,18 +351,30 @@ def as_dto_team_project(project) -> TeamProjectDTO: role=TeamRoles(project.role).name, ) - async def _get_team_members(self, session): - """Helper to get JSON serialized members""" - members = [] - for mem in self.members: - members.append( - { - "username": mem.member.username, - "pictureUrl": mem.member.picture_url, - "function": TeamMemberFunctions(mem.function).name, - "active": mem.active, - } - ) + async def _get_team_members(self, db: Database): + """Helper to get JSON serialized members using raw SQL queries""" + + # SQL query to fetch all members of the team, including their username, picture_url, function, and active status + query = """ + SELECT u.username, u.picture_url, tm.function, tm.active + FROM team_members tm + JOIN users u ON tm.user_id = u.id + WHERE tm.team_id = :team_id + """ + + # Execute the query and fetch all team members + rows = await db.fetch_all(query, {"team_id": self.id}) + + # Convert the fetched rows into a list of dictionaries (JSON serialized format) + members = [ + { + "username": row["username"], + "pictureUrl": row["picture_url"], + "function": TeamMemberFunctions(row["function"]).name, + "active": row["active"], + } + for row in rows + ] return members diff --git a/backend/services/messaging/message_service.py b/backend/services/messaging/message_service.py index b38a7e6f1e..2dd8db6923 100644 --- a/backend/services/messaging/message_service.py +++ b/backend/services/messaging/message_service.py @@ -474,13 +474,14 @@ def send_request_to_join_team( ) @staticmethod - def accept_reject_request_to_join_team( + async def accept_reject_request_to_join_team( from_user: int, from_username: str, to_user: int, team_name: str, team_id: int, response: str, + db: Database, ): message = Message() message.message_type = MessageType.REQUEST_TEAM_NOTIFICATION.value @@ -492,11 +493,10 @@ def accept_reject_request_to_join_team( message.message = ( f"{user_link} has {response}ed your request to join the {team_link} team." ) - message.add_message() - message.save() + await Message.save(message, db) @staticmethod - def accept_reject_invitation_request_for_team( + async def accept_reject_invitation_request_for_team( from_user: int, from_username: str, to_user: int, @@ -504,6 +504,7 @@ def accept_reject_invitation_request_for_team( team_name: str, team_id: int, response: str, + db: Database, ): message = Message() message.message_type = MessageType.INVITATION_NOTIFICATION.value @@ -520,17 +521,17 @@ def accept_reject_invitation_request_for_team( sending_member, MessageService.get_team_link(team_name, team_id, True), ) - message.add_message() - message.save() + await Message.save(message, db) @staticmethod - def send_team_join_notification( + async def send_team_join_notification( from_user: int, from_username: str, to_user: int, team_name: str, team_id: int, role: str, + db: Database, ): message = Message() message.message_type = MessageType.INVITATION_NOTIFICATION.value @@ -542,8 +543,7 @@ def send_team_join_notification( message.message = f"You have been added to the team {team_link} as {role} by {user_link}.\ Access the {team_link}'s page to view more info about this team." - message.add_message() - message.save() + await Message.save(message, db) @staticmethod def send_message_after_chat( @@ -631,10 +631,10 @@ def send_message_after_chat( MessageService._push_messages(messages) @staticmethod - def send_favorite_project_activities(user_id: int): + async def send_favorite_project_activities(user_id: int): logger.debug("Sending Favorite Project Activities") favorited_projects = UserService.get_projects_favorited(user_id) - contributed_projects = UserService.get_projects_mapped(user_id) + contributed_projects = await UserService.get_projects_mapped(user_id, db) if contributed_projects is None: contributed_projects = [] @@ -699,7 +699,7 @@ async def resend_email_validation(user_id: int, db: Database): @staticmethod async def _parse_message_for_bulk_mentions( - message: str, project_id: int, task_id: int, db: Database + message: str, project_id: int, task_id: int = None, db: Database = None ) -> List[str]: parser = re.compile(r"((?<=#)\w+|\[.+?\])") parsed = parser.findall(message) @@ -748,7 +748,7 @@ async def _parse_message_for_bulk_mentions( @staticmethod async def _parse_message_for_username( - message: str, project_id: int, task_id: int, db: Database + message: str, project_id: int, task_id: int = None, db: Database = None ) -> List[str]: """Extracts all usernames from a comment looking for format @[user name]""" parser = re.compile(r"((?<=@)\w+|\[.+?\])") diff --git a/backend/services/project_search_service.py b/backend/services/project_search_service.py index c787da078f..beff290af4 100644 --- a/backend/services/project_search_service.py +++ b/backend/services/project_search_service.py @@ -315,7 +315,7 @@ async def _filter_projects(search_dto: ProjectSearchDTO, user, db: Database): if search_dto.mapped_by: mapped_projects = await UserService.get_projects_mapped( - search_dto.mapped_by + search_dto.mapped_by, db ) filters.append("p.id IN :mapped_projects") params["mapped_projects"] = tuple(mapped_projects) diff --git a/backend/services/team_service.py b/backend/services/team_service.py index 765e37854d..cb0a451e03 100644 --- a/backend/services/team_service.py +++ b/backend/services/team_service.py @@ -1,8 +1,8 @@ -# from flask import current_app -from sqlalchemy import and_ +from databases import Database +from fastapi.responses import JSONResponse +from loguru import logger from markdown import markdown -from backend import create_app from backend.exceptions import NotFound from backend.models.dtos.team_dto import ( ListTeamsDTO, @@ -29,41 +29,45 @@ from backend.services.organisation_service import OrganisationService from backend.services.users.user_service import UserService from backend.services.messaging.message_service import MessageService -from backend.db import get_session - -session = get_session() -from sqlalchemy import select -from databases import Database class TeamServiceError(Exception): """Custom Exception to notify callers an error occurred when handling teams""" def __init__(self, message): - if current_app: - current_app.logger.debug(message) + logger.debug(message) class TeamJoinNotAllowed(Exception): """Custom Exception to notify bad user level on joining team""" def __init__(self, message): - if current_app: - current_app.logger.debug(message) + logger.debug(message) class TeamService: @staticmethod - def request_to_join_team(team_id: int, user_id: int): - team = TeamService.get_team_by_id(team_id) + async def get_team_by_id_user(team_id: int, user_id: int, db: Database): + query = """ + SELECT * FROM team_members + WHERE team_id = :team_id AND user_id = :user_id + """ + team_member = await db.fetch_one( + query, values={"team_id": team_id, "user_id": user_id} + ) + return team_member + + @staticmethod + async def request_to_join_team(team_id: int, user_id: int, db: Database): + team = await TeamService.get_team_by_id(team_id, db) # If user has team manager permission add directly to the team without request.E.G. Admins, Org managers - if TeamService.is_user_team_member(team_id, user_id): + if await TeamService.is_user_team_member(team_id, user_id, db): raise TeamServiceError( "The user is already a member of the team or has requested to join." ) - if TeamService.is_user_team_manager(team_id, user_id): - TeamService.add_team_member( - team_id, user_id, TeamMemberFunctions.MEMBER.value, True + if await TeamService.is_user_team_manager(team_id, user_id, db): + await TeamService.add_team_member( + team_id, user_id, TeamMemberFunctions.MEMBER.value, True, db ) return @@ -74,16 +78,16 @@ def request_to_join_team(team_id: int, user_id: int): ) role = TeamMemberFunctions.MEMBER.value - user = UserService.get_user_by_id(user_id) + user = await UserService.get_user_by_id(user_id, db) active = False # Set active=True for team with join method ANY as no approval is required to join this team type. if team.join_method == TeamJoinMethod.ANY.value: active = True - TeamService.add_team_member(team_id, user_id, role, active) + await TeamService.add_team_member(team_id, user_id, role, active, db) # Notify team managers about a join request in BY_REQUEST team. if team.join_method == TeamJoinMethod.BY_REQUEST.value: - team_managers = team.get_team_managers() + team_managers = Team.get_team_managers(db, team) for manager in team_managers: # Only send notifications to team managers who have join request notification enabled. if manager.join_request_notifications: @@ -92,21 +96,29 @@ def request_to_join_team(team_id: int, user_id: int): ) @staticmethod - def add_user_to_team( - team_id: int, requesting_user: int, username: str, role: str = None + async def add_user_to_team( + team_id: int, + requesting_user: int, + username: str, + role: str = None, + db: Database = None, ): - is_manager = TeamService.is_user_team_manager(team_id, requesting_user) + is_manager = await TeamService.is_user_team_manager( + team_id, requesting_user, db + ) if not is_manager: raise TeamServiceError("User is not allowed to add member to the team") - team = TeamService.get_team_by_id(team_id) - from_user = UserService.get_user_by_id(requesting_user) - to_user = UserService.get_user_by_username(username) - member = TeamMembers.get(team_id, to_user.id) + team = await TeamService.get_team_by_id(team_id, db) + from_user = await UserService.get_user_by_id(requesting_user, db) + to_user = await UserService.get_user_by_username(username, db) + member = await TeamMembers.get(team_id, to_user.id, db) if member: member.function = TeamMemberFunctions[role].value member.active = True - member.update() - return {"Success": "User role updated"} + await TeamMembers.update(member, db) + return JSONResponse( + content={"Success": "User role updated"}, status_code=200 + ) else: if role: try: @@ -115,62 +127,68 @@ def add_user_to_team( raise Exception("Invalid TeamMemberFunction") else: role = TeamMemberFunctions.MEMBER.value - TeamService.add_team_member(team_id, to_user.id, role, True) - MessageService.send_team_join_notification( + await TeamService.add_team_member(team_id, to_user.id, role, True, db) + await MessageService.send_team_join_notification( requesting_user, from_user.username, to_user.id, team.name, team_id, TeamMemberFunctions(role).name, + db, ) @staticmethod - def add_team_member(team_id, user_id, function, active=False): + async def add_team_member( + team_id, user_id, function, active=False, db: Database = None + ): team_member = TeamMembers() team_member.team_id = team_id team_member.user_id = user_id team_member.function = function team_member.active = active - team_member.create() + TeamMembers.create(team_member, db) @staticmethod - def send_invite(team_id, from_user_id, username): - to_user = UserService.get_user_by_username(username) - from_user = UserService.get_user_by_id(from_user_id) - team = TeamService.get_team_by_id(team_id) + async def send_invite(team_id, from_user_id, username, db: Database): + to_user = await UserService.get_user_by_username(username, db) + from_user = await UserService.get_user_by_id(from_user_id, db) + team = await TeamService.get_team_by_id(team_id, db) MessageService.send_invite_to_join_team( from_user_id, from_user.username, to_user.id, team.name, team_id ) @staticmethod - def accept_reject_join_request(team_id, from_user_id, username, function, action): - from_user = UserService.get_user_by_id(from_user_id) - to_user_id = UserService.get_user_by_username(username).id - team = TeamService.get_team_by_id(team_id) + async def accept_reject_join_request( + team_id, from_user_id, username, function, action, db: Database + ): + from_user = await UserService.get_user_by_id(from_user_id, db) + user = await UserService.get_user_by_username(username, db) + to_user_id = user.id + team = await TeamService.get_team_by_id(team_id, db) - if not TeamService.is_user_team_member(team_id, to_user_id): + if not await TeamService.is_user_team_member(team_id, to_user_id, db): raise NotFound(sub_code="JOIN_REQUEST_NOT_FOUND", username=username) if action not in ["accept", "reject"]: raise TeamServiceError("Invalid action type") if action == "accept": - TeamService.activate_team_member(team_id, to_user_id) + await TeamService.activate_team_member(team_id, to_user_id, db) elif action == "reject": - TeamService.delete_invite(team_id, to_user_id) + await TeamService.delete_invite(team_id, to_user_id, db) - MessageService.accept_reject_request_to_join_team( - from_user_id, from_user.username, to_user_id, team.name, team_id, action + await MessageService.accept_reject_request_to_join_team( + from_user_id, from_user.username, to_user_id, team.name, team_id, action, db ) @staticmethod - def accept_reject_invitation_request( - team_id, from_user_id, username, function, action + async def accept_reject_invitation_request( + team_id, from_user_id, username, function, action, db: Database ): - from_user = UserService.get_user_by_id(from_user_id) - to_user = UserService.get_user_by_username(username) - team = TeamService.get_team_by_id(team_id) - team_members = team.get_team_managers() + from_user = await UserService.get_user_by_id(from_user_id, db) + to_user = await UserService.get_user_by_username(username, db) + team = await TeamService.get_team_by_id(team_id, db) + team_members = await Team.get_team_managers(team) for member in team_members: MessageService.accept_reject_invitation_request_for_team( @@ -183,43 +201,69 @@ def accept_reject_invitation_request( action, ) if action == "accept": - TeamService.add_team_member( - team_id, from_user_id, TeamMemberFunctions[function.upper()].value + await TeamService.add_team_member( + team_id, from_user_id, TeamMemberFunctions[function.upper()].value, db ) @staticmethod - def leave_team(team_id, username): - user = UserService.get_user_by_username(username) - team_member = TeamMembers.query.filter( - TeamMembers.team_id == team_id, TeamMembers.user_id == user.id - ).one_or_none() + async def leave_team(team_id, username, db: Database = None): + user = await UserService.get_user_by_username(username, db) + team_member = await TeamService.get_team_by_id_user(team_id, user.id, db) + + # Raise an exception if the team member is not found if not team_member: raise NotFound( sub_code="USER_NOT_IN_TEAM", username=username, team_id=team_id ) - team_member.delete() + + # If found, delete the team member + delete_query = """ + DELETE FROM team_members + WHERE team_id = :team_id AND user_id = :user_id + """ + await db.execute(delete_query, values={"team_id": team_id, "user_id": user.id}) @staticmethod - def add_team_project(team_id, project_id, role): + async def add_team_project(team_id, project_id, role, db: Database): team_project = ProjectTeams() team_project.project_id = project_id team_project.team_id = team_id team_project.role = TeamRoles[role].value - team_project.create() + await ProjectTeams.create(team_project, db) @staticmethod - def delete_team_project(team_id, project_id): - project = ( - session.query(ProjectTeams) - .filter( - and_( - ProjectTeams.team_id == team_id, - ProjectTeams.project_id == project_id, - ) + async def delete_team_project(team_id: int, project_id: int, db: Database): + """ + Deletes a project team by team_id and project_id. + :param team_id: ID of the team + :param project_id: ID of the project + :param db: async database connection + """ + # Query to find the project team + query = """ + SELECT * FROM project_teams + WHERE team_id = :team_id AND project_id = :project_id + """ + project_team = await db.fetch_one( + query, values={"team_id": team_id, "project_id": project_id} + ) + + # Check if the project team exists + if not project_team: + raise NotFound( + sub_code="PROJECT_TEAM_NOT_FOUND", + team_id=team_id, + project_id=project_id, ) - .one() + + # If found, delete the project team + delete_query = """ + DELETE FROM project_teams + WHERE team_id = :team_id AND project_id = :project_id + """ + await db.execute( + delete_query, values={"team_id": team_id, "project_id": project_id} ) - project.delete() @staticmethod async def get_all_teams(search_dto: TeamSearchDTO, db: Database) -> TeamsListDTO: @@ -428,7 +472,7 @@ async def get_projects_by_team_id(team_id: int, db: Database): projects = await db.fetch_all(query=projects_query, values={"team_id": team_id}) if not projects: - raise NotFound(sub_code="PROJECTS_NOT_FOUND", team_id=team_id) + projects = [] return projects @@ -460,14 +504,33 @@ async def get_project_teams_as_dto(project_id: int, db: Database) -> TeamsListDT return teams_list_dto @staticmethod - async def change_team_role(team_id: int, project_id: int, role: str, session): - query = select(ProjectTeams).filter( - and_(ProjectTeams.team_id == team_id, ProjectTeams.project_id == project_id) + async def change_team_role(team_id: int, project_id: int, role: str, db: Database): + """ + Change the role of a team in a project. + :param team_id: ID of the team + :param project_id: ID of the project + :param role: New role to assign + :param db: Database instance for executing queries + """ + # Assuming `TeamRoles[role].value` gives the correct integer or string value for the role + new_role_value = TeamRoles[role].value + + # Write the raw SQL query to update the role in the `project_teams` table + query = """ + UPDATE project_teams + SET role = :new_role_value + WHERE team_id = :team_id AND project_id = :project_id + """ + + # Execute the query + await db.execute( + query, + { + "new_role_value": new_role_value, + "team_id": team_id, + "project_id": project_id, + }, ) - result = await session.execute(query) - project = result.scalar_one() - project.role = TeamRoles[role].value - await session.commit() @staticmethod async def get_team_by_id(team_id: int, db: Database): @@ -500,34 +563,34 @@ def get_team_by_name(team_name: str) -> Team: return team @staticmethod - def create_team(new_team_dto: NewTeamDTO) -> int: + async def create_team(new_team_dto: NewTeamDTO, db: Database) -> int: """ Creates a new team using a team dto :param new_team_dto: Team DTO :returns: ID of new Team """ - TeamService.assert_validate_organisation(new_team_dto.organisation_id) + await TeamService.assert_validate_organisation(new_team_dto.organisation_id, db) - team = Team.create_from_dto(new_team_dto) - return team.id + team = await Team.create_from_dto(new_team_dto, db) + return team @staticmethod - def update_team(team_dto: TeamDTO) -> Team: + async def update_team(team_dto: TeamDTO, db: Database) -> Team: """ Updates a team :param team_dto: DTO with updated info :returns updated Team """ - team = TeamService.get_team_by_id(team_dto.team_id) - team.update(team_dto) + team = await TeamService.get_team_by_id(team_dto.team_id, db) + team = await Team.update(team, team_dto, db) - return team + return team["id"] if team else None @staticmethod - def assert_validate_organisation(org_id: int): + async def assert_validate_organisation(org_id: int, db: Database): """Makes sure an organisation exists""" try: - OrganisationService.get_organisation_by_id(org_id) + await OrganisationService.get_organisation_by_id(org_id, db) except NotFound: raise TeamServiceError(f"Organisation {org_id} does not exist") @@ -555,36 +618,75 @@ def assert_validate_members(team_dto: TeamDTO): team_dto.members = members @staticmethod - def _get_team_members(team_id: int): - return TeamMembers.query.filter_by(team_id=team_id).all() + async def _get_team_members(team_id: int, db: Database): + # Asynchronous query to fetch team members by team_id + query = "SELECT * FROM team_members WHERE team_id = :team_id" + return await db.fetch_all(query, values={"team_id": team_id}) @staticmethod - def _get_active_team_members(team_id: int): - return TeamMembers.query.filter_by(team_id=team_id, active=True).all() + async def _get_active_team_members(team_id: int, db: Database): + query = """ + SELECT * FROM team_members + WHERE team_id = :team_id AND active = TRUE + """ + return await db.fetch_all(query, values={"team_id": team_id}) @staticmethod - def activate_team_member(team_id: int, user_id: int): - member = TeamMembers.query.filter( - TeamMembers.team_id == team_id, TeamMembers.user_id == user_id - ).first() - member.active = True - session.add(member) - session.commit() + async def activate_team_member(team_id: int, user_id: int, db: Database): + # Fetch the member by team_id and user_id + member = await TeamService.get_team_by_id_user(team_id, user_id, db) + + if member: + # Update the 'active' status of the member + update_query = """ + UPDATE team_members + SET active = TRUE + WHERE team_id = :team_id AND user_id = :user_id + """ + await db.execute( + update_query, values={"team_id": team_id, "user_id": user_id} + ) + else: + # Handle case where member is not found + raise ValueError( + f"No member found with team_id {team_id} and user_id {user_id}" + ) @staticmethod - def delete_invite(team_id: int, user_id: int): - member = TeamMembers.query.filter( - TeamMembers.team_id == team_id, TeamMembers.user_id == user_id - ).first() - member.delete() + async def delete_invite(team_id: int, user_id: int, db: Database): + # Fetch the member by team_id and user_id to check if it exists + member = await TeamService.get_team_by_id_user(team_id, user_id, db) + + if member: + # Delete the member from the database + delete_query = """ + DELETE FROM team_members + WHERE team_id = :team_id AND user_id = :user_id + """ + await db.execute( + delete_query, values={"team_id": team_id, "user_id": user_id} + ) + else: + # Handle case where member is not found + raise ValueError( + f"No member found with team_id {team_id} and user_id {user_id}" + ) @staticmethod - def is_user_team_member(team_id: int, user_id: int): - query = TeamMembers.query.filter( - TeamMembers.team_id == team_id, - TeamMembers.user_id == user_id, - ).exists() - return session.query(query).scalar() + async def is_user_team_member(team_id: int, user_id: int, db: Database) -> bool: + # Query to check if the user is a member of the team + query = """ + SELECT EXISTS ( + SELECT 1 FROM team_members + WHERE team_id = :team_id AND user_id = :user_id + ) AS is_member + """ + result = await db.fetch_one( + query, values={"team_id": team_id, "user_id": user_id} + ) + + # The result contains the 'is_member' field, which is a boolean + return result["is_member"] @staticmethod async def is_user_an_active_team_member( @@ -616,10 +718,10 @@ async def is_user_an_active_team_member( return result["is_active"] @staticmethod - def is_user_team_manager(team_id: int, user_id: int): + async def is_user_team_manager(team_id: int, user_id: int, db: Database) -> bool: # Admin manages all teams - team = TeamService.get_team_by_id(team_id) - if UserService.is_user_an_admin(user_id): + team = await TeamService.get_team_by_id(team_id, db) + if await UserService.is_user_an_admin(user_id, db): return True managers = team.get_team_managers() @@ -629,7 +731,7 @@ def is_user_team_manager(team_id: int, user_id: int): # Org admin manages teams attached to their org user_managed_orgs = [ - org.id for org in OrganisationService.get_organisations(user_id) + org.id for org in await OrganisationService.get_organisations(user_id, db) ] if team.organisation_id in user_managed_orgs: return True @@ -637,18 +739,21 @@ def is_user_team_manager(team_id: int, user_id: int): return False @staticmethod - def delete_team(team_id: int): + async def delete_team(team_id: int, db: Database): """Deletes a team""" - team = TeamService.get_team_by_id(team_id) + team = await TeamService.get_team_by_id(team_id, db) - if team.can_be_deleted(): - team.delete() - return {"Success": "Team deleted"}, 200 + if Team.can_be_deleted(team): + await Team.delete(team, db) + return JSONResponse(content={"Success": "Team deleted"}, status_code=200) else: - return { - "Error": "Team has projects, cannot be deleted", - "SubCode": "This team has projects associated. Before deleting team, unlink any associated projects.", - }, 400 + return JSONResponse( + content={ + "Error": "Team has projects, cannot be deleted", + "SubCode": "This team has projects associated. Before deleting team, unlink any associated projects.", + }, + status_code=400, + ) @staticmethod async def check_team_membership( @@ -669,35 +774,32 @@ async def check_team_membership( return len(user_membership) > 0 @staticmethod - def send_message_to_all_team_members( - team_id: int, team_name: str, message_dto: MessageDTO + async def send_message_to_all_team_members( + team_id: int, + team_name: str, + message_dto: MessageDTO, + user_id: int, + db: Database, ): - """Sends supplied message to all contributors in a team. Message all team members can take - over a minute to run, so this method is expected to be called on its own thread - """ - app = ( - create_app() - ) # Because message-all run on background thread it needs it's own app context - - with app.app_context(): - team_members = TeamService._get_active_team_members(team_id) - sender = UserService.get_user_by_id(message_dto.from_user_id).username - - message_dto.message = ( - "A message from {}, manager of {} team:

{}".format( - MessageService.get_user_profile_link(sender), - MessageService.get_team_link(team_name, team_id, False), - markdown(message_dto.message, output_format="html"), - ) + team_members = await TeamService._get_active_team_members(team_id, db) + user = await UserService.get_user_by_id(user_id, db) + sender = user.username + + message_dto.message = ( + "A message from {}, manager of {} team:

{}".format( + MessageService.get_user_profile_link(sender), + MessageService.get_team_link(team_name, team_id, False), + markdown(message_dto.message, output_format="html"), ) + ) - messages = [] - for team_member in team_members: - if team_member.user_id != message_dto.from_user_id: - message = Message.from_dto(team_member.user_id, message_dto) - message.message_type = MessageType.TEAM_BROADCAST.value - message.save() - user = UserService.get_user_by_id(team_member.user_id) - messages.append(dict(message=message, user=user)) + messages = [] + for team_member in team_members: + if team_member.user_id != user_id: + message = Message.from_dto(team_member.user_id, message_dto) + message.message_type = MessageType.TEAM_BROADCAST.value + await Message.save(message, db) + user = await UserService.get_user_by_id(team_member.user_id, db) + messages.append(dict(message=message, user=user)) - MessageService._push_messages(messages) + MessageService._push_messages(messages) diff --git a/backend/services/users/authentication_service.py b/backend/services/users/authentication_service.py index 822d7bd48d..99a140f2fa 100644 --- a/backend/services/users/authentication_service.py +++ b/backend/services/users/authentication_service.py @@ -18,7 +18,7 @@ from backend.services.users.user_service import UserService, NotFound from random import SystemRandom from backend.config import settings -from fastapi import Depends, HTTPException, Header, Request, Security +from fastapi import Depends, HTTPException, Request, Security from fastapi.security.api_key import APIKeyHeader from databases import Database diff --git a/backend/services/users/user_service.py b/backend/services/users/user_service.py index 76cb2a64c9..13f8bb6b92 100644 --- a/backend/services/users/user_service.py +++ b/backend/services/users/user_service.py @@ -14,7 +14,6 @@ column, select, union, - alias, ) from databases import Database @@ -57,7 +56,6 @@ settings = Settings() session = get_session() -from databases import Database user_filter_cache = TTLCache(maxsize=1024, ttl=600) @@ -78,7 +76,7 @@ async def get_user_by_id(user_id: int, db: Database) -> User: return user @staticmethod - async def get_user_by_username(username: str, db) -> User: + async def get_user_by_username(username: str, db: Database) -> User: user = await User.get_by_username(username, db) if user is None: @@ -164,8 +162,8 @@ async def get_projects_favorited(user_id: int, db: Database) -> ProjectFavorites return fav_dto @staticmethod - def get_projects_mapped(user_id: int): - user = UserService.get_user_by_id(user_id) + async def get_projects_mapped(user_id: int, db: Database): + user = await UserService.get_user_by_id(user_id, db) projects_mapped = user.projects_mapped # Return empty list if the user has no projects_mapped. @@ -518,7 +516,7 @@ async def get_detailed_stats(username: str, db: Database) -> UserStatsDTO: # res = user_stats.union(others_stats).all() results = {key: value for key, value in res} - projects_mapped = UserService.get_projects_mapped(user.id) + projects_mapped = await UserService.get_projects_mapped(user.id, db) stats_dto.tasks_mapped = results["MAPPED"] stats_dto.tasks_validated = results["VALIDATED"] stats_dto.tasks_invalidated = results["INVALIDATED"]