diff --git a/backend/api/campaigns/resources.py b/backend/api/campaigns/resources.py index 05e421785d..d773314fd5 100644 --- a/backend/api/campaigns/resources.py +++ b/backend/api/campaigns/resources.py @@ -1,15 +1,16 @@ +from databases import Database +from fastapi import APIRouter, Depends, Request + +from backend.db import get_db from backend.models.dtos.campaign_dto import ( CampaignDTO, CampaignListDTO, NewCampaignDTO, ) +from backend.models.dtos.user_dto import AuthUserDTO from backend.services.campaign_service import CampaignService from backend.services.organisation_service import OrganisationService -from fastapi import APIRouter, Depends, Request -from backend.db import get_db -from databases import Database from backend.services.users.authentication_service import login_required -from backend.models.dtos.user_dto import AuthUserDTO router = APIRouter( prefix="/campaigns", diff --git a/backend/api/comments/resources.py b/backend/api/comments/resources.py index 0d05f6727d..5325b28876 100644 --- a/backend/api/comments/resources.py +++ b/backend/api/comments/resources.py @@ -1,5 +1,4 @@ -from datetime import datetime - +from backend.models.postgis.utils import timestamp from databases import Database from fastapi import APIRouter, Depends, Request from loguru import logger @@ -76,14 +75,13 @@ async def post( content={"Error": "User is on read only mode", "SubCode": "ReadOnly"}, status_code=403, ) - request_json = await request.json() message = request_json.get("message") chat_dto = ChatMessageDTO( message=message, user_id=user.id, project_id=project_id, - timestamp=datetime.utcnow(), + timestamp=timestamp(), username=user.username, ) try: @@ -345,10 +343,13 @@ async def get(request: Request, project_id, task_id): task_comment.validate() except Exception as e: logger.error(f"Error validating request: {str(e)}") - return { - "Error": "Unable to fetch task comments", - "SubCode": "InvalidData", - }, 400 + return JSONResponse( + content={ + "Error": "Unable to fetch task comments", + "SubCode": "InvalidData", + }, + status_code=400, + ) try: # NEW FUNCTION HAS TO BE ADDED @@ -356,4 +357,4 @@ async def get(request: Request, project_id, task_id): # return task.model_dump(by_alias=True), 200 return except MappingServiceError as e: - return {"Error": str(e)}, 403 + return JSONResponse(content={"Error": str(e)}, status_code=403) diff --git a/backend/api/teams/resources.py b/backend/api/teams/resources.py index 829d906da0..bcc7f9f378 100644 --- a/backend/api/teams/resources.py +++ b/backend/api/teams/resources.py @@ -1,22 +1,18 @@ -from backend.models.postgis.team import Team -from databases import Database from distutils.util import strtobool -from fastapi import APIRouter, Depends, Request, Body + +from databases import Database +from fastapi import APIRouter, Body, Depends, Request from fastapi.responses import JSONResponse from loguru import logger from backend.db import get_db -from backend.models.dtos.team_dto import ( - NewTeamDTO, - UpdateTeamDTO, - TeamSearchDTO, -) -from backend.services.team_service import TeamService, TeamServiceError +from backend.models.dtos.team_dto import NewTeamDTO, TeamSearchDTO, UpdateTeamDTO +from backend.models.dtos.user_dto import AuthUserDTO +from backend.models.postgis.team import Team from backend.services.organisation_service import OrganisationService +from backend.services.team_service import TeamService, TeamServiceError 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 - router = APIRouter( prefix="/teams", @@ -329,7 +325,6 @@ async def list_teams( 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) return teams diff --git a/backend/models/dtos/mapping_dto.py b/backend/models/dtos/mapping_dto.py index 49d68d4184..042d868a13 100644 --- a/backend/models/dtos/mapping_dto.py +++ b/backend/models/dtos/mapping_dto.py @@ -2,7 +2,7 @@ from backend.models.postgis.statuses import TaskStatus from backend.models.dtos.mapping_issues_dto import TaskMappingIssueDTO from backend.models.dtos.task_annotation_dto import TaskAnnotationDTO -from pydantic import BaseModel, Field, ValidationError, validator +from pydantic import BaseModel, Field, ValidationError, validator, root_validator from typing import List, Optional @@ -66,6 +66,12 @@ class TaskHistoryDTO(BaseModel): class Config: populate_by_name = True + @root_validator(pre=True) + def format_sent_date(cls, values): + if "action_date" in values and values["action_date"]: + values["action_date"] = values["action_date"].isoformat() + "Z" + return values + class TaskStatusDTO(BaseModel): """Describes a DTO for the current status of the task""" diff --git a/backend/models/dtos/message_dto.py b/backend/models/dtos/message_dto.py index b17675515e..479b68704d 100644 --- a/backend/models/dtos/message_dto.py +++ b/backend/models/dtos/message_dto.py @@ -1,8 +1,10 @@ -from backend.models.dtos.stats_dto import Pagination -from pydantic import BaseModel, Field from datetime import datetime from typing import List, Optional +from pydantic import BaseModel, Field, root_validator + +from backend.models.dtos.stats_dto import Pagination + class MessageDTO(BaseModel): """DTO used to define a message that will be sent to a user""" @@ -23,6 +25,12 @@ class MessageDTO(BaseModel): class Config: populate_by_name = True + @root_validator(pre=True) + def format_sent_date(cls, values): + if "sent_date" in values and values["sent_date"]: + values["sent_date"] = values["sent_date"].isoformat() + "Z" + return values + class MessagesDTO(BaseModel): """DTO used to return all user messages""" @@ -53,6 +61,12 @@ class ChatMessageDTO(BaseModel): class Config: populate_by_name = True + def dict(self, **kwargs): + data = super().dict(**kwargs) + if self.timestamp: + data["timestamp"] = self.timestamp.isoformat() + "Z" + return data + class ProjectChatDTO(BaseModel): """DTO describing all chat messages on one project""" diff --git a/backend/models/postgis/message.py b/backend/models/postgis/message.py index 18ec88aa7e..66c2133a4d 100644 --- a/backend/models/postgis/message.py +++ b/backend/models/postgis/message.py @@ -128,6 +128,8 @@ async def save(self, db: Database): project_id=self.project_id, task_id=self.task_id, message_type=self.message_type, + read=self.read, + date=self.date, ) ) diff --git a/backend/models/postgis/notification.py b/backend/models/postgis/notification.py index b82174e77e..e8f606b3b9 100644 --- a/backend/models/postgis/notification.py +++ b/backend/models/postgis/notification.py @@ -1,18 +1,20 @@ +from datetime import datetime, timedelta + +from databases import Database from sqlalchemy import ( - Column, - Integer, BigInteger, + Column, DateTime, ForeignKey, ForeignKeyConstraint, + Integer, ) from sqlalchemy.orm import relationship + +from backend.db import Base, get_session +from backend.models.dtos.notification_dto import NotificationDTO from backend.models.postgis.user import User from backend.models.postgis.utils import timestamp -from backend.models.dtos.notification_dto import NotificationDTO -from datetime import datetime, timedelta -from backend.db import Base, get_session -from databases import Database session = get_session() @@ -62,7 +64,7 @@ async def get_unread_message_count(user_id: int, db: Database) -> int: notification = await db.fetch_one(query, {"user_id": user_id}) if notification is None: - date_value = datetime.today() - timedelta(days=30) + date_value = datetime.utcnow() - timedelta(days=30) insert_query = """ INSERT INTO notifications (user_id, unread_count, date) VALUES (:user_id, :unread_count, :date) diff --git a/backend/models/postgis/project_chat.py b/backend/models/postgis/project_chat.py index e925db9cf0..917f83529e 100644 --- a/backend/models/postgis/project_chat.py +++ b/backend/models/postgis/project_chat.py @@ -1,13 +1,14 @@ import bleach -from markdown import markdown +from databases import Database from loguru import logger -from sqlalchemy import Column, Integer, BigInteger, String, DateTime, ForeignKey +from markdown import markdown +from sqlalchemy import BigInteger, Column, DateTime, ForeignKey, Integer, String from sqlalchemy.orm import relationship + +from backend.db import Base, get_session +from backend.models.dtos.message_dto import ChatMessageDTO, Pagination, ProjectChatDTO from backend.models.postgis.user import User from backend.models.postgis.utils import timestamp -from backend.models.dtos.message_dto import ChatMessageDTO, ProjectChatDTO, Pagination -from backend.db import Base, get_session -from databases import Database session = get_session() diff --git a/backend/models/postgis/task.py b/backend/models/postgis/task.py index 788cf4e72b..3138d796d4 100644 --- a/backend/models/postgis/task.py +++ b/backend/models/postgis/task.py @@ -998,7 +998,7 @@ async def set_task_history( "project_id": project_id, "action": action_name, "action_text": action_text, - "action_date": datetime.datetime.utcnow(), + "action_date": timestamp(), } task_history = await db.fetch_one(query=query, values=values) diff --git a/backend/models/postgis/utils.py b/backend/models/postgis/utils.py index 21fe2e5a56..a54835a95c 100644 --- a/backend/models/postgis/utils.py +++ b/backend/models/postgis/utils.py @@ -134,8 +134,7 @@ class ST_Y(GenericFunction): def timestamp(): """Used in SQL Alchemy models to ensure we refresh timestamp when new models initialised""" - return datetime.datetime.now(datetime.timezone.utc).replace(tzinfo=None) - # return datetime.datetime.now(datetime.timezone.utc) + return datetime.datetime.utcnow() # Based on https://stackoverflow.com/a/51916936 diff --git a/backend/services/grid/split_service.py b/backend/services/grid/split_service.py index b56ccb5220..c9446bd99a 100644 --- a/backend/services/grid/split_service.py +++ b/backend/services/grid/split_service.py @@ -4,6 +4,7 @@ # from flask import current_app from geoalchemy2 import shape from geoalchemy2.elements import WKBElement +from loguru import logger from shapely.geometry import LineString, MultiPolygon, Polygon from shapely.geometry import shape as shapely_shape from shapely.ops import split @@ -18,8 +19,7 @@ class SplitServiceError(Exception): """Custom Exception to notify callers an error occurred when handling splitting tasks""" def __init__(self, message): - if current_app: - current_app.logger.debug(message) + logger.debug(message) class SplitService: diff --git a/backend/services/interests_service.py b/backend/services/interests_service.py index 0f1cb6945f..c8dc831ec4 100644 --- a/backend/services/interests_service.py +++ b/backend/services/interests_service.py @@ -1,16 +1,15 @@ -from backend.models.postgis.project import Project +from databases import Database +from fastapi import HTTPException + from backend.models.dtos.interests_dto import ( + InterestDTO, InterestRateDTO, InterestRateListDTO, InterestsListDTO, - InterestDTO, -) -from backend.models.postgis.interests import ( - Interest, ) +from backend.models.postgis.interests import Interest +from backend.models.postgis.project import Project from backend.services.project_service import ProjectService -from databases import Database -from fastapi import HTTPException class InterestService: diff --git a/backend/services/messaging/chat_service.py b/backend/services/messaging/chat_service.py index f72e878537..2f46590e74 100644 --- a/backend/services/messaging/chat_service.py +++ b/backend/services/messaging/chat_service.py @@ -1,5 +1,4 @@ import threading - from databases import Database from backend.exceptions import NotFound @@ -74,6 +73,7 @@ async def post_message( chat_message.message, chat_dto.project_id, project_name, + db, ), ).start() # Ensure we return latest messages after post diff --git a/backend/services/messaging/message_service.py b/backend/services/messaging/message_service.py index 5c57c1ce07..19aaf4a5cd 100644 --- a/backend/services/messaging/message_service.py +++ b/backend/services/messaging/message_service.py @@ -1,38 +1,34 @@ +import datetime import re import time -import datetime -import bleach +from typing import List +import bleach from cachetools import TTLCache, cached -from backend.models.postgis.utils import timestamp +from databases import Database from loguru import logger -from typing import List - -from sqlalchemy import text, func from markdown import markdown +from sqlalchemy import func, insert, text -from backend import db, create_app +from backend import create_app, db +from backend.config import settings from backend.exceptions import NotFound from backend.models.dtos.message_dto import MessageDTO, MessagesDTO from backend.models.dtos.stats_dto import Pagination from backend.models.postgis.message import Message, MessageType from backend.models.postgis.notification import Notification from backend.models.postgis.project import Project, ProjectInfo -from backend.models.postgis.task import TaskStatus, TaskAction, TaskHistory +from backend.models.postgis.task import TaskAction, TaskHistory, TaskStatus +from backend.models.postgis.utils import timestamp from backend.services.messaging.smtp_service import SMTPService from backend.services.messaging.template_service import ( + clean_html, get_template, get_txt_template, template_var_replacing, - clean_html, ) from backend.services.organisation_service import OrganisationService -from backend.services.users.user_service import UserService, User - -from databases import Database -from backend.config import settings -from sqlalchemy import insert - +from backend.services.users.user_service import User, UserService message_cache = TTLCache(maxsize=512, ttl=30) @@ -65,6 +61,8 @@ async def send_welcome_message(user: User, db: Database): welcome_message.to_user_id = user.id welcome_message.subject = "Welcome to the {} Tasking Manager".format(org_code) welcome_message.message = text_template + welcome_message.date = timestamp() + welcome_message.read = False await Message.save(welcome_message, db) @staticmethod @@ -255,8 +253,8 @@ async def _push_messages(messages: list, db: Database): "project_id": msg.project_id, "task_id": msg.task_id, "message_type": msg.message_type, - "date": msg.date, - "read": msg.read, + "date": timestamp(), + "read": False, } for msg in messages_objs ] @@ -272,11 +270,12 @@ async def send_message_after_comment( """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) - + print(comment, "The comment....") # Parse the comment for mentions usernames = await MessageService._parse_message_for_username( comment, project_id, task_id, db ) + print(usernames, "The list of usernamess....") if comment_from_user.username in usernames: usernames.remove(comment_from_user.username) @@ -331,19 +330,10 @@ async def send_message_after_comment( for username in usernames: try: user = await UserService.get_user_by_username(username, db) + print(user, "The userrrr...") 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, - # } - message = Message() message.message_type = MessageType.MENTION_NOTIFICATION.value message.project_id = project_id @@ -353,7 +343,10 @@ async def send_message_after_comment( 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)) + message.read = False + messages.append( + dict(message=message, user=user, project_name=project_name) + ) await MessageService._push_messages(messages, db) @@ -391,16 +384,6 @@ async def send_message_after_comment( 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, - # } - message = Message() message.message_type = MessageType.TASK_COMMENT_NOTIFICATION.value message.project_id = project_id @@ -410,6 +393,7 @@ async def send_message_after_comment( message.subject = f"{user_link} left a comment in {task_link} of Project {project_link}" message.message = comment message.date = timestamp() + message.read = False messages.append( dict(message=message, user=user, project_name=project_name) ) @@ -425,9 +409,10 @@ async def send_project_transfer_message( """Will send a message to the manager of the organization after a project is transferred""" project = await Project.get(project_id, db) project_name = project.get_project_title(project.default_locale) - message = Message() message.message_type = MessageType.SYSTEM.value + message.date = timestamp() + message.read = False message.subject = ( f"Project {project_name} #{project_id} was transferred to {transferred_to}" ) @@ -475,7 +460,7 @@ def get_team_link(team_name: str, team_id: int, management: bool): return f'{team_name}' @staticmethod - def send_request_to_join_team( + async def send_request_to_join_team( from_user: int, from_username: str, to_user: int, @@ -487,14 +472,14 @@ def send_request_to_join_team( message.message_type = MessageType.REQUEST_TEAM_NOTIFICATION.value message.from_user_id = from_user message.to_user_id = to_user + message.date = timestamp() + message.read = False user_link = MessageService.get_user_link(from_username) team_link = MessageService.get_team_link(team_name, team_id, True) message.subject = f"{user_link} requested to join {team_link}" message.message = f"{user_link} has requested to join the {team_link} team.\ Access the team management page to accept or reject that request." - MessageService._push_messages( - [dict(message=message, user=session.get(User, to_user))] - ) + await Message.save(message, db) @staticmethod async def accept_reject_request_to_join_team( @@ -510,6 +495,8 @@ async def accept_reject_request_to_join_team( message.message_type = MessageType.REQUEST_TEAM_NOTIFICATION.value message.from_user_id = from_user message.to_user_id = to_user + message.date = timestamp() + message.read = False team_link = MessageService.get_team_link(team_name, team_id, False) user_link = MessageService.get_user_link(from_username) message.subject = f"Your request to join team {team_link} has been {response}ed" @@ -533,6 +520,8 @@ async def accept_reject_invitation_request_for_team( message.message_type = MessageType.INVITATION_NOTIFICATION.value message.from_user_id = from_user message.to_user_id = to_user + message.date = timestamp() + message.read = False message.subject = "{} {}ed to join {}".format( MessageService.get_user_link(from_username), response, @@ -565,12 +554,14 @@ async def send_team_join_notification( message.subject = f"You have been added to team {team_link}" 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.date = timestamp() + message.read = False await Message.save(message, db) @staticmethod def send_message_after_chat( - chat_from: int, chat: str, project_id: int, project_name: str + chat_from: int, chat: str, project_id: int, project_name: str, db: Database ): """Send alert to user if they were @'d in a chat message""" app = ( @@ -600,13 +591,15 @@ def send_message_after_chat( message.project_id = project_id message.from_user_id = chat_from message.to_user_id = user.id + message.date = timestamp() + message.read = False message.subject = f"You were mentioned in Project {link} chat" message.message = chat messages.append( dict(message=message, user=user, project_name=project_name) ) - MessageService._push_messages(messages) + MessageService._push_messages(messages, db) query = f""" select user_id from project_favorites where project_id ={project_id}""" with db.engine.connect() as conn: @@ -642,6 +635,8 @@ def send_message_after_chat( message.project_id = project_id message.from_user_id = chat_from message.to_user_id = user.id + message.date = timestamp() + message.read = False message.subject = ( f"{from_user_link} left a comment in project {project_link}" ) @@ -651,7 +646,7 @@ def send_message_after_chat( ) # it's important to keep that line inside the if to avoid duplicated emails - MessageService._push_messages(messages) + MessageService._push_messages(messages, db) @staticmethod async def send_favorite_project_activities(user_id: int): @@ -702,6 +697,8 @@ async def send_favorite_project_activities(user_id: int): message.message_type = MessageType.PROJECT_ACTIVITY_NOTIFICATION.value message.project_id = project.id message.to_user_id = user.id + message.date = timestamp() + message.read = False message.subject = ( "Recent activities from your contributed/favorited Projects" ) @@ -1014,6 +1011,7 @@ async def get_message_as_dto(message_id: int, user_id: int, db: Database): message_dict = dict(message) message_dict["message_type"] = MessageType(message_dict["message_type"]).name + print(message_dict, "blaaaaaa...") return message_dict @staticmethod diff --git a/backend/services/team_service.py b/backend/services/team_service.py index b9503a61dc..99e71f61c4 100644 --- a/backend/services/team_service.py +++ b/backend/services/team_service.py @@ -335,7 +335,6 @@ async def get_all_teams(search_dto: TeamSearchDTO, db: Database) -> TeamsListDTO user = await UserService.get_user_by_id(search_dto.user_id, db) is_admin = UserRole(user.role) == UserRole.ADMIN - if not is_admin: public_or_member_query = """ t.visibility = :public_visibility OR t.id IN ( @@ -352,11 +351,14 @@ async def get_all_teams(search_dto: TeamSearchDTO, db: Database) -> TeamsListDTO final_query = base_query if search_dto.paginate: - final_query += " LIMIT :limit OFFSET :offset" - params["limit"] = search_dto.per_page - params["offset"] = (search_dto.page - 1) * search_dto.per_page + final_query_paginated = final_query + limit = search_dto.per_page + offset = (search_dto.page - 1) * search_dto.per_page + final_query_paginated += f" LIMIT {limit} OFFSET {offset}" + rows = await db.fetch_all(query=final_query_paginated, values=params) - rows = await db.fetch_all(query=final_query, values=params) + else: + rows = await db.fetch_all(query=final_query, values=params) teams_list_dto = TeamsListDTO() for row in rows: @@ -393,7 +395,7 @@ async def get_all_teams(search_dto: TeamSearchDTO, db: Database) -> TeamsListDTO if search_dto.paginate: total_query = "SELECT COUNT(*) FROM (" + final_query + ") as total" total = await db.fetch_val(query=total_query, values=params) - teams_list_dto.pagination = Pagination( + teams_list_dto.pagination = Pagination.from_total_count( total=total, page=search_dto.page, per_page=search_dto.per_page ) return teams_list_dto