From e4c3b6ced51398ce8e52da59c62e95ad467c5746 Mon Sep 17 00:00:00 2001 From: prabinoid <38830224+prabinoid@users.noreply.github.com> Date: Tue, 19 Nov 2024 17:10:53 +0545 Subject: [PATCH] * Team messaging background task, * Project creation date for creation date sorting * Email services fixed * User statistics for validation time spent. --- backend/api/teams/actions.py | 33 ++----- backend/db.py | 7 +- backend/models/dtos/user_dto.py | 14 +-- backend/models/postgis/message.py | 2 + backend/models/postgis/project.py | 1 + backend/services/messaging/message_service.py | 2 +- backend/services/messaging/smtp_service.py | 21 +++-- backend/services/team_service.py | 72 +++++++-------- backend/services/users/user_service.py | 92 +++++++++---------- 9 files changed, 109 insertions(+), 135 deletions(-) diff --git a/backend/api/teams/actions.py b/backend/api/teams/actions.py index d83260ee7e..d765df266b 100644 --- a/backend/api/teams/actions.py +++ b/backend/api/teams/actions.py @@ -1,18 +1,18 @@ from databases import Database -from fastapi import APIRouter, Depends, Request, Body, BackgroundTasks +from fastapi import APIRouter, BackgroundTasks, Body, Depends, Request from fastapi.responses import JSONResponse from loguru import logger -from backend.db import get_db +from backend.db import db_connection, get_db from backend.models.dtos.message_dto import MessageDTO +from backend.models.dtos.user_dto import AuthUserDTO +from backend.models.postgis.user import User from backend.services.team_service import ( - TeamService, TeamJoinNotAllowed, + TeamService, TeamServiceError, ) -from backend.models.postgis.user import User from backend.services.users.authentication_service import login_required -from backend.models.dtos.user_dto import AuthUserDTO router = APIRouter( prefix="/teams", @@ -314,19 +314,6 @@ async def post( ) -import asyncio - - -# Function to run async code in a thread -def run_asyncio_in_thread(func, *args, **kwargs): - # Create a new event loop for the thread - loop = asyncio.new_event_loop() - asyncio.set_event_loop(loop) - # Create a new database connection (to be used in this thread) - db = get_db() - loop.run_until_complete(func(*args, db=db, **kwargs)) - - @router.post("/{team_id}/actions/message-members/") async def post( request: Request, @@ -411,22 +398,14 @@ async def post( ) try: - # Start a new thread for sending messages - # Use threading to run the async function in a separate thread - # threading.Thread( - # target=run_asyncio_in_thread, - # args=(TeamService.send_message_to_all_team_members, team_id, team.name, message_dto, user.id) - # ).start() - background_tasks.add_task( TeamService.send_message_to_all_team_members, team_id, team.name, message_dto, user.id, - db, + db_connection.database, ) - return JSONResponse( content={"Success": "Message sent successfully"}, status_code=200 ) diff --git a/backend/db.py b/backend/db.py index d5ad20bf8c..a59234a8fe 100644 --- a/backend/db.py +++ b/backend/db.py @@ -1,9 +1,8 @@ -from backend.config import settings -from sqlalchemy.orm import declarative_base - from databases import Database from sqlalchemy import create_engine -from sqlalchemy.orm import sessionmaker +from sqlalchemy.orm import declarative_base, sessionmaker + +from backend.config import settings Base = declarative_base() diff --git a/backend/models/dtos/user_dto.py b/backend/models/dtos/user_dto.py index 17d59aa160..0418a892a5 100644 --- a/backend/models/dtos/user_dto.py +++ b/backend/models/dtos/user_dto.py @@ -1,12 +1,14 @@ -from backend.models.dtos.stats_dto import Pagination -from backend.models.dtos.mapping_dto import TaskDTO -from backend.models.dtos.interests_dto import InterestDTO -from backend.models.postgis.statuses import MappingLevel, UserRole -from pydantic import BaseModel, Field -from typing import List, Optional from datetime import datetime +from typing import List, Optional + +from pydantic import BaseModel, Field from pydantic.functional_validators import field_validator +from backend.models.dtos.interests_dto import InterestDTO +from backend.models.dtos.mapping_dto import TaskDTO +from backend.models.dtos.stats_dto import Pagination +from backend.models.postgis.statuses import MappingLevel, UserRole + def is_known_role(value): """Validates that supplied user role is known value""" diff --git a/backend/models/postgis/message.py b/backend/models/postgis/message.py index 66c2133a4d..59e68956d6 100644 --- a/backend/models/postgis/message.py +++ b/backend/models/postgis/message.py @@ -86,6 +86,8 @@ def from_dto(cls, to_user_id: int, dto: MessageDTO): message.to_user_id = to_user_id message.project_id = dto.project_id message.task_id = dto.task_id + message.date = timestamp() + message.read = False if dto.message_type is not None: message.message_type = MessageType(dto.message_type) diff --git a/backend/models/postgis/project.py b/backend/models/postgis/project.py index 7553543133..261a668bf4 100644 --- a/backend/models/postgis/project.py +++ b/backend/models/postgis/project.py @@ -264,6 +264,7 @@ def create_draft_project(self, draft_project_dto: DraftProjectDTO): self.organisation_id = self.organisation.id self.status = ProjectStatus.DRAFT.value self.author_id = draft_project_dto.user_id + self.created = timestamp() self.last_updated = timestamp() async def set_project_aoi(self, draft_project_dto: DraftProjectDTO, db: Database): diff --git a/backend/services/messaging/message_service.py b/backend/services/messaging/message_service.py index 8e22eafded..64f535f831 100644 --- a/backend/services/messaging/message_service.py +++ b/backend/services/messaging/message_service.py @@ -221,7 +221,7 @@ async def _push_messages(messages: list, db: Database): continue # If the notification is enabled, send an email messages_objs.append(obj) - SMTPService.send_email_alert( + await SMTPService.send_email_alert( user.email_address, user.username, user.is_email_verified, diff --git a/backend/services/messaging/smtp_service.py b/backend/services/messaging/smtp_service.py index e7bdbe5c0a..7e94b2f7e7 100644 --- a/backend/services/messaging/smtp_service.py +++ b/backend/services/messaging/smtp_service.py @@ -1,17 +1,18 @@ import urllib.parse -from loguru import logger -from itsdangerous import URLSafeTimedSerializer + from fastapi_mail import MessageSchema, MessageType +from itsdangerous import URLSafeTimedSerializer +from loguru import logger # from backend import mail, create_app from backend import create_app, mail +from backend.config import settings from backend.models.postgis.message import Message as PostgisMessage from backend.models.postgis.statuses import EncouragingEmailType from backend.services.messaging.template_service import ( - get_template, format_username_link, + get_template, ) -from backend.config import settings class SMTPService: @@ -32,7 +33,7 @@ async def send_verification_email(to_address: str, username: str): return True @staticmethod - def send_welcome_email(to_address: str, username: str): + async def send_welcome_email(to_address: str, username: str): """Sends email welcoming new user to tasking manager""" values = { "USERNAME": username, @@ -40,7 +41,7 @@ def send_welcome_email(to_address: str, username: str): html_template = get_template("welcome.html", values) subject = "Welcome to Tasking Manager" - SMTPService._send_message(to_address, subject, html_template) + await SMTPService._send_message(to_address, subject, html_template) return True @staticmethod @@ -63,7 +64,7 @@ async def send_contact_admin_email(data): await SMTPService._send_message(email_to, subject, message, message) @staticmethod - def send_email_to_contributors_on_project_progress( + async def send_email_to_contributors_on_project_progress( email_type: str, project_id: int = None, project_name: str = None, @@ -120,12 +121,12 @@ def send_email_to_contributors_on_project_progress( logger.debug( f"Sending {email_type} email to {contributor.email_address} for project {project_id}" ) - SMTPService._send_message( + await SMTPService._send_message( contributor.email_address, subject, html_template ) @staticmethod - def send_email_alert( + async def send_email_alert( to_address: str, username: str, user_email_verified: bool, @@ -172,7 +173,7 @@ def send_email_alert( "MESSAGE_TYPE": message_type, } html_template = get_template("message_alert_en.html", values) - SMTPService._send_message(to_address, subject, html_template) + await SMTPService._send_message(to_address, subject, html_template) return True diff --git a/backend/services/team_service.py b/backend/services/team_service.py index 6e8fbac864..a93c698828 100644 --- a/backend/services/team_service.py +++ b/backend/services/team_service.py @@ -4,31 +4,30 @@ from markdown import markdown from backend.exceptions import NotFound +from backend.models.dtos.message_dto import MessageDTO +from backend.models.dtos.stats_dto import Pagination from backend.models.dtos.team_dto import ( ListTeamsDTO, - TeamDTO, NewTeamDTO, - TeamsListDTO, ProjectTeamDTO, TeamDetailsDTO, + TeamDTO, TeamSearchDTO, + TeamsListDTO, ) - -from backend.models.dtos.message_dto import MessageDTO -from backend.models.dtos.stats_dto import Pagination from backend.models.postgis.message import Message, MessageType -from backend.models.postgis.team import Team, TeamMembers from backend.models.postgis.project import ProjectTeams from backend.models.postgis.statuses import ( TeamJoinMethod, TeamMemberFunctions, - TeamVisibility, TeamRoles, + TeamVisibility, UserRole, ) +from backend.models.postgis.team import Team, TeamMembers +from backend.services.messaging.message_service import MessageService from backend.services.organisation_service import OrganisationService from backend.services.users.user_service import UserService -from backend.services.messaging.message_service import MessageService class TeamServiceError(Exception): @@ -790,34 +789,31 @@ async def send_message_to_all_team_members( team_name: str, message_dto: MessageDTO, user_id: int, - db: Database = None, + database: Database, ): - if db is None: - print("inside....") - db = await acquire_connection() - print("Sending message to the team...") - print(db) - team_members = await TeamService._get_active_team_members(team_id, db) - user = await UserService.get_user_by_id(user_id, db) - print("Fetched User....") - - 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: - print("Looping teams.......") - - 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)) - - await MessageService._push_messages(messages) + try: + async with database.connection() as conn: + team_members = await TeamService._get_active_team_members(team_id, conn) + user = await UserService.get_user_by_id(user_id, conn) + 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 != user_id: + message = Message.from_dto(team_member.user_id, message_dto) + message.message_type = MessageType.TEAM_BROADCAST.value + user = await UserService.get_user_by_id( + team_member.user_id, conn + ) + messages.append(dict(message=message, user=user)) + # Push messages + await MessageService._push_messages(messages, conn) + logger.info("Messages sent successfully.") + except Exception as e: + logger.error(f"Error sending messages in background task: {str(e)}") diff --git a/backend/services/users/user_service.py b/backend/services/users/user_service.py index 95465a7a39..9ba2e30103 100644 --- a/backend/services/users/user_service.py +++ b/backend/services/users/user_service.py @@ -1,46 +1,43 @@ -from cachetools import TTLCache, cached - import datetime + +from cachetools import TTLCache, cached +from databases import Database from loguru import logger +from sqlalchemy import Time, and_, cast, desc, distinct, func, insert, or_, select from sqlalchemy.sql import outerjoin -from sqlalchemy import func, or_, desc, and_, distinct, cast, Time, select, insert -from databases import Database +from backend.config import Settings +from backend.db import get_session from backend.exceptions import NotFound +from backend.models.dtos.interests_dto import InterestDTO, InterestsListDTO from backend.models.dtos.project_dto import ProjectFavoritesDTO, ProjectSearchResultsDTO +from backend.models.dtos.stats_dto import Pagination from backend.models.dtos.user_dto import ( + UserContributionDTO, + UserCountriesContributed, + UserCountryContributed, UserDTO, - UserOSMDTO, UserFilterDTO, - UserSearchQuery, + UserOSMDTO, + UserRegisterEmailDTO, UserSearchDTO, + UserSearchQuery, UserStatsDTO, - UserContributionDTO, - UserRegisterEmailDTO, - UserCountryContributed, - UserCountriesContributed, -) -from backend.models.dtos.interests_dto import ( - InterestsListDTO, - InterestDTO, + UserTaskDTOs, ) from backend.models.postgis.interests import Interest, project_interests from backend.models.postgis.message import MessageType from backend.models.postgis.project import Project -from backend.models.postgis.user import User, UserRole, MappingLevel, UserEmail -from backend.models.postgis.task import TaskHistory, TaskAction, Task +from backend.models.postgis.statuses import ProjectStatus, TaskStatus +from backend.models.postgis.task import Task, TaskAction, TaskHistory +from backend.models.postgis.user import MappingLevel, User, UserEmail, UserRole from backend.models.postgis.utils import timestamp -from backend.models.postgis.statuses import TaskStatus, ProjectStatus -from backend.models.dtos.user_dto import UserTaskDTOs -from backend.models.dtos.stats_dto import Pagination -from backend.services.users.osm_service import OSMService, OSMServiceError from backend.services.messaging.smtp_service import SMTPService from backend.services.messaging.template_service import ( get_txt_template, template_var_replacing, ) -from backend.db import get_session -from backend.config import Settings +from backend.services.users.osm_service import OSMService, OSMServiceError settings = Settings() session = get_session() @@ -281,10 +278,7 @@ async def get_interests_stats(user_id: int, db: Database): interests = await db.fetch_all(interests_query) # Map results to DTOs - interests_dto = [ - InterestDTO(dict(id=i[0], name=i[1], count_projects=i[2])) - for i in interests - ] + interests_dto = [InterestDTO(**i) for i in interests] return interests_dto @@ -498,31 +492,30 @@ async def get_detailed_stats(username: str, db: Database) -> UserStatsDTO: stats_dto.time_spent_mapping = 0 stats_dto.time_spent_validating = 0 - # Total validation time - # Subquery to get max(action_date) grouped by minute - subquery = ( - select( - func.date_trunc("minute", TaskHistory.action_date).label("minute"), - func.max(TaskHistory.action_date).label("max_action_date"), + total_validation_time_query = """ + WITH max_action_text_per_minute AS ( + SELECT + date_trunc('minute', action_date) AS trn, + MAX(action_text) AS tm + FROM task_history + WHERE user_id = :user_id + AND action = 'LOCKED_FOR_VALIDATION' + GROUP BY date_trunc('minute', action_date) ) - .where( - TaskHistory.user_id == user["id"], - TaskHistory.action == "LOCKED_FOR_VALIDATION", - ) - .group_by("minute") - .subquery() - ) + SELECT + SUM(EXTRACT(EPOCH FROM (tm || ' seconds')::interval)) AS total_time + FROM max_action_text_per_minute + """ - # Outer query to sum up the epoch values of the max action dates - total_validation_time_query = select( - func.sum(func.extract("epoch", subquery.c.max_action_date)) + # Execute the query + result = await db.fetch_one( + total_validation_time_query, values={"user_id": user.id} ) - # Execute the query and fetch the result - total_validation_time = await db.fetch_one(total_validation_time_query) - - if total_validation_time and total_validation_time[0]: - stats_dto.time_spent_validating = total_validation_time[0] + if result and result["total_time"]: + total_validation_time = result["total_time"] + # TODO Handle typecasting. + stats_dto.time_spent_validating = round(float(total_validation_time), 1) stats_dto.total_time_spent += stats_dto.time_spent_validating # Total mapping time @@ -539,9 +532,10 @@ async def get_detailed_stats(username: str, db: Database) -> UserStatsDTO: ) total_mapping_time = await db.fetch_one(total_mapping_time_query) - if total_mapping_time and total_mapping_time[0]: - stats_dto.time_spent_mapping = total_mapping_time[0].total_seconds() + stats_dto.time_spent_mapping = round( + total_mapping_time[0].total_seconds(), 1 + ) stats_dto.total_time_spent += stats_dto.time_spent_mapping stats_dto.contributions_interest = await UserService.get_interests_stats(