diff --git a/.travis.yml b/.travis.yml index dbd5c95..602e547 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,6 +5,13 @@ python: - "3.5" - "3.6" +services: + - docker + - redis-server + +before_install: + - docker pull mailhog/mailhog + - docker run -d -p 1025:1025 -p 8025:8025 mailhog/mailhog install: - pip install --upgrade pip setuptools - pip install -e ".[testing]" diff --git a/README.md b/README.md index d247b6b..77b040e 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,7 @@ on Debian Stretch (9) with sudo: sudo apt update sudo apt install git sudo apt install python3 python3-venv python3-dev python3-pip + sudo apt install redis-server ### Get the source ### @@ -112,10 +113,20 @@ run wsgidav server: ## Run Tests and others checks ## +Before running some functional test related to email, you need a local working *MailHog* +see here : https://github.com/mailhog/MailHog + +You can run it this way with docker : + + docker pull mailhog/mailhog + docker run -d -p 1025:1025 -p 8025:8025 mailhog/mailhog + Run your project's tests: pytest +### Lints and others checks ### + Run mypy checks: mypy --ignore-missing-imports --disallow-untyped-defs tracim diff --git a/setup.py b/setup.py index 3f8df60..db80dca 100644 --- a/setup.py +++ b/setup.py @@ -34,6 +34,11 @@ # others 'filedepot', 'babel', + # mail-notifier + 'mako', + 'lxml', + 'redis', + 'rq', ] tests_require = [ @@ -42,6 +47,7 @@ 'pytest-cov', 'pep8', 'mypy', + 'requests' ] mysql_require = [ diff --git a/tests_configs.ini b/tests_configs.ini new file mode 100644 index 0000000..4c2b2ce --- /dev/null +++ b/tests_configs.ini @@ -0,0 +1,57 @@ +[base_test] +sqlalchemy.url = sqlite:///:memory: +depot_storage_name = test +depot_storage_dir = /tmp/test/depot +user.auth_token.validity = 604800 +preview_cache_dir = /tmp/test/preview_cache + +[mail_test] +sqlalchemy.url = sqlite:///:memory: +depot_storage_name = test +depot_storage_dir = /tmp/test/depot +user.auth_token.validity = 604800 +preview_cache_dir = /tmp/test/preview_cache +email.notification.activated = true +email.notification.from.email = test_user_from+{user_id}@localhost +email.notification.from.default_label = Tracim Notifications +email.notification.reply_to.email = test_user_reply+{content_id}@localhost +email.notification.references.email = test_user_refs+{content_id}@localhost +email.notification.content_update.template.html = %(here)s/tracim/templates/mail/content_update_body_html.mak +email.notification.content_update.template.text = %(here)s/tracim/templates/mail/content_update_body_text.mak +email.notification.created_account.template.html = %(here)s/tracim/templates/mail/created_account_body_html.mak +email.notification.created_account.template.text = %(here)s/tracim/templates/mail/created_account_body_text.mak +# Note: items between { and } are variable names. Do not remove / rename them +email.notification.content_update.subject = [{website_title}] [{workspace_label}] {content_label} ({content_status_label}) +email.notification.created_account.subject = [{website_title}] Created account +# processing_mode may be sync or async +email.notification.processing_mode = sync +email.notification.smtp.server = 127.0.0.1 +email.notification.smtp.port = 1025 +email.notification.smtp.user = test_user +email.notification.smtp.password = just_a_password + +[mail_test_async] +sqlalchemy.url = sqlite:///:memory: +depot_storage_name = test +depot_storage_dir = /tmp/test/depot +user.auth_token.validity = 604800 +preview_cache_dir = /tmp/test/preview_cache +email.notification.activated = true +email.notification.from.email = test_user_from+{user_id}@localhost +email.notification.from.default_label = Tracim Notifications +email.notification.reply_to.email = test_user_reply+{content_id}@localhost +email.notification.references.email = test_user_refs+{content_id}@localhost +email.notification.content_update.template.html = %(here)s/tracim/templates/mail/content_update_body_html.mak +email.notification.content_update.template.text = %(here)s/tracim/templates/mail/content_update_body_text.mak +email.notification.created_account.template.html = %(here)s/tracim/templates/mail/created_account_body_html.mak +email.notification.created_account.template.text = %(here)s/tracim/templates/mail/created_account_body_text.mak +# Note: items between { and } are variable names. Do not remove / rename them +email.notification.content_update.subject = [{website_title}] [{workspace_label}] {content_label} ({content_status_label}) +email.notification.created_account.subject = [{website_title}] Created account +# processing_mode may be sync or async +email.notification.processing_mode = sync +email.processing_mode = async +email.notification.smtp.server = 127.0.0.1 +email.notification.smtp.port = 1025 +email.notification.smtp.user = test_user +email.notification.smtp.password = just_a_password \ No newline at end of file diff --git a/tracim/command/user.py b/tracim/command/user.py index 4687945..88bc6e2 100644 --- a/tracim/command/user.py +++ b/tracim/command/user.py @@ -12,6 +12,7 @@ #from tracim.lib.daemons import RadicaleDaemon #from tracim.lib.email import get_email_manager from tracim.exceptions import AlreadyExistError +from tracim.exceptions import NotificationNotSend from tracim.exceptions import CommandAbortedError from tracim.lib.core.group import GroupApi from tracim.lib.core.user import UserApi @@ -106,7 +107,13 @@ def _remove_user_from_named_group( group.users.remove(user) self._session.flush() - def _create_user(self, login: str, password: str, **kwargs) -> User: + def _create_user( + self, + login: str, + password: str, + do_notify: bool, + **kwargs + ) -> User: if not password: if self._password_required(): raise CommandAbortedError( @@ -115,18 +122,23 @@ def _create_user(self, login: str, password: str, **kwargs) -> User: password = '' try: - user = self._user_api.create_user(email=login) - user.password = password - self._user_api.save(user) + user = self._user_api.create_user( + email=login, + password=password, + do_save=True, + do_notify=do_notify, + ) # TODO - G.M - 04-04-2018 - [Caldav] Check this code # # We need to enable radicale if it not already done # daemons = DaemonsManager() # daemons.run('radicale', RadicaleDaemon) - self._user_api.execute_created_user_actions(user) except IntegrityError: self._session.rollback() raise AlreadyExistError() + except NotificationNotSend as exception: + self._session.rollback() + raise exception return user @@ -166,10 +178,13 @@ def _proceed_user(self, parsed_args: argparse.Namespace) -> User: try: user = self._create_user( login=parsed_args.login, - password=parsed_args.password + password=parsed_args.password, + do_notify=parsed_args.send_email, ) except AlreadyExistError: raise CommandAbortedError("Error: User already exist (use `user update` command instead)") + except NotificationNotSend: + raise CommandAbortedError("Error: Cannot send email notification, user not created.") # TODO - G.M - 04-04-2018 - [Email] Check this code # if parsed_args.send_email: # email_manager = get_email_manager() diff --git a/tracim/config.py b/tracim/config.py index 82ce5a3..451cf6d 100644 --- a/tracim/config.py +++ b/tracim/config.py @@ -4,6 +4,8 @@ from tracim.lib.utils.logger import logger from depot.manager import DepotManager +from tracim.models.data import ActionDescription, ContentType + class CFG(object): """Object used for easy access to config file parameters.""" @@ -131,87 +133,87 @@ def __init__(self, settings): # TODO - G.M - 27-03-2018 - [Email] Restore email config ### # EMAIL related stuff (notification, reply) - ### - # - # self.EMAIL_NOTIFICATION_NOTIFIED_EVENTS = [ - # # ActionDescription.COMMENT, - # # ActionDescription.CREATION, - # # ActionDescription.EDITION, - # # ActionDescription.REVISION, - # # ActionDescription.STATUS_UPDATE - # ] - # - # self.EMAIL_NOTIFICATION_NOTIFIED_CONTENTS = [ - # # ContentType.Page, - # # ContentType.Thread, - # # ContentType.File, - # # ContentType.Comment, - # # ContentType.Folder -- Folder is skipped - # ] - # if settings.get('email.notification.from'): - # raise Exception( - # 'email.notification.from configuration is deprecated. ' - # 'Use instead email.notification.from.email and ' - # 'email.notification.from.default_label.' - # ) - # - # self.EMAIL_NOTIFICATION_FROM_EMAIL = settings.get( - # 'email.notification.from.email', - # ) - # self.EMAIL_NOTIFICATION_FROM_DEFAULT_LABEL = settings.get( - # 'email.notification.from.default_label' - # ) - # self.EMAIL_NOTIFICATION_REPLY_TO_EMAIL = settings.get( - # 'email.notification.reply_to.email', - # ) - # self.EMAIL_NOTIFICATION_REFERENCES_EMAIL = settings.get( - # 'email.notification.references.email' - # ) - # self.EMAIL_NOTIFICATION_CONTENT_UPDATE_TEMPLATE_HTML = settings.get( - # 'email.notification.content_update.template.html', - # ) - # self.EMAIL_NOTIFICATION_CONTENT_UPDATE_TEMPLATE_TEXT = settings.get( - # 'email.notification.content_update.template.text', - # ) - # self.EMAIL_NOTIFICATION_CREATED_ACCOUNT_TEMPLATE_HTML = settings.get( - # 'email.notification.created_account.template.html', - # './tracim/templates/mail/created_account_body_html.mak', - # ) - # self.EMAIL_NOTIFICATION_CREATED_ACCOUNT_TEMPLATE_TEXT = settings.get( - # 'email.notification.created_account.template.text', - # './tracim/templates/mail/created_account_body_text.mak', - # ) - # self.EMAIL_NOTIFICATION_CONTENT_UPDATE_SUBJECT = settings.get( - # 'email.notification.content_update.subject', - # ) - # self.EMAIL_NOTIFICATION_CREATED_ACCOUNT_SUBJECT = settings.get( - # 'email.notification.created_account.subject', - # '[{website_title}] Created account', - # ) - # self.EMAIL_NOTIFICATION_PROCESSING_MODE = settings.get( - # 'email.notification.processing_mode', - # ) - # + ## + + self.EMAIL_NOTIFICATION_NOTIFIED_EVENTS = [ + ActionDescription.COMMENT, + ActionDescription.CREATION, + ActionDescription.EDITION, + ActionDescription.REVISION, + ActionDescription.STATUS_UPDATE + ] + + self.EMAIL_NOTIFICATION_NOTIFIED_CONTENTS = [ + ContentType.Page, + ContentType.Thread, + ContentType.File, + ContentType.Comment, + # ContentType.Folder -- Folder is skipped + ] + if settings.get('email.notification.from'): + raise Exception( + 'email.notification.from configuration is deprecated. ' + 'Use instead email.notification.from.email and ' + 'email.notification.from.default_label.' + ) + + self.EMAIL_NOTIFICATION_FROM_EMAIL = settings.get( + 'email.notification.from.email', + ) + self.EMAIL_NOTIFICATION_FROM_DEFAULT_LABEL = settings.get( + 'email.notification.from.default_label' + ) + self.EMAIL_NOTIFICATION_REPLY_TO_EMAIL = settings.get( + 'email.notification.reply_to.email', + ) + self.EMAIL_NOTIFICATION_REFERENCES_EMAIL = settings.get( + 'email.notification.references.email' + ) + self.EMAIL_NOTIFICATION_CONTENT_UPDATE_TEMPLATE_HTML = settings.get( + 'email.notification.content_update.template.html', + ) + self.EMAIL_NOTIFICATION_CONTENT_UPDATE_TEMPLATE_TEXT = settings.get( + 'email.notification.content_update.template.text', + ) + self.EMAIL_NOTIFICATION_CREATED_ACCOUNT_TEMPLATE_HTML = settings.get( + 'email.notification.created_account.template.html', + './tracim/templates/mail/created_account_body_html.mak', + ) + self.EMAIL_NOTIFICATION_CREATED_ACCOUNT_TEMPLATE_TEXT = settings.get( + 'email.notification.created_account.template.text', + './tracim/templates/mail/created_account_body_text.mak', + ) + self.EMAIL_NOTIFICATION_CONTENT_UPDATE_SUBJECT = settings.get( + 'email.notification.content_update.subject', + ) + self.EMAIL_NOTIFICATION_CREATED_ACCOUNT_SUBJECT = settings.get( + 'email.notification.created_account.subject', + '[{website_title}] Created account', + ) + self.EMAIL_NOTIFICATION_PROCESSING_MODE = settings.get( + 'email.notification.processing_mode', + ) + self.EMAIL_NOTIFICATION_ACTIVATED = asbool(settings.get( 'email.notification.activated', )) - # self.EMAIL_NOTIFICATION_SMTP_SERVER = settings.get( - # 'email.notification.smtp.server', - # ) - # self.EMAIL_NOTIFICATION_SMTP_PORT = settings.get( - # 'email.notification.smtp.port', - # ) - # self.EMAIL_NOTIFICATION_SMTP_USER = settings.get( - # 'email.notification.smtp.user', - # ) - # self.EMAIL_NOTIFICATION_SMTP_PASSWORD = settings.get( - # 'email.notification.smtp.password', - # ) - # self.EMAIL_NOTIFICATION_LOG_FILE_PATH = settings.get( - # 'email.notification.log_file_path', - # None, - # ) - # + self.EMAIL_NOTIFICATION_SMTP_SERVER = settings.get( + 'email.notification.smtp.server', + ) + self.EMAIL_NOTIFICATION_SMTP_PORT = settings.get( + 'email.notification.smtp.port', + ) + self.EMAIL_NOTIFICATION_SMTP_USER = settings.get( + 'email.notification.smtp.user', + ) + self.EMAIL_NOTIFICATION_SMTP_PASSWORD = settings.get( + 'email.notification.smtp.password', + ) + self.EMAIL_NOTIFICATION_LOG_FILE_PATH = settings.get( + 'email.notification.log_file_path', + None, + ) + # self.EMAIL_REPLY_ACTIVATED = asbool(settings.get( # 'email.reply.activated', # False, @@ -267,36 +269,36 @@ def __init__(self, settings): # mandatory_msg.format('email.reply.lockfile_path') # ) # - # self.EMAIL_PROCESSING_MODE = settings.get( - # 'email.processing_mode', - # 'sync', - # ).upper() - # - # if self.EMAIL_PROCESSING_MODE not in ( - # self.CST.ASYNC, - # self.CST.SYNC, - # ): - # raise Exception( - # 'email.processing_mode ' - # 'can ''be "{}" or "{}", not "{}"'.format( - # self.CST.ASYNC, - # self.CST.SYNC, - # self.EMAIL_PROCESSING_MODE, - # ) - # ) - # - # self.EMAIL_SENDER_REDIS_HOST = settings.get( - # 'email.async.redis.host', - # 'localhost', - # ) - # self.EMAIL_SENDER_REDIS_PORT = int(settings.get( - # 'email.async.redis.port', - # 6379, - # )) - # self.EMAIL_SENDER_REDIS_DB = int(settings.get( - # 'email.async.redis.db', - # 0, - # )) + self.EMAIL_PROCESSING_MODE = settings.get( + 'email.processing_mode', + 'sync', + ).upper() + + if self.EMAIL_PROCESSING_MODE not in ( + self.CST.ASYNC, + self.CST.SYNC, + ): + raise Exception( + 'email.processing_mode ' + 'can ''be "{}" or "{}", not "{}"'.format( + self.CST.ASYNC, + self.CST.SYNC, + self.EMAIL_PROCESSING_MODE, + ) + ) + + self.EMAIL_SENDER_REDIS_HOST = settings.get( + 'email.async.redis.host', + 'localhost', + ) + self.EMAIL_SENDER_REDIS_PORT = int(settings.get( + 'email.async.redis.port', + 6379, + )) + self.EMAIL_SENDER_REDIS_DB = int(settings.get( + 'email.async.redis.db', + 0, + )) ### # WSGIDAV (Webdav server) diff --git a/tracim/exceptions.py b/tracim/exceptions.py index 2d88e96..ae55eab 100644 --- a/tracim/exceptions.py +++ b/tracim/exceptions.py @@ -92,5 +92,10 @@ class AuthenticationFailed(TracimException): class WrongUserPassword(TracimException): pass + class UserNotExist(TracimException): - pass \ No newline at end of file + pass + + +class NotificationNotSend(TracimException): + pass diff --git a/tracim/fixtures/content.py b/tracim/fixtures/content.py index 181bbb3..7bb458b 100644 --- a/tracim/fixtures/content.py +++ b/tracim/fixtures/content.py @@ -58,12 +58,14 @@ def insert(self): workspace=w1, label='w1f1', do_save=True, + do_notify=False, ) w1f2 = content_api.create( content_type=ContentType.Folder, workspace=w1, label='w1f2', do_save=True, + do_notify=False, ) w2f1 = content_api.create( @@ -71,12 +73,14 @@ def insert(self): workspace=w2, label='w2f1', do_save=True, + do_notify=False, ) w2f2 = content_api.create( content_type=ContentType.Folder, workspace=w2, label='w2f2', do_save=True, + do_notify=False, ) w3f1 = content_api.create( @@ -84,6 +88,7 @@ def insert(self): workspace=w3, label='w3f3', do_save=True, + do_notify=False, ) # Pages, threads, .. @@ -93,6 +98,7 @@ def insert(self): parent=w1f1, label='w1f1p1', do_save=True, + do_notify=False, ) w1f1t1 = content_api.create( content_type=ContentType.Thread, @@ -100,6 +106,7 @@ def insert(self): parent=w1f1, label='w1f1t1', do_save=False, + do_notify=False, ) w1f1t1.description = 'w1f1t1 description' self._session.add(w1f1t1) @@ -109,6 +116,7 @@ def insert(self): parent=w1f1, label='w1f1d1', do_save=False, + do_notify=False, ) w1f1d1_txt.file_extension = '.txt' w1f1d1_txt.depot_file = FileIntent( @@ -123,6 +131,7 @@ def insert(self): parent=w1f1, label='w1f1d2', do_save=False, + do_notify=False, ) w1f1d2_html.file_extension = '.html' w1f1d2_html.depot_file = FileIntent( @@ -137,6 +146,7 @@ def insert(self): label='w1f1f1', parent=w1f1, do_save=True, + do_notify=False, ) w2f1p1 = content_api.create( @@ -145,5 +155,6 @@ def insert(self): parent=w2f1, label='w2f1p1', do_save=True, + do_notify=False, ) self._session.flush() diff --git a/tracim/lib/core/content.py b/tracim/lib/core/content.py index e2a20f7..ec769e7 100644 --- a/tracim/lib/core/content.py +++ b/tracim/lib/core/content.py @@ -376,7 +376,7 @@ def get_child_folders(self, parent: Content=None, workspace: Workspace=None, fil return result - def create(self, content_type: str, workspace: Workspace, parent: Content=None, label:str ='', do_save=False, is_temporary: bool=False) -> Content: + def create(self, content_type: str, workspace: Workspace, parent: Content=None, label:str ='', do_save=False, is_temporary: bool=False, do_notify=True) -> Content: assert content_type in ContentType.allowed_types() if content_type == ContentType.Folder and not label: @@ -399,7 +399,7 @@ def create(self, content_type: str, workspace: Workspace, parent: Content=None, if do_save: self._session.add(content) - self.save(content, ActionDescription.CREATION) + self.save(content, ActionDescription.CREATION, do_notify=do_notify) return content @@ -1127,8 +1127,9 @@ def do_notify(self, content: Content): :return: """ NotifierFactory.create( - self._config, - self._user + config=self._config, + current_user=self._user, + session=self._session, ).notify_content_update(content) def get_keywords(self, search_string, search_string_separators=None) -> [str]: diff --git a/tracim/lib/core/notifications.py b/tracim/lib/core/notifications.py index 9ad502a..44d0546 100644 --- a/tracim/lib/core/notifications.py +++ b/tracim/lib/core/notifications.py @@ -1,5 +1,7 @@ # -*- coding: utf-8 -*- +from sqlalchemy.orm import Session +from tracim import CFG from tracim.lib.utils.logger import logger from tracim.models.auth import User from tracim.models.data import Content @@ -9,7 +11,11 @@ class INotifier(object): """ Interface for Notifier instances """ - def __init__(self, config, current_user: User=None): + def __init__(self, + config: CFG, + session: Session, + current_user: User=None, + ) -> None: pass def notify_content_update(self, content: Content): @@ -19,17 +25,28 @@ def notify_content_update(self, content: Content): class NotifierFactory(object): @classmethod - def create(cls, config, current_user: User=None) -> INotifier: + def create(cls, config, session, current_user: User=None) -> INotifier: if not config.EMAIL_NOTIFICATION_ACTIVATED: - return DummyNotifier(config, current_user) - return EmailNotifier(config, current_user) + return DummyNotifier(config, session, current_user) + from tracim.lib.mail_notifier.notifier import EmailNotifier + return EmailNotifier(config, session, current_user) class DummyNotifier(INotifier): send_count = 0 - def __init__(self, config, current_user: User=None): - INotifier.__init__(config, current_user) + def __init__( + self, + config: CFG, + session: Session, + current_user: User=None + ) -> None: + INotifier.__init__( + self, + config, + session, + current_user, + ) logger.info(self, 'Instantiating Dummy Notifier') def notify_content_update(self, content: Content): @@ -38,8 +55,3 @@ def notify_content_update(self, content: Content): self, 'Fake notifier, do not send notification for update of content {}'.format(content.content_id) # nopep8 ) - - -class EmailNotifier(INotifier): - # TODO - G.M [emailNotif] move and restore Email Notifier in another file. - pass diff --git a/tracim/lib/core/user.py b/tracim/lib/core/user.py index 76e9150..91fbc6d 100644 --- a/tracim/lib/core/user.py +++ b/tracim/lib/core/user.py @@ -1,9 +1,12 @@ # -*- coding: utf-8 -*- import threading +from smtplib import SMTPException import transaction import typing as typing +from tracim.exceptions import NotificationNotSend +from tracim.lib.mail_notifier.notifier import get_email_manager from sqlalchemy.orm import Session from tracim import CFG @@ -105,8 +108,9 @@ def update( user: User, name: str=None, email: str=None, - do_save=True, + password: str=None, timezone: str='', + do_save=True, ) -> None: if name is not None: user.display_name = name @@ -114,16 +118,56 @@ def update( if email is not None: user.email = email + if password is not None: + user.password = password + user.timezone = timezone if do_save: self.save(user) - def create_user(self, email=None, groups=[], save_now=False) -> User: + def create_user( + self, + email, + password: str = None, + name: str = None, + timezone: str = '', + groups=[], + do_save: bool=True, + do_notify: bool=True, + ) -> User: + new_user = self.create_minimal_user(email, groups, save_now=False) + self.update( + user=new_user, + name=name, + email=email, + password=password, + timezone=timezone, + do_save=False, + ) + if do_notify: + try: + email_manager = get_email_manager(self._config, self._session) + email_manager.notify_created_account( + new_user, + password=password + ) + except SMTPException as e: + raise NotificationNotSend() + if do_save: + self.save(new_user) + return new_user + + def create_minimal_user( + self, + email, + groups=[], + save_now=False + ) -> User: + """Previous create_user method""" user = User() - if email: - user.email = email + user.email = email for group in groups: user.groups.append(group) diff --git a/tracim/lib/mail_notifier/daemon.py b/tracim/lib/mail_notifier/daemon.py new file mode 100644 index 0000000..f9ea7ab --- /dev/null +++ b/tracim/lib/mail_notifier/daemon.py @@ -0,0 +1,61 @@ +from sqlalchemy.orm import collections + +from tracim.lib.utils.logger import logger +from tracim.lib.utils.utils import get_rq_queue +from tracim.lib.utils.utils import get_redis_connection +from rq.dummy import do_nothing +from rq.worker import StopRequested +from rq import Connection as RQConnection +from rq import Worker as BaseRQWorker + + +class FakeDaemon(object): + """ + Temporary class for transition between tracim 1 and tracim 2 + """ + def __init__(self, config, *args, **kwargs): + pass + + +class MailSenderDaemon(FakeDaemon): + # NOTE: use *args and **kwargs because parent __init__ use strange + # * parameter + def __init__(self, config, *args, **kwargs): + super().__init__(*args, **kwargs) + self.config = config + self.worker = None # type: RQWorker + + def append_thread_callback(self, callback: collections.Callable) -> None: + logger.warning('MailSenderDaemon not implement append_thread_callback') + pass + + def stop(self) -> None: + # When _stop_requested at False, tracim.lib.daemons.RQWorker + # will raise StopRequested exception in worker thread after receive a + # job. + self.worker._stop_requested = True + redis_connection = get_redis_connection(self.config) + queue = get_rq_queue(redis_connection, 'mail_sender') + queue.enqueue(do_nothing) + + def run(self) -> None: + + with RQConnection(get_redis_connection(self.config)): + self.worker = RQWorker(['mail_sender']) + self.worker.work() + + +class RQWorker(BaseRQWorker): + def _install_signal_handlers(self): + # RQ Worker is designed to work in main thread + # So we have to disable these signals (we implement server stop in + # MailSenderDaemon.stop method). + pass + + def dequeue_job_and_maintain_ttl(self, timeout): + # RQ Worker is designed to work in main thread, so we add behaviour + # here: if _stop_requested has been set to True, raise the standard way + # StopRequested exception to stop worker. + if self._stop_requested: + raise StopRequested() + return super().dequeue_job_and_maintain_ttl(timeout) \ No newline at end of file diff --git a/tracim/lib/mail_notifier/notifier.py b/tracim/lib/mail_notifier/notifier.py new file mode 100644 index 0000000..c8fc9d3 --- /dev/null +++ b/tracim/lib/mail_notifier/notifier.py @@ -0,0 +1,570 @@ +# -*- coding: utf-8 -*- +import datetime +import typing + +from email.mime.multipart import MIMEMultipart +from email.mime.text import MIMEText +from email.utils import formataddr + +from lxml.html.diff import htmldiff +from mako.template import Template +from sqlalchemy.orm import Session + +from tracim import CFG +from tracim.lib.core.notifications import INotifier +from tracim.lib.mail_notifier.sender import EmailSender +from tracim.lib.mail_notifier.utils import SmtpConfiguration, EST +from tracim.lib.mail_notifier.sender import send_email_through +from tracim.lib.core.workspace import WorkspaceApi +from tracim.lib.utils.logger import logger +from tracim.models import User +from tracim.models.auth import User +from tracim.models.data import ActionDescription +from tracim.models.data import Content +from tracim.models.data import ContentType +from tracim.models.data import UserRoleInWorkspace +from tracim.lib.utils.translation import fake_translator as l_, \ + fake_translator as _ + + +class EmailNotifier(INotifier): + """ + EmailNotifier, this class will decide how to notify by mail + in order to let a EmailManager create email + """ + + def __init__( + self, + config: CFG, + session: Session, + current_user: User=None + ): + """ + :param current_user: the user that has triggered the notification + :return: + """ + INotifier.__init__(self, config, session, current_user) + logger.info(self, 'Instantiating Email Notifier') + + self._user = current_user + self.session = session + self.config = config + self._smtp_config = SmtpConfiguration( + self.config.EMAIL_NOTIFICATION_SMTP_SERVER, + self.config.EMAIL_NOTIFICATION_SMTP_PORT, + self.config.EMAIL_NOTIFICATION_SMTP_USER, + self.config.EMAIL_NOTIFICATION_SMTP_PASSWORD + ) + + def notify_content_update(self, content: Content): + + if content.get_last_action().id not \ + in self.config.EMAIL_NOTIFICATION_NOTIFIED_EVENTS: + logger.info( + self, + 'Skip email notification for update of content {}' + 'by user {} (the action is {})'.format( + content.content_id, + # below: 0 means "no user" + self._user.user_id if self._user else 0, + content.get_last_action().id + ) + ) + return + + logger.info(self, + 'About to email-notify update' + 'of content {} by user {}'.format( + content.content_id, + # Below: 0 means "no user" + self._user.user_id if self._user else 0 + ) + ) + + if content.type not \ + in self.config.EMAIL_NOTIFICATION_NOTIFIED_CONTENTS: + logger.info( + self, + 'Skip email notification for update of content {}' + 'by user {} (the content type is {})'.format( + content.type, + # below: 0 means "no user" + self._user.user_id if self._user else 0, + content.get_last_action().id + ) + ) + return + + logger.info(self, + 'About to email-notify update' + 'of content {} by user {}'.format( + content.content_id, + # Below: 0 means "no user" + self._user.user_id if self._user else 0 + ) + ) + + #### + # + # INFO - D.A. - 2014-11-05 - Emails are sent through asynchronous jobs. + # For that reason, we do not give SQLAlchemy objects but ids only + # (SQLA objects are related to a given thread/session) + # + try: + if self.config.EMAIL_NOTIFICATION_PROCESSING_MODE.lower() == self.config.CST.ASYNC.lower(): + logger.info(self, 'Sending email in ASYNC mode') + # TODO - D.A - 2014-11-06 + # This feature must be implemented in order to be able to scale to large communities + raise NotImplementedError('Sending emails through ASYNC mode is not working yet') + else: + logger.info(self, 'Sending email in SYNC mode') + EmailManager( + self._smtp_config, + self.config, + self.session, + ).notify_content_update(self._user.user_id, content.content_id) + except TypeError as e: + logger.error(self, 'Exception catched during email notification: {}'.format(e.__str__())) + + +class EmailManager(object): + """ + Compared to Notifier, this class is independant from the HTTP request thread + This class will build Email and send it for both created account and content + update + """ + + def __init__( + self, + smtp_config: SmtpConfiguration, + config: CFG, + session: Session + ) -> None: + self._smtp_config = smtp_config + self.config = config + self.session = session + # FIXME - G.M - We need to have a session for the emailNotifier + + # if not self.session: + # engine = get_engine(settings) + # session_factory = get_session_factory(engine) + # app_config = CFG(settings) + + def _get_sender(self, user: User=None) -> str: + """ + Return sender string like "Bob Dylan + (via Tracim) " + :param user: user to extract display name + :return: sender string + """ + + email_template = self.config.EMAIL_NOTIFICATION_FROM_EMAIL + mail_sender_name = self.config.EMAIL_NOTIFICATION_FROM_DEFAULT_LABEL # nopep8 + if user: + mail_sender_name = '{name} via Tracim'.format(name=user.display_name) + email_address = email_template.replace('{user_id}', str(user.user_id)) + # INFO - D.A. - 2017-08-04 + # We use email_template.replace() instead of .format() because this + # method is more robust to errors in config file. + # + # For example, if the email is info+{userid}@tracim.fr + # email.format(user_id='bob') will raise an exception + # email.replace('{user_id}', 'bob') will just ignore {userid} + else: + email_address = email_template.replace('{user_id}', '0') + + return formataddr((mail_sender_name, email_address)) + + # Content Notification + + @staticmethod + def log_notification( + config: CFG, + action: str, + recipient: typing.Optional[str], + subject: typing.Optional[str], + ) -> None: + """Log notification metadata.""" + log_path = config.EMAIL_NOTIFICATION_LOG_FILE_PATH + if log_path: + # TODO - A.P - 2017-09-06 - file logging inefficiency + # Updating a document with 100 users to notify will leads to open + # and close the file 100 times. + with open(log_path, 'a') as log_file: + print( + datetime.datetime.now(), + action, + recipient, + subject, + sep='|', + file=log_file, + ) + + def notify_content_update( + self, + event_actor_id: int, + event_content_id: int + ) -> None: + """ + Look for all users to be notified about the new content and send them an + individual email + :param event_actor_id: id of the user that has triggered the event + :param event_content_id: related content_id + :return: + """ + # FIXME - D.A. - 2014-11-05 + # Dirty import. It's here in order to avoid circular import + from tracim.lib.core.content import ContentApi + from tracim.lib.core.user import UserApi + user = UserApi( + None, + config=self.config, + session=self.session, + ).get_one(event_actor_id) + logger.debug(self, 'Content: {}'.format(event_content_id)) + content_api = ContentApi( + current_user=user, + session=self.session, + config=self.config, + ) + content = ContentApi( + session=self.session, + current_user=user, # TODO - use a system user instead of the user that has triggered the event + config=self.config, + show_archived=True, + show_deleted=True, + ).get_one(event_content_id, ContentType.Any) + main_content = content.parent if content.type == ContentType.Comment else content + notifiable_roles = WorkspaceApi( + current_user=user, + session=self.session, + ).get_notifiable_roles(content.workspace) + + if len(notifiable_roles) <= 0: + logger.info(self, 'Skipping notification as nobody subscribed to in workspace {}'.format(content.workspace.label)) + return + + + logger.info(self, 'Sending asynchronous emails to {} user(s)'.format(len(notifiable_roles))) + # INFO - D.A. - 2014-11-06 + # The following email sender will send emails in the async task queue + # This allow to build all mails through current thread but really send them (including SMTP connection) + # In the other thread. + # + # This way, the webserver will return sooner (actually before notification emails are sent + async_email_sender = EmailSender( + self.config, + self._smtp_config, + self.config.EMAIL_NOTIFICATION_ACTIVATED + ) + for role in notifiable_roles: + logger.info(self, 'Sending email to {}'.format(role.user.email)) + to_addr = formataddr((role.user.display_name, role.user.email)) + # + # INFO - G.M - 2017-11-15 - set content_id in header to permit reply + # references can have multiple values, but only one in this case. + replyto_addr = self.config.EMAIL_NOTIFICATION_REPLY_TO_EMAIL.replace( # nopep8 + '{content_id}',str(content.content_id) + ) + + reference_addr = self.config.EMAIL_NOTIFICATION_REFERENCES_EMAIL.replace( #nopep8 + '{content_id}',str(content.content_id) + ) + # + # INFO - D.A. - 2014-11-06 + # We do not use .format() here because the subject defined in the .ini file + # may not include all required labels. In order to avoid partial format() (which result in an exception) + # we do use replace and force the use of .__str__() in order to process LazyString objects + # + subject = self.config.EMAIL_NOTIFICATION_CONTENT_UPDATE_SUBJECT + subject = subject.replace(EST.WEBSITE_TITLE, self.config.WEBSITE_TITLE.__str__()) + subject = subject.replace(EST.WORKSPACE_LABEL, main_content.workspace.label.__str__()) + subject = subject.replace(EST.CONTENT_LABEL, main_content.label.__str__()) + subject = subject.replace(EST.CONTENT_STATUS_LABEL, main_content.get_status().label.__str__()) + reply_to_label = l_('{username} & all members of {workspace}').format( + username=user.display_name, + workspace=main_content.workspace.label) + + message = MIMEMultipart('alternative') + message['Subject'] = subject + message['From'] = self._get_sender(user) + message['To'] = to_addr + message['Reply-to'] = formataddr((reply_to_label, replyto_addr)) + # INFO - G.M - 2017-11-15 + # References can theorically have label, but in pratice, references + # contains only message_id from parents post in thread. + # To link this email to a content we create a virtual parent + # in reference who contain the content_id. + message['References'] = formataddr(('',reference_addr)) + body_text = self._build_email_body_for_content(self.config.EMAIL_NOTIFICATION_CONTENT_UPDATE_TEMPLATE_TEXT, role, content, user) + body_html = self._build_email_body_for_content(self.config.EMAIL_NOTIFICATION_CONTENT_UPDATE_TEMPLATE_HTML, role, content, user) + + part1 = MIMEText(body_text, 'plain', 'utf-8') + part2 = MIMEText(body_html, 'html', 'utf-8') + # Attach parts into message container. + # According to RFC 2046, the last part of a multipart message, in this case + # the HTML message, is best and preferred. + message.attach(part1) + message.attach(part2) + + self.log_notification( + action='CREATED', + recipient=message['To'], + subject=message['Subject'], + config=self.config, + ) + + send_email_through( + self.config, + async_email_sender.send_mail, + message + ) + + def notify_created_account( + self, + user: User, + password: str, + ) -> None: + """ + Send created account email to given user. + + :param password: choosed password + :param user: user to notify + """ + # TODO BS 20160712: Cyclic import + logger.debug(self, 'user: {}'.format(user.user_id)) + logger.info(self, 'Sending asynchronous email to 1 user ({0})'.format( + user.email, + )) + + async_email_sender = EmailSender( + self.config, + self._smtp_config, + self.config.EMAIL_NOTIFICATION_ACTIVATED + ) + + subject = \ + self.config.EMAIL_NOTIFICATION_CREATED_ACCOUNT_SUBJECT \ + .replace( + EST.WEBSITE_TITLE, + self.config.WEBSITE_TITLE.__str__() + ) + message = MIMEMultipart('alternative') + message['Subject'] = subject + message['From'] = self._get_sender() + message['To'] = formataddr((user.get_display_name(), user.email)) + + text_template_file_path = self.config.EMAIL_NOTIFICATION_CREATED_ACCOUNT_TEMPLATE_TEXT # nopep8 + html_template_file_path = self.config.EMAIL_NOTIFICATION_CREATED_ACCOUNT_TEMPLATE_HTML # nopep8 + + context = { + 'user': user, + 'password': password, + # TODO - G.M - 11-06-2018 - [emailTemplateURL] correct value for logo_url # nopep8 + 'logo_url': '', + # TODO - G.M - 11-06-2018 - [emailTemplateURL] correct value for login_url # nopep8 + 'login_url': self.config.WEBSITE_BASE_URL, + } + body_text = self._render_template( + mako_template_filepath=text_template_file_path, + context=context + ) + + body_html = self._render_template( + mako_template_filepath=html_template_file_path, + context=context, + ) + + part1 = MIMEText(body_text, 'plain', 'utf-8') + part2 = MIMEText(body_html, 'html', 'utf-8') + + # Attach parts into message container. + # According to RFC 2046, the last part of a multipart message, + # in this case the HTML message, is best and preferred. + message.attach(part1) + message.attach(part2) + + send_email_through( + config=self.config, + sendmail_callable=async_email_sender.send_mail, + message=message + ) + + def _render_template( + self, + mako_template_filepath: str, + context: dict + ) -> str: + """ + Render mako template with all needed current variables. + + :param mako_template_filepath: file path of mako template + :param context: dict with template context + :return: template rendered string + """ + + template = Template(filename=mako_template_filepath) + return template.render( + _=_, + config=self.config, + **context + ) + + def _build_email_body_for_content( + self, + mako_template_filepath: str, + role: UserRoleInWorkspace, + content: Content, + actor: User + ) -> str: + """ + Build an email body and return it as a string + :param mako_template_filepath: the absolute path to the mako template to be used for email body building + :param role: the role related to user to whom the email must be sent. The role is required (and not the user only) in order to show in the mail why the user receive the notification + :param content: the content item related to the notification + :param actor: the user at the origin of the action / notification (for example the one who wrote a comment + :param config: the global configuration + :return: the built email body as string. In case of multipart email, this method must be called one time for text and one time for html + """ + logger.debug(self, 'Building email content from MAKO template {}'.format(mako_template_filepath)) + + main_title = content.label + content_intro = '' + content_text = '' + call_to_action_text = '' + + # TODO - G.M - 11-06-2018 - [emailTemplateURL] correct value for call_to_action_url # nopep8 + call_to_action_url ='' + # TODO - G.M - 11-06-2018 - [emailTemplateURL] correct value for status_icon_url # nopep8 + status_icon_url = '' + # TODO - G.M - 11-06-2018 - [emailTemplateURL] correct value for workspace_url # nopep8 + workspace_url = '' + # TODO - G.M - 11-06-2018 - [emailTemplateURL] correct value for logo_url # nopep8 + logo_url = '' + + action = content.get_last_action().id + if ActionDescription.COMMENT == action: + content_intro = l_('{} added a comment:').format(actor.display_name) + content_text = content.description + call_to_action_text = l_('Answer') + + elif ActionDescription.CREATION == action: + + # Default values (if not overriden) + content_text = content.description + call_to_action_text = l_('View online') + + if ContentType.Thread == content.type: + call_to_action_text = l_('Answer') + content_intro = l_('{} started a thread entitled:').format(actor.display_name) + content_text = '

