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(