Skip to content

Commit

Permalink
Send weekly project update email to project author
Browse files Browse the repository at this point in the history
  • Loading branch information
Aadesh-Baral committed Aug 7, 2022
1 parent 7a258ac commit 596b7ea
Show file tree
Hide file tree
Showing 7 changed files with 362 additions and 1 deletion.
3 changes: 3 additions & 0 deletions backend/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ class EnvironmentConfig:
# The default tag used in the OSM changeset comment
DEFAULT_CHANGESET_COMMENT = os.getenv("TM_DEFAULT_CHANGESET_COMMENT", None)

# API url to fetch OSM stats for project using default changeset comment
PROJECT_STATS_API_URL = os.getenv("TM_PROJECT_STATS_API_URL", None)

# The address to use as the sender on auto generated emails
EMAIL_FROM_ADDRESS = os.getenv("TM_EMAIL_FROM_ADDRESS", None)

Expand Down
39 changes: 39 additions & 0 deletions backend/services/messaging/smtp_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@
from itsdangerous import URLSafeTimedSerializer
from flask import current_app
from flask_mail import Message
import datetime

from backend import mail, create_app
from backend.models.postgis.message import Message as PostgisMessage
from backend.models.postgis.statuses import EncouragingEmailType
from backend.models.postgis.task import TaskHistory, TaskAction
from backend.services.messaging.template_service import (
get_template,
format_username_link,
Expand Down Expand Up @@ -118,6 +120,43 @@ def send_email_to_contributors_on_project_progress(
contributor.email_address, subject, html_template
)

@staticmethod
def send_weekly_project_updates(
start_date: datetime.date, end_date=datetime.date.today()
):
from backend.services.project_service import ProjectService

task_history = TaskHistory.query.filter(
TaskHistory.action == TaskAction.STATE_CHANGE.name,
TaskHistory.action_date.between(start_date, end_date),
)

active_projects = task_history.with_entities(
TaskHistory.project_id.distinct()
).all()
messages_sent = 0
for project_id in active_projects:
stats = ProjectService.get_contrib_between_time_period(
start_date, end_date, project_id[0]
)
project = ProjectService.get_project_by_id(project_id[0])
stats["START_DATE"] = start_date.strftime("%b %d %Y")
stats["END_DATE"] = end_date.strftime("%b %d %Y")
stats["USERNAME"] = project.author.username
stats["PROJECT_NAME"] = ProjectService.get_project_title(project.id)
html_template = get_template("weekly_project_update_en.html", values=stats)
subject = f"Weekly project summary for {stats['PROJECT_NAME']}"
if (
project.author.email_address
and project.author.is_email_verified
and project.author.projects_notifications
):
SMTPService._send_message(
project.author.email_address, subject, html_template
)
messages_sent += 1
return messages_sent

@staticmethod
def send_email_alert(
to_address: str,
Expand Down
208 changes: 208 additions & 0 deletions backend/services/messaging/templates/weekly_project_update_en.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,208 @@
{% extends "base.html" %}
{% block content %}
<div
style="
border: 1px solid #d8dee4;
padding: 16px 32px 0px 32px;
border-radius: 5px;
">
<p style="text-align:right; font-size: 12px; color:#68707f;">
{{values["START_DATE"]}} - {{values["END_DATE"]}}
</p>
<div
style="
border-bottom: 1px solid #d8dee4;
padding-bottom: 8px;
margin-bottom: 16px;
line-height: 1.5;
">
<p style="font-size:14px">Hi, {{values["USERNAME"]}}</p>
<p style="color: #24292F; font-size: 14px;">
This is the weekly update for your project:
<a
style="
text-decoration: none;
color: #d73f3f;
font-weight:700;"
href="{{values['APP_BASE_URL']}}/projects/{{values['PROJECT_ID']}}"
>
{{values["PROJECT_NAME"]}} - #{{values["PROJECT_ID"]}}
</a>
</p>

</div>
<div style="margin: 22px 0px">
<h1
style="
font-weight: 500;
font-size: 1.2rem;
line-height: 0.916;
margin: 16px 0;
"
>
Total project progress so far:
</h1>
<table aria-label="" style="text-align: center;"> <!-- Noncompliant -->
<th style="width:100px;">
<h3 style="
font-size: 32px;
line-height: 1.2;
font-weight: 500;
color: #d73f3f;
align-self: center;
margin: 0;
"
>
{{values["TOTAL_PERCENTAGE_MAPPED"]}}%
</h3>
</th>
<th style="padding: 0 10px;">
&nbsp;
</th>
<th style="width: 100px;" >
<h3 style="
font-size: 32px;
line-height: 1.2;
font-weight: 500;
color: #d73f3f;
align-self: center;
margin: 0;
"
>
{{values["TOTAL_PERCENTAGE_VALIDATED"]}}%
</h3>
</th>
<th style="width: 150px;" >
<h3 style="
font-size: 32px;
line-height: 1.2;
font-weight: 500;
color: #d73f3f;
align-self: center;
margin: 0;
"
>
{{values["TOTAL_BUILDINGS_MAPPED"]}}
</h3>
</th>
<th style="width: 120px;" >
<h3 style="
font-size: 32px;
line-height: 1.2;
font-weight: 500;
color: #d73f3f;
align-self: center;
margin: 0;
"
>
{{values["TOTAL_ROAD_MAPPED"]}}
</h3>
</th>
<tr>
<td style="color: #68707f;">
Tasks mapped
</td>
<td>
&nbsp;
</td>
<td style="color: #68707f;">
Tasks validated
</td>
<td style="color: #68707f;">
Buildings mapped
</td>
<td style="color: #68707f;">
Km road mapped
</td>
</tr>
</table>
</div>
<div>
<h1 style="
font-weight: 500;
font-size: 1.2rem;
line-height: 0.916;
margin: 16px 0px;">
This week's activity overview:
</h1>
<table aria-label="">
<th
style="
width:120px;
height: 87px;
border-radius: 5px;
background-color:#b5ecf5;
font-size: 10px;
color: #168b9e;
">
Mapped
<p style="font-size: 28px">{{values["MAPPED_THIS_PERIOD"]}}</p>
tasks
</th>
<th></th>
<th
style="
width:120px;
height: 87px;
border-radius: 5px;
background-color:#64d1ae;
font-size: 10px;
color: #367562;
">
Validated
<p style="font-size: 28px">{{values["VALIDATED_THIS_PERIOD"]}}</p>
tasks
</th>
<th></th>
<th
style="
width:120px;
height: 87px;
border-radius: 5px;
background-color:#FCECA4;
font-size: 10px;
color: #a38f2f;
">
Invalidated
<p style="font-size: 28px">{{values["INVALIDATED_THIS_PERIOD"]}}</p>
tasks
</th>
<th></th>
<th
style="
width:120px;
height: 87px;
border-radius:5px;
background-color:#DEDFE6;
font-size: 10px;
color: #8e8f91;
">
Bad Imagery
<p style="font-size: 28px">{{values["BADIMAGERY_THIS_PERIOD"]}}</p>
tasks
</th>
</table>
<p style="color: #68707f; margin:16px 0px; line-height: 1.5;">
<span style="color:#24292F; ">{{values["CONTRIBUTORS"]}}</span>
total contributors this week
</p>
<p
style="font-size:12px;
line-height: 1.2;
font-weight: 500;
color: #68707f;
margin-top: 16px;
">
For more stats related to this project please visit
<a
style="text-decoration: none;
color: #d73f3f;
font-weight:700"
href="{{values['APP_BASE_URL']}}/projects/{{values['PROJECT_ID']}}/stats
">
project stats page
</a>.
</p>
</div>
</div>
{% endblock %}
21 changes: 21 additions & 0 deletions backend/services/project_admin_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -350,3 +350,24 @@ def is_user_action_permitted_on_project(
)

return is_admin or is_author or is_org_manager or is_manager_team

@staticmethod
def get_project_managers(project_id: int):
"""Get all project managers"""
managers = []
project = ProjectAdminService._get_project_by_id(project_id)
# Author has manager role by default
managers.append(project.author.username)
# Managers of organization associated with the project also has project manager role
managers.extend([manager.username for manager in project.organisation.managers])
# Add members of team with PM role in a project
teams_dto = TeamService.get_project_teams_as_dto(project_id)
teams_allowed = [
team_dto
for team_dto in teams_dto.teams
if team_dto.role == TeamRoles.PROJECT_MANAGER.value
]
if teams_allowed:
for teams_dto in teams_allowed:
managers.extend([member.username for member in teams_dto.members])
return set(managers) # Remove duplicates
71 changes: 70 additions & 1 deletion backend/services/project_service.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
from datetime import datetime
import threading
from cachetools import TTLCache, cached
from flask import current_app
import requests

from backend.models.dtos.mapping_dto import TaskDTOs
from backend.models.dtos.project_dto import (
Expand All @@ -23,7 +25,7 @@
TeamRoles,
EncouragingEmailType,
)
from backend.models.postgis.task import Task, TaskHistory
from backend.models.postgis.task import Task, TaskHistory, TaskAction, TaskStatus
from backend.models.postgis.utils import NotFound
from backend.services.messaging.smtp_service import SMTPService
from backend.services.users.user_service import UserService
Expand Down Expand Up @@ -603,3 +605,70 @@ def send_email_on_project_progress(project_id):
project_completion,
),
).start()