{}

'.format(content.label) + \ + content.get_last_comment_from(actor).description + + elif ContentType.File == content.type: + content_intro = l_('{} added a file entitled:').format(actor.display_name) + if content.description: + content_text = content.description + else: + content_text = '{}'.format(content.label) + + elif ContentType.Page == content.type: + content_intro = l_('{} added a page entitled:').format(actor.display_name) + content_text = '{}'.format(content.label) + + elif ActionDescription.REVISION == action: + content_text = content.description + call_to_action_text = l_('View online') + + if ContentType.File == content.type: + content_intro = l_('{} uploaded a new revision.').format(actor.display_name) + content_text = '' + + elif ContentType.Page == content.type: + content_intro = l_('{} updated this page.').format(actor.display_name) + previous_revision = content.get_previous_revision() + title_diff = '' + if previous_revision.label != content.label: + title_diff = htmldiff(previous_revision.label, content.label) + content_text = str(l_('

Here is an overview of the changes:

'))+ \ + title_diff + \ + htmldiff(previous_revision.description, content.description) + + elif ContentType.Thread == content.type: + content_intro = l_('{} updated the thread description.').format(actor.display_name) + previous_revision = content.get_previous_revision() + title_diff = '' + if previous_revision.label != content.label: + title_diff = htmldiff(previous_revision.label, content.label) + content_text = str(l_('

