Skip to content

Commit

Permalink
Merge pull request #3748 from hotosm/develop
Browse files Browse the repository at this point in the history
v4.1.11 Release
  • Loading branch information
willemarcel committed Oct 20, 2020
2 parents 4f7bfee + 2972007 commit 92c9aad
Show file tree
Hide file tree
Showing 66 changed files with 722 additions and 839 deletions.
8 changes: 8 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
.*
*.yml
**/*.pyc
**/node_modules
**/npm-debug.log
logs/
docs/
scripts/
4 changes: 1 addition & 3 deletions backend/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,9 +49,7 @@ def create_app(env=None):
if EnvironmentConfig.SENTRY_BACKEND_DSN:
sentry_init()

app = Flask(
__name__,
)
app = Flask(__name__, template_folder="services/messaging/templates/")

# Load configuration options from environment
app.config.from_object("backend.config.EnvironmentConfig")
Expand Down
4 changes: 4 additions & 0 deletions backend/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@ class EnvironmentConfig:
API_VERSION = os.getenv("TM_APP_API_VERSION", "v2")
ORG_CODE = os.getenv("TM_ORG_CODE", "")
ORG_NAME = os.getenv("TM_ORG_NAME", "")
ORG_LOGO = os.getenv(
"TM_ORG_LOGO",
"https://cdn.hotosm.org/tasking-manager/uploads/1588741335578_hot-logo.png",
)
ENVIRONMENT = os.getenv("TM_ENVIRONMENT", "")
# The default tag used in the OSM changeset comment
DEFAULT_CHANGESET_COMMENT = os.getenv("TM_DEFAULT_CHANGESET_COMMENT", None)
Expand Down
10 changes: 7 additions & 3 deletions backend/services/messaging/chat_service.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import threading

from flask import current_app

