Pārlūkot izejas kodu

Merge branch 'feature/51_restore_mail_notifier' into fix/few_more_tests

Guénaël Muller 6 gadus atpakaļ
vecāks
revīzija
8d6247f428

+ 7 - 0
.travis.yml Parādīt failu

@@ -5,6 +5,13 @@ python:
5 5
   - "3.5"
6 6
   - "3.6"
7 7
 
8
+services:
9
+  - docker
10
+  - redis-server
11
+
12
+before_install:
13
+  - docker pull mailhog/mailhog
14
+  - docker run -d -p 1025:1025 -p 8025:8025 mailhog/mailhog
8 15
 install:
9 16
   - pip install --upgrade pip setuptools
10 17
   - pip install -e ".[testing]"

+ 15 - 0
README.md Parādīt failu

@@ -19,6 +19,7 @@ on Debian Stretch (9) with sudo:
19 19
     sudo apt update
20 20
     sudo apt install git
21 21
     sudo apt install python3 python3-venv python3-dev python3-pip
22
+    sudo apt install redis-server
22 23
 
23 24
 ### Get the source ###
24 25
 
@@ -110,12 +111,26 @@ run wsgidav server:
110 111
 
111 112
     tracimcli webdav start
112 113
 
114
+
113 115
 ## Run Tests and others checks ##
114 116
 
117
+### Run Tests ###
118
+
119
+Before running some functional test related to email, you need a local working *MailHog*
120
+see here : https://github.com/mailhog/MailHog
121
+
122
+You can run it this way with docker :
123
+
124
+    docker pull mailhog/mailhog
125
+    docker run -d -p 1025:1025 -p 8025:8025 mailhog/mailhog
126
+
127
+
115 128
 Run your project's tests:
116 129
 
117 130
     pytest
118 131
 
132
+### Lints and others checks ###
133
+
119 134
 Run mypy checks:
120 135
 
121 136
     mypy --ignore-missing-imports --disallow-untyped-defs tracim

+ 6 - 0
setup.py Parādīt failu