@staticmethod
def get_contrib_between_time_period(
start_date: datetime.date, end_date: datetime.date, project_id: int
):
"""
Get the number of contributions between two dates
"""
values = {}
project = ProjectService.get_project_by_id(project_id)
values["PROJECT_ID"] = project_id

# Calculate project stats for provided time period
project_task_history = TaskHistory.query.filter(
TaskHistory.action == TaskAction.STATE_CHANGE.name,
TaskHistory.action_date.between(start_date, end_date),
TaskHistory.project_id == project_id,
)
values["MAPPED_THIS_PERIOD"] = (
project_task_history.filter(
TaskHistory.action_text == TaskStatus.MAPPED.name
)
.distinct()
.count()
)
values["VALIDATED_THIS_PERIOD"] = project_task_history.filter(
TaskHistory.action_text == TaskStatus.VALIDATED.name
).count()
values["INVALIDATED_THIS_PERIOD"] = (
project_task_history.filter(
TaskHistory.action_text == TaskStatus.INVALIDATED.name
)
.distinct()
.count()
)
values["BADIMAGERY_THIS_PERIOD"] = project_task_history.filter(
TaskHistory.action_text == TaskStatus.BADIMAGERY.name
).count()

values["CONTRIBUTORS"] = project_task_history.distinct(
TaskHistory.user_id
).count()

