Skip to content

Commit

Permalink
Merge pull request #3421 from hotosm/develop
Browse files Browse the repository at this point in the history
v4.1.3 release
  • Loading branch information
willemarcel authored Aug 3, 2020
2 parents be78cae + 6263b4f commit 1fee3b3
Show file tree
Hide file tree
Showing 62 changed files with 695 additions and 502 deletions.
2 changes: 2 additions & 0 deletions backend/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ class EnvironmentConfig:

FRONTEND_BASE_URL = os.getenv("TM_FRONTEND_BASE_URL", APP_BASE_URL)
API_VERSION = os.getenv("TM_APP_API_VERSION", "v2")
ORG_CODE = os.getenv("TM_ORG_CODE", "")
ORG_NAME = os.getenv("TM_ORG_NAME", "")
# The default tag used in the OSM changeset comment
DEFAULT_CHANGESET_COMMENT = os.getenv("TM_DEFAULT_CHANGESET_COMMENT", None)

Expand Down
2 changes: 1 addition & 1 deletion backend/models/dtos/user_dto.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,6 @@ def is_known_role(value):
class UserDTO(Model):
""" DTO for User """

validation_message = BooleanType(default=True)
id = LongType()
username = StringType()
role = StringType()
Expand Down Expand Up @@ -74,6 +73,7 @@ class UserDTO(Model):
mentions_notifications = BooleanType(serialized_name="mentionsNotifications")
comments_notifications = BooleanType(serialized_name="commentsNotifications")
projects_notifications = BooleanType(serialized_name="projectsNotifications")
tasks_notifications = BooleanType(serialized_name="tasksNotifications")

# these are read only
missing_maps_profile = StringType(serialized_name="missingMapsProfile")
Expand Down
3 changes: 0 additions & 3 deletions backend/models/postgis/project.py
Original file line number Diff line number Diff line change
Expand Up @@ -317,8 +317,6 @@ def clone(project_id: int, author_id: int):
"created": timestamp(),
"author_id": author_id,
"status": ProjectStatus.DRAFT.value,
"geometry": None,
"centroid": None,
}
)

Expand Down Expand Up @@ -351,7 +349,6 @@ def clone(project_id: int, author_id: int):
setattr(new_proj, field, value)

new_proj.custom_editor = orig.custom_editor
db.session.commit()

return new_proj

Expand Down
5 changes: 5 additions & 0 deletions backend/models/postgis/team.py
Original file line number Diff line number Diff line change
Expand Up @@ -192,3 +192,8 @@ def _get_team_members(self):
)

return members

def get_team_managers(self):
return TeamMembers.query.filter_by(
team_id=self.id, function=TeamMemberFunctions.MANAGER.value, active=True
).all()
4 changes: 2 additions & 2 deletions backend/models/postgis/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@ class User(db.Model):
__tablename__ = "users"

id = db.Column(db.BigInteger, primary_key=True, index=True)
validation_message = db.Column(db.Boolean, default=True, nullable=False)
username = db.Column(db.String, unique=True)
role = db.Column(db.Integer, default=0, nullable=False)
mapping_level = db.Column(db.Integer, default=1, nullable=False)
Expand All @@ -58,6 +57,7 @@ class User(db.Model):
mentions_notifications = db.Column(db.Boolean, default=True, nullable=False)
comments_notifications = db.Column(db.Boolean, default=False, nullable=False)
projects_notifications = db.Column(db.Boolean, default=True, nullable=False)
tasks_notifications = db.Column(db.Boolean, default=True, nullable=False)
date_registered = db.Column(db.DateTime, default=timestamp)
# Represents the date the user last had one of their tasks validated
last_validation_date = db.Column(db.DateTime, default=timestamp)
Expand Down Expand Up @@ -368,7 +368,7 @@ def as_dto(self, logged_in_username: str) -> UserDTO:
user_dto.mentions_notifications = self.mentions_notifications
user_dto.projects_notifications = self.projects_notifications
user_dto.comments_notifications = self.comments_notifications
user_dto.validation_message = self.validation_message
user_dto.tasks_notifications = self.tasks_notifications
gender = None
if self.gender is not None:
gender = UserGender(self.gender).name
Expand Down
95 changes: 61 additions & 34 deletions backend/services/messaging/message_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,11 @@
from backend.models.postgis.task import TaskStatus, TaskAction, TaskHistory
from backend.models.postgis.statuses import TeamRoles
from backend.services.messaging.smtp_service import SMTPService
from backend.services.messaging.template_service import get_template, get_profile_url
from backend.services.messaging.template_service import (
get_template,
template_var_replacing,
clean_html,
)
from backend.services.users.user_service import UserService, User