Here is an overview of the changes:

'))+ \ + title_diff + \ + htmldiff(previous_revision.description, content.description) + + elif ActionDescription.EDITION == action: + call_to_action_text = l_('View online') + + if ContentType.File == content.type: + content_intro = l_('{} updated the file description.').format(actor.display_name) + content_text = '

{}

'.format(content.get_label()) + \ + content.description + + elif ActionDescription.STATUS_UPDATE == action: + call_to_action_text = l_('View online') + intro_user_msg = l_( + '{} ' + 'updated the following status:' + ) + content_intro = intro_user_msg.format(actor.display_name) + intro_body_msg = '

{}: {}

' + content_text = intro_body_msg.format( + content.get_label(), + content.get_status().label, + ) + + if '' == content_intro and content_text == '': + # Skip notification, but it's not normal + logger.error( + self, 'A notification is being sent but no content. ' + 'Here are some debug informations: [content_id: {cid}]' + '[action: {act}][author: {actor}]'.format( + cid=content.content_id, act=action, actor=actor + ) + ) + raise ValueError('Unexpected empty notification') + + context = { + 'user': role.user, + 'workspace': role.workspace, + 'workspace_url': workspace_url, + 'main_title': main_title, + 'status_label': content.get_status().label, + 'status_icon_url': status_icon_url, + 'role_label': role.role_as_label(), + 'content_intro': content_intro, + 'content_text': content_text, + 'call_to_action_text': call_to_action_text, + 'call_to_action_url': call_to_action_url, + 'logo_url': logo_url, + } + user = role.user + workspace = role.workspace + body_content = self._render_template( + mako_template_filepath=mako_template_filepath, + context=context, + ) + return body_content + + +def get_email_manager(config: CFG, session: Session): + """ + :return: EmailManager instance + """ + #  TODO: Find a way to import properly without cyclic import + + smtp_config = SmtpConfiguration( + config.EMAIL_NOTIFICATION_SMTP_SERVER, + config.EMAIL_NOTIFICATION_SMTP_PORT, + config.EMAIL_NOTIFICATION_SMTP_USER, + config.EMAIL_NOTIFICATION_SMTP_PASSWORD + ) + + return EmailManager(config=config, smtp_config=smtp_config, session=session) diff --git a/tracim/lib/mail_notifier/sender.py b/tracim/lib/mail_notifier/sender.py new file mode 100644 index 0000000..1332f13 --- /dev/null +++ b/tracim/lib/mail_notifier/sender.py @@ -0,0 +1,114 @@ +# -*- coding: utf-8 -*- +import smtplib +import typing +from email.message import Message +from email.mime.multipart import MIMEMultipart + +from tracim.config import CFG +from tracim.lib.utils.logger import logger +from tracim.lib.utils.utils import get_rq_queue +from tracim.lib.utils.utils import get_redis_connection +from tracim.lib.mail_notifier.utils import SmtpConfiguration + +def send_email_through( + config: CFG, + sendmail_callable: typing.Callable[[Message], None], + message: Message, +) -> None: + """ + Send mail encapsulation to send it in async or sync mode. + + TODO BS 20170126: A global mail/sender management should be a good + thing. Actually, this method is an fast solution. + :param config: system configuration + :param sendmail_callable: A callable who get message on first parameter + :param message: The message who have to be sent + """ + + if config.EMAIL_PROCESSING_MODE == config.CST.SYNC: + sendmail_callable(message) + elif config.EMAIL_PROCESSING_MODE == config.CST.ASYNC: + redis_connection = get_redis_connection(config) + queue = get_rq_queue(redis_connection, 'mail_sender') + queue.enqueue(sendmail_callable, message) + else: + raise NotImplementedError( + 'Mail sender processing mode {} is not implemented'.format( + config.EMAIL_PROCESSING_MODE, + ) + ) + + +class EmailSender(object): + """ + Independent email sender class. + + To allow its use in any thread, as an asyncjob_perform() call for + example, it has no dependencies on SQLAlchemy nor tg HTTP request. + """ + + def __init__( + self, + config: CFG, + smtp_config: SmtpConfiguration, + really_send_messages + ) -> None: + self._smtp_config = smtp_config + self.config = config + self._smtp_connection = None + self._is_active = really_send_messages + + def connect(self): + if not self._smtp_connection: + log = 'Connecting from SMTP server {}' + logger.info(self, log.format(self._smtp_config.server)) + self._smtp_connection = smtplib.SMTP( + self._smtp_config.server, + self._smtp_config.port + ) + self._smtp_connection.ehlo() + + if self._smtp_config.login: + try: + starttls_result = self._smtp_connection.starttls() + log = 'SMTP start TLS result: {}' + logger.debug(self, log.format(starttls_result)) + except Exception as e: + log = 'SMTP start TLS error: {}' + logger.debug(self, log.format(e.__str__())) + + if self._smtp_config.login: + try: + login_res = self._smtp_connection.login( + self._smtp_config.login, + self._smtp_config.password + ) + log = 'SMTP login result: {}' + logger.debug(self, log.format(login_res)) + except Exception as e: + log = 'SMTP login error: {}' + logger.debug(self, log.format(e.__str__())) + logger.info(self, 'Connection OK') + + def disconnect(self): + if self._smtp_connection: + log = 'Disconnecting from SMTP server {}' + logger.info(self, log.format(self._smtp_config.server)) + self._smtp_connection.quit() + logger.info(self, 'Connection closed.') + + def send_mail(self, message: MIMEMultipart): + if not self._is_active: + log = 'Not sending email to {} (service disabled)' + logger.info(self, log.format(message['To'])) + else: + self.connect() # Actually, this connects to SMTP only if required + logger.info(self, 'Sending email to {}'.format(message['To'])) + self._smtp_connection.send_message(message) + from tracim.lib.mail_notifier.notifier import EmailManager + EmailManager.log_notification( + action=' SENT', + recipient=message['To'], + subject=message['Subject'], + config=self.config, + ) diff --git a/tracim/lib/mail_notifier/utils.py b/tracim/lib/mail_notifier/utils.py new file mode 100644 index 0000000..5b7b190 --- /dev/null +++ b/tracim/lib/mail_notifier/utils.py @@ -0,0 +1,33 @@ +class SmtpConfiguration(object): + """Container class for SMTP configuration used in Tracim.""" + + def __init__(self, server: str, port: int, login: str, password: str): + self.server = server + self.port = port + self.login = login + self.password = password + + +class EST(object): + """ + EST = Email Subject Tags - this is a convenient class - no business logic + here + This class is intended to agregate all dynamic content that may be included + in email subjects + """ + + WEBSITE_TITLE = '{website_title}' + WORKSPACE_LABEL = '{workspace_label}' + CONTENT_LABEL = '{content_label}' + CONTENT_STATUS_LABEL = '{content_status_label}' + + @classmethod + def all(cls): + return [ + cls.CONTENT_LABEL, + cls.CONTENT_STATUS_LABEL, + cls.WEBSITE_TITLE, + cls.WORKSPACE_LABEL + ] + + diff --git a/tracim/lib/utils/utils.py b/tracim/lib/utils/utils.py index 6a14939..25d3507 100644 --- a/tracim/lib/utils/utils.py +++ b/tracim/lib/utils/utils.py @@ -1,10 +1,35 @@ # -*- coding: utf-8 -*- import datetime +from redis import Redis +from rq import Queue + +from tracim.config import CFG DEFAULT_WEBDAV_CONFIG_FILE = "wsgidav.conf" DEFAULT_TRACIM_CONFIG_FILE = "development.ini" +def get_redis_connection(config: CFG) -> Redis: + """ + :param config: current app_config + :return: redis connection + """ + return Redis( + host=config.EMAIL_SENDER_REDIS_HOST, + port=config.EMAIL_SENDER_REDIS_PORT, + db=config.EMAIL_SENDER_REDIS_DB, + ) + + +def get_rq_queue(redis_connection: Redis, queue_name: str ='default') -> Queue: + """ + :param queue_name: name of queue + :return: wanted queue + """ + + return Queue(name=queue_name, connection=redis_connection) + + def cmp_to_key(mycmp): """ List sort related function diff --git a/tracim/models/data.py b/tracim/models/data.py index 0fef9e7..12dd4f5 100644 --- a/tracim/models/data.py +++ b/tracim/models/data.py @@ -131,12 +131,12 @@ class UserRoleInWorkspace(DeclarativeBase): WORKSPACE_MANAGER = 8 # TODO - G.M - 10-04-2018 - [Cleanup] Drop this - # LABEL = dict() - # LABEL[0] = l_('N/A') - # LABEL[1] = l_('Reader') - # LABEL[2] = l_('Contributor') - # LABEL[4] = l_('Content Manager') - # LABEL[8] = l_('Workspace Manager') + LABEL = dict() + LABEL[0] = l_('N/A') + LABEL[1] = l_('Reader') + LABEL[2] = l_('Contributor') + LABEL[4] = l_('Content Manager') + LABEL[8] = l_('Workspace Manager') # # STYLE = dict() # STYLE[0] = '' @@ -161,8 +161,9 @@ class UserRoleInWorkspace(DeclarativeBase): # def style(self): # return UserRoleInWorkspace.STYLE[self.role] # - # def role_as_label(self): - # return UserRoleInWorkspace.LABEL[self.role] + + def role_as_label(self): + return UserRoleInWorkspace.LABEL[self.role] @classmethod def get_all_role_values(self): @@ -318,6 +319,7 @@ def __init__(self, # type='' ): self.id = id + self.label = self.id # TODO - G.M - 10-04-2018 - [Cleanup] Drop this # self.icon = ContentStatus._ICONS[id] # self.css = ContentStatus._CSS[id] diff --git a/tracim/templates/mail/__init__.py b/tracim/templates/mail/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tracim/templates/mail/content_update_body_html.mak b/tracim/templates/mail/content_update_body_html.mak new file mode 100644 index 0000000..b2c4c37 --- /dev/null +++ b/tracim/templates/mail/content_update_body_html.mak @@ -0,0 +1,73 @@ +## -*- coding: utf-8 -*- + + + + + + + + + + + +
+ logo + + + ${main_title} + —  + ${status_label|n} + status_icon + +
+ +

