From dd04469945ffb1547e6e0af1014a30bc704c1ac9 Mon Sep 17 00:00:00 2001 From: prabinoid Date: Tue, 29 Oct 2024 17:20:07 +0545 Subject: [PATCH] Messages, teams, comments, notification modules and project filters and favourites fixes --- backend/api/comments/resources.py | 47 +++--- backend/api/projects/favorites.py | 2 +- backend/api/teams/resources.py | 10 +- backend/api/users/resources.py | 4 +- backend/models/dtos/team_dto.py | 25 +-- backend/models/postgis/message.py | 43 ++--- backend/models/postgis/team.py | 150 +++++++++--------- backend/services/mapping_service.py | 32 ++-- backend/services/messaging/message_service.py | 98 +++++++----- backend/services/messaging/smtp_service.py | 4 +- backend/services/project_search_service.py | 20 +-- backend/services/team_service.py | 22 +-- 12 files changed, 261 insertions(+), 196 deletions(-) diff --git a/backend/api/comments/resources.py b/backend/api/comments/resources.py index df5c26075c..642a548412 100644 --- a/backend/api/comments/resources.py +++ b/backend/api/comments/resources.py @@ -1,12 +1,8 @@ -# from flask_restful import , request, current_app -# from schematics.exceptions import Exception - from datetime import datetime from databases import Database from fastapi import APIRouter, Depends, Request from loguru import logger -from starlette.authentication import requires from fastapi.responses import JSONResponse from backend.db import get_db, get_session @@ -16,7 +12,7 @@ from backend.services.mapping_service import MappingService, MappingServiceError from backend.services.messaging.chat_service import ChatService from backend.services.project_service import ProjectService -from backend.services.users.authentication_service import login_required, tm +from backend.services.users.authentication_service import login_required from backend.services.users.user_service import UserService session = get_session() @@ -204,9 +200,15 @@ async def delete( @router.post("/{project_id}/comments/tasks/{task_id}/") -@requires("authenticated") -@tm.pm_only(False) -def post(request: Request, project_id: int, task_id: int): +# TODO Decorator +# @tm.pm_only(False) +async def post( + request: Request, + project_id: int, + task_id: int, + user: AuthUserDTO = Depends(login_required), + db: Database = Depends(get_db), +): """ Adds a comment to the task outside of mapping/validation --- @@ -260,24 +262,29 @@ def post(request: Request, project_id: int, task_id: int): description: Internal Server Error """ authenticated_user_id = request.user.display_name - if UserService.is_user_blocked(authenticated_user_id): - return {"Error": "User is on read only mode", "SubCode": "ReadOnly"}, 403 + if await UserService.is_user_blocked(authenticated_user_id, db): + return JSONResponse( + content={"Error": "User is on read only mode", "SubCode": "ReadOnly"}, + status_code=403, + ) try: - task_comment = TaskCommentDTO(request.json()) - task_comment.user_id = request.user.display_name - task_comment.task_id = task_id - task_comment.project_id = project_id - task_comment.validate() + request_json = await request.json() + comment = request_json.get("comment") + task_comment = TaskCommentDTO( + user_id=user.id, task_id=task_id, project_id=project_id, comment=comment + ) except Exception as e: logger.error(f"Error validating request: {str(e)}") - return {"Error": "Unable to add comment", "SubCode": "InvalidData"}, 400 - + return JSONResponse( + content={"Error": "Unable to add comment", "SubCode": "InvalidData"}, + status_code=400, + ) try: - task = MappingService.add_task_comment(task_comment) - return task.model_dump(by_alias=True), 201 + task = await MappingService.add_task_comment(task_comment, db) + return task except MappingServiceError: - return {"Error": "Task update failed"}, 403 + return JSONResponse(content={"Error": "Task update failed"}, status_code=403) @router.get("/{project_id}/comments/tasks/{task_id}/") diff --git a/backend/api/projects/favorites.py b/backend/api/projects/favorites.py index 2521585b19..45ee25de9b 100644 --- a/backend/api/projects/favorites.py +++ b/backend/api/projects/favorites.py @@ -55,7 +55,7 @@ async def get( favorited = await ProjectService.is_favorited(project_id, user_id, db) if favorited is True: return JSONResponse(content={"favorited": True}, status_code=200) - return JSONResponse({"favorited": False}, status_code=200) + return JSONResponse(content={"favorited": False}, status_code=200) @router.post("/{project_id}/favorite/") diff --git a/backend/api/teams/resources.py b/backend/api/teams/resources.py index 7e741eb098..33760a6223 100644 --- a/backend/api/teams/resources.py +++ b/backend/api/teams/resources.py @@ -1,3 +1,4 @@ +from backend.models.postgis.team import Team from databases import Database from distutils.util import strtobool from fastapi import APIRouter, Depends, Request, Body @@ -94,7 +95,7 @@ async def patch( try: team = await TeamService.get_team_by_id(team_id, db) team_dto.team_id = team_id - + data = await request.json() if not await TeamService.is_user_team_manager( team_id, user.id, db ) and not await OrganisationService.can_user_manage_organisation( @@ -112,9 +113,12 @@ async def patch( return JSONResponse( content={"Error": str(e), "SubCode": "InvalidData"}, status_code=400 ) - try: - await TeamService.update_team(team_dto, db) + if ("joinMethod" or "organisations_id") not in data.keys(): + print("inside......") + await Team.update_team_members(team, team_dto, db) + else: + await TeamService.update_team(team_dto, db) return JSONResponse(content={"Status": "Updated"}, status_code=200) except TeamServiceError as e: return JSONResponse(content={"Error": str(e)}, status_code=402) diff --git a/backend/api/users/resources.py b/backend/api/users/resources.py index c30582a91f..7bfc2344e2 100644 --- a/backend/api/users/resources.py +++ b/backend/api/users/resources.py @@ -255,7 +255,9 @@ async def get( page = ( int(request.query_params.get("page")) if request.query_params.get("page") else 1 ) - project_id = int(request.query_params.get("projectId", None)) + project_id = request.query_params.get("projectId", None) + if project_id: + project_id = int(project_id) users_dto = await UserService.filter_users(username, project_id, page, db) return users_dto diff --git a/backend/models/dtos/team_dto.py b/backend/models/dtos/team_dto.py index 900f1dc1c5..3fd53c4964 100644 --- a/backend/models/dtos/team_dto.py +++ b/backend/models/dtos/team_dto.py @@ -73,6 +73,9 @@ class TeamMembersDTO(BaseModel): def validate_function(cls, value): return validate_team_member_function(value) + class Config: + populate_by_name = True + class TeamProjectDTO(BaseModel): """Describes a JSON model to create a project team""" @@ -108,7 +111,7 @@ class TeamDetailsDTO(BaseModel): visibility: str is_org_admin: bool = Field(False) is_general_admin: bool = Field(False) - members: List[TeamMembersDTO] = Field([], alias="team_members") + members: List[TeamMembersDTO] = Field([], alias="members") team_projects: List[TeamProjectDTO] = Field([], alias="team_projects") @field_validator("join_method") @@ -200,16 +203,16 @@ class Config: class UpdateTeamDTO(BaseModel): """Describes a JSON model to update a team""" - 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) + creator: Optional[int] = Field(None, alias="creator") + team_id: Optional[int] = Field(None, alias="team_id") + organisation: Optional[str] = Field(None, alias="organisation") + organisation_id: Optional[int] = Field(None, alias="organisation_id") + name: Optional[str] = Field(None, alias="name") + logo: Optional[str] = Field(None, alias="logo") + description: Optional[str] = Field(None, alias="description") + join_method: Optional[str] = Field(None, alias="joinMethod") + visibility: Optional[str] = Field(None, serialize_when_none=False) + members: Optional[List[TeamMembersDTO]] = Field([], serialize_when_none=False) @field_validator("join_method") def validate_join_method(cls, value): diff --git a/backend/models/postgis/message.py b/backend/models/postgis/message.py index 941890acac..18ec88aa7e 100644 --- a/backend/models/postgis/message.py +++ b/backend/models/postgis/message.py @@ -132,20 +132,27 @@ async def save(self, db: Database): ) @staticmethod - def get_all_contributors(project_id: int): - """Get all contributors to a project""" - - contributors = ( - session.query(Task.mapped_by) - .filter(Task.project_id == project_id) - .filter(Task.mapped_by.isnot(None)) - .union( - session.query(Task.validated_by) - .filter(Task.project_id == project_id) - .filter(Task.validated_by.isnot(None)) - ) - .distinct() - ).all() + async def get_all_contributors(project_id: int, db: Database): + """Get all contributors to a project using async raw SQL""" + + query = """ + SELECT DISTINCT contributor + FROM ( + SELECT mapped_by AS contributor + FROM tasks + WHERE project_id = :project_id + AND mapped_by IS NOT NULL + UNION + SELECT validated_by AS contributor + FROM tasks + WHERE project_id = :project_id + AND validated_by IS NOT NULL + ) AS contributors + """ + + rows = await db.fetch_all(query=query, values={"project_id": project_id}) + + contributors = [row["contributor"] for row in rows] return contributors @staticmethod @@ -222,14 +229,12 @@ async def delete_all_messages( DELETE FROM messages WHERE to_user_id = :user_id """ + params = {"user_id": user_id} if message_type_filters: delete_query += " AND message_type = ANY(:message_type_filters)" - - await db.execute( - delete_query, - {"user_id": user_id, "message_type_filters": message_type_filters}, - ) + params["message_type_filters"] = message_type_filters + await db.execute(delete_query, params) def delete(self): """Deletes the current model from the DB""" diff --git a/backend/models/postgis/team.py b/backend/models/postgis/team.py index b357e66465..40c9c00a6a 100644 --- a/backend/models/postgis/team.py +++ b/backend/models/postgis/team.py @@ -58,7 +58,7 @@ async def create(self, db: Database): user_id=self.user_id, function=self.function, active=self.active, - join_request_notifications=self.join_request_notifications, + join_request_notifications=False, ) ) return team_member @@ -160,10 +160,10 @@ async def create_from_dto(cls, new_team_dto: NewTeamDTO, db: Database): team = await Team.create(new_team, db) return team - async def update(self, team_dto: TeamDTO, db: Database): + async def update(team, team_dto: TeamDTO, db: Database): """Updates Team from DTO""" if team_dto.organisation: - self.organisation = Organisation.get_organisation_by_name( + team.organisation = Organisation.get_organisation_by_name( team_dto.organisation, db ) @@ -188,74 +188,11 @@ async def update(self, team_dto: TeamDTO, db: Database): + ", ".join([f"{k} = :{k}" for k in update_fields.keys()]) + " WHERE id = :id" ) - await db.execute(update_query, {**update_fields, "id": self.id}) + await db.execute(update_query, {**update_fields, "id": team.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 = 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"] - ) - - # 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: - # 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: - # 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" - ], - }, - ) + if team_dto.members: + await Team.update_team_members(team, team_dto, db) async def delete(self, db: Database): """Deletes the current model from the DB""" @@ -317,7 +254,9 @@ async def as_dto_inside_org(self, db: Database): return team_dto - async def as_dto_team_member(user_id: int, db: Database) -> TeamMembersDTO: + async def as_dto_team_member( + user_id: int, team_id: int, db: Database + ) -> TeamMembersDTO: """Returns a DTO for the team member""" user_query = """ SELECT username, picture_url FROM users WHERE id = :user_id @@ -326,13 +265,13 @@ async def as_dto_team_member(user_id: int, db: Database) -> TeamMembersDTO: if not user: raise NotFound(sub_code="USER_NOT_FOUND", user_id=user_id) - member_query = """ SELECT function, active, join_request_notifications - FROM team_members WHERE user_id = :user_id + FROM team_members WHERE user_id = :user_id AND team_id = :team_id """ - member = await db.fetch_one(query=member_query, values={"user_id": user_id}) - + member = await db.fetch_one( + query=member_query, values={"user_id": user_id, "team_id": team_id} + ) if not member: raise NotFound(sub_code="MEMBER_NOT_FOUND", user_id=user_id) @@ -509,3 +448,66 @@ async def get_members_count_by_role( values = {"team_id": team_id, "function": role.value} return await db.fetch_val(query=query, values=values) + + @staticmethod + async def update_team_members(team, team_dto: TeamDTO, db: Database): + # 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": team.id}, + ) + existing_members_list = list( + set([existing_member.user_id for existing_member in existing_members]) + ) + + new_member_usernames = list( + set([member.username for member in team_dto.members]) + ) + new_members_records = await db.fetch_all( + "SELECT id FROM users WHERE username = ANY(:new_member_usernames)", + {"new_member_usernames": new_member_usernames}, + ) + new_member_list = list( + set([new_member.id for new_member in new_members_records]) + ) + if existing_members_list != new_member_list: + for member in existing_members_list: + if member.user_id not in new_member_list: + await db.execute( + "DELETE FROM team_members WHERE team_id = :team_id AND user_id = :user_id", + {"team_id": team.id, "user_id": member.user_id}, + ) + + # Add or update members from the new member list + for member in team_dto.members: + 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) + # 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": team.id, "user_id": user["id"]}, + ) + + if team_member: + 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": team.id, + "user_id": user["id"], + }, + ) + else: + 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": team.id, + "user_id": user["id"], + "function": TeamMemberFunctions[member["function"]].value, + "join_request_notifications": member.join_request_notifications, + }, + ) diff --git a/backend/services/mapping_service.py b/backend/services/mapping_service.py index 7131a090d0..7b49078e42 100644 --- a/backend/services/mapping_service.py +++ b/backend/services/mapping_service.py @@ -165,7 +165,6 @@ async def unlock_task_after_mapping( ) if mapped_task.comment: - # TODO Verify this messaging functionality. await MessageService.send_message_after_comment( mapped_task.user_id, mapped_task.comment, @@ -251,12 +250,12 @@ async def get_task_locked_by_user( return task @staticmethod - async def add_task_comment(task_comment: TaskCommentDTO) -> TaskDTO: + async def add_task_comment(task_comment: TaskCommentDTO, db: Database) -> TaskDTO: """Adds the comment to the task history""" # Check if project exists - await ProjectService.exists(task_comment.project_id) + await ProjectService.exists(task_comment.project_id, db) - task = Task.get(task_comment.task_id, task_comment.project_id) + task = await Task.get(task_comment.task_id, task_comment.project_id, db) if task is None: raise NotFound( sub_code="TASK_NOT_FOUND", @@ -264,15 +263,28 @@ async def add_task_comment(task_comment: TaskCommentDTO) -> TaskDTO: task_id=task_comment.task_id, ) - task.set_task_history( - TaskAction.COMMENT, task_comment.user_id, task_comment.comment + await Task.set_task_history( + task_id=task_comment.task_id, + project_id=task_comment.project_id, + user_id=task_comment.user_id, + action=TaskAction.COMMENT, + db=db, + comment=task_comment.comment, ) # Parse comment to see if any users have been @'d - MessageService.send_message_after_comment( - task_comment.user_id, task_comment.comment, task.id, task_comment.project_id + await MessageService.send_message_after_comment( + task_comment.user_id, + task_comment.comment, + task.id, + task_comment.project_id, + db, + ) + return await Task.as_dto_with_instructions( + task_comment.task_id, + task_comment.project_id, + db, + task_comment.preferred_locale, ) - task.update() - return task.as_dto_with_instructions(task_comment.preferred_locale) @staticmethod async def generate_gpx( diff --git a/backend/services/messaging/message_service.py b/backend/services/messaging/message_service.py index adbea7aedb..6d22a824ff 100644 --- a/backend/services/messaging/message_service.py +++ b/backend/services/messaging/message_service.py @@ -4,6 +4,7 @@ import bleach from cachetools import TTLCache, cached +from backend.models.postgis.utils import timestamp from loguru import logger from typing import List @@ -131,20 +132,22 @@ async def send_message_after_validation( await MessageService._push_messages(messages, db) @staticmethod - def send_message_to_all_contributors(project_id: int, message_dto: MessageDTO): + async def send_message_to_all_contributors( + project_id: int, message_dto: MessageDTO, db: Database + ): """Sends supplied message to all contributors on specified project. Message all contributors can take over a minute to run, so this method is expected to be called on its own thread """ - + # TODO: Background task. app = ( create_app() ) # Because message-all run on background thread it needs it's own app context with app.app_context(): - contributors = Message.get_all_contributors(project_id) - project = Project.get(project_id) - project_name = ProjectInfo.get_dto_for_locale( - project_id, project.default_locale + contributors = await Message.get_all_contributors(project_id, db) + project = await Project.get(project_id, db) + project_name = await ProjectInfo.get_dto_for_locale( + db, project_id, project.default_locale ).name message_dto.message = "A message from {} managers:

{}".format( MessageService.get_project_link( @@ -155,15 +158,15 @@ def send_message_to_all_contributors(project_id: int, message_dto: MessageDTO): messages = [] for contributor in contributors: - message = Message.from_dto(contributor[0], message_dto) + message = Message.from_dto(contributor.id, message_dto) message.message_type = MessageType.BROADCAST.value message.project_id = project_id - user = UserService.get_user_by_id(contributor[0]) + user = await UserService.get_user_by_id(contributor.id, db) messages.append( dict(message=message, user=user, project_name=project_name) ) - MessageService._push_messages(messages) + await MessageService._push_messages(messages, db) @staticmethod async def _push_messages(messages: list, db: Database): @@ -269,7 +272,6 @@ async def send_message_after_comment( comment_from: int, comment: str, task_id: int, project_id: int, db: Database ): """Will send a canned message to anyone @'d in a comment""" - # Fetch the user who made the comment comment_from_user = await UserService.get_user_by_id(comment_from, db) @@ -334,16 +336,26 @@ async def send_message_after_comment( except NotFound: continue - message = { - "message_type": MessageType.MENTION_NOTIFICATION.value, - "project_id": project_id, - "task_id": task_id, - "from_user_id": comment_from, - "to_user_id": user["id"], - "subject": f"You were mentioned in a comment in {task_link} of Project {project_link}", - "message": clean_comment, - } - messages.append(message) + # message = { + # "message_type": MessageType.MENTION_NOTIFICATION.value, + # "project_id": project_id, + # "task_id": task_id, + # "from_user_id": comment_from, + # "to_user_id": user["id"], + # "subject": f"You were mentioned in a comment in {task_link} of Project {project_link}", + # "message": clean_comment, + # } + + message = Message() + message.message_type = MessageType.MENTION_NOTIFICATION.value + message.project_id = project_id + message.task_id = task_id + message.from_user_id = comment_from + message.to_user_id = user["id"] + message.subject = f"You were mentioned in a comment in {task_link} of Project {project_link}" + message.message = clean_comment + message.date = timestamp() + messages.append(dict(message=message, user=user)) await MessageService._push_messages(messages, db) @@ -368,8 +380,7 @@ async def send_message_after_comment( if contributed_users: user_from = await UserService.get_user_by_id(comment_from, db) - user_link = MessageService.get_user_link(user_from["username"]) - + user_link = MessageService.get_user_link(user_from.username) task_link = MessageService.get_task_link(project_id, task_id) project_link = MessageService.get_project_link(project_id, project_name) @@ -377,22 +388,33 @@ async def send_message_after_comment( for user_id in contributed_users: try: user = await UserService.get_user_by_id(user_id, db) - if user["username"] in usernames: + if user.username in usernames: break except NotFound: continue - message = { - "message_type": MessageType.TASK_COMMENT_NOTIFICATION.value, - "project_id": project_id, - "from_user_id": comment_from, - "task_id": task_id, - "to_user_id": user["id"], - "subject": f"{user_link} left a comment in {task_link} of Project {project_link}", - "message": comment, - } - messages.append(message) - + # message = { + # "message_type": MessageType.TASK_COMMENT_NOTIFICATION.value, + # "project_id": project_id, + # "from_user_id": comment_from, + # "task_id": task_id, + # "to_user_id": user["id"], + # "subject": f"{user_link} left a comment in {task_link} of Project {project_link}", + # "message": comment, + # } + + message = Message() + message.message_type = MessageType.TASK_COMMENT_NOTIFICATION.value + message.project_id = project_id + message.task_id = task_id + message.from_user_id = comment_from + message.to_user_id = user.id + message.subject = f"{user_link} left a comment in {task_link} of Project {project_link}" + message.message = comment + message.date = timestamp() + messages.append( + dict(message=message, user=user, project_name=project_name) + ) await MessageService._push_messages(messages, db) @staticmethod @@ -456,7 +478,12 @@ def get_team_link(team_name: str, team_id: int, management: bool): @staticmethod def send_request_to_join_team( - from_user: int, from_username: str, to_user: int, team_name: str, team_id: int + from_user: int, + from_username: str, + to_user: int, + team_name: str, + team_id: int, + db: Database, ): message = Message() message.message_type = MessageType.REQUEST_TEAM_NOTIFICATION.value @@ -856,7 +883,6 @@ async def get_all_messages( messages_dto = MessagesDTO() for msg in messages: message_dict = dict(msg) - print(message_dict) if message_dict["message_type"]: message_dict["message_type"] = MessageType( message_dict["message_type"] diff --git a/backend/services/messaging/smtp_service.py b/backend/services/messaging/smtp_service.py index bc365be902..c703491694 100644 --- a/backend/services/messaging/smtp_service.py +++ b/backend/services/messaging/smtp_service.py @@ -178,7 +178,7 @@ def send_email_alert( return True @staticmethod - async def _send_message( + def _send_message( to_address: str, subject: str, html_message: str, text_message: str = None ): """Helper sends SMTP message""" @@ -196,7 +196,7 @@ async def _send_message( logger.debug(msg.as_string()) else: try: - await mail.send_message(msg) + mail.send_message(msg) logger.debug(f"Email sent {to_address}") except Exception as e: # ERROR level logs are automatically captured by sentry so that admins are notified diff --git a/backend/services/project_search_service.py b/backend/services/project_search_service.py index 9655aa6c7b..153fcacba6 100644 --- a/backend/services/project_search_service.py +++ b/backend/services/project_search_service.py @@ -422,11 +422,11 @@ async def _filter_projects(search_dto: ProjectSearchDTO, user, db: Database): if search_dto.managed_by and user.role != UserRole.ADMIN.value: # Fetch project IDs for user's organisations org_projects_query = """ - SELECT p.id + SELECT p.id AS id FROM projects p JOIN organisations o ON o.id = p.organisation_id - JOIN user_organisations uo ON uo.organisation_id = o.id - WHERE uo.user_id = :user_id + JOIN organisation_managers om ON om.organisation_id = o.id + WHERE om.user_id = :user_id """ orgs_projects_ids = await db.fetch_all( org_projects_query, {"user_id": user.id} @@ -434,12 +434,12 @@ async def _filter_projects(search_dto: ProjectSearchDTO, user, db: Database): # Fetch project IDs for user's teams team_projects_query = """ - SELECT p.id - FROM projects p - JOIN teams t ON t.id = p.team_id - JOIN user_teams ut ON ut.team_id = t.id - WHERE ut.user_id = :user_id - AND ut.role = :project_manager_role + SELECT pt.project_id AS id + FROM project_teams pt + JOIN team_members tm ON tm.team_id = pt.team_id + WHERE tm.user_id = :user_id + AND pt.role = :project_manager_role + AND tm.active = TRUE """ team_project_ids = await db.fetch_all( team_projects_query, @@ -457,7 +457,7 @@ async def _filter_projects(search_dto: ProjectSearchDTO, user, db: Database): ) ) if project_ids: - filters.append("p.id IN :managed_projects") + filters.append("p.id = ANY(:managed_projects)") params["managed_projects"] = project_ids order_by_clause = "" diff --git a/backend/services/team_service.py b/backend/services/team_service.py index cb0a451e03..606d9ae927 100644 --- a/backend/services/team_service.py +++ b/backend/services/team_service.py @@ -87,12 +87,12 @@ async def request_to_join_team(team_id: int, user_id: int, db: Database): # 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(db, team) + team_managers = await Team.get_team_managers(db, team.id) for manager in team_managers: # Only send notifications to team managers who have join request notification enabled. if manager.join_request_notifications: MessageService.send_request_to_join_team( - user.id, user.username, manager.user_id, team.name, team_id + user.id, user.username, manager.user_id, team.name, team_id, db ) @staticmethod @@ -119,6 +119,7 @@ async def add_user_to_team( return JSONResponse( content={"Success": "User role updated"}, status_code=200 ) + else: if role: try: @@ -147,7 +148,7 @@ async def add_team_member( team_member.user_id = user_id team_member.function = function team_member.active = active - TeamMembers.create(team_member, db) + await TeamMembers.create(team_member, db) @staticmethod async def send_invite(team_id, from_user_id, username, db: Database): @@ -188,7 +189,7 @@ async def accept_reject_invitation_request( 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) + team_members = await Team.get_team_managers(db, team.id) for member in team_members: MessageService.accept_reject_invitation_request_for_team( @@ -444,9 +445,11 @@ async def get_team_as_dto( SELECT user_id FROM team_members WHERE team_id = :team_id """ members = await db.fetch_all(query=members_query, values={"team_id": team_id}) - team_dto.members = ( - [await Team.as_dto_team_member(member.user_id, db) for member in members] + [ + await Team.as_dto_team_member(member.user_id, team_id, db) + for member in members + ] if members else [] ) @@ -724,9 +727,10 @@ async def is_user_team_manager(team_id: int, user_id: int, db: Database) -> bool if await UserService.is_user_an_admin(user_id, db): return True - managers = team.get_team_managers() + managers = await Team.get_team_managers(db, team.id) for member in managers: - if member.user_id == user_id: + team_manager = await UserService.get_user_by_username(member.username, db) + if team_manager.id == user_id: return True # Org admin manages teams attached to their org @@ -802,4 +806,4 @@ async def send_message_to_all_team_members( user = await UserService.get_user_by_id(team_member.user_id, db) messages.append(dict(message=message, user=user)) - MessageService._push_messages(messages) + await MessageService._push_messages(messages)