from backend.models.dtos.message_dto import ChatMessageDTO, ProjectChatDTO
from backend.models.postgis.project_chat import ProjectChat
from backend.services.messaging.message_service import MessageService
Expand Down Expand Up @@ -57,10 +60,11 @@ def post_message(

if is_manager_permission or is_team_member or is_allowed_user:
chat_message = ProjectChat.create_from_dto(chat_dto)
MessageService.send_message_after_chat(
chat_dto.user_id, chat_message.message, chat_dto.project_id
)
db.session.commit()
threading.Thread(
target=MessageService.send_message_after_chat,
args=(chat_dto.user_id, chat_message.message, chat_dto.project_id),
).start()
# Ensure we return latest messages after post
return ProjectChat.get_messages(chat_dto.project_id, 1, 5)
else:
Expand Down
92 changes: 47 additions & 45 deletions backend/services/messaging/message_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
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_txt_template,
template_var_replacing,
clean_html,
)
Expand All @@ -41,7 +41,7 @@ class MessageService:
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 = get_txt_template("welcome_message_en.txt")
replace_list = [
["[USERNAME]", user.username],
["[ORG_CODE]", org_code],
Expand All @@ -68,7 +68,7 @@ def send_message_after_validation(
return # No need to send a message to yourself

user = UserService.get_user_by_id(mapped_by)
text_template = get_template(
text_template = get_txt_template(
"invalidation_message_en.txt"
if status == TaskStatus.INVALIDATED
else "validation_message_en.txt"
Expand Down Expand Up @@ -183,6 +183,7 @@ def _push_messages(messages):
message["message"].project_id,
clean_html(message["message"].subject),
message["message"].message,
obj.message_type,
)

if i + 1 % 10 == 0:
Expand Down Expand Up @@ -370,64 +371,65 @@ def send_invite_to_join_team(
@staticmethod
def send_message_after_chat(chat_from: int, chat: str, project_id: int):
""" Send alert to user if they were @'d in a chat message """
current_app.logger.debug("Sending Message After Chat")
usernames = MessageService._parse_message_for_username(chat, project_id)

if len(usernames) == 0:
return # Nobody @'d so return

link = MessageService.get_project_link(project_id, include_chat_section=True)

messages = []
for username in usernames:
current_app.logger.debug(f"Searching for {username}")
try:
user = UserService.get_user_by_username(username)
except NotFound:
current_app.logger.error(f"Username {username} not found")
continue # If we can't find the user, keep going no need to fail

message = Message()
message.message_type = MessageType.MENTION_NOTIFICATION.value
message.project_id = project_id
message.from_user_id = chat_from
message.to_user_id = user.id
message.subject = f"You were mentioned in {link} chat"
message.message = chat
messages.append(dict(message=message, user=user))

MessageService._push_messages(messages)

query = (
""" select user_id from project_favorites where project_id = :project_id"""
)
result = db.engine.execute(text(query), project_id=project_id)
favorited_users = [r[0] for r in result]
# Because message-all run on background thread it needs it's own app context
app = create_app()
with app.app_context():
usernames = MessageService._parse_message_for_username(chat, project_id)
if len(usernames) == 0:
return # Nobody @'d so return

if len(favorited_users) != 0:
project_link = MessageService.get_project_link(
link = MessageService.get_project_link(
project_id, include_chat_section=True
)
# project_title = ProjectService.get_project_title(project_id)
messages = []
for user_id in favorited_users:

messages = []
for username in usernames:
current_app.logger.debug(f"Searching for {username}")
try:
user = UserService.get_user_dto_by_id(user_id)
user = UserService.get_user_by_username(username)
except NotFound:
current_app.logger.error(f"Username {username} not found")
continue # If we can't find the user, keep going no need to fail

message = Message()
message.message_type = MessageType.PROJECT_CHAT_NOTIFICATION.value
message.message_type = MessageType.MENTION_NOTIFICATION.value
message.project_id = project_id
message.from_user_id = chat_from
message.to_user_id = user.id
message.subject = f"{chat_from} left a comment in {project_link}"
message.subject = f"You were mentioned in {link} chat"
message.message = chat
messages.append(dict(message=message, user=user))

# it's important to keep that line inside the if to avoid duplicated emails
MessageService._push_messages(messages)

query = """ select user_id from project_favorites where project_id = :project_id"""
result = db.engine.execute(text(query), project_id=project_id)
favorited_users = [r[0] for r in result]

if len(favorited_users) != 0:
project_link = MessageService.get_project_link(
project_id, include_chat_section=True
)
# project_title = ProjectService.get_project_title(project_id)
messages = []
for user_id in favorited_users:

try:
user = UserService.get_user_dto_by_id(user_id)
except NotFound:
continue # If we can't find the user, keep going no need to fail

message = Message()
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_link}"
message.message = chat
messages.append(dict(message=message, user=user))

# it's important to keep that line inside the if to avoid duplicated emails
MessageService._push_messages(messages)

@staticmethod
def send_favorite_project_activities(user_id: int):
current_app.logger.debug("Sending Favorite Project Activities")
Expand Down
66 changes: 25 additions & 41 deletions backend/services/messaging/smtp_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
from flask import current_app
from backend.services.messaging.template_service import (
get_template,
template_var_replacing,
format_username_link,
)

Expand All @@ -15,26 +14,18 @@ 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
)
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)

values = {
"USERNAME": username,
"VERIFICATION_LINK": verification_url,
}
html_template = get_template("email_verification_en.html", values)

subject = "Confirm your email address"
SMTPService._send_message(to_address, subject, html_template)
return True

@staticmethod
Expand Down Expand Up @@ -63,10 +54,10 @@ def send_email_alert(
project_id: int,
subject: str,
content: str,
message_type: int,
):
"""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"]
from_user_link = f"{current_app.config['APP_BASE_URL']}/users/{from_username}"
project_link = f"{current_app.config['APP_BASE_URL']}/projects/{project_id}"
settings_url = "{}/settings#notifications".format(
Expand All @@ -79,33 +70,25 @@ def send_email_alert(
if message_id is not None:
message_path = f"/message/{message_id}"

# TODO these could be localised if needed, in the future
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 = [
["[FROM_USER_LINK]", from_user_link],
["[FROM_USERNAME]", from_username],
["[PROJECT_LINK]", project_link],
["[PROJECT_ID]", str(project_id)],
["[ORG_CODE]", org_code],
["[PROFILE_LINK]", inbox_url],
["[SETTINGS_LINK]", settings_url],
]
html_replace_list = replace_list + [
["[CONTENT]", format_username_link(content)]
]
html_template = template_var_replacing(html_template, html_replace_list)
replace_list += [["[CONTENT]", content]]
text_template = template_var_replacing(text_template, replace_list)

SMTPService._send_message(to_address, subject, html_template, text_template)
values = {
"FROM_USER_LINK": from_user_link,
"FROM_USERNAME": from_username,
"PROJECT_LINK": project_link,
"PROJECT_ID": str(project_id) if project_id is not None else None,
"PROFILE_LINK": inbox_url,
"SETTINGS_LINK": settings_url,
"CONTENT": format_username_link(content),
"MESSAGE_TYPE": message_type,
}
html_template = get_template("message_alert_en.html", values)
SMTPService._send_message(to_address, subject, html_template)

return True

@staticmethod
def _send_message(
to_address: str, subject: str, html_message: str, text_message: str
to_address: str, subject: str, html_message: str, text_message: str = None
):
""" Helper sends SMTP message """
from_address = current_app.config["EMAIL_FROM_ADDRESS"]
Expand All @@ -120,10 +103,11 @@ def _send_message(
msg["To"] = to_address

# Record the MIME types of both parts - text/plain and text/html.
part1 = MIMEText(text_message, "plain")
part2 = MIMEText(html_message, "html")
msg.attach(part1)
msg.attach(part2)
if text_message:
part1 = MIMEText(text_message, "plain")
msg.attach(part1)

current_app.logger.debug(f"Sending email via SMTP {to_address}")
if current_app.config["LOG_LEVEL"] == "DEBUG":
Expand Down
24 changes: 21 additions & 3 deletions backend/services/messaging/template_service.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import os
import re

from flask import current_app
from flask import current_app, render_template


def get_template(template_name: str) -> str:
def get_txt_template(template_name: str):
"""
Helper function to read the template from disk and return as a string to be manipulated
:param template_name: The template we want to load
Expand All @@ -21,6 +21,24 @@ def get_template(template_name: str) -> str:
raise ValueError("Unable open file {0}".format(template_location))


def get_template(template_name: str, values: dict) -> str:
"""
Helper function to read a HTML template from disk and return it using flask's
render_template function
:param template_name: The template we want to load
:return: Template as a string
"""
try:
values["ORG_CODE"] = current_app.config["ORG_CODE"]
values["ORG_NAME"] = current_app.config["ORG_NAME"]
values["ORG_LOGO"] = current_app.config["ORG_LOGO"]
values["APP_BASE_URL"] = current_app.config["APP_BASE_URL"]
return render_template(template_name, values=values)
except (FileNotFoundError, TypeError):
current_app.logger.error("Unable open file {0}".format(template_name))
raise ValueError("Unable open file {0}".format(template_name))


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:
Expand All @@ -41,6 +59,6 @@ def format_username_link(content):
username = name[2:-1]
content = content.replace(
name,
f'<a href="{current_app.config["APP_BASE_URL"]}/users/{username}/">@{username}</a>',
f'<a style="color: #d73f3f" href="{current_app.config["APP_BASE_URL"]}/users/{username}/">@{username}</a>',
)
return content
Loading

0 comments on commit 92c9aad

Please sign in to comment.