${content_intro|n}

+
+
${content_text|n}
+
+
+
+ + + + diff --git a/tracim/templates/mail/content_update_body_text.mak b/tracim/templates/mail/content_update_body_text.mak new file mode 100644 index 0000000..a18c330 --- /dev/null +++ b/tracim/templates/mail/content_update_body_text.mak @@ -0,0 +1,31 @@ +## -*- coding: utf-8 -*- + +Dear ${user.display_name}, + +This email is intended to be read as HTML content. +Please configure your email client to get the best of Tracim notifications. + +We understand that Email was originally intended to carry raw text only. +And you probably understand on your own that we are a decades after email +was created ;) + +Hope you'll switch your mail client configuration and enjoy Tracim :) + + +-------------------------------------------------------------------------------- + + +You receive this email because you are registered on /${config.WEBSITE_TITLE}/ +and you are /${role_label}/ in the workspace /${workspace.label}/ + +---- + +This email was automatically sent by *Tracim*, +a collaborative software developped by Algoo. + +**Algoo SAS** +340 Rue de l'Eygala +38430 Moirans +France +http://algoo.fr + diff --git a/tracim/templates/mail/created_account_body_html.mak b/tracim/templates/mail/created_account_body_html.mak new file mode 100644 index 0000000..56d790f --- /dev/null +++ b/tracim/templates/mail/created_account_body_html.mak @@ -0,0 +1,88 @@ +## -*- coding: utf-8 -*- + + + + + + + + + + + +
+ logo + + + + ${config.WEBSITE_TITLE}: ${_('Created account')} + + +
+ +
+
+ ${_('An administrator just create account for you on {website_title}'.format( + website_title=config.WEBSITE_TITLE + ))} + +
    +
  • + ${_('Login')}: ${user.email} +
  • +
  • + ${_('Password')}: ${password} +
  • +
