+ {% if values['ORG_LOGO'] %} + + {% endif %} + {{values['ORG_CODE']}} Tasking Manager +
+
+
|
+
diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000000..cb06c8a56e --- /dev/null +++ b/.dockerignore @@ -0,0 +1,8 @@ +.* +*.yml +**/*.pyc +**/node_modules +**/npm-debug.log +logs/ +docs/ +scripts/ diff --git a/backend/__init__.py b/backend/__init__.py index 174c108bc0..40532a491b 100644 --- a/backend/__init__.py +++ b/backend/__init__.py @@ -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") diff --git a/backend/config.py b/backend/config.py index f6c6026597..12f335d21b 100644 --- a/backend/config.py +++ b/backend/config.py @@ -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) diff --git a/backend/services/messaging/chat_service.py b/backend/services/messaging/chat_service.py index d318d4f1fe..1d50dcf230 100644 --- a/backend/services/messaging/chat_service.py +++ b/backend/services/messaging/chat_service.py @@ -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 @@ -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: diff --git a/backend/services/messaging/message_service.py b/backend/services/messaging/message_service.py index 9b3fa6b7d7..f8a9a47b19 100644 --- a/backend/services/messaging/message_service.py +++ b/backend/services/messaging/message_service.py @@ -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, ) @@ -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], @@ -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" @@ -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: @@ -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") diff --git a/backend/services/messaging/smtp_service.py b/backend/services/messaging/smtp_service.py index 88162c2612..cf664f56a2 100644 --- a/backend/services/messaging/smtp_service.py +++ b/backend/services/messaging/smtp_service.py @@ -6,7 +6,6 @@ from flask import current_app from backend.services.messaging.template_service import ( get_template, - template_var_replacing, format_username_link, ) @@ -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 @@ -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( @@ -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"] @@ -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": diff --git a/backend/services/messaging/template_service.py b/backend/services/messaging/template_service.py index 635a370287..f02c00ed0d 100644 --- a/backend/services/messaging/template_service.py +++ b/backend/services/messaging/template_service.py @@ -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 @@ -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: @@ -41,6 +59,6 @@ def format_username_link(content): username = name[2:-1] content = content.replace( name, - f'@{username}', + f'@{username}', ) return content diff --git a/backend/services/messaging/templates/base.html b/backend/services/messaging/templates/base.html new file mode 100644 index 0000000000..d3a428658b --- /dev/null +++ b/backend/services/messaging/templates/base.html @@ -0,0 +1,96 @@ + + +
+ + + + + + ++ |
+
+
+ + {% if values['ORG_LOGO'] %} + + {% endif %} + {{values['ORG_CODE']}} Tasking Manager ++
|
+ + |
- Hi [USERNAME]
- Thank you for supplying your email address to the [ORG_CODE] Tasking Manager, please click the link below to verify you own this address
- Click here to verify your email address
+{% extends "base.html" %}
+{% block content %}
+ Hi {{values['USERNAME']}},
+ Thank you for supplying your email address to the {{values['ORG_CODE']}} Tasking Manager, please click the link below to verify you own this address
+ Click here to verify your email address
Please ignore this email if you have received it by mistake.
Many thanks,
- [ORG_NAME]
-
@[FROM_USERNAME] mentioned you in a comment on Project [PROJECT_ID]
-
- [CONTENT]
-
- Access your inbox to read all your messages on the [ORG_CODE] Tasking Manager.
- You can opt-out of these emails by visiting your user settings page and adjusting your notification preferences.
-