Browse Source

Restore-mail-notifier WIP

Guénaël Muller 6 years ago
parent
commit
da81fb2c8d

+ 5 - 0
setup.py View File

@@ -34,6 +34,11 @@ requires = [
34 34
     # others
35 35
     'filedepot',
36 36
     'babel',
37
+    # mail-notifier
38
+    'mako',
39
+    'lxml',
40
+    'redis',
41
+    'rq',
37 42
 ]
38 43
 
39 44
 tests_require = [

+ 1 - 1
tracim/command/user.py View File

@@ -115,7 +115,7 @@ class UserCommand(AppContextCommand):
115 115
             password = ''
116 116
 
117 117
         try:
118
-            user = self._user_api.create_user(email=login)
118
+            user = self._user_api.create_minimal_user(email=login)
119 119
             user.password = password
120 120
             self._user_api.save(user)
121 121
             # TODO - G.M - 04-04-2018 - [Caldav] Check this code

+ 110 - 108
tracim/config.py View File

@@ -4,6 +4,8 @@ from paste.deploy.converters import asbool
4 4
 from tracim.lib.utils.logger import logger
5 5
 from depot.manager import DepotManager
6 6
 
7
+from tracim.models.data import ActionDescription, ContentType
8
+
7 9
 
8 10
 class CFG(object):
9 11
     """Object used for easy access to config file parameters."""
@@ -131,87 +133,87 @@ class CFG(object):
131 133
         # TODO - G.M - 27-03-2018 - [Email] Restore email config
132 134
         ###
133 135
         # EMAIL related stuff (notification, reply)
134
-        ###
135
-        #
136
-        # self.EMAIL_NOTIFICATION_NOTIFIED_EVENTS = [
137
-        #     # ActionDescription.COMMENT,
138
-        #     # ActionDescription.CREATION,
139
-        #     # ActionDescription.EDITION,
140
-        #     # ActionDescription.REVISION,
141
-        #     # ActionDescription.STATUS_UPDATE
142
-        # ]
143
-        #
144
-        # self.EMAIL_NOTIFICATION_NOTIFIED_CONTENTS = [
145
-        #     # ContentType.Page,
146
-        #     # ContentType.Thread,
147
-        #     # ContentType.File,
148
-        #     # ContentType.Comment,
149
-        #     # ContentType.Folder -- Folder is skipped
150
-        # ]
151
-        # if settings.get('email.notification.from'):
152
-        #     raise Exception(
153
-        #         'email.notification.from configuration is deprecated. '
154
-        #         'Use instead email.notification.from.email and '
155
-        #         'email.notification.from.default_label.'
156
-        #     )
157
-        #
158
-        # self.EMAIL_NOTIFICATION_FROM_EMAIL = settings.get(
159
-        #     'email.notification.from.email',
160
-        # )
161
-        # self.EMAIL_NOTIFICATION_FROM_DEFAULT_LABEL = settings.get(
162
-        #     'email.notification.from.default_label'
163
-        # )
164
-        # self.EMAIL_NOTIFICATION_REPLY_TO_EMAIL = settings.get(
165
-        #     'email.notification.reply_to.email',
166
-        # )
167
-        # self.EMAIL_NOTIFICATION_REFERENCES_EMAIL = settings.get(
168
-        #     'email.notification.references.email'
169
-        # )
170
-        # self.EMAIL_NOTIFICATION_CONTENT_UPDATE_TEMPLATE_HTML = settings.get(
171
-        #     'email.notification.content_update.template.html',
172
-        # )
173
-        # self.EMAIL_NOTIFICATION_CONTENT_UPDATE_TEMPLATE_TEXT = settings.get(
174
-        #     'email.notification.content_update.template.text',
175
-        # )
176
-        # self.EMAIL_NOTIFICATION_CREATED_ACCOUNT_TEMPLATE_HTML = settings.get(
177
-        #     'email.notification.created_account.template.html',
178
-        #     './tracim/templates/mail/created_account_body_html.mak',
179
-        # )
180
-        # self.EMAIL_NOTIFICATION_CREATED_ACCOUNT_TEMPLATE_TEXT = settings.get(
181
-        #     'email.notification.created_account.template.text',
182
-        #     './tracim/templates/mail/created_account_body_text.mak',
183
-        # )
184
-        # self.EMAIL_NOTIFICATION_CONTENT_UPDATE_SUBJECT = settings.get(
185
-        #     'email.notification.content_update.subject',
186
-        # )
187
-        # self.EMAIL_NOTIFICATION_CREATED_ACCOUNT_SUBJECT = settings.get(
188
-        #     'email.notification.created_account.subject',
189
-        #     '[{website_title}] Created account',
190
-        # )
191
-        # self.EMAIL_NOTIFICATION_PROCESSING_MODE = settings.get(
192
-        #     'email.notification.processing_mode',
193
-        # )
194
-        #
136
+        ##
137
+
138
+        self.EMAIL_NOTIFICATION_NOTIFIED_EVENTS = [
139
+            ActionDescription.COMMENT,
140
+            ActionDescription.CREATION,
141
+            ActionDescription.EDITION,
142
+            ActionDescription.REVISION,
143
+            ActionDescription.STATUS_UPDATE
144
+        ]
145
+
146
+        self.EMAIL_NOTIFICATION_NOTIFIED_CONTENTS = [
147
+            ContentType.Page,
148
+            ContentType.Thread,
149
+            ContentType.File,
150
+            ContentType.Comment,
151
+            # ContentType.Folder -- Folder is skipped
152
+        ]
153
+        if settings.get('email.notification.from'):
154
+            raise Exception(
155
+                'email.notification.from configuration is deprecated. '
156
+                'Use instead email.notification.from.email and '
157
+                'email.notification.from.default_label.'
158
+            )
159
+
160
+        self.EMAIL_NOTIFICATION_FROM_EMAIL = settings.get(
161
+            'email.notification.from.email',
162
+        )
163
+        self.EMAIL_NOTIFICATION_FROM_DEFAULT_LABEL = settings.get(
164
+            'email.notification.from.default_label'
165
+        )
166
+        self.EMAIL_NOTIFICATION_REPLY_TO_EMAIL = settings.get(
167
+            'email.notification.reply_to.email',
168
+        )
169
+        self.EMAIL_NOTIFICATION_REFERENCES_EMAIL = settings.get(
170
+            'email.notification.references.email'
171
+        )
172
+        self.EMAIL_NOTIFICATION_CONTENT_UPDATE_TEMPLATE_HTML = settings.get(
173
+            'email.notification.content_update.template.html',
174
+        )
175
+        self.EMAIL_NOTIFICATION_CONTENT_UPDATE_TEMPLATE_TEXT = settings.get(
176
+            'email.notification.content_update.template.text',
177
+        )
178
+        self.EMAIL_NOTIFICATION_CREATED_ACCOUNT_TEMPLATE_HTML = settings.get(
179
+            'email.notification.created_account.template.html',
180
+            './tracim/templates/mail/created_account_body_html.mak',
181
+        )
182
+        self.EMAIL_NOTIFICATION_CREATED_ACCOUNT_TEMPLATE_TEXT = settings.get(
183
+            'email.notification.created_account.template.text',
184
+            './tracim/templates/mail/created_account_body_text.mak',
185
+        )
186
+        self.EMAIL_NOTIFICATION_CONTENT_UPDATE_SUBJECT = settings.get(
187
+            'email.notification.content_update.subject',
188
+        )
189
+        self.EMAIL_NOTIFICATION_CREATED_ACCOUNT_SUBJECT = settings.get(
190
+            'email.notification.created_account.subject',
191
+            '[{website_title}] Created account',
192
+        )
193
+        self.EMAIL_NOTIFICATION_PROCESSING_MODE = settings.get(
194
+            'email.notification.processing_mode',
195
+        )
196
+
195 197
         self.EMAIL_NOTIFICATION_ACTIVATED = asbool(settings.get(
196 198
             'email.notification.activated',
197 199
         ))
198
-        # self.EMAIL_NOTIFICATION_SMTP_SERVER = settings.get(
199
-        #     'email.notification.smtp.server',
200
-        # )
201
-        # self.EMAIL_NOTIFICATION_SMTP_PORT = settings.get(
202
-        #     'email.notification.smtp.port',
203
-        # )
204
-        # self.EMAIL_NOTIFICATION_SMTP_USER = settings.get(
205
-        #     'email.notification.smtp.user',
206
-        # )
207
-        # self.EMAIL_NOTIFICATION_SMTP_PASSWORD = settings.get(
208
-        #     'email.notification.smtp.password',
209
-        # )
210
-        # self.EMAIL_NOTIFICATION_LOG_FILE_PATH = settings.get(
211
-        #     'email.notification.log_file_path',
212
-        #     None,
213
-        # )
214
-        #
200
+        self.EMAIL_NOTIFICATION_SMTP_SERVER = settings.get(
201
+            'email.notification.smtp.server',
202
+        )
203
+        self.EMAIL_NOTIFICATION_SMTP_PORT = settings.get(
204
+            'email.notification.smtp.port',
205
+        )
206
+        self.EMAIL_NOTIFICATION_SMTP_USER = settings.get(
207
+            'email.notification.smtp.user',
208
+        )
209
+        self.EMAIL_NOTIFICATION_SMTP_PASSWORD = settings.get(
210
+            'email.notification.smtp.password',
211
+        )
212
+        self.EMAIL_NOTIFICATION_LOG_FILE_PATH = settings.get(
213
+            'email.notification.log_file_path',
214
+            None,
215
+        )
216
+
215 217
         # self.EMAIL_REPLY_ACTIVATED = asbool(settings.get(
216 218
         #     'email.reply.activated',
217 219
         #     False,
@@ -267,36 +269,36 @@ class CFG(object):
267 269
         #         mandatory_msg.format('email.reply.lockfile_path')
268 270
         #     )
269 271
         #
270
-        # self.EMAIL_PROCESSING_MODE = settings.get(
271
-        #     'email.processing_mode',
272
-        #     'sync',
273
-        # ).upper()
274
-        #
275
-        # if self.EMAIL_PROCESSING_MODE not in (
276
-        #         self.CST.ASYNC,
277
-        #         self.CST.SYNC,
278
-        # ):
279
-        #     raise Exception(
280
-        #         'email.processing_mode '
281
-        #         'can ''be "{}" or "{}", not "{}"'.format(
282
-        #             self.CST.ASYNC,
283
-        #             self.CST.SYNC,
284
-        #             self.EMAIL_PROCESSING_MODE,
285
-        #         )
286
-        #     )
287
-        #
288
-        # self.EMAIL_SENDER_REDIS_HOST = settings.get(
289
-        #     'email.async.redis.host',
290
-        #     'localhost',
291
-        # )
292
-        # self.EMAIL_SENDER_REDIS_PORT = int(settings.get(
293
-        #     'email.async.redis.port',
294
-        #     6379,
295
-        # ))
296
-        # self.EMAIL_SENDER_REDIS_DB = int(settings.get(
297
-        #     'email.async.redis.db',
298
-        #     0,
299
-        # ))
272
+        self.EMAIL_PROCESSING_MODE = settings.get(
273
+            'email.processing_mode',
274
+            'sync',
275
+        ).upper()
276
+
277
+        if self.EMAIL_PROCESSING_MODE not in (
278
+                self.CST.ASYNC,
279
+                self.CST.SYNC,
280
+        ):
281
+            raise Exception(
282
+                'email.processing_mode '
283
+                'can ''be "{}" or "{}", not "{}"'.format(
284
+                    self.CST.ASYNC,
285
+                    self.CST.SYNC,
286
+                    self.EMAIL_PROCESSING_MODE,
287
+                )
288
+            )
289
+
290
+        self.EMAIL_SENDER_REDIS_HOST = settings.get(
291
+            'email.async.redis.host',
292
+            'localhost',
293
+        )
294
+        self.EMAIL_SENDER_REDIS_PORT = int(settings.get(
295
+            'email.async.redis.port',
296
+            6379,
297
+        ))
298
+        self.EMAIL_SENDER_REDIS_DB = int(settings.get(
299
+            'email.async.redis.db',
300
+            0,
301
+        ))
300 302
 
301 303
         ###
302 304
         # WSGIDAV (Webdav server)

+ 5 - 4
tracim/lib/core/content.py View File

@@ -376,7 +376,7 @@ class ContentApi(object):
376 376
 
377 377
         return result
378 378
 
379
-    def create(self, content_type: str, workspace: Workspace, parent: Content=None, label:str ='', do_save=False, is_temporary: bool=False) -> Content:
379
+    def create(self, content_type: str, workspace: Workspace, parent: Content=None, label:str ='', do_save=False, is_temporary: bool=False, do_notify=False) -> Content:
380 380
         assert content_type in ContentType.allowed_types()
381 381
 
382 382
         if content_type == ContentType.Folder and not label:
@@ -399,7 +399,7 @@ class ContentApi(object):
399 399
 
400 400
         if do_save:
401 401
             self._session.add(content)
402
-            self.save(content, ActionDescription.CREATION)
402
+            self.save(content, ActionDescription.CREATION, do_notify=do_notify)
403 403
         return content
404 404
 
405 405
 
@@ -1127,8 +1127,9 @@ class ContentApi(object):
1127 1127
         :return:
1128 1128
         """
1129 1129
         NotifierFactory.create(
1130
-            self._config,
1131
-            self._user
1130
+            config=self._config,
1131
+            current_user=self._user,
1132
+            session=self._session,
1132 1133
         ).notify_content_update(content)
1133 1134
 
1134 1135
     def get_keywords(self, search_string, search_string_separators=None) -> [str]:

+ 23 - 11
tracim/lib/core/notifications.py View File

@@ -1,5 +1,7 @@
1 1
 # -*- coding: utf-8 -*-
2
+from sqlalchemy.orm import Session
2 3
 
4
+from tracim import CFG
3 5
 from tracim.lib.utils.logger import logger
4 6
 from tracim.models.auth import User
5 7
 from tracim.models.data import Content
@@ -9,7 +11,11 @@ class INotifier(object):
9 11
     """
10 12
     Interface for Notifier instances
11 13
     """
12
-    def __init__(self, config, current_user: User=None):
14
+    def __init__(self,
15
+                 config: CFG,
16
+                 session: Session,
17
+                 current_user: User=None,
18
+    ) -> None:
13 19
         pass
14 20
 
15 21
     def notify_content_update(self, content: Content):
@@ -19,17 +25,28 @@ class INotifier(object):
19 25
 class NotifierFactory(object):
20 26
 
21 27
     @classmethod
22
-    def create(cls, config, current_user: User=None) -> INotifier:
28
+    def create(cls, config, session, current_user: User=None) -> INotifier:
23 29
         if not config.EMAIL_NOTIFICATION_ACTIVATED:
24
-            return DummyNotifier(config, current_user)
25
-        return EmailNotifier(config, current_user)
30
+            return DummyNotifier(config, session, current_user)
31
+        from tracim.lib.mail_notifier.notifier import EmailNotifier
32
+        return EmailNotifier(config, session, current_user)
26 33
 
27 34
 
28 35
 class DummyNotifier(INotifier):
29 36
     send_count = 0
30 37
 
31
-    def __init__(self, config, current_user: User=None):
32
-        INotifier.__init__(config, current_user)
38
+    def __init__(
39
+            self,
40
+            config: CFG,
41
+            session: Session,
42
+            current_user: User=None
43
+    ) -> None:
44
+        INotifier.__init__(
45
+            self,
46
+            config,
47
+            session,
48
+            current_user,
49
+        )
33 50
         logger.info(self, 'Instantiating Dummy Notifier')
34 51
 
35 52
     def notify_content_update(self, content: Content):
@@ -38,8 +55,3 @@ class DummyNotifier(INotifier):
38 55
             self,
39 56
             'Fake notifier, do not send notification for update of content {}'.format(content.content_id)  # nopep8
40 57
         )
41
-
42
-
43
-class EmailNotifier(INotifier):
44
-    # TODO - G.M [emailNotif] move and restore Email Notifier in another file.
45
-    pass

+ 40 - 2
tracim/lib/core/user.py View File

@@ -4,6 +4,7 @@ import threading
4 4
 import transaction
5 5
 import typing as typing
6 6
 
7
+from tracim.lib.mail_notifier.notifier import get_email_manager
7 8
 from tracim.models.auth import User
8 9
 
9 10
 
@@ -34,8 +35,9 @@ class UserApi(object):
34 35
             user: User,
35 36
             name: str=None,
36 37
             email: str=None,
37
-            do_save=True,
38
+            password: str=None,
38 39
             timezone: str='',
40
+            do_save=True,
39 41
     ):
40 42
         if name is not None:
41 43
             user.display_name = name
@@ -43,6 +45,9 @@ class UserApi(object):
43 45
         if email is not None:
44 46
             user.email = email
45 47
 
48
+        if password is not None:
49
+            user.password = password
50
+
46 51
         user.timezone = timezone
47 52
 
48 53
         if do_save:
@@ -56,7 +61,40 @@ class UserApi(object):
56 61
         except:
57 62
             return False
58 63
 
59
-    def create_user(self, email=None, groups=[], save_now=False) -> User:
64
+    def create_user(
65
+        self,
66
+        email: str = None,
67
+        password: str = None,
68
+        name: str = None,
69
+        timezone: str = '',
70
+        groups=[],
71
+        do_save: bool=True,
72
+        do_notify: bool=True,
73
+    ) -> User:
74
+        new_user = self.create_minimal_user(email, groups, save_now=False)
75
+        self.update(
76
+            user=new_user,
77
+            name=name,
78
+            email=email,
79
+            password=password,
80
+            timezone=timezone,
81
+            do_save=do_save,
82
+        )
83
+        if do_notify:
84
+            email_manager = get_email_manager(self._config, self._session)
85
+            email_manager.notify_created_account(
86
+                new_user,
87
+                password=password
88
+            )
89
+        return new_user
90
+
91
+    def create_minimal_user(
92
+            self,
93
+            email=None,
94
+            groups=[],
95
+            save_now=False
96
+    ) -> User:
97
+        """Previous create_user method"""
60 98
         user = User()
61 99
 
62 100
         if email:

+ 61 - 0
tracim/lib/mail_notifier/daemon.py View File

@@ -0,0 +1,61 @@
1
+from sqlalchemy.orm import collections
2
+
3
+from tracim.lib.utils.logger import logger
4
+from tracim.lib.utils.utils import get_rq_queue
5
+from tracim.lib.utils.utils import get_redis_connection
6
+from rq.dummy import do_nothing
7
+from rq.worker import StopRequested
8
+from rq import Connection as RQConnection
9
+from rq import Worker as BaseRQWorker
10
+
11
+
12
+class FakeDaemon(object):
13
+    """
14
+    Temporary class for transition between tracim 1 and tracim 2
15
+    """
16
+    def __init__(self, config, *args, **kwargs):
17
+        pass
18
+
19
+
20
+class MailSenderDaemon(FakeDaemon):
21
+    # NOTE: use *args and **kwargs because parent __init__ use strange
22
+    # * parameter
23
+    def __init__(self, config, *args, **kwargs):
24
+        super().__init__(*args, **kwargs)
25
+        self.config = config
26
+        self.worker = None  # type: RQWorker
27
+
28
+    def append_thread_callback(self, callback: collections.Callable) -> None:
29
+        logger.warning('MailSenderDaemon not implement append_thread_callback')
30
+        pass
31
+
32
+    def stop(self) -> None:
33
+        # When _stop_requested at False, tracim.lib.daemons.RQWorker
34
+        # will raise StopRequested exception in worker thread after receive a
35
+        # job.
36
+        self.worker._stop_requested = True
37
+        redis_connection = get_redis_connection(self.config)
38
+        queue = get_rq_queue(redis_connection, 'mail_sender')
39
+        queue.enqueue(do_nothing)
40
+
41
+    def run(self) -> None:
42
+
43
+        with RQConnection(get_redis_connection(self.config)):
44
+            self.worker = RQWorker(['mail_sender'])
45
+            self.worker.work()
46
+
47
+
48
+class RQWorker(BaseRQWorker):
49
+    def _install_signal_handlers(self):
50
+        # RQ Worker is designed to work in main thread
51
+        # So we have to disable these signals (we implement server stop in
52
+        # MailSenderDaemon.stop method).
53
+        pass
54
+
55
+    def dequeue_job_and_maintain_ttl(self, timeout):
56
+        # RQ Worker is designed to work in main thread, so we add behaviour
57
+        # here: if _stop_requested has been set to True, raise the standard way
58
+        # StopRequested exception to stop worker.
59
+        if self._stop_requested:
60
+            raise StopRequested()
61
+        return super().dequeue_job_and_maintain_ttl(timeout)

+ 556 - 0
tracim/lib/mail_notifier/notifier.py View File

@@ -0,0 +1,556 @@
1
+# -*- coding: utf-8 -*-
2
+import datetime
3
+import typing
4
+
5
+from email.mime.multipart import MIMEMultipart
6
+from email.mime.text import MIMEText
7
+from email.utils import formataddr
8
+
9
+from lxml.html.diff import htmldiff
10
+from mako.template import Template
11
+from sqlalchemy.orm import Session
12
+
13
+from tracim import CFG
14
+from tracim.lib.core.notifications import INotifier
15
+from tracim.lib.mail_notifier.sender import EmailSender
16
+from tracim.lib.mail_notifier.utils import SmtpConfiguration, EST
17
+from tracim.lib.mail_notifier.sender import send_email_through
18
+from tracim.lib.core.workspace import WorkspaceApi
19
+from tracim.lib.utils.logger import logger
20
+from tracim.models import User
21
+from tracim.models.auth import User
22
+from tracim.models.data import ActionDescription
23
+from tracim.models.data import Content
24
+from tracim.models.data import ContentType
25
+from tracim.models.data import UserRoleInWorkspace
26
+from tracim.lib.utils.translation import fake_translator as l_, \
27
+    fake_translator as _
28
+
29
+
30
+class EmailNotifier(INotifier):
31
+    """
32
+    EmailNotifier, this class will decide how to notify by mail
33
+    in order to let a EmailManager create email
34
+    """
35
+
36
+    def __init__(
37
+            self,
38
+            config: CFG,
39
+            session: Session,
40
+            current_user: User=None
41
+    ):
42
+        """
43
+        :param current_user: the user that has triggered the notification
44
+        :return:
45
+        """
46
+        INotifier.__init__(self, config, session, current_user)
47
+        logger.info(self, 'Instantiating Email Notifier')
48
+
49
+        self._user = current_user
50
+        self.session = session
51
+        self.config = config
52
+        self._smtp_config = SmtpConfiguration(
53
+            self.config.EMAIL_NOTIFICATION_SMTP_SERVER,
54
+            self.config.EMAIL_NOTIFICATION_SMTP_PORT,
55
+            self.config.EMAIL_NOTIFICATION_SMTP_USER,
56
+            self.config.EMAIL_NOTIFICATION_SMTP_PASSWORD
57
+        )
58
+
59
+    def notify_content_update(self, content: Content):
60
+
61
+        if content.get_last_action().id not \
62
+                in self.config.EMAIL_NOTIFICATION_NOTIFIED_EVENTS:
63
+            logger.info(
64
+                self,
65
+                'Skip email notification for update of content {}'
66
+                'by user {} (the action is {})'.format(
67
+                    content.content_id,
68
+                    # below: 0 means "no user"
69
+                    self._user.user_id if self._user else 0,
70
+                    content.get_last_action().id
71
+                )
72
+            )
73
+            return
74
+
75
+        logger.info(self,
76
+                    'About to email-notify update'
77
+                    'of content {} by user {}'.format(
78
+                        content.content_id,
79
+                        # Below: 0 means "no user"
80
+                        self._user.user_id if self._user else 0
81
+                    )
82
+        )
83
+
84
+        if content.type not \
85
+                in self.config.EMAIL_NOTIFICATION_NOTIFIED_CONTENTS:
86
+            logger.info(
87
+                self,
88
+                'Skip email notification for update of content {}'
89
+                'by user {} (the content type is {})'.format(
90
+                    content.type,
91
+                    # below: 0 means "no user"
92
+                    self._user.user_id if self._user else 0,
93
+                    content.get_last_action().id
94
+                )
95
+            )
96
+            return
97
+
98
+        logger.info(self,
99
+                    'About to email-notify update'
100
+                    'of content {} by user {}'.format(
101
+                        content.content_id,
102
+                        # Below: 0 means "no user"
103
+                        self._user.user_id if self._user else 0
104
+                    )
105
+        )
106
+
107
+        ####
108
+        #
109
+        # INFO - D.A. - 2014-11-05 - Emails are sent through asynchronous jobs.
110
+        # For that reason, we do not give SQLAlchemy objects but ids only
111
+        # (SQLA objects are related to a given thread/session)
112
+        #
113
+        try:
114
+            if self.config.EMAIL_NOTIFICATION_PROCESSING_MODE.lower() == self.config.CST.ASYNC.lower():
115
+                logger.info(self, 'Sending email in ASYNC mode')
116
+                # TODO - D.A - 2014-11-06
117
+                # This feature must be implemented in order to be able to scale to large communities
118
+                raise NotImplementedError('Sending emails through ASYNC mode is not working yet')
119
+            else:
120
+                logger.info(self, 'Sending email in SYNC mode')
121
+                EmailManager(
122
+                    self._smtp_config,
123
+                    self.config,
124
+                    self.session,
125
+                ).notify_content_update(self._user.user_id, content.content_id)
126
+        except TypeError as e:
127
+            logger.error(self, 'Exception catched during email notification: {}'.format(e.__str__()))
128
+
129
+
130
+class EmailManager(object):
131
+    """
132
+    Compared to Notifier, this class is independant from the HTTP request thread
133
+    This class will build Email and send it for both created account and content
134
+    update
135
+    """
136
+
137
+    def __init__(
138
+            self,
139
+            smtp_config: SmtpConfiguration,
140
+            config: CFG,
141
+            session: Session
142
+    ) -> None:
143
+        self._smtp_config = smtp_config
144
+        self.config = config
145
+        self.session = session
146
+        # FIXME - G.M - We need to have a session for the emailNotifier
147
+
148
+        # if not self.session:
149
+        #     engine = get_engine(settings)
150
+        #     session_factory = get_session_factory(engine)
151
+        #     app_config = CFG(settings)
152
+
153
+    def _get_sender(self, user: User=None) -> str:
154
+        """
155
+        Return sender string like "Bob Dylan
156
+            (via Tracim) <notification@mail.com>"
157
+        :param user: user to extract display name
158
+        :return: sender string
159
+        """
160
+
161
+        email_template = self.config.EMAIL_NOTIFICATION_FROM_EMAIL
162
+        mail_sender_name = self.config.EMAIL_NOTIFICATION_FROM_DEFAULT_LABEL  # nopep8
163
+        if user:
164
+            mail_sender_name = '{name} via Tracim'.format(name=user.display_name)
165
+            email_address = email_template.replace('{user_id}', str(user.user_id))
166
+            # INFO - D.A. - 2017-08-04
167
+            # We use email_template.replace() instead of .format() because this
168
+            # method is more robust to errors in config file.
169
+            #
170
+            # For example, if the email is info+{userid}@tracim.fr
171
+            # email.format(user_id='bob') will raise an exception
172
+            # email.replace('{user_id}', 'bob') will just ignore {userid}
173
+        else:
174
+            email_address = email_template.replace('{user_id}', '0')
175
+
176
+        return formataddr((mail_sender_name, email_address))
177
+
178
+    # Content Notification
179
+
180
+    @staticmethod
181
+    def log_notification(
182
+            config: CFG,
183
+            action: str,
184
+            recipient: typing.Optional[str],
185
+            subject: typing.Optional[str],
186
+    ) -> None:
187
+        """Log notification metadata."""
188
+        log_path = config.EMAIL_NOTIFICATION_LOG_FILE_PATH
189
+        if log_path:
190
+            # TODO - A.P - 2017-09-06 - file logging inefficiency
191
+            # Updating a document with 100 users to notify will leads to open
192
+            # and close the file 100 times.
193
+            with open(log_path, 'a') as log_file:
194
+                print(
195
+                    datetime.datetime.now(),
196
+                    action,
197
+                    recipient,
198
+                    subject,
199
+                    sep='|',
200
+                    file=log_file,
201
+                )
202
+
203
+    def notify_content_update(
204
+            self,
205
+            event_actor_id: int,
206
+            event_content_id: int
207
+    ) -> None:
208
+        """
209
+        Look for all users to be notified about the new content and send them an
210
+        individual email
211
+        :param event_actor_id: id of the user that has triggered the event
212
+        :param event_content_id: related content_id
213
+        :return:
214
+        """
215
+        # FIXME - D.A. - 2014-11-05
216
+        # Dirty import. It's here in order to avoid circular import
217
+        from tracim.lib.core.content import ContentApi
218
+        from tracim.lib.core.user import UserApi
219
+        user = UserApi(
220
+            None,
221
+            config=self.config,
222
+            session=self.session,
223
+            ).get_one(event_actor_id)
224
+        logger.debug(self, 'Content: {}'.format(event_content_id))
225
+        content_api = ContentApi(
226
+            current_user=user,
227
+            session=self.session,
228
+            config=self.config,
229
+            )
230
+        content = ContentApi(
231
+            session=self.session,
232
+            current_user=user, # TODO - use a system user instead of the user that has triggered the event
233
+            config=self.config,
234
+            show_archived=True,
235
+            show_deleted=True,
236
+        ).get_one(event_content_id, ContentType.Any)
237
+        main_content = content.parent if content.type == ContentType.Comment else content
238
+        notifiable_roles = WorkspaceApi(
239
+            current_user=user,
240
+            session=self.session,
241
+        ).get_notifiable_roles(content.workspace)
242
+
243
+        if len(notifiable_roles) <= 0:
244
+            logger.info(self, 'Skipping notification as nobody subscribed to in workspace {}'.format(content.workspace.label))
245
+            return
246
+
247
+
248
+        logger.info(self, 'Sending asynchronous emails to {} user(s)'.format(len(notifiable_roles)))
249
+        # INFO - D.A. - 2014-11-06
250
+        # The following email sender will send emails in the async task queue
251
+        # This allow to build all mails through current thread but really send them (including SMTP connection)
252
+        # In the other thread.
253
+        #
254
+        # This way, the webserver will return sooner (actually before notification emails are sent
255
+        async_email_sender = EmailSender(
256
+            self.config,
257
+            self._smtp_config,
258
+            self.config.EMAIL_NOTIFICATION_ACTIVATED
259
+        )
260
+        for role in notifiable_roles:
261
+            logger.info(self, 'Sending email to {}'.format(role.user.email))
262
+            to_addr = formataddr((role.user.display_name, role.user.email))
263
+            #
264
+            # INFO - G.M - 2017-11-15 - set content_id in header to permit reply
265
+            # references can have multiple values, but only one in this case.
266
+            replyto_addr = self.config.EMAIL_NOTIFICATION_REPLY_TO_EMAIL.replace( # nopep8
267
+                '{content_id}',str(content.content_id)
268
+            )
269
+
270
+            reference_addr = self.config.EMAIL_NOTIFICATION_REFERENCES_EMAIL.replace( #nopep8
271
+                '{content_id}',str(content.content_id)
272
+             )
273
+            #
274
+            #  INFO - D.A. - 2014-11-06
275
+            # We do not use .format() here because the subject defined in the .ini file
276
+            # may not include all required labels. In order to avoid partial format() (which result in an exception)
277
+            # we do use replace and force the use of .__str__() in order to process LazyString objects
278
+            #
279
+            subject = self.config.EMAIL_NOTIFICATION_CONTENT_UPDATE_SUBJECT
280
+            subject = subject.replace(EST.WEBSITE_TITLE, self.config.WEBSITE_TITLE.__str__())
281
+            subject = subject.replace(EST.WORKSPACE_LABEL, main_content.workspace.label.__str__())
282
+            subject = subject.replace(EST.CONTENT_LABEL, main_content.label.__str__())
283
+            subject = subject.replace(EST.CONTENT_STATUS_LABEL, main_content.get_status().label.__str__())
284
+            reply_to_label = l_('{username} & all members of {workspace}').format(
285
+                username=user.display_name,
286
+                workspace=main_content.workspace.label)
287
+
288
+            message = MIMEMultipart('alternative')
289
+            message['Subject'] = subject
290
+            message['From'] = self._get_sender(user)
291
+            message['To'] = to_addr
292
+            message['Reply-to'] = formataddr((reply_to_label, replyto_addr))
293
+            # INFO - G.M - 2017-11-15
294
+            # References can theorically have label, but in pratice, references
295
+            # contains only message_id from parents post in thread.
296
+            # To link this email to a content we create a virtual parent
297
+            # in reference who contain the content_id.
298
+            message['References'] = formataddr(('',reference_addr))
299
+            body_text = self._build_email_body_for_content(self.config.EMAIL_NOTIFICATION_CONTENT_UPDATE_TEMPLATE_TEXT, role, content, user)
300
+            body_html = self._build_email_body_for_content(self.config.EMAIL_NOTIFICATION_CONTENT_UPDATE_TEMPLATE_HTML, role, content, user)
301
+
302
+            part1 = MIMEText(body_text, 'plain', 'utf-8')
303
+            part2 = MIMEText(body_html, 'html', 'utf-8')
304
+            # Attach parts into message container.
305
+            # According to RFC 2046, the last part of a multipart message, in this case
306
+            # the HTML message, is best and preferred.
307
+            message.attach(part1)
308
+            message.attach(part2)
309
+
310
+            self.log_notification(
311
+                action='CREATED',
312
+                recipient=message['To'],
313
+                subject=message['Subject'],
314
+                config=self.config,
315
+            )
316
+
317
+            send_email_through(
318
+                self.config,
319
+                async_email_sender.send_mail,
320
+                message
321
+            )
322
+
323
+    def notify_created_account(
324
+            self,
325
+            user: User,
326
+            password: str,
327
+    ) -> None:
328
+        """
329
+        Send created account email to given user.
330
+
331
+        :param password: choosed password
332
+        :param user: user to notify
333
+        """
334
+        # TODO BS 20160712: Cyclic import
335
+        logger.debug(self, 'user: {}'.format(user.user_id))
336
+        logger.info(self, 'Sending asynchronous email to 1 user ({0})'.format(
337
+            user.email,
338
+        ))
339
+
340
+        async_email_sender = EmailSender(
341
+            self.config,
342
+            self._smtp_config,
343
+            self.config.EMAIL_NOTIFICATION_ACTIVATED
344
+        )
345
+
346
+        subject = \
347
+            self.config.EMAIL_NOTIFICATION_CREATED_ACCOUNT_SUBJECT \
348
+            .replace(
349
+                EST.WEBSITE_TITLE,
350
+                self.config.WEBSITE_TITLE.__str__()
351
+            )
352
+        message = MIMEMultipart('alternative')
353
+        message['Subject'] = subject
354
+        message['From'] = self._get_sender()
355
+        message['To'] = formataddr((user.get_display_name(), user.email))
356
+
357
+        text_template_file_path = self.config.EMAIL_NOTIFICATION_CREATED_ACCOUNT_TEMPLATE_TEXT  # nopep8
358
+        html_template_file_path = self.config.EMAIL_NOTIFICATION_CREATED_ACCOUNT_TEMPLATE_HTML  # nopep8
359
+
360
+        body_text = self._render_template(
361
+            mako_template_filepath=text_template_file_path,
362
+            context={
363
+                'user': user,
364
+                'password': password,
365
+                'login_url': self.config.WEBSITE_BASE_URL,
366
+            }
367
+        )
368
+
369
+        body_html = self._render_template(
370
+            mako_template_filepath=html_template_file_path,
371
+            context={
372
+                'user': user,
373
+                'password': password,
374
+                'login_url': self.config.WEBSITE_BASE_URL,
375
+            }
376
+        )
377
+
378
+        part1 = MIMEText(body_text, 'plain', 'utf-8')
379
+        part2 = MIMEText(body_html, 'html', 'utf-8')
380
+
381
+        # Attach parts into message container.
382
+        # According to RFC 2046, the last part of a multipart message,
383
+        # in this case the HTML message, is best and preferred.
384
+        message.attach(part1)
385
+        message.attach(part2)
386
+
387
+        send_email_through(
388
+            config=self.config,
389
+            sendmail_callable=async_email_sender.send_mail,
390
+            message=message
391
+        )
392
+
393
+    def _render_template(
394
+            self,
395
+            mako_template_filepath: str,
396
+            context: dict
397
+    ) -> str:
398
+        """
399
+        Render mako template with all needed current variables.
400
+
401
+        :param mako_template_filepath: file path of mako template
402
+        :param context: dict with template context
403
+        :return: template rendered string
404
+        """
405
+
406
+        template = Template(filename=mako_template_filepath)
407
+        return template.render(
408
+            _=_,
409
+            config=self.config,
410
+            **context
411
+        )
412
+
413
+    def _build_email_body_for_content(
414
+            self,
415
+            mako_template_filepath: str,
416
+            role: UserRoleInWorkspace,
417
+            content: Content,
418
+            actor: User
419
+    ) -> str:
420
+        """
421
+        Build an email body and return it as a string
422
+        :param mako_template_filepath: the absolute path to the mako template to be used for email body building
423
+        :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
424
+        :param content: the content item related to the notification
425
+        :param actor: the user at the origin of the action / notification (for example the one who wrote a comment
426
+        :param config: the global configuration
427
+        :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
428
+        """
429
+        logger.debug(self, 'Building email content from MAKO template {}'.format(mako_template_filepath))
430
+
431
+        main_title = content.label
432
+        content_intro = ''
433
+        content_text = ''
434
+        call_to_action_text = ''
435
+
436
+        action = content.get_last_action().id
437
+        if ActionDescription.COMMENT == action:
438
+            content_intro = l_('<span id="content-intro-username">{}</span> added a comment:').format(actor.display_name)
439
+            content_text = content.description
440
+            call_to_action_text = l_('Answer')
441
+
442
+        elif ActionDescription.CREATION == action:
443
+
444
+            # Default values (if not overriden)
445
+            content_text = content.description
446
+            call_to_action_text = l_('View online')
447
+
448
+            if ContentType.Thread == content.type:
449
+                call_to_action_text = l_('Answer')
450
+                content_intro = l_('<span id="content-intro-username">{}</span> started a thread entitled:').format(actor.display_name)
451
+                content_text = '<p id="content-body-intro">{}</p>'.format(content.label) + \
452
+                               content.get_last_comment_from(actor).description
453
+
454
+            elif ContentType.File == content.type:
455
+                content_intro = l_('<span id="content-intro-username">{}</span> added a file entitled:').format(actor.display_name)
456
+                if content.description:
457
+                    content_text = content.description
458
+                else:
459
+                    content_text = '<span id="content-body-only-title">{}</span>'.format(content.label)
460
+
461
+            elif ContentType.Page == content.type:
462
+                content_intro = l_('<span id="content-intro-username">{}</span> added a page entitled:').format(actor.display_name)
463
+                content_text = '<span id="content-body-only-title">{}</span>'.format(content.label)
464
+
465
+        elif ActionDescription.REVISION == action:
466
+            content_text = content.description
467
+            call_to_action_text = l_('View online')
468
+
469
+            if ContentType.File == content.type:
470
+                content_intro = l_('<span id="content-intro-username">{}</span> uploaded a new revision.').format(actor.display_name)
471
+                content_text = ''
472
+
473
+            elif ContentType.Page == content.type:
474
+                content_intro = l_('<span id="content-intro-username">{}</span> updated this page.').format(actor.display_name)
475
+                previous_revision = content.get_previous_revision()
476
+                title_diff = ''
477
+                if previous_revision.label != content.label:
478
+                    title_diff = htmldiff(previous_revision.label, content.label)
479
+                content_text = str(l_('<p id="content-body-intro">Here is an overview of the changes:</p>'))+ \
480
+                    title_diff + \
481
+                    htmldiff(previous_revision.description, content.description)
482
+
483
+            elif ContentType.Thread == content.type:
484
+                content_intro = l_('<span id="content-intro-username">{}</span> updated the thread description.').format(actor.display_name)
485
+                previous_revision = content.get_previous_revision()
486
+                title_diff = ''
487
+                if previous_revision.label != content.label:
488
+                    title_diff = htmldiff(previous_revision.label, content.label)
489
+                content_text = str(l_('<p id="content-body-intro">Here is an overview of the changes:</p>'))+ \
490
+                    title_diff + \
491
+                    htmldiff(previous_revision.description, content.description)
492
+
493
+        elif ActionDescription.EDITION == action:
494
+            call_to_action_text = l_('View online')
495
+
496
+            if ContentType.File == content.type:
497
+                content_intro = l_('<span id="content-intro-username">{}</span> updated the file description.').format(actor.display_name)
498
+                content_text = '<p id="content-body-intro">{}</p>'.format(content.get_label()) + \
499
+                    content.description
500
+
501
+        elif ActionDescription.STATUS_UPDATE == action:
502
+            call_to_action_text = l_('View online')
503
+            intro_user_msg = l_(
504
+                '<span id="content-intro-username">{}</span> '
505
+                'updated the following status:'
506
+            )
507
+            content_intro = intro_user_msg.format(actor.display_name)
508
+            intro_body_msg = '<p id="content-body-intro">{}: {}</p>'
509
+            content_text = intro_body_msg.format(
510
+                content.get_label(),
511
+                content.get_status().label,
512
+            )
513
+
514
+        if '' == content_intro and content_text == '':
515
+            # Skip notification, but it's not normal
516
+            logger.error(
517
+                self, 'A notification is being sent but no content. '
518
+                      'Here are some debug informations: [content_id: {cid}]'
519
+                      '[action: {act}][author: {actor}]'.format(
520
+                    cid=content.content_id, act=action, actor=actor
521
+                )
522
+            )
523
+            raise ValueError('Unexpected empty notification')
524
+
525
+        user = role.user
526
+        workspace = role.workspace
527
+        body_content = self._render_template(
528
+            mako_template_filepath=mako_template_filepath,
529
+            context={
530
+                'user': role.user,
531
+                'workspace': role.workspace,
532
+                'main_title': main_title,
533
+                'status': content.get_status().label,
534
+                'role': role.role_as_label(),
535
+                'content_intro': content_intro,
536
+                'content_text': content_text,
537
+                'call_to_action_text': call_to_action_text,
538
+            }
539
+        )
540
+        return body_content
541
+
542
+
543
+def get_email_manager(config: CFG, session: Session):
544
+    """
545
+    :return: EmailManager instance
546
+    """
547
+    #  TODO: Find a way to import properly without cyclic import
548
+
549
+    smtp_config = SmtpConfiguration(
550
+        config.EMAIL_NOTIFICATION_SMTP_SERVER,
551
+        config.EMAIL_NOTIFICATION_SMTP_PORT,
552
+        config.EMAIL_NOTIFICATION_SMTP_USER,
553
+        config.EMAIL_NOTIFICATION_SMTP_PASSWORD
554
+    )
555
+
556
+    return EmailManager(config=config, smtp_config=smtp_config, session=session)

+ 114 - 0
tracim/lib/mail_notifier/sender.py View File

@@ -0,0 +1,114 @@
1
+# -*- coding: utf-8 -*-
2
+import smtplib
3
+import typing
4
+from email.message import Message
5
+from email.mime.multipart import MIMEMultipart
6
+
7
+from tracim.config import CFG
8
+from tracim.lib.utils.logger import logger
9
+from tracim.lib.utils.utils import get_rq_queue
10
+from tracim.lib.utils.utils import get_redis_connection
11
+from tracim.lib.mail_notifier.utils import SmtpConfiguration
12
+
13
+def send_email_through(
14
+        config: CFG,
15
+        sendmail_callable: typing.Callable[[Message], None],
16
+        message: Message,
17
+) -> None:
18
+    """
19
+    Send mail encapsulation to send it in async or sync mode.
20
+
21
+    TODO BS 20170126: A global mail/sender management should be a good
22
+                      thing. Actually, this method is an fast solution.
23
+    :param config: system configuration
24
+    :param sendmail_callable: A callable who get message on first parameter
25
+    :param message: The message who have to be sent
26
+    """
27
+
28
+    if config.EMAIL_PROCESSING_MODE == config.CST.SYNC:
29
+        sendmail_callable(message)
30
+    elif config.EMAIL_PROCESSING_MODE == config.CST.ASYNC:
31
+        redis_connection = get_redis_connection(config)
32
+        queue = get_rq_queue(redis_connection, 'mail_sender')
33
+        queue.enqueue(sendmail_callable, message)
34
+    else:
35
+        raise NotImplementedError(
36
+            'Mail sender processing mode {} is not implemented'.format(
37
+                config.EMAIL_PROCESSING_MODE,
38
+            )
39
+        )
40
+
41
+
42
+class EmailSender(object):
43
+    """
44
+    Independent email sender class.
45
+
46
+    To allow its use in any thread, as an asyncjob_perform() call for
47
+    example, it has no dependencies on SQLAlchemy nor tg HTTP request.
48
+    """
49
+
50
+    def __init__(
51
+            self,
52
+            config: CFG,
53
+            smtp_config: SmtpConfiguration,
54
+            really_send_messages
55
+    ) -> None:
56
+        self._smtp_config = smtp_config
57
+        self.config = config
58
+        self._smtp_connection = None
59
+        self._is_active = really_send_messages
60
+
61
+    def connect(self):
62
+        if not self._smtp_connection:
63
+            log = 'Connecting from SMTP server {}'
64
+            logger.info(self, log.format(self._smtp_config.server))
65
+            self._smtp_connection = smtplib.SMTP(
66
+                self._smtp_config.server,
67
+                self._smtp_config.port
68
+            )
69
+            self._smtp_connection.ehlo()
70
+
71
+            if self._smtp_config.login:
72
+                try:
73
+                    starttls_result = self._smtp_connection.starttls()
74
+                    log = 'SMTP start TLS result: {}'
75
+                    logger.debug(self, log.format(starttls_result))
76
+                except Exception as e:
77
+                    log = 'SMTP start TLS error: {}'
78
+                    logger.debug(self, log.format(e.__str__()))
79
+
80
+            if self._smtp_config.login:
81
+                try:
82
+                    login_res = self._smtp_connection.login(
83
+                        self._smtp_config.login,
84
+                        self._smtp_config.password
85
+                    )
86
+                    log = 'SMTP login result: {}'
87
+                    logger.debug(self, log.format(login_res))
88
+                except Exception as e:
89
+                    log = 'SMTP login error: {}'
90
+                    logger.debug(self, log.format(e.__str__()))
91
+            logger.info(self, 'Connection OK')
92
+
93
+    def disconnect(self):
94
+        if self._smtp_connection:
95
+            log = 'Disconnecting from SMTP server {}'
96
+            logger.info(self, log.format(self._smtp_config.server))
97
+            self._smtp_connection.quit()
98
+            logger.info(self, 'Connection closed.')
99
+
100
+    def send_mail(self, message: MIMEMultipart):
101
+        if not self._is_active:
102
+            log = 'Not sending email to {} (service disabled)'
103
+            logger.info(self, log.format(message['To']))
104
+        else:
105
+            self.connect()  # Actually, this connects to SMTP only if required
106
+            logger.info(self, 'Sending email to {}'.format(message['To']))
107
+            self._smtp_connection.send_message(message)
108
+            from tracim.lib.mail_notifier.notifier import EmailManager
109
+            EmailManager.log_notification(
110
+                action='   SENT',
111
+                recipient=message['To'],
112
+                subject=message['Subject'],
113
+                config=self.config,
114
+            )

+ 33 - 0
tracim/lib/mail_notifier/utils.py View File

@@ -0,0 +1,33 @@
1
+class SmtpConfiguration(object):
2
+    """Container class for SMTP configuration used in Tracim."""
3
+
4
+    def __init__(self, server: str, port: int, login: str, password: str):
5
+        self.server = server
6
+        self.port = port
7
+        self.login = login
8
+        self.password = password
9
+
10
+
11
+class EST(object):
12
+    """
13
+    EST = Email Subject Tags - this is a convenient class - no business logic
14
+    here
15
+    This class is intended to agregate all dynamic content that may be included
16
+    in email subjects
17
+    """
18
+
19
+    WEBSITE_TITLE = '{website_title}'
20
+    WORKSPACE_LABEL = '{workspace_label}'
21
+    CONTENT_LABEL = '{content_label}'
22
+    CONTENT_STATUS_LABEL = '{content_status_label}'
23
+
24
+    @classmethod
25
+    def all(cls):
26
+        return [
27
+            cls.CONTENT_LABEL,
28
+            cls.CONTENT_STATUS_LABEL,
29
+            cls.WEBSITE_TITLE,
30
+            cls.WORKSPACE_LABEL
31
+        ]
32
+
33
+

+ 25 - 0
tracim/lib/utils/utils.py View File

@@ -1,10 +1,35 @@
1 1
 # -*- coding: utf-8 -*-
2 2
 import datetime
3
+from redis import Redis
4
+from rq import Queue
5
+
6
+from tracim.config import CFG
3 7
 
4 8
 DEFAULT_WEBDAV_CONFIG_FILE = "wsgidav.conf"
5 9
 DEFAULT_TRACIM_CONFIG_FILE = "development.ini"
6 10
 
7 11
 
12
+def get_redis_connection(config: CFG) -> Redis:
13
+    """
14
+    :param config: current app_config
15
+    :return: redis connection
16
+    """
17
+    return Redis(
18
+        host=config.EMAIL_SENDER_REDIS_HOST,
19
+        port=config.EMAIL_SENDER_REDIS_PORT,
20
+        db=config.EMAIL_SENDER_REDIS_DB,
21
+    )
22
+
23
+
24
+def get_rq_queue(redis_connection: Redis, queue_name: str ='default') -> Queue:
25
+    """
26
+    :param queue_name: name of queue
27
+    :return: wanted queue
28
+    """
29
+
30
+    return Queue(name=queue_name, connection=redis_connection)
31
+
32
+
8 33
 def cmp_to_key(mycmp):
9 34
     """
10 35
     List sort related function

+ 10 - 8
tracim/models/data.py View File

@@ -131,12 +131,12 @@ class UserRoleInWorkspace(DeclarativeBase):
131 131
     WORKSPACE_MANAGER = 8
132 132
 
133 133
     # TODO - G.M - 10-04-2018 - [Cleanup] Drop this
134
-    # LABEL = dict()
135
-    # LABEL[0] = l_('N/A')
136
-    # LABEL[1] = l_('Reader')
137
-    # LABEL[2] = l_('Contributor')
138
-    # LABEL[4] = l_('Content Manager')
139
-    # LABEL[8] = l_('Workspace Manager')
134
+    LABEL = dict()
135
+    LABEL[0] = l_('N/A')
136
+    LABEL[1] = l_('Reader')
137
+    LABEL[2] = l_('Contributor')
138
+    LABEL[4] = l_('Content Manager')
139
+    LABEL[8] = l_('Workspace Manager')
140 140
     #
141 141
     # STYLE = dict()
142 142
     # STYLE[0] = ''
@@ -161,8 +161,9 @@ class UserRoleInWorkspace(DeclarativeBase):
161 161
     # def style(self):
162 162
     #     return UserRoleInWorkspace.STYLE[self.role]
163 163
     #
164
-    # def role_as_label(self):
165
-    #     return UserRoleInWorkspace.LABEL[self.role]
164
+
165
+    def role_as_label(self):
166
+        return UserRoleInWorkspace.LABEL[self.role]
166 167
 
167 168
     @classmethod
168 169
     def get_all_role_values(self):
@@ -318,6 +319,7 @@ class ContentStatus(object):
318 319
                  # type=''
319 320
     ):
320 321
         self.id = id
322
+        self.label = self.id
321 323
         # TODO - G.M - 10-04-2018 - [Cleanup] Drop this
322 324
         # self.icon = ContentStatus._ICONS[id]
323 325
         # self.css = ContentStatus._CSS[id]

+ 0 - 0
tracim/templates/mail/__init__.py View File


+ 72 - 0
tracim/templates/mail/content_update_body_html.mak View File

@@ -0,0 +1,72 @@
1
+## -*- coding: utf-8 -*-
2
+<html>
3
+  <head>
4
+    <style>
5
+      a { color: #3465af;}
6
+      a.call-to-action {
7
+        background: #3465af;
8
+        padding: 3px 4px 5px 4px;
9
+        border: 1px solid #12438d;
10
+        font-weight: bold;
11
+        color: #FFF;
12
+        text-decoration: none;
13
+        margin-left: 5px;
14
+      }
15
+      a.call-to-action img { vertical-align: middle;}
16
+      th { vertical-align: top;}
17
+      
18
+      #content-intro-username { font-size: 1.5em; color: #666; font-weight: bold; }
19
+      #content-intro { margin: 0; border: 1em solid #DDD; border-width: 0 0 0 0em; padding: 1em 1em 1em 1em; }
20
+      #content-body { margin: 0em; border: 2em solid #DDD; border-width: 0 0 0 4em; padding: 0.5em 2em 1em 1em; }
21
+      #content-body-intro { font-size: 2em; color: #666; }
22
+      #content-body-only-title { font-size: 1.5em; }
23
+
24
+      #content-body ins { background-color: #AFA; }
25
+      #content-body del { background-color: #FAA; }
26
+
27
+
28
+      #call-to-action-button { background-color: #5CB85C; border: 1px solid #4CAE4C; color: #FFF; text-decoration: none; font-weight: bold; border-radius: 3px; font-size: 2em; padding: 4px 0.3em;}
29
+      #call-to-action-container { text-align: right; margin-top: 2em; }
30
+
31
+      #footer hr { border: 0px solid #CCC; border-top-width: 1px; width: 8em; max-width:25%; margin-left: 0;}
32
+      #footer { color: #999; margin: 4em auto auto 0.5em; }
33
+      #footer a { color: #999; }
34
+    </style>
35
+  </head>
36
+  <body style="font-family: Arial; font-size: 12px; max-width: 600px; margin: 0; padding: 0;">
37
+
38
+    <table style="width: 100%; cell-padding: 0; border-collapse: collapse; margin: 0">
39
+      <tr style="background-color: F5F5F5; border-bottom: 1px solid #CCC;" >
40
+        <td style="background-color: #666;">
41
+            FIXME restore logo
42
+        </td>
43
+        <td style="padding: 0.5em; background-color: #666; text-align: left;">
44
+          <span style="font-size: 1.3em; color: #FFF; font-weight: bold;">
45
+            ${main_title}
46
+            &mdash;&nbsp;<span style="font-weight: bold; color: #999; font-weight: bold;">
47
+              ${status|n}
48
+              FIXME restore icon
49
+            </span>
50
+        </td>
51
+      </tr>
52
+    </table>
53
+
54
+    <p id="content-intro">${content_intro|n}</p>
55
+    <div id="content-body">
56
+        <div>${content_text|n}</div>
57
+        <div href="FIXME url" id="call-to-action-container">
58
+        </div>
59
+    </div>
60
+    
61
+    <div id="footer">
62
+        <p>
63
+            ${_('{user_display_name}, you receive this email because you are registered on <i>{website_title}</i> and you are <i>{user_role_label}</i> in the workspace <a href="{workspace_url}">{workspace_label}</a>.').format(user_display_name=user.display_name, user_role_label=role, workspace_url='FIXME url', workspace_label=workspace.label, website_title=config.WEBSITE_TITLE)|n}
64
+        </p>
65
+        <hr/>
66
+        <p>
67
+            ${_('This email was automatically sent by <i>Tracim</i>, a collaborative software developped by Algoo.')}<br/>
68
+            Algoo SAS &mdash; 9 rue du rocher de Lorzier, 38430 Moirans, France &mdash; <a style="text-decoration: none;" href="http://algoo.fr">www.algoo.fr</a>
69
+        </p>
70
+    </div>
71
+  </body>
72
+</html>

+ 32 - 0
tracim/templates/mail/content_update_body_text.mak View File

@@ -0,0 +1,32 @@
1
+## -*- coding: utf-8 -*-
2
+
3
+Dear ${user.display_name},
4
+
5
+This email is intended to be read as HTML content.
6
+Please configure your email client to get the best of Tracim notifications.
7
+
8
+We understand that Email was originally intended to carry raw text only.
9
+And you probably understand on your own that we are a decades after email
10
+was created ;)
11
+
12
+Hope you'll switch your mail client configuration and enjoy Tracim :)
13
+
14
+
15
+--------------------------------------------------------------------------------
16
+
17
+
18
+You receive this email because you are registered on /${config.WEBSITE_TITLE}/
19
+and you are /${role}/ in the workspace /${workspace.label}/
20
+
21
+----
22
+
23
+This email was automatically sent by *Tracim*,
24
+a collaborative software developped by Algoo.
25
+
26
+FIXME ADDR
27
+**Algoo SAS**
28
+9 rue du rocher de Lorzier
29
+38430 Moirans
30
+France
31
+http://algoo.fr
32
+

+ 89 - 0
tracim/templates/mail/created_account_body_html.mak View File

@@ -0,0 +1,89 @@
1
+## -*- coding: utf-8 -*-
2
+<html>
3
+  <head>
4
+    <style>
5
+      a { color: #3465af;}
6
+      a.call-to-action {
7
+        background: #3465af;
8
+        padding: 3px 4px 5px 4px;
9
+        border: 1px solid #12438d;
10
+        font-weight: bold;
11
+        color: #FFF;
12
+        text-decoration: none;
13
+        margin-left: 5px;
14
+      }
15
+      a.call-to-action img { vertical-align: middle;}
16
+      th { vertical-align: top;}
17
+      
18
+      #content-intro-username { font-size: 1.5em; color: #666; font-weight: bold; }
19
+      #content-intro { margin: 0; border: 1em solid #DDD; border-width: 0 0 0 0em; padding: 1em 1em 1em 1em; }
20
+      #content-body { margin: 0em; border: 2em solid #DDD; border-width: 0 0 0 4em; padding: 0.5em 2em 1em 1em; }
21
+      #content-body-intro { font-size: 2em; color: #666; }
22
+      #content-body-only-title { font-size: 1.5em; }
23
+
24
+      #content-body ins { background-color: #AFA; }
25
+      #content-body del { background-color: #FAA; }
26
+
27
+
28
+      #call-to-action-button { background-color: #5CB85C; border: 1px solid #4CAE4C; color: #FFF; text-decoration: none; font-weight: bold; border-radius: 3px; font-size: 2em; padding: 4px 0.3em;}
29
+      #call-to-action-container { text-align: right; margin-top: 2em; }
30
+
31
+      #footer hr { border: 0px solid #CCC; border-top-width: 1px; width: 8em; max-width:25%; margin-left: 0;}
32
+      #footer { color: #999; margin: 4em auto auto 0.5em; }
33
+      #footer a { color: #999; }
34
+    </style>
35
+  </head>
36
+  <body style="font-family: Arial; font-size: 12px; max-width: 600px; margin: 0; padding: 0;">
37
+
38
+    <table style="width: 100%; cell-padding: 0; border-collapse: collapse; margin: 0">
39
+      <tr style="background-color: F5F5F5; border-bottom: 1px solid #CCC;" >
40
+        <td style="background-color: #666;">
41
+            FIXME restore logo
42
+        </td>
43
+        <td style="padding: 0.5em; background-color: #666; text-align: left;">
44
+          <span style="font-size: 1.3em; color: #FFF; font-weight: bold;">
45
+
46
+            ${config.WEBSITE_TITLE}: ${_('Created account')}
47
+
48
+          </span>
49
+        </td>
50
+      </tr>
51
+    </table>
52
+
53
+    <div id="content-body">
54
+        <div>
55
+            ${_('An administrator just create account for you on {website_title}'.format(
56
+                website_title=config.WEBSITE_TITLE
57
+            ))}
58
+
59
+            <ul>
60
+                <li>
61
+                    <b>${_('Login')}</b>: ${user.email}
62
+                </li>
63
+                <li>
64
+                    <b>${_('Password')}</b>: ${password}
65
+                </li>
66
+            </ul>
67
+
68
+        </div>
69
+        <div id="call-to-action-container">
70
+
71
+            ${_('To go to {website_title}, please click on following link'.format(
72
+                website_title=config.WEBSITE_TITLE
73
+            ))}
74
+
75
+            <span style="">
76
+                <a href="${login_url}" id='call-to-action-button'>${login_url}</a>
77
+            </span>
78
+        </div>
79
+    </div>
80
+    
81
+    <div id="footer">
82
+        <p>
83
+            ${_('This email was sent by <i>Tracim</i>, a collaborative software developped by Algoo.')}<br/>
84
+            FIXME ADDR
85
+            Algoo SAS &mdash; 9 rue du rocher de Lorzier, 38430 Moirans, France &mdash; <a style="text-decoration: none;" href="http://algoo.fr">www.algoo.fr</a>
86
+        </p>
87
+    </div>
88
+  </body>
89
+</html>

+ 26 - 0
tracim/templates/mail/created_account_body_text.mak View File

@@ -0,0 +1,26 @@
1
+## -*- coding: utf-8 -*-
2
+
3
+${_('An administrator just create account for you on {website_title}'.format(
4
+    website_title=config.WEBSITE_TITLE
5
+))}
6
+
7
+* ${_('Login')}: ${user.email}
8
+* ${_('Password')}: ${password}
9
+
10
+${_('To go to {website_title}, please click on following link'.format(
11
+    website_title=config.WEBSITE_TITLE
12
+))}
13
+
14
+${login_url}
15
+
16
+--------------------------------------------------------------------------------
17
+
18
+This email was sent by *Tracim*,
19
+a collaborative software developped by Algoo.
20
+
21
+FIXME ADDR
22
+**Algoo SAS**
23
+9 rue du rocher de Lorzier
24
+38430 Moirans
25
+France
26
+http://algoo.fr

+ 45 - 45
tracim/tests/library/test_content_api.py View File

@@ -112,8 +112,8 @@ class TestContentApi(DefaultTest):
112 112
                   group_api.get_one(Group.TIM_MANAGER),
113 113
                   group_api.get_one(Group.TIM_ADMIN)]
114 114
 
115
-        user = uapi.create_user(email='this.is@user',
116
-                                groups=groups, save_now=True)
115
+        user = uapi.create_minimal_user(email='this.is@user',
116
+                                        groups=groups, save_now=True)
117 117
         workspace = WorkspaceApi(
118 118
             current_user=user,
119 119
             session=self.session
@@ -189,8 +189,8 @@ class TestContentApi(DefaultTest):
189 189
                   group_api.get_one(Group.TIM_MANAGER),
190 190
                   group_api.get_one(Group.TIM_ADMIN)]
191 191
 
192
-        user = uapi.create_user(email='this.is@user',
193
-                                groups=groups, save_now=True)
192
+        user = uapi.create_minimal_user(email='this.is@user',
193
+                                        groups=groups, save_now=True)
194 194
         workspace_api = WorkspaceApi(current_user=user, session=self.session)
195 195
         workspace = workspace_api.create_workspace(
196 196
             'test workspace',
@@ -274,7 +274,7 @@ class TestContentApi(DefaultTest):
274 274
                   group_api.get_one(Group.TIM_MANAGER),
275 275
                   group_api.get_one(Group.TIM_ADMIN)]
276 276
 
277
-        user = uapi.create_user(
277
+        user = uapi.create_minimal_user(
278 278
             email='this.is@user',
279 279
             groups=groups,
280 280
             save_now=True
@@ -331,8 +331,8 @@ class TestContentApi(DefaultTest):
331 331
                   group_api.get_one(Group.TIM_MANAGER),
332 332
                   group_api.get_one(Group.TIM_ADMIN)]
333 333
 
334
-        user = uapi.create_user(email='this.is@user',
335
-                                groups=groups, save_now=True)
334
+        user = uapi.create_minimal_user(email='this.is@user',
335
+                                        groups=groups, save_now=True)
336 336
         workspace = WorkspaceApi(
337 337
             current_user=user,
338 338
             session=self.session
@@ -400,8 +400,8 @@ class TestContentApi(DefaultTest):
400 400
                   group_api.get_one(Group.TIM_MANAGER),
401 401
                   group_api.get_one(Group.TIM_ADMIN)]
402 402
 
403
-        user = uapi.create_user(email='this.is@user',
404
-                                groups=groups, save_now=True)
403
+        user = uapi.create_minimal_user(email='this.is@user',
404
+                                        groups=groups, save_now=True)
405 405
 
406 406
         workspace = WorkspaceApi(
407 407
             current_user=user,
@@ -438,8 +438,8 @@ class TestContentApi(DefaultTest):
438 438
                   group_api.get_one(Group.TIM_MANAGER),
439 439
                   group_api.get_one(Group.TIM_ADMIN)]
440 440
 
441
-        user = uapi.create_user(email='this.is@user',
442
-                                groups=groups, save_now=True)
441
+        user = uapi.create_minimal_user(email='this.is@user',
442
+                                        groups=groups, save_now=True)
443 443
 
444 444
         workspace = WorkspaceApi(
445 445
             current_user=user,
@@ -480,8 +480,8 @@ class TestContentApi(DefaultTest):
480 480
                   group_api.get_one(Group.TIM_MANAGER),
481 481
                   group_api.get_one(Group.TIM_ADMIN)]
482 482
 
483
-        user = uapi.create_user(email='this.is@user',
484
-                                groups=groups, save_now=True)
483
+        user = uapi.create_minimal_user(email='this.is@user',
484
+                                        groups=groups, save_now=True)
485 485
 
486 486
         workspace = WorkspaceApi(
487 487
             current_user=user,
@@ -522,12 +522,12 @@ class TestContentApi(DefaultTest):
522 522
                   group_api.get_one(Group.TIM_MANAGER),
523 523
                   group_api.get_one(Group.TIM_ADMIN)]
524 524
 
525
-        user = uapi.create_user(
525
+        user = uapi.create_minimal_user(
526 526
             email='user1@user',
527 527
             groups=groups,
528 528
             save_now=True
529 529
         )
530
-        user2 = uapi.create_user(
530
+        user2 = uapi.create_minimal_user(
531 531
             email='user2@user',
532 532
             groups=groups,
533 533
             save_now=True
@@ -634,12 +634,12 @@ class TestContentApi(DefaultTest):
634 634
                   group_api.get_one(Group.TIM_MANAGER),
635 635
                   group_api.get_one(Group.TIM_ADMIN)]
636 636
 
637
-        user = uapi.create_user(
637
+        user = uapi.create_minimal_user(
638 638
             email='user1@user',
639 639
             groups=groups,
640 640
             save_now=True
641 641
         )
642
-        user2 = uapi.create_user(
642
+        user2 = uapi.create_minimal_user(
643 643
             email='user2@user',
644 644
             groups=groups,
645 645
             save_now=True
@@ -744,12 +744,12 @@ class TestContentApi(DefaultTest):
744 744
                   group_api.get_one(Group.TIM_MANAGER),
745 745
                   group_api.get_one(Group.TIM_ADMIN)]
746 746
 
747
-        user = uapi.create_user(
747
+        user = uapi.create_minimal_user(
748 748
             email='user1@user',
749 749
             groups=groups,
750 750
             save_now=True,
751 751
         )
752
-        user2 = uapi.create_user(
752
+        user2 = uapi.create_minimal_user(
753 753
             email='user2@user',
754 754
             groups=groups,
755 755
             save_now=True
@@ -843,10 +843,10 @@ class TestContentApi(DefaultTest):
843 843
                   group_api.get_one(Group.TIM_MANAGER),
844 844
                   group_api.get_one(Group.TIM_ADMIN)]
845 845
 
846
-        user_a = uapi.create_user(email='this.is@user',
847
-                                  groups=groups, save_now=True)
848
-        user_b = uapi.create_user(email='this.is@another.user',
849
-                                  groups=groups, save_now=True)
846
+        user_a = uapi.create_minimal_user(email='this.is@user',
847
+                                          groups=groups, save_now=True)
848
+        user_b = uapi.create_minimal_user(email='this.is@another.user',
849
+                                          groups=groups, save_now=True)
850 850
 
851 851
         wapi = WorkspaceApi(
852 852
             current_user=user_a,
@@ -946,12 +946,12 @@ class TestContentApi(DefaultTest):
946 946
                   group_api.get_one(Group.TIM_MANAGER),
947 947
                   group_api.get_one(Group.TIM_ADMIN)]
948 948
 
949
-        user_a = uapi.create_user(
949
+        user_a = uapi.create_minimal_user(
950 950
             email='this.is@user',
951 951
             groups=groups,
952 952
             save_now=True
953 953
         )
954
-        user_b = uapi.create_user(
954
+        user_b = uapi.create_minimal_user(
955 955
             email='this.is@another.user',
956 956
             groups=groups,
957 957
             save_now=True
@@ -1009,12 +1009,12 @@ class TestContentApi(DefaultTest):
1009 1009
                   group_api.get_one(Group.TIM_MANAGER),
1010 1010
                   group_api.get_one(Group.TIM_ADMIN)]
1011 1011
 
1012
-        user_a = uapi.create_user(
1012
+        user_a = uapi.create_minimal_user(
1013 1013
             email='this.is@user',
1014 1014
             groups=groups,
1015 1015
             save_now=True
1016 1016
         )
1017
-        user_b = uapi.create_user(
1017
+        user_b = uapi.create_minimal_user(
1018 1018
             email='this.is@another.user',
1019 1019
             groups=groups,
1020 1020
             save_now=True
@@ -1102,7 +1102,7 @@ class TestContentApi(DefaultTest):
1102 1102
                   group_api.get_one(Group.TIM_MANAGER),
1103 1103
                   group_api.get_one(Group.TIM_ADMIN)]
1104 1104
 
1105
-        user1 = uapi.create_user(
1105
+        user1 = uapi.create_minimal_user(
1106 1106
             email='this.is@user',
1107 1107
             groups=groups,
1108 1108
             save_now=True
@@ -1116,7 +1116,7 @@ class TestContentApi(DefaultTest):
1116 1116
         
1117 1117
         wid = workspace.workspace_id
1118 1118
 
1119
-        user2 = uapi.create_user()
1119
+        user2 = uapi.create_minimal_user()
1120 1120
         user2.email = 'this.is@another.user'
1121 1121
         uapi.save(user2)
1122 1122
 
@@ -1223,7 +1223,7 @@ class TestContentApi(DefaultTest):
1223 1223
                   group_api.get_one(Group.TIM_MANAGER),
1224 1224
                   group_api.get_one(Group.TIM_ADMIN)]
1225 1225
 
1226
-        user1 = uapi.create_user(
1226
+        user1 = uapi.create_minimal_user(
1227 1227
             email='this.is@user',
1228 1228
             groups=groups,
1229 1229
             save_now=True,
@@ -1237,7 +1237,7 @@ class TestContentApi(DefaultTest):
1237 1237
             save_now=True
1238 1238
         )
1239 1239
 
1240
-        user2 = uapi.create_user()
1240
+        user2 = uapi.create_minimal_user()
1241 1241
         user2.email = 'this.is@another.user'
1242 1242
         uapi.save(user2)
1243 1243
 
@@ -1301,7 +1301,7 @@ class TestContentApi(DefaultTest):
1301 1301
                   group_api.get_one(Group.TIM_MANAGER),
1302 1302
                   group_api.get_one(Group.TIM_ADMIN)]
1303 1303
 
1304
-        user1 = uapi.create_user(
1304
+        user1 = uapi.create_minimal_user(
1305 1305
             email='this.is@user',
1306 1306
             groups=groups,
1307 1307
             save_now=True
@@ -1314,7 +1314,7 @@ class TestContentApi(DefaultTest):
1314 1314
         )
1315 1315
         wid = workspace.workspace_id
1316 1316
 
1317
-        user2 = uapi.create_user()
1317
+        user2 = uapi.create_minimal_user()
1318 1318
         user2.email = 'this.is@another.user'
1319 1319
         uapi.save(user2)
1320 1320
 
@@ -1415,7 +1415,7 @@ class TestContentApi(DefaultTest):
1415 1415
                   group_api.get_one(Group.TIM_MANAGER),
1416 1416
                   group_api.get_one(Group.TIM_ADMIN)]
1417 1417
 
1418
-        user1 = uapi.create_user(
1418
+        user1 = uapi.create_minimal_user(
1419 1419
             email='this.is@user',
1420 1420
             groups=groups,
1421 1421
             save_now=True,
@@ -1427,7 +1427,7 @@ class TestContentApi(DefaultTest):
1427 1427
             save_now=True
1428 1428
         )
1429 1429
 
1430
-        user2 = uapi.create_user()
1430
+        user2 = uapi.create_minimal_user()
1431 1431
         user2.email = 'this.is@another.user'
1432 1432
         uapi.save(user2)
1433 1433
 
@@ -1494,7 +1494,7 @@ class TestContentApi(DefaultTest):
1494 1494
                   group_api.get_one(Group.TIM_MANAGER),
1495 1495
                   group_api.get_one(Group.TIM_ADMIN)]
1496 1496
 
1497
-        user1 = uapi.create_user(
1497
+        user1 = uapi.create_minimal_user(
1498 1498
             email='this.is@user',
1499 1499
             groups=groups,
1500 1500
             save_now=True
@@ -1508,7 +1508,7 @@ class TestContentApi(DefaultTest):
1508 1508
         )
1509 1509
         wid = workspace.workspace_id
1510 1510
 
1511
-        user2 = uapi.create_user()
1511
+        user2 = uapi.create_minimal_user()
1512 1512
         user2.email = 'this.is@another.user'
1513 1513
         uapi.save(user2)
1514 1514
 
@@ -1643,7 +1643,7 @@ class TestContentApi(DefaultTest):
1643 1643
                   group_api.get_one(Group.TIM_MANAGER),
1644 1644
                   group_api.get_one(Group.TIM_ADMIN)]
1645 1645
 
1646
-        user1 = uapi.create_user(
1646
+        user1 = uapi.create_minimal_user(
1647 1647
             email='this.is@user',
1648 1648
             groups=groups,
1649 1649
             save_now=True
@@ -1657,7 +1657,7 @@ class TestContentApi(DefaultTest):
1657 1657
         )
1658 1658
         wid = workspace.workspace_id
1659 1659
 
1660
-        user2 = uapi.create_user()
1660
+        user2 = uapi.create_minimal_user()
1661 1661
         user2.email = 'this.is@another.user'
1662 1662
         uapi.save(user2)
1663 1663
 
@@ -1793,8 +1793,8 @@ class TestContentApi(DefaultTest):
1793 1793
                   group_api.get_one(Group.TIM_MANAGER),
1794 1794
                   group_api.get_one(Group.TIM_ADMIN)]
1795 1795
 
1796
-        user = uapi.create_user(email='this.is@user',
1797
-                                groups=groups, save_now=True)
1796
+        user = uapi.create_minimal_user(email='this.is@user',
1797
+                                        groups=groups, save_now=True)
1798 1798
 
1799 1799
         workspace = WorkspaceApi(
1800 1800
             current_user=user,
@@ -1848,8 +1848,8 @@ class TestContentApi(DefaultTest):
1848 1848
                   group_api.get_one(Group.TIM_MANAGER),
1849 1849
                   group_api.get_one(Group.TIM_ADMIN)]
1850 1850
 
1851
-        user = uapi.create_user(email='this.is@user',
1852
-                                groups=groups, save_now=True)
1851
+        user = uapi.create_minimal_user(email='this.is@user',
1852
+                                        groups=groups, save_now=True)
1853 1853
 
1854 1854
         workspace = WorkspaceApi(
1855 1855
             current_user=user,
@@ -1899,8 +1899,8 @@ class TestContentApi(DefaultTest):
1899 1899
                   group_api.get_one(Group.TIM_MANAGER),
1900 1900
                   group_api.get_one(Group.TIM_ADMIN)]
1901 1901
 
1902
-        user = uapi.create_user(email='this.is@user',
1903
-                                groups=groups, save_now=True)
1902
+        user = uapi.create_minimal_user(email='this.is@user',
1903
+                                        groups=groups, save_now=True)
1904 1904
 
1905 1905
         workspace = WorkspaceApi(
1906 1906
             current_user=user,

+ 4 - 2
tracim/tests/library/test_notification.py View File

@@ -4,18 +4,20 @@ import re
4 4
 
5 5
 
6 6
 from tracim.lib.core.notifications import DummyNotifier
7
-from tracim.lib.core.notifications import EmailNotifier
7
+
8 8
 from tracim.lib.core.notifications import NotifierFactory
9
+from tracim.lib.mail_notifier.notifier import EmailNotifier
9 10
 from tracim.models.auth import User
10 11
 from tracim.models.data import Content
11 12
 from tracim.tests import DefaultTest
12 13
 from tracim.tests import eq_
13 14
 
15
+
14 16
 class TestDummyNotifier(DefaultTest):
15 17
 
16 18
     def test_dummy_notifier__notify_content_update(self):
17 19
         c = Content()
18
-        notifier = DummyNotifier(self.app_config)
20
+        notifier = DummyNotifier(self.app_config, self.session)
19 21
         notifier.notify_content_update(c)
20 22
         # INFO - D.A. - 2014-12-09 -
21 23
         # Old notification_content_update raised an exception

+ 7 - 6
tracim/tests/library/test_user_api.py View File

@@ -17,7 +17,7 @@ class TestUserApi(DefaultTest):
17 17
             session=self.session,
18 18
             config=self.config,
19 19
         )
20
-        u = api.create_user()
20
+        u = api.create_minimal_user()
21 21
         api.update(u, 'bob', 'bob@bob', True)
22 22
 
23 23
         nu = api.get_one_by_email('bob@bob')
@@ -31,7 +31,7 @@ class TestUserApi(DefaultTest):
31 31
             session=self.session,
32 32
             config=self.config,
33 33
         )
34
-        u = api.create_user()
34
+        u = api.create_minimal_user()
35 35
         api.update(u, 'bibi', 'bibi@bibi', True)
36 36
         transaction.commit()
37 37
 
@@ -44,7 +44,8 @@ class TestUserApi(DefaultTest):
44 44
             session=self.session,
45 45
             config=self.config,
46 46
         )
47
-        u = api.create_user()
47
+        u = api.create_minimal_user()
48
+        self.session.flush()
48 49
         api.update(u, 'bibi', 'bibi@bibi', True)
49 50
         uid = u.user_id
50 51
         transaction.commit()
@@ -67,8 +68,8 @@ class TestUserApi(DefaultTest):
67 68
             session=self.session,
68 69
             config=self.config,
69 70
         )
70
-        # u1 = api.create_user(True)
71
-        # u2 = api.create_user(True)
71
+        # u1 = api.create_minimal_user(True)
72
+        # u2 = api.create_minimal_user(True)
72 73
 
73 74
         # users = api.get_all()
74 75
         # ok_(2==len(users))
@@ -79,7 +80,7 @@ class TestUserApi(DefaultTest):
79 80
             session=self.session,
80 81
             config=self.config,
81 82
         )
82
-        u = api.create_user()
83
+        u = api.create_minimal_user()
83 84
         api.update(u, 'titi', 'titi@titi', True)
84 85
         one = api.get_one(u.user_id)
85 86
         eq_(u.user_id, one.user_id)

+ 2 - 2
tracim/tests/library/test_workspace.py View File

@@ -52,7 +52,7 @@ class TestThread(DefaultTest):
52 52
             current_user=admin,
53 53
             config=self.config
54 54
         )
55
-        u = uapi.create_user(email='u.u@u.u', save_now=True)
55
+        u = uapi.create_minimal_user(email='u.u@u.u', save_now=True)
56 56
         eq_([], wapi.get_notifiable_roles(workspace=w))
57 57
         rapi = RoleApi(
58 58
             session=self.session,
@@ -88,7 +88,7 @@ class TestThread(DefaultTest):
88 88
             session=self.session,
89 89
             current_user=None,
90 90
         )
91
-        u = uapi.create_user('u.s@e.r', [gapi.get_one(Group.TIM_USER)], True)
91
+        u = uapi.create_minimal_user('u.s@e.r', [gapi.get_one(Group.TIM_USER)], True)
92 92
         wapi = WorkspaceApi(
93 93
             session=self.session,
94 94
             current_user=u

+ 107 - 0
tracim/views/core_api/session_controller.py View File

@@ -1,6 +1,14 @@
1 1
 # coding=utf-8
2 2
 from pyramid.config import Configurator
3 3
 from sqlalchemy.orm.exc import NoResultFound
4
+
5
+from tracim.lib.core.content import ContentApi
6
+from tracim.lib.core.group import GroupApi
7
+from tracim.lib.core.userworkspace import RoleApi
8
+from tracim.lib.core.workspace import WorkspaceApi
9
+from tracim.models import Group
10
+from tracim.models.data import ContentType
11
+
4 12
 try:  # Python 3.5+
5 13
     from http import HTTPStatus
6 14
 except ImportError:
@@ -86,6 +94,99 @@ class SessionController(Controller):
86 94
             config=app_config,
87 95
         )
88 96
 
97
+    @hapic.with_api_doc()
98
+    @hapic.output_body(
99
+        UserSchema(),
100
+    )
101
+    def create_user(self, context, request: TracimRequest, hapic_data=None):
102
+        """
103
+        Return current logged in user or 401
104
+        """
105
+        app_config = request.registry.settings['CFG']
106
+        uapi = UserApi(
107
+            None,
108
+            session=request.dbsession,
109
+            config=app_config,
110
+        )
111
+        group_api = GroupApi(current_user=None, session=request.dbsession)
112
+        groups = [group_api.get_one(Group.TIM_USER),
113
+                  group_api.get_one(Group.TIM_MANAGER),
114
+                  group_api.get_one(Group.TIM_ADMIN)]
115
+        user = uapi.create_user(
116
+            email='dev.tracim.testuser@algoo.fr',
117
+            password='toto',
118
+            name='toto',
119
+            groups=groups,
120
+            timezone="lapin",
121
+            do_save=True,
122
+            do_notify=True,
123
+        )
124
+        wapi = WorkspaceApi(
125
+            current_user=user,
126
+            session=request.dbsession,
127
+        )
128
+        workspace = wapi.get_one_by_label('w1')
129
+        rapi = RoleApi(
130
+            session=request.dbsession,
131
+            current_user=user,
132
+        )
133
+        rapi.create_one(
134
+            user=user,
135
+            workspace=workspace,
136
+            role_level=8,
137
+            with_notif=True,
138
+            flush=True,
139
+        )
140
+        return UserInContext(
141
+            user=user,
142
+            dbsession=request.dbsession,
143
+            config=app_config,
144
+        )
145
+
146
+    @hapic.with_api_doc()
147
+    @hapic.handle_exception(
148
+        NotAuthentificated,
149
+        http_code=HTTPStatus.UNAUTHORIZED
150
+    )
151
+    @hapic.output_body(
152
+        NoContentSchema()
153
+    )
154
+    def add_content(self, context, request: TracimRequest, hapic_data=None):
155
+        """
156
+        Return current logged in user or 401
157
+        """
158
+        app_config = request.registry.settings['CFG']
159
+        uapi = UserApi(
160
+            current_user=request.current_user,
161
+            session=request.dbsession,
162
+            config=app_config,
163
+        )
164
+        workspace = WorkspaceApi(
165
+            current_user=request.current_user,
166
+            session=request.dbsession
167
+        ).get_one_by_label('w1')
168
+        api = ContentApi(
169
+            current_user=request.current_user,
170
+            session=request.dbsession,
171
+            config=app_config,
172
+        )
173
+        item = api.create(
174
+            ContentType.Folder,
175
+            workspace,
176
+            None,
177
+            'parent',
178
+            do_save=True,
179
+        )
180
+        item2 = api.create(
181
+            ContentType.File,
182
+            workspace,
183
+            item,
184
+            'file1',
185
+            do_save=True,
186
+            do_notify=True,
187
+        )
188
+        return
189
+
89 190
     def bind(self, configurator: Configurator):
90 191
 
91 192
         # Login
@@ -99,3 +200,9 @@ class SessionController(Controller):
99 200
         # Whoami
100 201
         configurator.add_route('whoami', '/sessions/whoami', request_method='GET')  # nopep8
101 202
         configurator.add_view(self.whoami, route_name='whoami',)
203
+
204
+        configurator.add_route('create_user_test', '/create_user', request_method='POST')  # nopep8
205
+        configurator.add_view(self.create_user, route_name='create_user_test',)
206
+
207
+        configurator.add_route('add_content', '/add_content', request_method='POST')  # nopep8
208
+        configurator.add_view(self.add_content, route_name='add_content',)