From 4fee4a7f4ba86e68690a441b83e341b6a22b1178 Mon Sep 17 00:00:00 2001 From: prabinoid Date: Wed, 23 Oct 2024 13:45:56 +0545 Subject: [PATCH] All messages, organisation retrieve and task activities fixes --- backend/api/organisations/resources.py | 1 - backend/models/dtos/message_dto.py | 13 +- backend/models/postgis/organisation.py | 160 ++++++++++-------- backend/models/postgis/task.py | 71 +++++--- backend/services/messaging/message_service.py | 5 +- backend/services/organisation_service.py | 26 +-- backend/services/project_search_service.py | 30 ++-- 7 files changed, 176 insertions(+), 130 deletions(-) diff --git a/backend/api/organisations/resources.py b/backend/api/organisations/resources.py index 90dfdef314..cfac010ea1 100644 --- a/backend/api/organisations/resources.py +++ b/backend/api/organisations/resources.py @@ -78,7 +78,6 @@ async def retrieve_organisation( user_id = 0 else: user_id = authenticated_user_id - # Validate abbreviated. organisation_dto = await OrganisationService.get_organisation_by_id_as_dto( organisation_id, user_id, omit_managers, db ) diff --git a/backend/models/dtos/message_dto.py b/backend/models/dtos/message_dto.py index 2b594c8cbc..531cce7064 100644 --- a/backend/models/dtos/message_dto.py +++ b/backend/models/dtos/message_dto.py @@ -7,17 +7,18 @@ class MessageDTO(BaseModel): """DTO used to define a message that will be sent to a user""" - message_id: int = Field(None, alias="message_id") - subject: str = Field(None, alias="subject") - message: str = Field(None, alias="message") + message_id: Optional[int] = Field(None, alias="messageId") + subject: str = Field(min_length=1, alias="subject") + message: str = Field(min_length=1, alias="message") + from_user_id: int = Field(alias="fromUserId") 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(None, alias="message_type") - sent_date: datetime = Field(None, alias="sentDate") - read: bool = False + message_type: Optional[str] = Field(None, alias="messageType") + sent_date: Optional[datetime] = Field(None, alias="sentDate") + read: Optional[bool] = None class Config: populate_by_name = True diff --git a/backend/models/postgis/organisation.py b/backend/models/postgis/organisation.py index a041a7c759..b091f19435 100644 --- a/backend/models/postgis/organisation.py +++ b/backend/models/postgis/organisation.py @@ -92,80 +92,78 @@ async def create_from_dto(new_organisation_dto: NewOrganisationDTO, db: Database } try: - async with db.transaction(): - organisation_id = await db.execute(query, values) + organisation_id = await db.execute(query, values) - for manager in new_organisation_dto.managers: - user_query = "SELECT id FROM users WHERE username = :username" - user = await db.fetch_one(user_query, {"username": manager}) + for manager in new_organisation_dto.managers: + user_query = "SELECT id FROM users WHERE username = :username" + user = await db.fetch_one(user_query, {"username": manager}) - if not user: - raise NotFound(sub_code="USER_NOT_FOUND", username=manager) + if not user: + raise NotFound(sub_code="USER_NOT_FOUND", username=manager) - manager_query = """ - INSERT INTO organisation_managers (organisation_id, user_id) - VALUES (:organisation_id, :user_id) - """ - await db.execute( - manager_query, - {"organisation_id": organisation_id, "user_id": user.id}, - ) + manager_query = """ + INSERT INTO organisation_managers (organisation_id, user_id) + VALUES (:organisation_id, :user_id) + """ + await db.execute( + manager_query, + {"organisation_id": organisation_id, "user_id": user.id}, + ) - return organisation_id + return organisation_id except Exception as e: raise HTTPException(status_code=500, detail=str(e)) from e async def update(organisation_dto: UpdateOrganisationDTO, db: Database): """Updates Organisation from DTO""" - async with db.transaction(): - try: - org_id = organisation_dto.organisation_id - org_dict = organisation_dto.dict(exclude_unset=True) - if "type" in org_dict and org_dict["type"] is not None: - org_dict["type"] = OrganisationType[org_dict["type"].upper()].value - - update_keys = { - key: org_dict[key] - for key in org_dict.keys() - if key not in ["organisation_id", "managers"] - } - set_clause = ", ".join(f"{key} = :{key}" for key in update_keys.keys()) - update_query = f""" - UPDATE organisations - SET {set_clause} - WHERE id = :id + try: + org_id = organisation_dto.organisation_id + org_dict = organisation_dto.dict(exclude_unset=True) + if "type" in org_dict and org_dict["type"] is not None: + org_dict["type"] = OrganisationType[org_dict["type"].upper()].value + + update_keys = { + key: org_dict[key] + for key in org_dict.keys() + if key not in ["organisation_id", "managers"] + } + set_clause = ", ".join(f"{key} = :{key}" for key in update_keys.keys()) + update_query = f""" + UPDATE organisations + SET {set_clause} + WHERE id = :id + """ + await db.execute(update_query, values={**update_keys, "id": org_id}) + + if organisation_dto.managers: + clear_managers_query = """ + DELETE FROM organisation_managers + WHERE organisation_id = :id """ - await db.execute(update_query, values={**update_keys, "id": org_id}) + await db.execute(clear_managers_query, values={"id": org_id}) - if organisation_dto.managers: - clear_managers_query = """ - DELETE FROM organisation_managers - WHERE organisation_id = :id - """ - await db.execute(clear_managers_query, values={"id": org_id}) + for manager_username in organisation_dto.managers: + user_query = "SELECT id FROM users WHERE username = :username" + user = await db.fetch_one( + user_query, {"username": manager_username} + ) - for manager_username in organisation_dto.managers: - user_query = "SELECT id FROM users WHERE username = :username" - user = await db.fetch_one( - user_query, {"username": manager_username} + if not user: + raise NotFound( + sub_code="USER_NOT_FOUND", username=manager_username ) - if not user: - raise NotFound( - sub_code="USER_NOT_FOUND", username=manager_username - ) - - insert_manager_query = """ - INSERT INTO organisation_managers (organisation_id, user_id) - VALUES (:organisation_id, :user_id) - """ - await db.execute( - insert_manager_query, - {"organisation_id": org_id, "user_id": user.id}, - ) - except Exception as e: - raise HTTPException(status_code=500, detail=str(e)) from e + insert_manager_query = """ + INSERT INTO organisation_managers (organisation_id, user_id) + VALUES (:organisation_id, :user_id) + """ + await db.execute( + insert_manager_query, + {"organisation_id": org_id, "user_id": user.id}, + ) + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) from e def delete(self): """Deletes the current model from the DB""" @@ -305,23 +303,47 @@ async def fetch_managers(self, session): """Fetch managers asynchronously""" await session.refresh(self, ["managers"]) - def as_dto(self, omit_managers=False): + # def as_dto(self, omit_managers=False): + # """Returns a dto for an organisation""" + # organisation_dto = OrganisationDTO() + # organisation_dto.organisation_id = self.id + # organisation_dto.name = self.name + # organisation_dto.slug = self.slug + # organisation_dto.logo = self.logo + # organisation_dto.description = self.description + # organisation_dto.url = self.url + # organisation_dto.managers = [] + # organisation_dto.type = OrganisationType(self.type).name + # organisation_dto.subscription_tier = self.subscription_tier + + # if omit_managers: + # return organisation_dto + + # for manager in self.managers: + # org_manager_dto = OrganisationManagerDTO() + # org_manager_dto.username = manager.username + # org_manager_dto.picture_url = manager.picture_url + # organisation_dto.managers.append(org_manager_dto) + + # return organisation_dto + + def as_dto(org, omit_managers=False): """Returns a dto for an organisation""" organisation_dto = OrganisationDTO() - organisation_dto.organisation_id = self.id - organisation_dto.name = self.name - organisation_dto.slug = self.slug - organisation_dto.logo = self.logo - organisation_dto.description = self.description - organisation_dto.url = self.url + organisation_dto.organisation_id = org.organisation_id + organisation_dto.name = org.name + organisation_dto.slug = org.slug + organisation_dto.logo = org.logo + organisation_dto.description = org.description + organisation_dto.url = org.url organisation_dto.managers = [] - organisation_dto.type = OrganisationType(self.type).name - organisation_dto.subscription_tier = self.subscription_tier + organisation_dto.type = org.type + organisation_dto.subscription_tier = org.subscription_tier if omit_managers: return organisation_dto - for manager in self.managers: + for manager in org.managers: org_manager_dto = OrganisationManagerDTO() org_manager_dto.username = manager.username org_manager_dto.picture_url = manager.picture_url diff --git a/backend/models/postgis/task.py b/backend/models/postgis/task.py index 1512d4e4cd..0c2abde81c 100644 --- a/backend/models/postgis/task.py +++ b/backend/models/postgis/task.py @@ -1,55 +1,60 @@ -import bleach import datetime -from backend.models.dtos.task_annotation_dto import TaskAnnotationDTO -import geojson import json from enum import Enum +from typing import Any, Dict, List -# # from flask import current_app -from sqlalchemy import desc, func, distinct -from sqlalchemy.orm.exc import MultipleResultsFound +import bleach +import geojson from geoalchemy2 import Geometry -from typing import Any, Dict, List from shapely.geometry import shape +# # from flask import current_app from sqlalchemy import ( - Column, - Integer, BigInteger, + Boolean, + Column, DateTime, - String, ForeignKey, - Boolean, - Index, ForeignKeyConstraint, + Index, + Integer, + String, Unicode, + desc, + distinct, + func, ) from sqlalchemy.orm import relationship +from sqlalchemy.orm.exc import MultipleResultsFound + +from backend.db import Base, get_session from backend.exceptions import NotFound from backend.models.dtos.mapping_dto import TaskDTO, TaskHistoryDTO -from backend.models.dtos.validator_dto import MappedTasksByUser, MappedTasks +from backend.models.dtos.mapping_issues_dto import TaskMappingIssueDTO from backend.models.dtos.project_dto import ( + LockedTasksForUser, ProjectComment, ProjectCommentsDTO, - LockedTasksForUser, ) -from backend.models.dtos.mapping_issues_dto import TaskMappingIssueDTO -from backend.models.postgis.statuses import TaskStatus, MappingLevel +from backend.models.dtos.task_annotation_dto import TaskAnnotationDTO +from backend.models.dtos.validator_dto import MappedTasks, MappedTasksByUser +from backend.models.postgis.statuses import MappingLevel, TaskStatus +from backend.models.postgis.task_annotation import TaskAnnotation from backend.models.postgis.user import User from backend.models.postgis.utils import ( InvalidData, InvalidGeoJson, - timestamp, parse_duration, + timestamp, ) -from backend.models.postgis.task_annotation import TaskAnnotation -from backend.db import Base, get_session session = get_session() -from backend.config import settings -from sqlalchemy import select from typing import Optional + from databases import Database +from sqlalchemy import select + +from backend.config import settings class TaskAction(Enum): @@ -1738,9 +1743,27 @@ async def as_dto_with_instructions( task_id, project_id, preferred_locale, db ) if not per_task_instructions: - default_locale = rows[0]["default_locale"] - per_task_instructions = await Task.get_per_task_instructions( - task_id, project_id, default_locale, db + query_locale = """ + SELECT + p.default_locale + FROM + projects p + WHERE + p.id = :project_id + """ + default_locale_row = await db.fetch_one( + query=query_locale, values={"project_id": project_id} + ) + default_locale = ( + default_locale_row["default_locale"] if default_locale_row else None + ) + + per_task_instructions = ( + await Task.get_per_task_instructions( + task_id, project_id, default_locale, db + ) + if default_locale + else None ) task_dto.per_task_instructions = per_task_instructions diff --git a/backend/services/messaging/message_service.py b/backend/services/messaging/message_service.py index 6e9440c717..bfb2ea8b25 100644 --- a/backend/services/messaging/message_service.py +++ b/backend/services/messaging/message_service.py @@ -804,6 +804,7 @@ async def get_all_messages( m.id AS message_id, m.subject, m.message, + m.from_user_id, m.to_user_id, m.task_id, m.message_type, @@ -862,7 +863,9 @@ async def get_all_messages( message_dict["message_type"] = MessageType( message_dict["message_type"] ).name - msg_dto = MessageDTO(**message_dict) + msg_dto = MessageDTO(**message_dict).dict( + exclude={"from_user_id"}, by_alias=True + ) messages_dto.user_messages.append(msg_dto) total_count_query = """ diff --git a/backend/services/organisation_service.py b/backend/services/organisation_service.py index d2a0a2a467..94e236516b 100644 --- a/backend/services/organisation_service.py +++ b/backend/services/organisation_service.py @@ -87,7 +87,6 @@ async def get_organisation_by_id(organisation_id: int, db: Database): managers_records = await db.fetch_all( managers_query, values={"organisation_id": organisation_id} ) - # Assign manager records initially org_record.managers = managers_records return org_record @@ -124,7 +123,6 @@ async def get_organisation_by_slug_as_dto( WHERE slug = :slug """ org_record = await db.fetch_one(org_query, values={"slug": slug}) - if not org_record: raise NotFound(sub_code="ORGANISATION_NOT_FOUND", slug=slug) @@ -189,18 +187,19 @@ async def get_organisation_dto(org, user_id: int, abbreviated: bool, db): if org is None: raise NotFound(sub_code="ORGANISATION_NOT_FOUND") - if not abbreviated: - org.managers = [] + organisation_dto = Organisation.as_dto(org, abbreviated) if user_id != 0: - org.is_manager = await OrganisationService.can_user_manage_organisation( - org.organisation_id, user_id, db + organisation_dto.is_manager = ( + await OrganisationService.can_user_manage_organisation( + organisation_dto.organisation_id, user_id, db + ) ) else: - org.is_manager = False + organisation_dto.is_manager = False if abbreviated: - return org + return organisation_dto teams_query = """ SELECT @@ -228,11 +227,13 @@ async def get_organisation_dto(org, user_id: int, abbreviated: bool, db): OrganisationService.team_as_dto_inside_org(record) for record in teams_records ] - if org.is_manager: - org.teams = teams + if organisation_dto.is_manager: + organisation_dto.teams = teams else: - org.teams = [team for team in teams if team.visibility == "PUBLIC"] - return org + organisation_dto.teams = [ + team for team in teams if team.visibility == "PUBLIC" + ] + return organisation_dto @staticmethod def get_organisation_by_name(organisation_name: str) -> Organisation: @@ -475,7 +476,6 @@ async def assert_validate_users(organisation_dto: OrganisationDTO, db): raise NotFound(sub_code="USER_NOT_FOUND", username=user) managers.append(admin.username) - organisation_dto.managers = managers @staticmethod diff --git a/backend/services/project_search_service.py b/backend/services/project_search_service.py index f6c31cd0ee..027c69f953 100644 --- a/backend/services/project_search_service.py +++ b/backend/services/project_search_service.py @@ -1,38 +1,38 @@ # # from flask import current_app import math +from typing import List + import geojson -from geoalchemy2 import shape -from shapely.geometry import Polygon, box from cachetools import TTLCache, cached +from databases import Database +from fastapi import HTTPException +from geoalchemy2 import shape from loguru import logger +from shapely.geometry import Polygon, box -from backend.exceptions import NotFound from backend.api.utils import validate_date_input +from backend.db import get_session +from backend.exceptions import NotFound from backend.models.dtos.project_dto import ( - ProjectSearchDTO, - ProjectSearchResultsDTO, ListSearchResultDTO, Pagination, ProjectSearchBBoxDTO, + ProjectSearchDTO, + ProjectSearchResultsDTO, ) from backend.models.postgis.project import Project, ProjectInfo from backend.models.postgis.statuses import ( - ProjectStatus, MappingLevel, + MappingPermission, MappingTypes, + ProjectDifficulty, ProjectPriority, - UserRole, + ProjectStatus, TeamRoles, + UserRole, ValidationPermission, - MappingPermission, - ProjectDifficulty, ) from backend.services.users.user_service import UserService -from backend.db import get_session -from databases import Database -from fastapi import HTTPException -from typing import List - session = get_session() @@ -186,8 +186,6 @@ async def get_total_contributions( project_ids: List[int], db: Database ) -> List[int]: """Fetch total contributions for given project IDs.""" - print(f"Fetching total contributions for projects: {project_ids}") - if not project_ids: return []