@@ -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 = [
@@ -42,6 +47,7 @@ tests_require = [
42 47
     'pytest-cov',
43 48
     'pep8',
44 49
     'mypy',
50
+    'requests'
45 51
 ]
46 52
 
47 53
 mysql_require = [

+ 57 - 0
tests_configs.ini Parādīt failu

@@ -0,0 +1,57 @@
1
+[base_test]
2
+sqlalchemy.url = sqlite:///:memory:
3
+depot_storage_name = test
4
+depot_storage_dir = /tmp/test/depot
5
+user.auth_token.validity = 604800
6
+preview_cache_dir = /tmp/test/preview_cache
7
+
8
+[mail_test]
9
+sqlalchemy.url = sqlite:///:memory:
10
+depot_storage_name = test
11
+depot_storage_dir = /tmp/test/depot
12
+user.auth_token.validity = 604800
13
+preview_cache_dir = /tmp/test/preview_cache
14
+email.notification.activated = true
15
+email.notification.from.email = test_user_from+{user_id}@localhost
16
+email.notification.from.default_label = Tracim Notifications
17
+email.notification.reply_to.email = test_user_reply+{content_id}@localhost
18
+email.notification.references.email = test_user_refs+{content_id}@localhost
19
+email.notification.content_update.template.html = %(here)s/tracim/templates/mail/content_update_body_html.mak
20
+email.notification.content_update.template.text = %(here)s/tracim/templates/mail/content_update_body_text.mak
21
+email.notification.created_account.template.html = %(here)s/tracim/templates/mail/created_account_body_html.mak
22
+email.notification.created_account.template.text = %(here)s/tracim/templates/mail/created_account_body_text.mak
23
+# Note: items between { and } are variable names. Do not remove / rename them
24
+email.notification.content_update.subject = [{website_title}] [{workspace_label}] {content_label} ({content_status_label})
25
+email.notification.created_account.subject = [{website_title}] Created account
26
+# processing_mode may be sync or async
27
+email.notification.processing_mode = sync
28
+email.notification.smtp.server = 127.0.0.1
29
+email.notification.smtp.port = 1025
30
+email.notification.smtp.user = test_user
31
+email.notification.smtp.password = just_a_password
32
+
33
+[mail_test_async]
34
+sqlalchemy.url = sqlite:///:memory:
35
+depot_storage_name = test
36
+depot_storage_dir = /tmp/test/depot
37
+user.auth_token.validity = 604800
38
+preview_cache_dir = /tmp/test/preview_cache
39
+email.notification.activated = true
40
+email.notification.from.email = test_user_from+{user_id}@localhost
41
+email.notification.from.default_label = Tracim Notifications
42
+email.notification.reply_to.email = test_user_reply+{content_id}@localhost
43
+email.notification.references.email = test_user_refs+{content_id}@localhost
44
+email.notification.content_update.template.html = %(here)s/tracim/templates/mail/content_update_body_html.mak
45
+email.notification.content_update.template.text = %(here)s/tracim/templates/mail/content_update_body_text.mak
46
+email.notification.created_account.template.html = %(here)s/tracim/templates/mail/created_account_body_html.mak
47
+email.notification.created_account.template.text = %(here)s/tracim/templates/mail/created_account_body_text.mak
48
+# Note: items between { and } are variable names. Do not remove / rename them
49
+email.notification.content_update.subject = [{website_title}] [{workspace_label}] {content_label} ({content_status_label})
50
+email.notification.created_account.subject = [{website_title}] Created account
51
+# processing_mode may be sync or async
52
+email.notification.processing_mode = sync
53
+email.processing_mode = async
54
+email.notification.smtp.server = 127.0.0.1
55
+email.notification.smtp.port = 1025
56
+email.notification.smtp.user = test_user
57
+email.notification.smtp.password = just_a_password

+ 21 - 6
tracim/command/user.py Parādīt failu

@@ -12,6 +12,7 @@ from tracim.command import Extender
12 12
 #from tracim.lib.daemons import RadicaleDaemon
13 13
 #from tracim.lib.email import get_email_manager
14 14
 from tracim.exceptions import AlreadyExistError
15
+from tracim.exceptions import NotificationNotSend
15 16
 from tracim.exceptions import CommandAbortedError
16 17
 from tracim.lib.core.group import GroupApi
17 18
 from tracim.lib.core.user import UserApi
@@ -106,7 +107,13 @@ class UserCommand(AppContextCommand):
106 107
             group.users.remove(user)
107 108
         self._session.flush()
108 109
 
109
-    def _create_user(self, login: str, password: str, **kwargs) -> User:
110
+    def _create_user(
111
+            self,
112
+            login: str,
113
+            password: str,
114
+            do_notify: bool,
115
+            **kwargs
116
+    ) -> User:
110 117
         if not password:
111 118
             if self._password_required():
112 119
                 raise CommandAbortedError(
@@ -115,18 +122,23 @@ class UserCommand(AppContextCommand):
115 122
             password = ''
116 123
 
117 124
         try:
118
-            user = self._user_api.create_user(email=login)
119
-            user.password = password
120
-            self._user_api.save(user)
125
+            user = self._user_api.create_user(
126
+                email=login,
127
+                password=password,
128
+                do_save=True,
129
+                do_notify=do_notify,
130
+            )
121 131
             # TODO - G.M - 04-04-2018 - [Caldav] Check this code
122 132
             # # We need to enable radicale if it not already done
123 133
             # daemons = DaemonsManager()
124 134
             # daemons.run('radicale', RadicaleDaemon)
125
-
126 135
             self._user_api.execute_created_user_actions(user)
127 136
         except IntegrityError:
128 137
             self._session.rollback()
129 138
             raise AlreadyExistError()
139
+        except NotificationNotSend as exception:
140
+            self._session.rollback()
141
+            raise exception
130 142
 
131 143
         return user
132 144
 
@@ -166,10 +178,13 @@ class UserCommand(AppContextCommand):
166 178
             try:
167 179
                 user = self._create_user(
168 180
                     login=parsed_args.login,
169
-                    password=parsed_args.password
181
+                    password=parsed_args.password,
182
+                    do_notify=parsed_args.send_email,
170 183
                 )
171 184
             except AlreadyExistError:
172 185
                 raise CommandAbortedError("Error: User already exist (use `user update` command instead)")
186
+            except NotificationNotSend:
187
+                raise CommandAbortedError("Error: Cannot send email notification, user not created.")
173 188
             # TODO - G.M - 04-04-2018 - [Email] Check this code
174 189
             # if parsed_args.send_email:
175 190
             #     email_manager = get_email_manager()

+ 110 - 108
tracim/config.py Parādīt failu

@@ -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)

+ 6 - 1
tracim/exceptions.py Parādīt failu

@@ -92,5 +92,10 @@ class AuthenticationFailed(TracimException):
92 92
 class WrongUserPassword(TracimException):
93 93
     pass
94 94
 
95
+
95 96
 class UserNotExist(TracimException):
96
-    pass
97
+    pass
98
+
99
+
100
+class NotificationNotSend(TracimException):
101
+    pass

+ 11 - 0
tracim/fixtures/content.py Parādīt failu

@@ -58,12 +58,14 @@ class Content(Fixture):
58 58
             workspace=w1,
59 59
             label='w1f1',
60 60
             do_save=True,
61
+            do_notify=False,
61 62
         )
62 63
         w1f2 = content_api.create(
63 64
             content_type=ContentType.Folder,
64 65
             workspace=w1,
65 66
             label='w1f2',
66 67
             do_save=True,
68
+            do_notify=False,
67 69
         )
68 70
 
69 71
         w2f1 = content_api.create(
@@ -71,12 +73,14 @@ class Content(Fixture):
71 73
             workspace=w2,
72 74
             label='w2f1',
73 75
             do_save=True,
76
+            do_notify=False,
74 77
         )
75 78
         w2f2 = content_api.create(
76 79
             content_type=ContentType.Folder,
77 80
             workspace=w2,
78 81
             label='w2f2',
79 82
             do_save=True,
83
+            do_notify=False,
80 84
         )
81 85
 
82 86
         w3f1 = content_api.create(
@@ -84,6 +88,7 @@ class Content(Fixture):
84 88
             workspace=w3,
85 89
             label='w3f3',
86 90
             do_save=True,
91
+            do_notify=False,
87 92
         )
88 93
 
89 94
         # Pages, threads, ..
@@ -93,6 +98,7 @@ class Content(Fixture):
93 98
             parent=w1f1,
94 99
             label='w1f1p1',
95 100
             do_save=True,
101
+            do_notify=False,
96 102
         )