Expand All @@ -35,17 +39,20 @@ class MessageService:
@staticmethod
def send_welcome_message(user: User):
""" Sends welcome message to all new users at Sign up"""
org_code = current_app.config["ORG_CODE"]
text_template = get_template("welcome_message_en.txt")

text_template = text_template.replace("[USERNAME]", user.username)
text_template = text_template.replace(
"[PROFILE_LINK]", get_profile_url(user.username)
)
replace_list = [
["[USERNAME]", user.username],
["[ORG_CODE]", org_code],
["[ORG_NAME]", current_app.config["ORG_NAME"]],
["[SETTINGS_LINK]", MessageService.get_user_settings_link()],
]
text_template = template_var_replacing(text_template, replace_list)

welcome_message = Message()
welcome_message.message_type = MessageType.SYSTEM.value
welcome_message.to_user_id = user.id
welcome_message.subject = "Welcome to the HOT Tasking Manager"
welcome_message.subject = "Welcome to the {} Tasking Manager".format(org_code)
welcome_message.message = text_template
welcome_message.save()

Expand All @@ -60,11 +67,6 @@ def send_message_after_validation(
return # No need to send a message to yourself

user = UserService.get_user_by_id(mapped_by)
if user.validation_message is False:
return # No need to send validation message
if user.projects_notifications is False:
return

text_template = get_template(
"invalidation_message_en.txt"
if status == TaskStatus.INVALIDATED
Expand All @@ -74,9 +76,14 @@ def send_message_after_validation(
"marked invalid" if status == TaskStatus.INVALIDATED else "validated"
)
task_link = MessageService.get_task_link(project_id, task_id)
text_template = text_template.replace("[USERNAME]", user.username)
text_template = text_template.replace("[TASK_LINK]", task_link)
replace_list = [
["[USERNAME]", user.username],
["[TASK_LINK]", task_link],
["[ORG_NAME]", current_app.config["ORG_NAME"]],
]
text_template = template_var_replacing(text_template, replace_list)

messages = []
validation_message = Message()
validation_message.message_type = (
MessageType.INVALIDATION_NOTIFICATION.value
Expand All @@ -87,13 +94,14 @@ def send_message_after_validation(
validation_message.task_id = task_id
validation_message.from_user_id = validated_by
validation_message.to_user_id = mapped_by
validation_message.subject = f"Your mapping in Project {project_id} on {task_link} has just been {status_text}"
validation_message.subject = (
f"{task_link} mapped by you in Project {project_id} has been {status_text}"
)
validation_message.message = text_template
validation_message.add_message()
messages.append(dict(message=validation_message, user=user))

SMTPService.send_email_alert(
user.email_address, user.username, validation_message.id
)
# For email alerts
MessageService._push_messages(messages)

@staticmethod
def send_message_to_all_contributors(project_id: int, message_dto: MessageDTO):
Expand All @@ -118,7 +126,6 @@ def send_message_to_all_contributors(project_id: int, message_dto: MessageDTO):
message = Message.from_dto(contributor[0], message_dto)
message.message_type = MessageType.BROADCAST.value
message.project_id = project_id
message.save()
user = UserService.get_user_by_id(contributor[0])
messages.append(dict(message=message, user=user))

Expand All @@ -145,13 +152,30 @@ def _push_messages(messages):
and obj.message_type == MessageType.PROJECT_ACTIVITY_NOTIFICATION.value
):
continue
if (
user.projects_notifications is False
and obj.message_type == MessageType.BROADCAST.value
):
continue
if user.comments_notifications is False and obj.message_type in (
MessageType.TASK_COMMENT_NOTIFICATION.value,
MessageType.PROJECT_CHAT_NOTIFICATION.value,
):
continue
if user.tasks_notifications is False and obj.message_type in (
MessageType.VALIDATION_NOTIFICATION.value,
MessageType.INVALIDATION_NOTIFICATION.value,
):
messages_objs.append(obj)
continue
messages_objs.append(obj)

SMTPService.send_email_alert(
user.email_address, user.username, message["message"].id
user.email_address,
user.username,
message["message"].id,
clean_html(message["message"].subject),
message["message"].message,
)

if i + 1 % 10 == 0:
Expand Down Expand Up @@ -186,7 +210,7 @@ def send_message_after_comment(
message.task_id = task_id
message.from_user_id = comment_from
message.to_user_id = user.id
message.subject = f"You were mentioned in a comment in Project {project_id} on {task_link}"
message.subject = f"You were mentioned in a comment in {task_link} of Project {project_id}"
message.message = comment
messages.append(dict(message=message, user=user))

Expand Down Expand Up @@ -221,7 +245,7 @@ def send_message_after_comment(
message.project_id = project_id
message.task_id = task_id
message.to_user_id = user.id
message.subject = f"{user_from.username} left a comment in Project {project_id} on {task_link}"
message.subject = f"{user_from.username} left a comment in {task_link} of Project {project_id}"
message.message = comment
messages.append(dict(message=message, user=user))

Expand Down Expand Up @@ -357,7 +381,7 @@ def send_message_after_chat(chat_from: int, chat: str, project_id: int):
message.project_id = project_id
message.from_user_id = chat_from
message.to_user_id = user.id
message.subject = f"You were mentioned in Project Chat on {link}"
message.subject = f"You were mentioned in {link} chat"
message.message = chat
messages.append(dict(message=message, user=user))

Expand All @@ -384,9 +408,7 @@ def send_message_after_chat(chat_from: int, chat: str, project_id: int):
message.message_type = MessageType.PROJECT_CHAT_NOTIFICATION.value
message.project_id = project_id
message.to_user_id = user.id
message.subject = (
f"{chat_from} left a comment in Project {project_link}"
)
message.subject = f"{chat_from} left a comment in {project_link}"
message.message = chat
messages.append(dict(message=message, user=user))

Expand Down Expand Up @@ -441,7 +463,7 @@ def send_favorite_project_activities(user_id: int):
"Recent activities from your contributed/favorited Projects"
)
message.message = (
f"{activity_message} contributed to Project {project_link} recently"
f"{activity_message} contributed to {project_link} recently"
)
messages.append(dict(message=message, user=user))

Expand Down Expand Up @@ -616,23 +638,28 @@ def get_task_link(project_id: int, task_id: int, base_url=None) -> str:
if not base_url:
base_url = current_app.config["APP_BASE_URL"]

link = f'<a href="{base_url}/projects/{project_id}/tasks/?search={task_id}">Task {task_id}</a>'
return link
return f'<a href="{base_url}/projects/{project_id}/tasks/?search={task_id}">Task {task_id}</a>'

@staticmethod
def get_project_link(project_id: int, base_url=None) -> str:
""" Helper method to generate a link to project chat"""
if not base_url:
base_url = current_app.config["APP_BASE_URL"]

link = f'<a href="{base_url}/projects/{project_id}#questionsAndComments">Project {project_id}</a>'
return link
return f'<a href="{base_url}/projects/{project_id}#questionsAndComments">Project {project_id}</a>'

@staticmethod
def get_user_profile_link(user_name: str, base_url=None) -> str:
""" Helper method to generate a link to a user profile"""
if not base_url:
base_url = current_app.config["APP_BASE_URL"]

link = f'<a href="{base_url}/users/{user_name}>{user_name}</a>'
return link
return f'<a href="{base_url}/users/{user_name}">{user_name}</a>'

@staticmethod
def get_user_settings_link(section=None, base_url=None) -> str:
""" Helper method to generate a link to a user profile"""
if not base_url:
base_url = current_app.config["APP_BASE_URL"]

return f'<a href="{base_url}/settings#{section}">User Settings</a>'
51 changes: 32 additions & 19 deletions backend/services/messaging/smtp_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,29 +4,34 @@
from email.mime.text import MIMEText
from itsdangerous import URLSafeTimedSerializer
from flask import current_app
from backend.services.messaging.template_service import get_template
from backend.services.messaging.template_service import (
get_template,
template_var_replacing,
)


class SMTPService:
@staticmethod
def send_verification_email(to_address: str, username: str):
""" Sends a verification email with a unique token so we can verify user owns this email address """

org_code = current_app.config["ORG_CODE"]
# TODO these could be localised if needed, in the future
html_template = get_template("email_verification_en.html")
text_template = get_template("email_verification_en.txt")

verification_url = SMTPService._generate_email_verification_url(
to_address, username
)

html_template = html_template.replace("[USERNAME]", username)
html_template = html_template.replace("[VEFIFICATION_LINK]", verification_url)

text_template = text_template.replace("[USERNAME]", username)
text_template = text_template.replace("[VEFIFICATION_LINK]", verification_url)

subject = "HOT Tasking Manager - Email Verification"
replace_list = [
["[USERNAME]", username],
["[VERIFICATION_LINK]", verification_url],
["[ORG_CODE]", org_code],
["[ORG_NAME]", current_app.config["ORG_NAME"]],
]
html_template = template_var_replacing(html_template, replace_list)
text_template = template_var_replacing(text_template, replace_list)

subject = "{} Tasking Manager - Email Verification".format(org_code)
SMTPService._send_message(to_address, subject, html_template, text_template)

return True
Expand All @@ -47,9 +52,16 @@ def send_contact_admin_email(data):
SMTPService._send_message(email_to, subject, message, message)

@staticmethod
def send_email_alert(to_address: str, username: str, message_id: int = None):
""" Send an email to user to alert them they have a new message"""
def send_email_alert(
to_address: str, username: str, message_id: int, subject: str, content: str
):
"""Send an email to user to alert that they have a new message"""
current_app.logger.debug(f"Test if email required {to_address}")
org_code = current_app.config["ORG_CODE"]
settings_url = "{}/settings#notifications".format(
current_app.config["APP_BASE_URL"]
)

if not to_address:
return False # Many users will not have supplied email address so return
message_path = ""
Expand All @@ -60,14 +72,15 @@ def send_email_alert(to_address: str, username: str, message_id: int = None):
html_template = get_template("message_alert_en.html")
text_template = get_template("message_alert_en.txt")
inbox_url = f"{current_app.config['APP_BASE_URL']}/inbox{message_path}"
replace_list = [
["[ORG_CODE]", org_code],
["[PROFILE_LINK]", inbox_url],
["[SETTINGS_LINK]", settings_url],
["[CONTENT]", content],
]
html_template = template_var_replacing(html_template, replace_list)
text_template = template_var_replacing(text_template, replace_list)

html_template = html_template.replace("[USERNAME]", username)
html_template = html_template.replace("[PROFILE_LINK]", inbox_url)

text_template = text_template.replace("[USERNAME]", username)
text_template = text_template.replace("[PROFILE_LINK]", inbox_url)

subject = "You have a new message on the HOT Tasking Manager"
SMTPService._send_message(to_address, subject, html_template, text_template)

return True
Expand Down
17 changes: 12 additions & 5 deletions backend/services/messaging/template_service.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import os
import urllib.parse
import re
from flask import current_app


Expand All @@ -20,7 +20,14 @@ def get_template(template_name: str) -> str:
raise ValueError("Unable open file {0}".format(template_location))


def get_profile_url(username: str):
""" Helper function returns the URL of the supplied users profile """
base_url = current_app.config["APP_BASE_URL"]
return f"{base_url}/user/{urllib.parse.quote(username)}"
def template_var_replacing(content: str, replace_list: list) -> str:
"""Receives a content string and executes a replace operation to each item on the list. """
for term in replace_list:
content = content.replace(term[0], term[1])
return content


def clean_html(raw_html):
cleanr = re.compile("<.*?>")
clean_text = re.sub(cleanr, "", raw_html)
return clean_text
Loading

0 comments on commit 1fee3b3

Please sign in to comment.