# Get total buildings and roads mapped stats using project changeset comment
project_osm_stats_url = current_app.config[
"PROJECT_STATS_API_URL"
] + project.changeset_comment.split(" ")[0].replace("#", "")
project_osm_stats = requests.get(project_osm_stats_url).json()
values["TOTAL_BUILDINGS_MAPPED"] = project_osm_stats["buildings"]
values["TOTAL_ROAD_MAPPED"] = project_osm_stats["roads"]

# Calculate total stats for project
values["TOTAL_PERCENTAGE_MAPPED"] = Project.calculate_tasks_percent(
"mapped",
project.total_tasks,
project.tasks_mapped,
project.tasks_validated,
project.tasks_bad_imagery,
)
values["TOTAL_PERCENTAGE_VALIDATED"] = Project.calculate_tasks_percent(
"validated",
project.total_tasks,
project.tasks_mapped,
project.tasks_validated,
project.tasks_bad_imagery,
)
return values
1 change: 1 addition & 0 deletions example.env
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ OSM_REGISTER_URL=https://www.openstreetmap.org/user/new
#
TM_USER_STATS_API_URL=https://osm-stats-production-api.azurewebsites.net/users/
TM_HOMEPAGE_STATS_API_URL=https://osmstats-api.hotosm.org/wildcard?key=hotosm-project-*
TM_PROJECT_STATS_API_URL=https://osm-stats-production-api.azurewebsites.net/stats/

# Secret (required)
#
Expand Down
Loading

0 comments on commit 596b7ea

Please sign in to comment.