97 103
         w1f1t1 = content_api.create(
98 104
             content_type=ContentType.Thread,
@@ -100,6 +106,7 @@ class Content(Fixture):
100 106
             parent=w1f1,
101 107
             label='w1f1t1',
102 108
             do_save=False,
109
+            do_notify=False,
103 110
         )
104 111
         w1f1t1.description = 'w1f1t1 description'
105 112
         self._session.add(w1f1t1)
@@ -109,6 +116,7 @@ class Content(Fixture):
109 116
             parent=w1f1,
110 117
             label='w1f1d1',
111 118
             do_save=False,
119
+            do_notify=False,
112 120
         )
113 121
         w1f1d1_txt.file_extension = '.txt'
114 122
         w1f1d1_txt.depot_file = FileIntent(
@@ -123,6 +131,7 @@ class Content(Fixture):
123 131
             parent=w1f1,
124 132
             label='w1f1d2',
125 133
             do_save=False,
134
+            do_notify=False,
126 135
         )
127 136
         w1f1d2_html.file_extension = '.html'
128 137
         w1f1d2_html.depot_file = FileIntent(
@@ -137,6 +146,7 @@ class Content(Fixture):
137 146
             label='w1f1f1',
138 147
             parent=w1f1,
139 148
             do_save=True,
149
+            do_notify=False,
140 150
         )
141 151
 
142 152
         w2f1p1 = content_api.create(
@@ -145,5 +155,6 @@ class Content(Fixture):
145 155
             parent=w2f1,
146 156
             label='w2f1p1',
147 157
             do_save=True,
158
+            do_notify=False,
148 159
         )
149 160
         self._session.flush()

+ 5 - 4
tracim/lib/core/content.py Parādīt failu

@@ -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=True) -> 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 Parādīt failu

@@ -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

+ 48 - 4
tracim/lib/core/user.py Parādīt failu

@@ -1,9 +1,12 @@
1 1
 # -*- coding: utf-8 -*-
2 2
 import threading
3
+from smtplib import SMTPException
3 4
 
4 5
 import transaction
5 6
 import typing as typing
6 7
 
8
+from tracim.exceptions import NotificationNotSend
9
+from tracim.lib.mail_notifier.notifier import get_email_manager
7 10
 from sqlalchemy.orm import Session
8 11
 
9 12
 from tracim import CFG
@@ -105,8 +108,9 @@ class UserApi(object):
105 108
             user: User,
106 109
             name: str=None,
107 110
             email: str=None,
108
-            do_save=True,
111
+            password: str=None,
109 112
             timezone: str='',
113
+            do_save=True,
110 114
     ) -> None:
111 115
         if name is not None:
112 116
             user.display_name = name
@@ -114,16 +118,56 @@ class UserApi(object):
114 118
         if email is not None:
115 119
             user.email = email
116 120
 
121
+        if password is not None:
122
+            user.password = password
123
+
117 124
         user.timezone = timezone
118 125
 
119 126
         if do_save:
120 127
             self.save(user)
121 128
 
122
-    def create_user(self, email=None, groups=[], save_now=False) -> User:
129
+    def create_user(
130
+        self,
131
+        email,
132
+        password: str = None,
133
+        name: str = None,
134
+        timezone: str = '',
135
+        groups=[],
136
+        do_save: bool=True,
137
+        do_notify: bool=True,
138
+    ) -> User:
139
+        new_user = self.create_minimal_user(email, groups, save_now=False)
140
+        self.update(
141
+            user=new_user,
142
+            name=name,
143
+            email=email,
144
+            password=password,
145
+            timezone=timezone,
146
+            do_save=False,
147
+        )
148
+        if do_notify:
149
+            try:
150
+                email_manager = get_email_manager(self._config, self._session)
151
+                email_manager.notify_created_account(
152
+                    new_user,
153
+                    password=password
154
+                )
155
+            except SMTPException as e:
156
+                raise NotificationNotSend()
157
+        if do_save:
158
+            self.save(new_user)
159
+        return new_user
160
+
161
+    def create_minimal_user(
162
+            self,
163
+            email,
164
+            groups=[],
165
+            save_now=False
166
+    ) -> User:
167
+        """Previous create_user method"""
123 168
         user = User()
124 169
 
125
-        if email:
126
-            user.email = email
170
+        user.email = email
127 171
 
128 172
         for group in groups:
129 173
             user.groups.append(group)

+ 61 - 0
tracim/lib/mail_notifier/daemon.py Parādīt failu

@@ -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 Parādīt failu

@@ -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 Parādīt failu

@@ -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 Parādīt failu

@@ -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 Parādīt failu

@@ -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 Parādīt failu

@@ -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 Parādīt failu


+ 74 - 0
tracim/templates/mail/content_update_body_html.mak Parādīt failu