+ +
+
+ + ${_('To go to {website_title}, please click on following link'.format( + website_title=config.WEBSITE_TITLE + ))} + + + ${login_url} + +
+
+ + + + diff --git a/tracim/templates/mail/created_account_body_text.mak b/tracim/templates/mail/created_account_body_text.mak new file mode 100644 index 0000000..9d2e0ab --- /dev/null +++ b/tracim/templates/mail/created_account_body_text.mak @@ -0,0 +1,25 @@ +## -*- coding: utf-8 -*- + +${_('An administrator just create account for you on {website_title}'.format( + website_title=config.WEBSITE_TITLE +))} + +* ${_('Login')}: ${user.email} +* ${_('Password')}: ${password} + +${_('To go to {website_title}, please click on following link'.format( + website_title=config.WEBSITE_TITLE +))} + +${login_url} + +-------------------------------------------------------------------------------- + +This email was sent by *Tracim*, +a collaborative software developped by Algoo. + +**Algoo SAS** +340 Rue de l'Eygala +38430 Moirans +France +http://algoo.fr diff --git a/tracim/tests/__init__.py b/tracim/tests/__init__.py index c7d6d0a..5694b52 100644 --- a/tracim/tests/__init__.py +++ b/tracim/tests/__init__.py @@ -1,5 +1,8 @@ # -*- coding: utf-8 -*- import unittest + +import plaster +import requests import transaction from depot.manager import DepotManager from pyramid import testing @@ -92,16 +95,16 @@ class BaseTest(unittest.TestCase): Pyramid default test. """ + config_uri = 'tests_configs.ini' + config_section = 'base_test' + def setUp(self): logger.debug(self, 'Setup Test...') - self.config = testing.setUp(settings={ - 'sqlalchemy.url': 'sqlite:///:memory:', - 'user.auth_token.validity': '604800', - 'depot_storage_dir': '/tmp/test/depot', - 'depot_storage_name': 'test', - 'preview_cache_dir': '/tmp/test/preview_cache', - - }) + self.settings = plaster.get_settings( + self.config_uri, + self.config_section + ) + self.config = testing.setUp(settings = self.settings) self.config.include('tracim.models') DepotManager._clear() DepotManager.configure( @@ -225,3 +228,15 @@ def _create_thread_and_test(self, owner=user ) return thread + + +class MailHogTest(DefaultTest): + """ + Theses test need a working mailhog + """ + + config_section = 'mail_test' + + def tearDown(self): + logger.debug(self, 'Cleanup MailHog list...') + requests.delete('http://127.0.0.1:8025/api/v1/messages') diff --git a/tracim/tests/functional/test_mail_notification.py b/tracim/tests/functional/test_mail_notification.py new file mode 100644 index 0000000..6f82cae --- /dev/null +++ b/tracim/tests/functional/test_mail_notification.py @@ -0,0 +1,265 @@ +# coding=utf-8 +# INFO - G.M - 09-06-2018 - Those test need a working MailHog + +from email.mime.multipart import MIMEMultipart +from email.mime.text import MIMEText + +import requests +from rq import SimpleWorker + +from tracim.fixtures.users_and_groups import Base as BaseFixture +from tracim.fixtures.content import Content as ContentFixture +from tracim.lib.utils.utils import get_redis_connection +from tracim.lib.utils.utils import get_rq_queue +from tracim.models.data import ContentType + +from tracim.lib.core.content import ContentApi +from tracim.lib.core.user import UserApi +from tracim.lib.core.workspace import WorkspaceApi +from tracim.lib.mail_notifier.sender import EmailSender +from tracim.lib.mail_notifier.utils import SmtpConfiguration +from tracim.tests import MailHogTest + + +class TestEmailSender(MailHogTest): + + def test__func__connect_disconnect__ok__nominal_case(self): + smtp_config = SmtpConfiguration( + self.app_config.EMAIL_NOTIFICATION_SMTP_SERVER, + self.app_config.EMAIL_NOTIFICATION_SMTP_PORT, + self.app_config.EMAIL_NOTIFICATION_SMTP_USER, + self.app_config.EMAIL_NOTIFICATION_SMTP_PASSWORD + ) + sender = EmailSender( + self.app_config, + smtp_config, + True, + ) + sender.connect() + sender.disconnect() + + def test__func__send_email__ok__nominal_case(self): + smtp_config = SmtpConfiguration( + self.app_config.EMAIL_NOTIFICATION_SMTP_SERVER, + self.app_config.EMAIL_NOTIFICATION_SMTP_PORT, + self.app_config.EMAIL_NOTIFICATION_SMTP_USER, + self.app_config.EMAIL_NOTIFICATION_SMTP_PASSWORD + ) + sender = EmailSender( + self.app_config, + smtp_config, + True, + ) + + # Create test_mail + msg = MIMEMultipart() + msg['Subject'] = 'test__func__send_email__ok__nominal_case' + msg['From'] = 'test_send_mail@localhost' + msg['To'] = 'receiver_test_send_mail@localhost' + text = "test__func__send_email__ok__nominal_case" + html = """\ + + + +