@@ -0,0 +1,74 @@
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 - G.M - 09-06-2018 - 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 - G.M - 09-06-2018 - 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
+        <!-- FIXME - G.M - 09-06-2018 - fix action url -->
58
+        <div href='' id="call-to-action-container">
59
+        </div>
60
+    </div>
61
+    
62
+    <div id="footer">
63
+        <p>
64
+            <!-- FIXME - G.M - 09-06-2018 - Set correct workspace url -->
65
+            ${_('{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='', workspace_label=workspace.label, website_title=config.WEBSITE_TITLE)|n}
66
+        </p>
67
+        <hr/>
68
+        <p>
69
+            ${_('This email was automatically sent by <i>Tracim</i>, a collaborative software developped by Algoo.')}<br/>
70
+            Algoo SAS &mdash; 340 Rue de l'Eygala, 38430 Moirans, France &mdash; <a style="text-decoration: none;" href="http://algoo.fr">www.algoo.fr</a>
71
+        </p>
72
+    </div>
73
+  </body>
74
+</html>

+ 31 - 0
tracim/templates/mail/content_update_body_text.mak Parādīt failu

@@ -0,0 +1,31 @@
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
+**Algoo SAS**
27
+340 Rue de l'Eygala
28
+38430 Moirans
29
+France
30
+http://algoo.fr
31
+

+ 88 - 0
tracim/templates/mail/created_account_body_html.mak Parādīt failu

@@ -0,0 +1,88 @@
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 - G.M - 09-06-2018 - 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
+            Algoo SAS &mdash; 340 Rue de l'Eygala, 38430 Moirans, France &mdash; <a style="text-decoration: none;" href="http://algoo.fr">www.algoo.fr</a>
85
+        </p>
86
+    </div>
87
+  </body>
88
+</html>

+ 25 - 0
tracim/templates/mail/created_account_body_text.mak Parādīt failu

@@ -0,0 +1,25 @@
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
+**Algoo SAS**
22
+340 Rue de l'Eygala
23
+38430 Moirans
24
+France
25
+http://algoo.fr

+ 23 - 8
tracim/tests/__init__.py Parādīt failu

@@ -1,5 +1,8 @@
1 1
 # -*- coding: utf-8 -*-
2 2
 import unittest
3
+
4
+import plaster
5
+import requests
3 6
 import transaction
4 7
 from depot.manager import DepotManager
5 8
 from pyramid import testing
@@ -92,16 +95,16 @@ class BaseTest(unittest.TestCase):
92 95
     Pyramid default test.
93 96
     """
94 97
 
98
+    config_uri = 'tests_configs.ini'
99
+    config_section = 'base_test'
100
+
95 101
     def setUp(self):
96 102
         logger.debug(self, 'Setup Test...')
97
-        self.config = testing.setUp(settings={
98
-            'sqlalchemy.url': 'sqlite:///:memory:',
99
-            'user.auth_token.validity': '604800',
100
-            'depot_storage_dir': '/tmp/test/depot',
101
-            'depot_storage_name': 'test',
102
-            'preview_cache_dir': '/tmp/test/preview_cache',
103
-
104
-        })
103
+        self.settings = plaster.get_settings(
104
+            self.config_uri,
105
+            self.config_section
106
+        )
107
+        self.config = testing.setUp(settings = self.settings)
105 108
         self.config.include('tracim.models')
106 109
         DepotManager._clear()
107 110
         DepotManager.configure(
@@ -225,3 +228,15 @@ class DefaultTest(StandardTest):
225 228
             owner=user
226 229
         )
227 230
         return thread
231
+
232
+
233
+class MailHogTest(DefaultTest):
234
+    """
235
+    Theses test need a working mailhog
236
+    """
237
+
238
+    config_section = 'mail_test'
239
+
240
+    def tearDown(self):
241
+        logger.debug(self, 'Cleanup MailHog list...')
242
+        requests.delete('http://127.0.0.1:8025/api/v1/messages')

+ 265 - 0
tracim/tests/functional/test_mail_notification.py Parādīt failu

@@ -0,0 +1,265 @@
1
+# coding=utf-8
2
+# INFO - G.M - 09-06-2018 - Those test need a working MailHog
3
+
4
+from email.mime.multipart import MIMEMultipart
5
+from email.mime.text import MIMEText
6
+
7
+import requests
8
+from rq import SimpleWorker
9
+
10
+from tracim.fixtures.users_and_groups import Base as BaseFixture
11
+from tracim.fixtures.content import Content as ContentFixture
12
+from tracim.lib.utils.utils import get_redis_connection
13
+from tracim.lib.utils.utils import get_rq_queue
14
+from tracim.models.data import ContentType
15
+
16
+from tracim.lib.core.content import ContentApi
17
+from tracim.lib.core.user import UserApi
18
+from tracim.lib.core.workspace import WorkspaceApi
19
+from tracim.lib.mail_notifier.sender import EmailSender
20
+from tracim.lib.mail_notifier.utils import SmtpConfiguration
21
+from tracim.tests import MailHogTest
22
+
23
+
24
+class TestEmailSender(MailHogTest):
25
+
26
+    def test__func__connect_disconnect__ok__nominal_case(self):
27
+        smtp_config = SmtpConfiguration(
28
+            self.app_config.EMAIL_NOTIFICATION_SMTP_SERVER,
29
+            self.app_config.EMAIL_NOTIFICATION_SMTP_PORT,
30
+            self.app_config.EMAIL_NOTIFICATION_SMTP_USER,
31
+            self.app_config.EMAIL_NOTIFICATION_SMTP_PASSWORD
32
+        )
33
+        sender = EmailSender(
34
+            self.app_config,
35
+            smtp_config,
36
+            True,
37
+        )
38
+        sender.connect()
39
+        sender.disconnect()
40
+
41
+    def test__func__send_email__ok__nominal_case(self):
42
+        smtp_config = SmtpConfiguration(
43
+            self.app_config.EMAIL_NOTIFICATION_SMTP_SERVER,
44
+            self.app_config.EMAIL_NOTIFICATION_SMTP_PORT,
45
+            self.app_config.EMAIL_NOTIFICATION_SMTP_USER,
46
+            self.app_config.EMAIL_NOTIFICATION_SMTP_PASSWORD
47
+        )
48
+        sender = EmailSender(
49
+            self.app_config,
50
+            smtp_config,
51
+            True,
52
+        )
53
+
54
+        # Create test_mail
55
+        msg = MIMEMultipart()
56
+        msg['Subject'] = 'test__func__send_email__ok__nominal_case'
57
+        msg['From'] = 'test_send_mail@localhost'
58
+        msg['To'] = 'receiver_test_send_mail@localhost'
59
+        text = "test__func__send_email__ok__nominal_case"
60
+        html = """\
61
+        <html>
62
+          <head></head>
63
+          <body>
64
+            <p>test__func__send_email__ok__nominal_case</p>
65
+          </body>
66
+        </html>
67
+        """.replace(' ', '').replace('\n', '')
68
+        part1 = MIMEText(text, 'plain')
69
+        part2 = MIMEText(html, 'html')
70
+        msg.attach(part1)
71
+        msg.attach(part2)
72
+
73
+        sender.send_mail(msg)
74
+        sender.disconnect()
75
+
76
+        # check mail received
77
+        response = requests.get('http://127.0.0.1:8025/api/v1/messages')
78
+        response = response.json()
79
+        headers = response[0]['Content']['Headers']
80
+        assert headers['From'][0] == 'test_send_mail@localhost'
81
+        assert headers['To'][0] == 'receiver_test_send_mail@localhost'
82
+        assert headers['Subject'][0] == 'test__func__send_email__ok__nominal_case'  # nopep8
83
+        assert response[0]['MIME']['Parts'][0]['Body'] == text
84
+        assert response[0]['MIME']['Parts'][1]['Body'] == html
85
+
86
+
87
+class TestNotificationsSync(MailHogTest):
88
+
89
+    fixtures = [BaseFixture, ContentFixture]
90
+
91
+    def test_func__create_user_with_mail_notification__ok__nominal_case(self):
92
+        api = UserApi(
93
+            current_user=None,
94
+            session=self.session,
95
+            config=self.app_config,
96
+        )
97
+        u = api.create_user(
98
+            email='bob@bob',
99
+            password='pass',
100
+            name='bob',
101
+            timezone='+2',
102
+            do_save=True,
103
+            do_notify=True,
104
+        )
105
+        assert u is not None
106
+        assert u.email == "bob@bob"
107
+        assert u.validate_password('pass')
108
+        assert u.display_name == 'bob'
109
+        assert u.timezone == '+2'
110
+
111
+        # check mail received
112
+        response = requests.get('http://127.0.0.1:8025/api/v1/messages')
113
+        response = response.json()
114
+        headers = response[0]['Content']['Headers']
115
+        assert headers['From'][0] == 'Tracim Notifications <test_user_from+0@localhost>'  # nopep8
116
+        assert headers['To'][0] == 'bob <bob@bob>'
117
+        assert headers['Subject'][0] == '[TRACIM] Created account'
118
+
119
+    def test_func__create_new_content_with_notification__ok__nominal_case(self):
120
+        uapi = UserApi(
121
+            current_user=None,
122
+            session=self.session,
123
+            config=self.app_config,
124
+        )
125
+        current_user = uapi.get_one_by_email('admin@admin.admin')
126
+        # Create new user with notification enabled on w1 workspace
127
+        wapi = WorkspaceApi(
128
+            current_user=current_user,
129
+            session=self.session,
130
+        )
131
+        workspace = wapi.get_one_by_label('w1')
132
+        user = uapi.get_one_by_email('bob@fsf.local')
133
+        wapi.enable_notifications(user, workspace)
134
+
135
+        api = ContentApi(
136
+            current_user=user,
137
+            session=self.session,
138
+            config=self.app_config,
139
+        )
140
+        item = api.create(
141
+            ContentType.Folder,
142
+            workspace,
143
+            None,
144
+            'parent',
145
+            do_save=True,
146
+            do_notify=False,
147
+        )
148
+        item2 = api.create(
149
+            ContentType.File,
150
+            workspace,
151
+            item,
152
+            'file1',
153
+            do_save=True,
154
+            do_notify=True,
155
+        )
156
+
157
+        # check mail received
158
+        response = requests.get('http://127.0.0.1:8025/api/v1/messages')
159
+        response = response.json()
160
+        headers = response[0]['Content']['Headers']
161
+        assert headers['From'][0] == '"Bob i. via Tracim" <test_user_from+3@localhost>'  # nopep8
162
+        assert headers['To'][0] == 'Global manager <admin@admin.admin>'
163
+        assert headers['Subject'][0] == '[TRACIM] [w1] file1 (open)'
164
+        assert headers['References'][0] == 'test_user_refs+13@localhost'
165
+        assert headers['Reply-to'][0] == '"Bob i. & all members of w1" <test_user_reply+13@localhost>'  # nopep8
166
+
167
+
168
+class TestNotificationsAsync(MailHogTest):
169
+    fixtures = [BaseFixture, ContentFixture]
170
+    config_section = 'mail_test_async'
171
+
172
+    def test_func__create_user_with_mail_notification__ok__nominal_case(self):
173
+        api = UserApi(
174
+            current_user=None,
175
+            session=self.session,
176
+            config=self.app_config,
177
+        )
178
+        u = api.create_user(
179
+            email='bob@bob',
180
+            password='pass',
181
+            name='bob',
182
+            timezone='+2',
183
+            do_save=True,
184
+            do_notify=True,
185
+        )
186
+        assert u is not None
187
+        assert u.email == "bob@bob"
188
+        assert u.validate_password('pass')
189
+        assert u.display_name == 'bob'
190
+        assert u.timezone == '+2'
191
+
192
+        # Send mail async from redis queue
193
+        redis = get_redis_connection(
194
+            self.app_config
195
+        )
196
+        queue = get_rq_queue(
197
+            redis,
198
+            'mail_sender',
199
+        )
200
+        worker = SimpleWorker([queue], connection=queue.connection)
201
+        worker.work(burst=True)
202
+        # check mail received
203
+        response = requests.get('http://127.0.0.1:8025/api/v1/messages')
204
+        response = response.json()
205
+        headers = response[0]['Content']['Headers']
206
+        assert headers['From'][0] == 'Tracim Notifications <test_user_from+0@localhost>'  # nopep8
207
+        assert headers['To'][0] == 'bob <bob@bob>'
208
+        assert headers['Subject'][0] == '[TRACIM] Created account'
209
+
210
+    def test_func__create_new_content_with_notification__ok__nominal_case(self):
211
+        uapi = UserApi(
212
+            current_user=None,
213
+            session=self.session,
214
+            config=self.app_config,
215
+        )
216
+        current_user = uapi.get_one_by_email('admin@admin.admin')
217
+        # Create new user with notification enabled on w1 workspace
218
+        wapi = WorkspaceApi(
219
+            current_user=current_user,
220
+            session=self.session,
221
+        )
222
+        workspace = wapi.get_one_by_label('w1')
223
+        user = uapi.get_one_by_email('bob@fsf.local')
224
+        wapi.enable_notifications(user, workspace)
225
+
226
+        api = ContentApi(
227
+            current_user=user,
228
+            session=self.session,
229
+            config=self.app_config,
230
+        )
231
+        item = api.create(
232
+            ContentType.Folder,
233
+            workspace,
234
+            None,
235
+            'parent',
236
+            do_save=True,
237
+            do_notify=False,
238
+        )
239
+        item2 = api.create(
240
+            ContentType.File,
241
+            workspace,
242
+            item,
243
+            'file1',
244
+            do_save=True,
245
+            do_notify=True,
246
+        )
247
+        # Send mail async from redis queue
248
+        redis = get_redis_connection(
249
+            self.app_config
250
+        )
251
+        queue = get_rq_queue(
252
+            redis,
253
+            'mail_sender',
254
+        )
255
+        worker = SimpleWorker([queue], connection=queue.connection)
256
+        worker.work(burst=True)
257
+        # check mail received
258
+        response = requests.get('http://127.0.0.1:8025/api/v1/messages')
259
+        response = response.json()
260
+        headers = response[0]['Content']['Headers']
261
+        assert headers['From'][0] == '"Bob i. via Tracim" <test_user_from+3@localhost>'  # nopep8
262
+        assert headers['To'][0] == 'Global manager <admin@admin.admin>'
263
+        assert headers['Subject'][0] == '[TRACIM] [w1] file1 (open)'
264
+        assert headers['References'][0] == 'test_user_refs+13@localhost'
265
+        assert headers['Reply-to'][0] == '"Bob i. & all members of w1" <test_user_reply+13@localhost>'  # nopep8

+ 45 - 51
tracim/tests/library/test_content_api.py Parādīt failu

@@ -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,8 +1116,7 @@ class TestContentApi(DefaultTest):
1116 1116
         
1117 1117
         wid = workspace.workspace_id
1118 1118
 
1119
-        user2 = uapi.create_user()
1120
-        user2.email = 'this.is@another.user'
1119
+        user2 = uapi.create_minimal_user('this.is@another.user')
1121 1120
         uapi.save(user2)
1122 1121
 
1123 1122
         RoleApi(
@@ -1223,7 +1222,7 @@ class TestContentApi(DefaultTest):
1223 1222
                   group_api.get_one(Group.TIM_MANAGER),
1224 1223
                   group_api.get_one(Group.TIM_ADMIN)]
1225 1224
 
1226
-        user1 = uapi.create_user(
1225
+        user1 = uapi.create_minimal_user(
1227 1226
             email='this.is@user',
1228 1227
             groups=groups,
1229 1228
             save_now=True,
@@ -1237,8 +1236,7 @@ class TestContentApi(DefaultTest):
1237 1236
             save_now=True
1238 1237
         )
1239 1238
 
1240
-        user2 = uapi.create_user()
1241
-        user2.email = 'this.is@another.user'
1239
+        user2 = uapi.create_minimal_user('this.is@another.user')
1242 1240
         uapi.save(user2)
1243 1241
 
1244 1242
         RoleApi(
@@ -1301,7 +1299,7 @@ class TestContentApi(DefaultTest):
1301 1299
                   group_api.get_one(Group.TIM_MANAGER),
1302 1300
                   group_api.get_one(Group.TIM_ADMIN)]
1303 1301
 
1304
-        user1 = uapi.create_user(
1302
+        user1 = uapi.create_minimal_user(
1305 1303
             email='this.is@user',
1306 1304
             groups=groups,
1307 1305
             save_now=True
@@ -1314,8 +1312,7 @@ class TestContentApi(DefaultTest):
1314 1312
         )
1315 1313
         wid = workspace.workspace_id
1316 1314
 
1317
-        user2 = uapi.create_user()
1318
-        user2.email = 'this.is@another.user'
1315
+        user2 = uapi.create_minimal_user('this.is@another.user')
1319 1316
         uapi.save(user2)
1320 1317
 
1321 1318
         RoleApi(
@@ -1415,7 +1412,7 @@ class TestContentApi(DefaultTest):
1415 1412
                   group_api.get_one(Group.TIM_MANAGER),
1416 1413
                   group_api.get_one(Group.TIM_ADMIN)]
1417 1414
 
1418
-        user1 = uapi.create_user(
1415
+        user1 = uapi.create_minimal_user(
1419 1416
             email='this.is@user',
1420 1417
             groups=groups,
1421 1418
             save_now=True,
@@ -1427,8 +1424,7 @@ class TestContentApi(DefaultTest):
1427 1424
             save_now=True
1428 1425
         )
1429 1426
 
1430
-        user2 = uapi.create_user()
1431
-        user2.email = 'this.is@another.user'
1427
+        user2 = uapi.create_minimal_user('this.is@another.user')
1432 1428
         uapi.save(user2)
1433 1429
 
1434 1430
         RoleApi(
@@ -1494,7 +1490,7 @@ class TestContentApi(DefaultTest):
1494 1490
                   group_api.get_one(Group.TIM_MANAGER),
1495 1491
                   group_api.get_one(Group.TIM_ADMIN)]
1496 1492
 
1497
-        user1 = uapi.create_user(
1493
+        user1 = uapi.create_minimal_user(
1498 1494
             email='this.is@user',
1499 1495
             groups=groups,
1500 1496
             save_now=True
@@ -1508,8 +1504,7 @@ class TestContentApi(DefaultTest):
1508 1504
         )
1509 1505
         wid = workspace.workspace_id
1510 1506
 
1511
-        user2 = uapi.create_user()
1512
-        user2.email = 'this.is@another.user'
1507
+        user2 = uapi.create_minimal_user('this.is@another.user')
1513 1508
         uapi.save(user2)
1514 1509
 
1515 1510
         RoleApi(
@@ -1643,7 +1638,7 @@ class TestContentApi(DefaultTest):
1643 1638
                   group_api.get_one(Group.TIM_MANAGER),
1644 1639
                   group_api.get_one(Group.TIM_ADMIN)]
1645 1640
 
1646
-        user1 = uapi.create_user(
1641
+        user1 = uapi.create_minimal_user(
1647 1642
             email='this.is@user',
1648 1643
             groups=groups,
1649 1644
             save_now=True
@@ -1657,8 +1652,7 @@ class TestContentApi(DefaultTest):
1657 1652
         )
1658 1653
         wid = workspace.workspace_id
1659 1654
 
1660
-        user2 = uapi.create_user()
1661
-        user2.email = 'this.is@another.user'
1655
+        user2 = uapi.create_minimal_user('this.is@another.user')
1662 1656
         uapi.save(user2)
1663 1657
 
1664 1658
         RoleApi(
@@ -1793,8 +1787,8 @@ class TestContentApi(DefaultTest):
1793 1787
                   group_api.get_one(Group.TIM_MANAGER),
1794 1788
                   group_api.get_one(Group.TIM_ADMIN)]
1795 1789
 
1796
-        user = uapi.create_user(email='this.is@user',
1797
-                                groups=groups, save_now=True)
1790
+        user = uapi.create_minimal_user(email='this.is@user',
1791
+                                        groups=groups, save_now=True)
1798 1792
 
1799 1793
         workspace = WorkspaceApi(
1800 1794
             current_user=user,
@@ -1848,8 +1842,8 @@ class TestContentApi(DefaultTest):
1848 1842
                   group_api.get_one(Group.TIM_MANAGER),
1849 1843
                   group_api.get_one(Group.TIM_ADMIN)]
1850 1844
 
1851
-        user = uapi.create_user(email='this.is@user',
1852
-                                groups=groups, save_now=True)
1845
+        user = uapi.create_minimal_user(email='this.is@user',
1846
+                                        groups=groups, save_now=True)
1853 1847
 
1854 1848
         workspace = WorkspaceApi(
1855 1849
             current_user=user,
@@ -1899,8 +1893,8 @@ class TestContentApi(DefaultTest):
1899 1893
                   group_api.get_one(Group.TIM_MANAGER),
1900 1894
                   group_api.get_one(Group.TIM_ADMIN)]
1901 1895
 
1902
-        user = uapi.create_user(email='this.is@user',
1903
-                                groups=groups, save_now=True)
1896
+        user = uapi.create_minimal_user(email='this.is@user',
1897
+                                        groups=groups, save_now=True)
1904 1898
 
1905 1899
         workspace = WorkspaceApi(
1906 1900
             current_user=user,

+ 4 - 2
tracim/tests/library/test_notification.py Parādīt failu

@@ -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

+ 55 - 24
tracim/tests/library/test_user_api.py Parādīt failu

@@ -14,19 +14,49 @@ from tracim.tests import eq_
14 14
 
15 15
 class TestUserApi(DefaultTest):
16 16
 
17
-    def test_unit__create_and_update_user__ok__nominal_case(self):
17
+    def test_unit__create_minimal_user__ok__nominal_case(self):
18 18
         api = UserApi(
19 19
             current_user=None,
20 20
             session=self.session,
21 21
             config=self.config,
22 22
         )
23
-        u = api.create_user()
24
-        api.update(u, 'bob', 'bob@bob', True)
23
+        u = api.create_minimal_user('bob@bob')
24
+        assert u.email == 'bob@bob'
25
+        assert u.display_name is None
25 26
 
27
+    def test_unit__create_minimal_user_and_update__ok__nominal_case(self):
28
+        api = UserApi(
29
+            current_user=None,
30
+            session=self.session,
31
+            config=self.config,
32
+        )
33
+        u = api.create_minimal_user('bob@bob')
34
+        api.update(u, 'bob', 'bob@bob', 'pass', do_save=True)
26 35
         nu = api.get_one_by_email('bob@bob')
27
-        assert nu != None
28
-        eq_('bob@bob', nu.email)
29
-        eq_('bob', nu.display_name)
36
+        assert nu is not None
37
+        assert nu.email == 'bob@bob'
38
+        assert nu.display_name == 'bob'
39
+        assert nu.validate_password('pass')
40
+
41
+    def test__unit__create__user__ok_nominal_case(self):
42
+        api = UserApi(
43
+            current_user=None,
44
+            session=self.session,
45
+            config=self.config,
46
+        )
47
+        u = api.create_user(
48
+            email='bob@bob',
49
+            password='pass',
50
+            name='bob',
51
+            timezone='+2',
52
+            do_save=True,
53
+            do_notify=False,
54
+        )
55
+        assert u is not None
56
+        assert u.email == "bob@bob"
57
+        assert u.validate_password('pass')
58
+        assert u.display_name == 'bob'
59
+        assert u.timezone == '+2'
30 60
 
31 61
     def test_unit__user_with_email_exists__ok__nominal_case(self):
32 62
         api = UserApi(
@@ -34,21 +64,22 @@ class TestUserApi(DefaultTest):
34 64
             session=self.session,
35 65
             config=self.config,
36 66
         )
37
-        u = api.create_user()
38
-        api.update(u, 'bibi', 'bibi@bibi', True)
67
+        u = api.create_minimal_user('bibi@bibi')
68
+        api.update(u, 'bibi', 'bibi@bibi', 'pass', do_save=True)
39 69
         transaction.commit()
40 70
 
41 71
         eq_(True, api.user_with_email_exists('bibi@bibi'))
42 72
         eq_(False, api.user_with_email_exists('unknown'))
43 73
 
44
-    def test_unit__get_one_by_email__ok__nominal_case(self):
74
+    def test_get_one_by_email(self):
45 75
         api = UserApi(
46 76
             current_user=None,
47 77
             session=self.session,
48 78
             config=self.config,
49 79
         )
50
-        u = api.create_user()
51
-        api.update(u, 'bibi', 'bibi@bibi', True)
80
+        u = api.create_minimal_user('bibi@bibi')
81
+        self.session.flush()
82
+        api.update(u, 'bibi', 'bibi@bibi', 'pass', do_save=True)
52 83
         uid = u.user_id
53 84
         transaction.commit()
54 85
 
@@ -63,17 +94,17 @@ class TestUserApi(DefaultTest):
63 94
         with pytest.raises(NoResultFound):
64 95
             api.get_one_by_email('unknown')
65 96
 
66
-    # def test_unit__get_all__ok__nominal_case(self):
67
-    #     # TODO - G.M - 29-03-2018 Check why this method is not enabled
68
-    #     api = UserApi(
69
-    #         current_user=None,
70
-    #         session=self.session,
71
-    #         config=self.config,
72
-    #     )
73
-    #     u1 = api.create_user(True)
74
-    #     u2 = api.create_user(True)
75
-    #     users = api.get_all()
76
-    #     assert 2==len(users)
97
+    def test_unit__get_all__ok__nominal_case(self):
98
+        api = UserApi(
99
+            current_user=None,
100
+            session=self.session,
101
+            config=self.config,
102
+        )
103
+        u1 = api.create_minimal_user('bibi@bibi')
104
+
105
+        users = api.get_all()
106
+        # u1 + Admin user from BaseFixture
107
+        assert 2 == len(users)
77 108
 
78 109
     def test_unit__get_one__ok__nominal_case(self):
79 110
         api = UserApi(
@@ -81,8 +112,8 @@ class TestUserApi(DefaultTest):
81 112
             session=self.session,
82 113
             config=self.config,
83 114
         )
84
-        u = api.create_user()
85
-        api.update(u, 'titi', 'titi@titi', True)
115
+        u = api.create_minimal_user('titi@titi')
116
+        api.update(u, 'titi', 'titi@titi', 'pass', do_save=True)
86 117
         one = api.get_one(u.user_id)
87 118
         eq_(u.user_id, one.user_id)
88 119
 

+ 2 - 2
tracim/tests/library/test_workspace.py Parādīt failu

@@ -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