test__func__send_email__ok__nominal_case

+ + + """.replace(' ', '').replace('\n', '') + part1 = MIMEText(text, 'plain') + part2 = MIMEText(html, 'html') + msg.attach(part1) + msg.attach(part2) + + sender.send_mail(msg) + sender.disconnect() + + # check mail received + response = requests.get('http://127.0.0.1:8025/api/v1/messages') + response = response.json() + headers = response[0]['Content']['Headers'] + assert headers['From'][0] == 'test_send_mail@localhost' + assert headers['To'][0] == 'receiver_test_send_mail@localhost' + assert headers['Subject'][0] == 'test__func__send_email__ok__nominal_case' # nopep8 + assert response[0]['MIME']['Parts'][0]['Body'] == text + assert response[0]['MIME']['Parts'][1]['Body'] == html + + +class TestNotificationsSync(MailHogTest): + + fixtures = [BaseFixture, ContentFixture] + + def test_func__create_user_with_mail_notification__ok__nominal_case(self): + api = UserApi( + current_user=None, + session=self.session, + config=self.app_config, + ) + u = api.create_user( + email='bob@bob', + password='pass', + name='bob', + timezone='+2', + do_save=True, + do_notify=True, + ) + assert u is not None + assert u.email == "bob@bob" + assert u.validate_password('pass') + assert u.display_name == 'bob' + assert u.timezone == '+2' + + # check mail received + response = requests.get('http://127.0.0.1:8025/api/v1/messages') + response = response.json() + headers = response[0]['Content']['Headers'] + assert headers['From'][0] == 'Tracim Notifications ' # nopep8 + assert headers['To'][0] == 'bob ' + assert headers['Subject'][0] == '[TRACIM] Created account' + + def test_func__create_new_content_with_notification__ok__nominal_case(self): + uapi = UserApi( + current_user=None, + session=self.session, + config=self.app_config, + ) + current_user = uapi.get_one_by_email('admin@admin.admin') + # Create new user with notification enabled on w1 workspace + wapi = WorkspaceApi( + current_user=current_user, + session=self.session, + ) + workspace = wapi.get_one_by_label('w1') + user = uapi.get_one_by_email('bob@fsf.local') + wapi.enable_notifications(user, workspace) + + api = ContentApi( + current_user=user, + session=self.session, + config=self.app_config, + ) + item = api.create( + ContentType.Folder, + workspace, + None, + 'parent', + do_save=True, + do_notify=False, + ) + item2 = api.create( + ContentType.File, + workspace, + item, + 'file1', + do_save=True, + do_notify=True, + ) + + # check mail received + response = requests.get('http://127.0.0.1:8025/api/v1/messages') + response = response.json() + headers = response[0]['Content']['Headers'] + assert headers['From'][0] == '"Bob i. via Tracim" ' # nopep8 + assert headers['To'][0] == 'Global manager ' + assert headers['Subject'][0] == '[TRACIM] [w1] file1 (open)' + assert headers['References'][0] == 'test_user_refs+13@localhost' + assert headers['Reply-to'][0] == '"Bob i. & all members of w1" ' # nopep8 + + +class TestNotificationsAsync(MailHogTest): + fixtures = [BaseFixture, ContentFixture] + config_section = 'mail_test_async' + + def test_func__create_user_with_mail_notification__ok__nominal_case(self): + api = UserApi( + current_user=None, + session=self.session, + config=self.app_config, + ) + u = api.create_user( + email='bob@bob', + password='pass', + name='bob', + timezone='+2', + do_save=True, + do_notify=True, + ) + assert u is not None + assert u.email == "bob@bob" + assert u.validate_password('pass') + assert u.display_name == 'bob' + assert u.timezone == '+2' + + # Send mail async from redis queue + redis = get_redis_connection( + self.app_config + ) + queue = get_rq_queue( + redis, + 'mail_sender', + ) + worker = SimpleWorker([queue], connection=queue.connection) + worker.work(burst=True) + # check mail received + response = requests.get('http://127.0.0.1:8025/api/v1/messages') + response = response.json() + headers = response[0]['Content']['Headers'] + assert headers['From'][0] == 'Tracim Notifications ' # nopep8 + assert headers['To'][0] == 'bob ' + assert headers['Subject'][0] == '[TRACIM] Created account' + + def test_func__create_new_content_with_notification__ok__nominal_case(self): + uapi = UserApi( + current_user=None, + session=self.session, + config=self.app_config, + ) + current_user = uapi.get_one_by_email('admin@admin.admin') + # Create new user with notification enabled on w1 workspace + wapi = WorkspaceApi( + current_user=current_user, + session=self.session, + ) + workspace = wapi.get_one_by_label('w1') + user = uapi.get_one_by_email('bob@fsf.local') + wapi.enable_notifications(user, workspace) + + api = ContentApi( + current_user=user, + session=self.session, + config=self.app_config, + ) + item = api.create( + ContentType.Folder, + workspace, + None, + 'parent', + do_save=True, + do_notify=False, + ) + item2 = api.create( + ContentType.File, + workspace, + item, + 'file1', + do_save=True, + do_notify=True, + ) + # Send mail async from redis queue + redis = get_redis_connection( + self.app_config + ) + queue = get_rq_queue( + redis, + 'mail_sender', + ) + worker = SimpleWorker([queue], connection=queue.connection) + worker.work(burst=True) + # check mail received + response = requests.get('http://127.0.0.1:8025/api/v1/messages') + response = response.json() + headers = response[0]['Content']['Headers'] + assert headers['From'][0] == '"Bob i. via Tracim" ' # nopep8 + assert headers['To'][0] == 'Global manager ' + assert headers['Subject'][0] == '[TRACIM] [w1] file1 (open)' + assert headers['References'][0] == 'test_user_refs+13@localhost' + assert headers['Reply-to'][0] == '"Bob i. & all members of w1" ' # nopep8 diff --git a/tracim/tests/library/test_content_api.py b/tracim/tests/library/test_content_api.py index 5b35b3c..cffa497 100644 --- a/tracim/tests/library/test_content_api.py +++ b/tracim/tests/library/test_content_api.py @@ -112,8 +112,8 @@ def test_delete(self): group_api.get_one(Group.TIM_MANAGER), group_api.get_one(Group.TIM_ADMIN)] - user = uapi.create_user(email='this.is@user', - groups=groups, save_now=True) + user = uapi.create_minimal_user(email='this.is@user', + groups=groups, save_now=True) workspace = WorkspaceApi( current_user=user, session=self.session @@ -189,8 +189,8 @@ def test_archive(self): group_api.get_one(Group.TIM_MANAGER), group_api.get_one(Group.TIM_ADMIN)] - user = uapi.create_user(email='this.is@user', - groups=groups, save_now=True) + user = uapi.create_minimal_user(email='this.is@user', + groups=groups, save_now=True) workspace_api = WorkspaceApi(current_user=user, session=self.session) workspace = workspace_api.create_workspace( 'test workspace', @@ -274,7 +274,7 @@ def test_get_all_with_filter(self): group_api.get_one(Group.TIM_MANAGER), group_api.get_one(Group.TIM_ADMIN)] - user = uapi.create_user( + user = uapi.create_minimal_user( email='this.is@user', groups=groups, save_now=True @@ -331,8 +331,8 @@ def test_get_all_with_parent_id(self): group_api.get_one(Group.TIM_MANAGER), group_api.get_one(Group.TIM_ADMIN)] - user = uapi.create_user(email='this.is@user', - groups=groups, save_now=True) + user = uapi.create_minimal_user(email='this.is@user', + groups=groups, save_now=True) workspace = WorkspaceApi( current_user=user, session=self.session @@ -400,8 +400,8 @@ def test_set_status_unknown_status(self): group_api.get_one(Group.TIM_MANAGER), group_api.get_one(Group.TIM_ADMIN)] - user = uapi.create_user(email='this.is@user', - groups=groups, save_now=True) + user = uapi.create_minimal_user(email='this.is@user', + groups=groups, save_now=True) workspace = WorkspaceApi( current_user=user, @@ -438,8 +438,8 @@ def test_set_status_ok(self): group_api.get_one(Group.TIM_MANAGER), group_api.get_one(Group.TIM_ADMIN)] - user = uapi.create_user(email='this.is@user', - groups=groups, save_now=True) + user = uapi.create_minimal_user(email='this.is@user', + groups=groups, save_now=True) workspace = WorkspaceApi( current_user=user, @@ -480,8 +480,8 @@ def test_create_comment_ok(self): group_api.get_one(Group.TIM_MANAGER), group_api.get_one(Group.TIM_ADMIN)] - user = uapi.create_user(email='this.is@user', - groups=groups, save_now=True) + user = uapi.create_minimal_user(email='this.is@user', + groups=groups, save_now=True) workspace = WorkspaceApi( current_user=user, @@ -522,12 +522,12 @@ def test_unit_copy_file_different_label_different_parent_ok(self): group_api.get_one(Group.TIM_MANAGER), group_api.get_one(Group.TIM_ADMIN)] - user = uapi.create_user( + user = uapi.create_minimal_user( email='user1@user', groups=groups, save_now=True ) - user2 = uapi.create_user( + user2 = uapi.create_minimal_user( email='user2@user', groups=groups, save_now=True @@ -634,12 +634,12 @@ def test_unit_copy_file__same_label_different_parent_ok(self): group_api.get_one(Group.TIM_MANAGER), group_api.get_one(Group.TIM_ADMIN)] - user = uapi.create_user( + user = uapi.create_minimal_user( email='user1@user', groups=groups, save_now=True ) - user2 = uapi.create_user( + user2 = uapi.create_minimal_user( email='user2@user', groups=groups, save_now=True @@ -744,12 +744,12 @@ def test_unit_copy_file_different_label_same_parent_ok(self): group_api.get_one(Group.TIM_MANAGER), group_api.get_one(Group.TIM_ADMIN)] - user = uapi.create_user( + user = uapi.create_minimal_user( email='user1@user', groups=groups, save_now=True, ) - user2 = uapi.create_user( + user2 = uapi.create_minimal_user( email='user2@user', groups=groups, save_now=True @@ -843,10 +843,10 @@ def test_mark_read__workspace(self): group_api.get_one(Group.TIM_MANAGER), group_api.get_one(Group.TIM_ADMIN)] - user_a = uapi.create_user(email='this.is@user', - groups=groups, save_now=True) - user_b = uapi.create_user(email='this.is@another.user', - groups=groups, save_now=True) + user_a = uapi.create_minimal_user(email='this.is@user', + groups=groups, save_now=True) + user_b = uapi.create_minimal_user(email='this.is@another.user', + groups=groups, save_now=True) wapi = WorkspaceApi( current_user=user_a, @@ -946,12 +946,12 @@ def test_mark_read(self): group_api.get_one(Group.TIM_MANAGER), group_api.get_one(Group.TIM_ADMIN)] - user_a = uapi.create_user( + user_a = uapi.create_minimal_user( email='this.is@user', groups=groups, save_now=True ) - user_b = uapi.create_user( + user_b = uapi.create_minimal_user( email='this.is@another.user', groups=groups, save_now=True @@ -1009,12 +1009,12 @@ def test_mark_read__all(self): group_api.get_one(Group.TIM_MANAGER), group_api.get_one(Group.TIM_ADMIN)] - user_a = uapi.create_user( + user_a = uapi.create_minimal_user( email='this.is@user', groups=groups, save_now=True ) - user_b = uapi.create_user( + user_b = uapi.create_minimal_user( email='this.is@another.user', groups=groups, save_now=True @@ -1102,7 +1102,7 @@ def test_update(self): group_api.get_one(Group.TIM_MANAGER), group_api.get_one(Group.TIM_ADMIN)] - user1 = uapi.create_user( + user1 = uapi.create_minimal_user( email='this.is@user', groups=groups, save_now=True @@ -1116,8 +1116,7 @@ def test_update(self): wid = workspace.workspace_id - user2 = uapi.create_user() - user2.email = 'this.is@another.user' + user2 = uapi.create_minimal_user('this.is@another.user') uapi.save(user2) RoleApi( @@ -1223,7 +1222,7 @@ def test_update_no_change(self): group_api.get_one(Group.TIM_MANAGER), group_api.get_one(Group.TIM_ADMIN)] - user1 = uapi.create_user( + user1 = uapi.create_minimal_user( email='this.is@user', groups=groups, save_now=True, @@ -1237,8 +1236,7 @@ def test_update_no_change(self): save_now=True ) - user2 = uapi.create_user() - user2.email = 'this.is@another.user' + user2 = uapi.create_minimal_user('this.is@another.user') uapi.save(user2) RoleApi( @@ -1301,7 +1299,7 @@ def test_update_file_data(self): group_api.get_one(Group.TIM_MANAGER), group_api.get_one(Group.TIM_ADMIN)] - user1 = uapi.create_user( + user1 = uapi.create_minimal_user( email='this.is@user', groups=groups, save_now=True @@ -1314,8 +1312,7 @@ def test_update_file_data(self): ) wid = workspace.workspace_id - user2 = uapi.create_user() - user2.email = 'this.is@another.user' + user2 = uapi.create_minimal_user('this.is@another.user') uapi.save(user2) RoleApi( @@ -1415,7 +1412,7 @@ def test_update_no_change(self): group_api.get_one(Group.TIM_MANAGER), group_api.get_one(Group.TIM_ADMIN)] - user1 = uapi.create_user( + user1 = uapi.create_minimal_user( email='this.is@user', groups=groups, save_now=True, @@ -1427,8 +1424,7 @@ def test_update_no_change(self): save_now=True ) - user2 = uapi.create_user() - user2.email = 'this.is@another.user' + user2 = uapi.create_minimal_user('this.is@another.user') uapi.save(user2) RoleApi( @@ -1494,7 +1490,7 @@ def test_archive_unarchive(self): group_api.get_one(Group.TIM_MANAGER), group_api.get_one(Group.TIM_ADMIN)] - user1 = uapi.create_user( + user1 = uapi.create_minimal_user( email='this.is@user', groups=groups, save_now=True @@ -1508,8 +1504,7 @@ def test_archive_unarchive(self): ) wid = workspace.workspace_id - user2 = uapi.create_user() - user2.email = 'this.is@another.user' + user2 = uapi.create_minimal_user('this.is@another.user') uapi.save(user2) RoleApi( @@ -1643,7 +1638,7 @@ def test_delete_undelete(self): group_api.get_one(Group.TIM_MANAGER), group_api.get_one(Group.TIM_ADMIN)] - user1 = uapi.create_user( + user1 = uapi.create_minimal_user( email='this.is@user', groups=groups, save_now=True @@ -1657,8 +1652,7 @@ def test_delete_undelete(self): ) wid = workspace.workspace_id - user2 = uapi.create_user() - user2.email = 'this.is@another.user' + user2 = uapi.create_minimal_user('this.is@another.user') uapi.save(user2) RoleApi( @@ -1793,8 +1787,8 @@ def test_search_in_label(self): group_api.get_one(Group.TIM_MANAGER), group_api.get_one(Group.TIM_ADMIN)] - user = uapi.create_user(email='this.is@user', - groups=groups, save_now=True) + user = uapi.create_minimal_user(email='this.is@user', + groups=groups, save_now=True) workspace = WorkspaceApi( current_user=user, @@ -1848,8 +1842,8 @@ def test_search_in_description(self): group_api.get_one(Group.TIM_MANAGER), group_api.get_one(Group.TIM_ADMIN)] - user = uapi.create_user(email='this.is@user', - groups=groups, save_now=True) + user = uapi.create_minimal_user(email='this.is@user', + groups=groups, save_now=True) workspace = WorkspaceApi( current_user=user, @@ -1899,8 +1893,8 @@ def test_search_in_label_or_description(self): group_api.get_one(Group.TIM_MANAGER), group_api.get_one(Group.TIM_ADMIN)] - user = uapi.create_user(email='this.is@user', - groups=groups, save_now=True) + user = uapi.create_minimal_user(email='this.is@user', + groups=groups, save_now=True) workspace = WorkspaceApi( current_user=user, diff --git a/tracim/tests/library/test_notification.py b/tracim/tests/library/test_notification.py index 147c171..76f5e13 100644 --- a/tracim/tests/library/test_notification.py +++ b/tracim/tests/library/test_notification.py @@ -4,18 +4,20 @@ from tracim.lib.core.notifications import DummyNotifier -from tracim.lib.core.notifications import EmailNotifier + from tracim.lib.core.notifications import NotifierFactory +from tracim.lib.mail_notifier.notifier import EmailNotifier from tracim.models.auth import User from tracim.models.data import Content from tracim.tests import DefaultTest from tracim.tests import eq_ + class TestDummyNotifier(DefaultTest): def test_dummy_notifier__notify_content_update(self): c = Content() - notifier = DummyNotifier(self.app_config) + notifier = DummyNotifier(self.app_config, self.session) notifier.notify_content_update(c) # INFO - D.A. - 2014-12-09 - # Old notification_content_update raised an exception diff --git a/tracim/tests/library/test_user_api.py b/tracim/tests/library/test_user_api.py index 1caa69b..06e815f 100644 --- a/tracim/tests/library/test_user_api.py +++ b/tracim/tests/library/test_user_api.py @@ -14,19 +14,49 @@ class TestUserApi(DefaultTest): - def test_unit__create_and_update_user__ok__nominal_case(self): + def test_unit__create_minimal_user__ok__nominal_case(self): api = UserApi( current_user=None, session=self.session, config=self.config, ) - u = api.create_user() - api.update(u, 'bob', 'bob@bob', True) + u = api.create_minimal_user('bob@bob') + assert u.email == 'bob@bob' + assert u.display_name is None + def test_unit__create_minimal_user_and_update__ok__nominal_case(self): + api = UserApi( + current_user=None, + session=self.session, + config=self.config, + ) + u = api.create_minimal_user('bob@bob') + api.update(u, 'bob', 'bob@bob', 'pass', do_save=True) nu = api.get_one_by_email('bob@bob') - assert nu != None - eq_('bob@bob', nu.email) - eq_('bob', nu.display_name) + assert nu is not None + assert nu.email == 'bob@bob' + assert nu.display_name == 'bob' + assert nu.validate_password('pass') + + def test__unit__create__user__ok_nominal_case(self): + api = UserApi( + current_user=None, + session=self.session, + config=self.config, + ) + u = api.create_user( + email='bob@bob', + password='pass', + name='bob', + timezone='+2', + do_save=True, + do_notify=False, + ) + assert u is not None + assert u.email == "bob@bob" + assert u.validate_password('pass') + assert u.display_name == 'bob' + assert u.timezone == '+2' def test_unit__user_with_email_exists__ok__nominal_case(self): api = UserApi( @@ -34,21 +64,22 @@ def test_unit__user_with_email_exists__ok__nominal_case(self): session=self.session, config=self.config, ) - u = api.create_user() - api.update(u, 'bibi', 'bibi@bibi', True) + u = api.create_minimal_user('bibi@bibi') + api.update(u, 'bibi', 'bibi@bibi', 'pass', do_save=True) transaction.commit() eq_(True, api.user_with_email_exists('bibi@bibi')) eq_(False, api.user_with_email_exists('unknown')) - def test_unit__get_one_by_email__ok__nominal_case(self): + def test_get_one_by_email(self): api = UserApi( current_user=None, session=self.session, config=self.config, ) - u = api.create_user() - api.update(u, 'bibi', 'bibi@bibi', True) + u = api.create_minimal_user('bibi@bibi') + self.session.flush() + api.update(u, 'bibi', 'bibi@bibi', 'pass', do_save=True) uid = u.user_id transaction.commit() @@ -63,17 +94,17 @@ def test_unit__get_one_by_email__err__user_does_not_exist(self): with pytest.raises(NoResultFound): api.get_one_by_email('unknown') - # def test_unit__get_all__ok__nominal_case(self): - # # TODO - G.M - 29-03-2018 Check why this method is not enabled - # api = UserApi( - # current_user=None, - # session=self.session, - # config=self.config, - # ) - # u1 = api.create_user(True) - # u2 = api.create_user(True) - # users = api.get_all() - # assert 2==len(users) + def test_unit__get_all__ok__nominal_case(self): + api = UserApi( + current_user=None, + session=self.session, + config=self.config, + ) + u1 = api.create_minimal_user('bibi@bibi') + + users = api.get_all() + # u1 + Admin user from BaseFixture + assert 2 == len(users) def test_unit__get_one__ok__nominal_case(self): api = UserApi( @@ -81,8 +112,8 @@ def test_unit__get_one__ok__nominal_case(self): session=self.session, config=self.config, ) - u = api.create_user() - api.update(u, 'titi', 'titi@titi', True) + u = api.create_minimal_user('titi@titi') + api.update(u, 'titi', 'titi@titi', 'pass', do_save=True) one = api.get_one(u.user_id) eq_(u.user_id, one.user_id) diff --git a/tracim/tests/library/test_workspace.py b/tracim/tests/library/test_workspace.py index be401f7..418538a 100644 --- a/tracim/tests/library/test_workspace.py +++ b/tracim/tests/library/test_workspace.py @@ -52,7 +52,7 @@ def test_get_notifiable_roles(self): current_user=admin, config=self.config ) - u = uapi.create_user(email='u.u@u.u', save_now=True) + u = uapi.create_minimal_user(email='u.u@u.u', save_now=True) eq_([], wapi.get_notifiable_roles(workspace=w)) rapi = RoleApi( session=self.session, @@ -88,7 +88,7 @@ def test_unit__get_all_manageable(self): session=self.session, current_user=None, ) - u = uapi.create_user('u.s@e.r', [gapi.get_one(Group.TIM_USER)], True) + u = uapi.create_minimal_user('u.s@e.r', [gapi.get_one(Group.TIM_USER)], True) wapi = WorkspaceApi( session=self.session, current_user=u