Browse Source

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

Guénaël Muller 6 years ago
parent
commit
8d6247f428

+ 7 - 0
.travis.yml View File

5
   - "3.5"
5
   - "3.5"
6
   - "3.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
 install:
15
 install:
9
   - pip install --upgrade pip setuptools
16
   - pip install --upgrade pip setuptools
10
   - pip install -e ".[testing]"
17
   - pip install -e ".[testing]"

+ 15 - 0
README.md View File

19
     sudo apt update
19
     sudo apt update
20
     sudo apt install git
20
     sudo apt install git
21
     sudo apt install python3 python3-venv python3-dev python3-pip
21
     sudo apt install python3 python3-venv python3-dev python3-pip
22
+    sudo apt install redis-server
22
 
23
 
23
 ### Get the source ###
24
 ### Get the source ###
24
 
25
 
110
 
111
 
111
     tracimcli webdav start
112
     tracimcli webdav start
112
 
113
 
114
+
113
 ## Run Tests and others checks ##
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
 Run your project's tests:
128
 Run your project's tests:
116
 
129
 
117
     pytest
130
     pytest
118
 
131
 
132
+### Lints and others checks ###
133
+
119
 Run mypy checks:
134
 Run mypy checks:
120
 
135
 
121
     mypy --ignore-missing-imports --disallow-untyped-defs tracim
136
     mypy --ignore-missing-imports --disallow-untyped-defs tracim

+ 6 - 0
setup.py View File

34
     # others
34
     # others
35
     'filedepot',
35
     'filedepot',
36
     'babel',
36
     'babel',
37
+    # mail-notifier
38
+    'mako',
39
+    'lxml',
40
+    'redis',
41
+    'rq',
37
 ]
42
 ]
38
 
43
 
39
 tests_require = [
44
 tests_require = [
42
     'pytest-cov',
47
     'pytest-cov',
43
     'pep8',
48
     'pep8',
44
     'mypy',
49
     'mypy',
50
+    'requests'
45
 ]
51
 ]
46
 
52
 
47
 mysql_require = [
53
 mysql_require = [

+ 57 - 0
tests_configs.ini View File

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 View File

12
 #from tracim.lib.daemons import RadicaleDaemon
12
 #from tracim.lib.daemons import RadicaleDaemon
13
 #from tracim.lib.email import get_email_manager
13
 #from tracim.lib.email import get_email_manager
14
 from tracim.exceptions import AlreadyExistError
14
 from tracim.exceptions import AlreadyExistError
15
+from tracim.exceptions import NotificationNotSend
15
 from tracim.exceptions import CommandAbortedError
16
 from tracim.exceptions import CommandAbortedError
16
 from tracim.lib.core.group import GroupApi
17
 from tracim.lib.core.group import GroupApi
17
 from tracim.lib.core.user import UserApi
18
 from tracim.lib.core.user import UserApi
106
             group.users.remove(user)
107
             group.users.remove(user)
107
         self._session.flush()
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
         if not password:
117
         if not password:
111
             if self._password_required():
118
             if self._password_required():
112
                 raise CommandAbortedError(
119
                 raise CommandAbortedError(
115
             password = ''
122
             password = ''
116
 
123
 
117
         try:
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
             # TODO - G.M - 04-04-2018 - [Caldav] Check this code
131
             # TODO - G.M - 04-04-2018 - [Caldav] Check this code
122
             # # We need to enable radicale if it not already done
132
             # # We need to enable radicale if it not already done
123
             # daemons = DaemonsManager()
133
             # daemons = DaemonsManager()
124
             # daemons.run('radicale', RadicaleDaemon)
134
             # daemons.run('radicale', RadicaleDaemon)
125
-
126
             self._user_api.execute_created_user_actions(user)
135
             self._user_api.execute_created_user_actions(user)
127
         except IntegrityError:
136
         except IntegrityError:
128
             self._session.rollback()
137
             self._session.rollback()
129
             raise AlreadyExistError()
138
             raise AlreadyExistError()
139
+        except NotificationNotSend as exception:
140
+            self._session.rollback()
141
+            raise exception
130
 
142
 
131
         return user
143
         return user
132
 
144
 
166
             try:
178
             try:
167
                 user = self._create_user(
179
                 user = self._create_user(
168
                     login=parsed_args.login,
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
             except AlreadyExistError:
184
             except AlreadyExistError:
172
                 raise CommandAbortedError("Error: User already exist (use `user update` command instead)")
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
             # TODO - G.M - 04-04-2018 - [Email] Check this code
188
             # TODO - G.M - 04-04-2018 - [Email] Check this code
174
             # if parsed_args.send_email:
189
             # if parsed_args.send_email:
175
             #     email_manager = get_email_manager()
190
             #     email_manager = get_email_manager()

+ 110 - 108
tracim/config.py View File

4
 from tracim.lib.utils.logger import logger
4
 from tracim.lib.utils.logger import logger
5
 from depot.manager import DepotManager
5
 from depot.manager import DepotManager
6
 
6
 
7
+from tracim.models.data import ActionDescription, ContentType
8
+
7
 
9
 
8
 class CFG(object):
10
 class CFG(object):
9
     """Object used for easy access to config file parameters."""
11
     """Object used for easy access to config file parameters."""
131
         # TODO - G.M - 27-03-2018 - [Email] Restore email config
133
         # TODO - G.M - 27-03-2018 - [Email] Restore email config
132
         ###
134
         ###
133
         # EMAIL related stuff (notification, reply)
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
         self.EMAIL_NOTIFICATION_ACTIVATED = asbool(settings.get(
197
         self.EMAIL_NOTIFICATION_ACTIVATED = asbool(settings.get(
196
             'email.notification.activated',
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
         # self.EMAIL_REPLY_ACTIVATED = asbool(settings.get(
217
         # self.EMAIL_REPLY_ACTIVATED = asbool(settings.get(
216
         #     'email.reply.activated',
218
         #     'email.reply.activated',
217
         #     False,
219
         #     False,
267
         #         mandatory_msg.format('email.reply.lockfile_path')
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
         # WSGIDAV (Webdav server)
304
         # WSGIDAV (Webdav server)

+ 6 - 1
tracim/exceptions.py View File

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

+ 11 - 0
tracim/fixtures/content.py View File

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

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

376
 
376
 
377
         return result
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
         assert content_type in ContentType.allowed_types()
380
         assert content_type in ContentType.allowed_types()
381
 
381
 
382
         if content_type == ContentType.Folder and not label:
382
         if content_type == ContentType.Folder and not label:
399
 
399
 
400
         if do_save:
400
         if do_save:
401
             self._session.add(content)
401
             self._session.add(content)
402
-            self.save(content, ActionDescription.CREATION)
402
+            self.save(content, ActionDescription.CREATION, do_notify=do_notify)
403
         return content
403
         return content
404
 
404
 
405
 
405
 
1127
         :return:
1127
         :return:
1128
         """
1128
         """
1129
         NotifierFactory.create(
1129
         NotifierFactory.create(
1130
-            self._config,
1131
-            self._user
1130
+            config=self._config,
1131
+            current_user=self._user,
1132
+            session=self._session,
1132
         ).notify_content_update(content)
1133
         ).notify_content_update(content)
1133
 
1134
 
1134
     def get_keywords(self, search_string, search_string_separators=None) -> [str]:
1135
     def get_keywords(self, search_string, search_string_separators=None) -> [str]:

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

1
 # -*- coding: utf-8 -*-
1
 # -*- coding: utf-8 -*-
2
+from sqlalchemy.orm import Session
2
 
3
 
4
+from tracim import CFG
3
 from tracim.lib.utils.logger import logger
5
 from tracim.lib.utils.logger import logger
4
 from tracim.models.auth import User
6
 from tracim.models.auth import User
5
 from tracim.models.data import Content
7
 from tracim.models.data import Content
9
     """
11
     """
10
     Interface for Notifier instances
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
         pass
19
         pass
14
 
20
 
15
     def notify_content_update(self, content: Content):
21
     def notify_content_update(self, content: Content):
19
 class NotifierFactory(object):
25
 class NotifierFactory(object):
20
 
26
 
21
     @classmethod
27
     @classmethod
22
-    def create(cls, config, current_user: User=None) -> INotifier:
28
+    def create(cls, config, session, current_user: User=None) -> INotifier:
23
         if not config.EMAIL_NOTIFICATION_ACTIVATED:
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
 class DummyNotifier(INotifier):
35
 class DummyNotifier(INotifier):
29
     send_count = 0
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
         logger.info(self, 'Instantiating Dummy Notifier')
50
         logger.info(self, 'Instantiating Dummy Notifier')
34
 
51
 
35
     def notify_content_update(self, content: Content):
52
     def notify_content_update(self, content: Content):
38
             self,
55
             self,
39
             'Fake notifier, do not send notification for update of content {}'.format(content.content_id)  # nopep8
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 View File

1
 # -*- coding: utf-8 -*-
1
 # -*- coding: utf-8 -*-
2
 import threading
2
 import threading
3
+from smtplib import SMTPException
3
 
4
 
4
 import transaction
5
 import transaction
5
 import typing as typing
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
 from sqlalchemy.orm import Session
10
 from sqlalchemy.orm import Session
8
 
11
 
9
 from tracim import CFG
12
 from tracim import CFG
105
             user: User,
108
             user: User,
106
             name: str=None,
109
             name: str=None,
107
             email: str=None,
110
             email: str=None,
108
-            do_save=True,
111
+            password: str=None,
109
             timezone: str='',
112
             timezone: str='',
113
+            do_save=True,
110
     ) -> None:
114
     ) -> None:
111
         if name is not None:
115
         if name is not None:
112
             user.display_name = name
116
             user.display_name = name
114
         if email is not None:
118
         if email is not None:
115
             user.email = email
119
             user.email = email
116
 
120
 
121
+        if password is not None:
122
+            user.password = password
123
+
117
         user.timezone = timezone
124
         user.timezone = timezone
118
 
125
 
119
         if do_save:
126
         if do_save:
120
             self.save(user)
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
         user = User()
168
         user = User()
124
 
169
 
125
-        if email:
126
-            user.email = email
170
+        user.email = email
127
 
171
 
128
         for group in groups:
172
         for group in groups:
129
             user.groups.append(group)
173
             user.groups.append(group)

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

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

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

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

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

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

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

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

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

1
 # -*- coding: utf-8 -*-
1
 # -*- coding: utf-8 -*-
2
 import datetime
2
 import datetime
3
+from redis import Redis
4
+from rq import Queue
5
+
6
+from tracim.config import CFG
3
 
7
 
4
 DEFAULT_WEBDAV_CONFIG_FILE = "wsgidav.conf"
8
 DEFAULT_WEBDAV_CONFIG_FILE = "wsgidav.conf"
5
 DEFAULT_TRACIM_CONFIG_FILE = "development.ini"
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
 def cmp_to_key(mycmp):
33
 def cmp_to_key(mycmp):
9
     """
34
     """
10
     List sort related function
35
     List sort related function

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

131
     WORKSPACE_MANAGER = 8
131
     WORKSPACE_MANAGER = 8
132
 
132
 
133
     # TODO - G.M - 10-04-2018 - [Cleanup] Drop this
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
     # STYLE = dict()
141
     # STYLE = dict()
142
     # STYLE[0] = ''
142
     # STYLE[0] = ''
161
     # def style(self):
161
     # def style(self):
162
     #     return UserRoleInWorkspace.STYLE[self.role]
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
     @classmethod
168
     @classmethod
168
     def get_all_role_values(self):
169
     def get_all_role_values(self):
318
                  # type=''
319
                  # type=''
319
     ):
320
     ):
320
         self.id = id
321
         self.id = id
322
+        self.label = self.id
321
         # TODO - G.M - 10-04-2018 - [Cleanup] Drop this
323
         # TODO - G.M - 10-04-2018 - [Cleanup] Drop this
322
         # self.icon = ContentStatus._ICONS[id]
324
         # self.icon = ContentStatus._ICONS[id]
323
         # self.css = ContentStatus._CSS[id]
325
         # self.css = ContentStatus._CSS[id]

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


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

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 View File

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 View File

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 View File

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 View File

1
 # -*- coding: utf-8 -*-
1
 # -*- coding: utf-8 -*-
2
 import unittest
2
 import unittest
3
+
4
+import plaster
5
+import requests
3
 import transaction
6
 import transaction
4
 from depot.manager import DepotManager
7
 from depot.manager import DepotManager
5
 from pyramid import testing
8
 from pyramid import testing
92
     Pyramid default test.
95
     Pyramid default test.
93
     """
96
     """
94
 
97
 
98
+    config_uri = 'tests_configs.ini'
99
+    config_section = 'base_test'
100
+
95
     def setUp(self):
101
     def setUp(self):
96
         logger.debug(self, 'Setup Test...')
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
         self.config.include('tracim.models')
108
         self.config.include('tracim.models')
106
         DepotManager._clear()
109
         DepotManager._clear()
107
         DepotManager.configure(
110
         DepotManager.configure(
225
             owner=user
228
             owner=user
226
         )
229
         )
227
         return thread
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 View File

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 View File

112
                   group_api.get_one(Group.TIM_MANAGER),
112
                   group_api.get_one(Group.TIM_MANAGER),
113
                   group_api.get_one(Group.TIM_ADMIN)]
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
         workspace = WorkspaceApi(
117
         workspace = WorkspaceApi(
118
             current_user=user,
118
             current_user=user,
119
             session=self.session
119
             session=self.session
189
                   group_api.get_one(Group.TIM_MANAGER),
189
                   group_api.get_one(Group.TIM_MANAGER),
190
                   group_api.get_one(Group.TIM_ADMIN)]
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
         workspace_api = WorkspaceApi(current_user=user, session=self.session)
194
         workspace_api = WorkspaceApi(current_user=user, session=self.session)
195
         workspace = workspace_api.create_workspace(
195
         workspace = workspace_api.create_workspace(
196
             'test workspace',
196
             'test workspace',
274
                   group_api.get_one(Group.TIM_MANAGER),
274
                   group_api.get_one(Group.TIM_MANAGER),
275
                   group_api.get_one(Group.TIM_ADMIN)]
275
                   group_api.get_one(Group.TIM_ADMIN)]
276
 
276
 
277
-        user = uapi.create_user(
277
+        user = uapi.create_minimal_user(
278
             email='this.is@user',
278
             email='this.is@user',
279
             groups=groups,
279
             groups=groups,
280
             save_now=True
280
             save_now=True
331
                   group_api.get_one(Group.TIM_MANAGER),
331
                   group_api.get_one(Group.TIM_MANAGER),
332
                   group_api.get_one(Group.TIM_ADMIN)]
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
         workspace = WorkspaceApi(
336
         workspace = WorkspaceApi(
337
             current_user=user,
337
             current_user=user,
338
             session=self.session
338
             session=self.session
400
                   group_api.get_one(Group.TIM_MANAGER),
400
                   group_api.get_one(Group.TIM_MANAGER),
401
                   group_api.get_one(Group.TIM_ADMIN)]
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
         workspace = WorkspaceApi(
406
         workspace = WorkspaceApi(
407
             current_user=user,
407
             current_user=user,
438
                   group_api.get_one(Group.TIM_MANAGER),
438
                   group_api.get_one(Group.TIM_MANAGER),
439
                   group_api.get_one(Group.TIM_ADMIN)]
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
         workspace = WorkspaceApi(
444
         workspace = WorkspaceApi(
445
             current_user=user,
445
             current_user=user,
480
                   group_api.get_one(Group.TIM_MANAGER),
480
                   group_api.get_one(Group.TIM_MANAGER),
481
                   group_api.get_one(Group.TIM_ADMIN)]
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
         workspace = WorkspaceApi(
486
         workspace = WorkspaceApi(
487
             current_user=user,
487
             current_user=user,
522
                   group_api.get_one(Group.TIM_MANAGER),
522
                   group_api.get_one(Group.TIM_MANAGER),
523
                   group_api.get_one(Group.TIM_ADMIN)]
523
                   group_api.get_one(Group.TIM_ADMIN)]
524
 
524
 
525
-        user = uapi.create_user(
525
+        user = uapi.create_minimal_user(
526
             email='user1@user',
526
             email='user1@user',
527
             groups=groups,
527
             groups=groups,
528
             save_now=True
528
             save_now=True
529
         )
529
         )
530
-        user2 = uapi.create_user(
530
+        user2 = uapi.create_minimal_user(
531
             email='user2@user',
531
             email='user2@user',
532
             groups=groups,
532
             groups=groups,
533
             save_now=True
533
             save_now=True
634
                   group_api.get_one(Group.TIM_MANAGER),
634
                   group_api.get_one(Group.TIM_MANAGER),
635
                   group_api.get_one(Group.TIM_ADMIN)]
635
                   group_api.get_one(Group.TIM_ADMIN)]
636
 
636
 
637
-        user = uapi.create_user(
637
+        user = uapi.create_minimal_user(
638
             email='user1@user',
638
             email='user1@user',
639
             groups=groups,
639
             groups=groups,
640
             save_now=True
640
             save_now=True
641
         )
641
         )
642
-        user2 = uapi.create_user(
642
+        user2 = uapi.create_minimal_user(
643
             email='user2@user',
643
             email='user2@user',
644
             groups=groups,
644
             groups=groups,
645
             save_now=True
645
             save_now=True
744
                   group_api.get_one(Group.TIM_MANAGER),
744
                   group_api.get_one(Group.TIM_MANAGER),
745
                   group_api.get_one(Group.TIM_ADMIN)]
745
                   group_api.get_one(Group.TIM_ADMIN)]
746
 
746
 
747
-        user = uapi.create_user(
747
+        user = uapi.create_minimal_user(
748
             email='user1@user',
748
             email='user1@user',
749
             groups=groups,
749
             groups=groups,
750
             save_now=True,
750
             save_now=True,
751
         )
751
         )
752
-        user2 = uapi.create_user(
752
+        user2 = uapi.create_minimal_user(
753
             email='user2@user',
753
             email='user2@user',
754
             groups=groups,
754
             groups=groups,
755
             save_now=True
755
             save_now=True
843
                   group_api.get_one(Group.TIM_MANAGER),
843
                   group_api.get_one(Group.TIM_MANAGER),
844
                   group_api.get_one(Group.TIM_ADMIN)]
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
         wapi = WorkspaceApi(
851
         wapi = WorkspaceApi(
852
             current_user=user_a,
852
             current_user=user_a,
946
                   group_api.get_one(Group.TIM_MANAGER),
946
                   group_api.get_one(Group.TIM_MANAGER),
947
                   group_api.get_one(Group.TIM_ADMIN)]
947
                   group_api.get_one(Group.TIM_ADMIN)]
948
 
948
 
949
-        user_a = uapi.create_user(
949
+        user_a = uapi.create_minimal_user(
950
             email='this.is@user',
950
             email='this.is@user',
951
             groups=groups,
951
             groups=groups,
952
             save_now=True
952
             save_now=True
953
         )
953
         )
954
-        user_b = uapi.create_user(
954
+        user_b = uapi.create_minimal_user(
955
             email='this.is@another.user',
955
             email='this.is@another.user',
956
             groups=groups,
956
             groups=groups,
957
             save_now=True
957
             save_now=True
1009
                   group_api.get_one(Group.TIM_MANAGER),
1009
                   group_api.get_one(Group.TIM_MANAGER),
1010
                   group_api.get_one(Group.TIM_ADMIN)]
1010
                   group_api.get_one(Group.TIM_ADMIN)]
1011
 
1011
 
1012
-        user_a = uapi.create_user(
1012
+        user_a = uapi.create_minimal_user(
1013
             email='this.is@user',
1013
             email='this.is@user',
1014
             groups=groups,
1014
             groups=groups,
1015
             save_now=True
1015
             save_now=True
1016
         )
1016
         )
1017
-        user_b = uapi.create_user(
1017
+        user_b = uapi.create_minimal_user(
1018
             email='this.is@another.user',
1018
             email='this.is@another.user',
1019
             groups=groups,
1019
             groups=groups,
1020
             save_now=True
1020
             save_now=True
1102
                   group_api.get_one(Group.TIM_MANAGER),
1102
                   group_api.get_one(Group.TIM_MANAGER),
1103
                   group_api.get_one(Group.TIM_ADMIN)]
1103
                   group_api.get_one(Group.TIM_ADMIN)]
1104
 
1104
 
1105
-        user1 = uapi.create_user(
1105
+        user1 = uapi.create_minimal_user(
1106
             email='this.is@user',
1106
             email='this.is@user',
1107
             groups=groups,
1107
             groups=groups,
1108
             save_now=True
1108
             save_now=True
1116
         
1116
         
1117
         wid = workspace.workspace_id
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
         uapi.save(user2)
1120
         uapi.save(user2)
1122
 
1121
 
1123
         RoleApi(
1122
         RoleApi(
1223
                   group_api.get_one(Group.TIM_MANAGER),
1222
                   group_api.get_one(Group.TIM_MANAGER),
1224
                   group_api.get_one(Group.TIM_ADMIN)]
1223
                   group_api.get_one(Group.TIM_ADMIN)]
1225
 
1224
 
1226
-        user1 = uapi.create_user(
1225
+        user1 = uapi.create_minimal_user(
1227
             email='this.is@user',
1226
             email='this.is@user',
1228
             groups=groups,
1227
             groups=groups,
1229
             save_now=True,
1228
             save_now=True,
1237
             save_now=True
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
         uapi.save(user2)
1240
         uapi.save(user2)
1243
 
1241
 
1244
         RoleApi(
1242
         RoleApi(
1301
                   group_api.get_one(Group.TIM_MANAGER),
1299
                   group_api.get_one(Group.TIM_MANAGER),
1302
                   group_api.get_one(Group.TIM_ADMIN)]
1300
                   group_api.get_one(Group.TIM_ADMIN)]
1303
 
1301
 
1304
-        user1 = uapi.create_user(
1302
+        user1 = uapi.create_minimal_user(
1305
             email='this.is@user',
1303
             email='this.is@user',
1306
             groups=groups,
1304
             groups=groups,
1307
             save_now=True
1305
             save_now=True
1314
         )
1312
         )
1315
         wid = workspace.workspace_id
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
         uapi.save(user2)
1316
         uapi.save(user2)
1320
 
1317
 
1321
         RoleApi(
1318
         RoleApi(
1415
                   group_api.get_one(Group.TIM_MANAGER),
1412
                   group_api.get_one(Group.TIM_MANAGER),
1416
                   group_api.get_one(Group.TIM_ADMIN)]
1413
                   group_api.get_one(Group.TIM_ADMIN)]
1417
 
1414
 
1418
-        user1 = uapi.create_user(
1415
+        user1 = uapi.create_minimal_user(
1419
             email='this.is@user',
1416
             email='this.is@user',
1420
             groups=groups,
1417
             groups=groups,
1421
             save_now=True,
1418
             save_now=True,
1427
             save_now=True
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
         uapi.save(user2)
1428
         uapi.save(user2)
1433
 
1429
 
1434
         RoleApi(
1430
         RoleApi(
1494
                   group_api.get_one(Group.TIM_MANAGER),
1490
                   group_api.get_one(Group.TIM_MANAGER),
1495
                   group_api.get_one(Group.TIM_ADMIN)]
1491
                   group_api.get_one(Group.TIM_ADMIN)]
1496
 
1492
 
1497
-        user1 = uapi.create_user(
1493
+        user1 = uapi.create_minimal_user(
1498
             email='this.is@user',
1494
             email='this.is@user',
1499
             groups=groups,
1495
             groups=groups,
1500
             save_now=True
1496
             save_now=True
1508
         )
1504
         )
1509
         wid = workspace.workspace_id
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
         uapi.save(user2)
1508
         uapi.save(user2)
1514
 
1509
 
1515
         RoleApi(
1510
         RoleApi(
1643
                   group_api.get_one(Group.TIM_MANAGER),
1638
                   group_api.get_one(Group.TIM_MANAGER),
1644
                   group_api.get_one(Group.TIM_ADMIN)]
1639
                   group_api.get_one(Group.TIM_ADMIN)]
1645
 
1640
 
1646
-        user1 = uapi.create_user(
1641
+        user1 = uapi.create_minimal_user(
1647
             email='this.is@user',
1642
             email='this.is@user',
1648
             groups=groups,
1643
             groups=groups,
1649
             save_now=True
1644
             save_now=True
1657
         )
1652
         )
1658
         wid = workspace.workspace_id
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
         uapi.save(user2)
1656
         uapi.save(user2)
1663
 
1657
 
1664
         RoleApi(
1658
         RoleApi(
1793
                   group_api.get_one(Group.TIM_MANAGER),
1787
                   group_api.get_one(Group.TIM_MANAGER),
1794
                   group_api.get_one(Group.TIM_ADMIN)]
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
         workspace = WorkspaceApi(
1793
         workspace = WorkspaceApi(
1800
             current_user=user,
1794
             current_user=user,
1848
                   group_api.get_one(Group.TIM_MANAGER),
1842
                   group_api.get_one(Group.TIM_MANAGER),
1849
                   group_api.get_one(Group.TIM_ADMIN)]
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
         workspace = WorkspaceApi(
1848
         workspace = WorkspaceApi(
1855
             current_user=user,
1849
             current_user=user,
1899
                   group_api.get_one(Group.TIM_MANAGER),
1893
                   group_api.get_one(Group.TIM_MANAGER),
1900
                   group_api.get_one(Group.TIM_ADMIN)]
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
         workspace = WorkspaceApi(
1899
         workspace = WorkspaceApi(
1906
             current_user=user,
1900
             current_user=user,

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

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

+ 55 - 24
tracim/tests/library/test_user_api.py View File

14
 
14
 
15
 class TestUserApi(DefaultTest):
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
         api = UserApi(
18
         api = UserApi(
19
             current_user=None,
19
             current_user=None,
20
             session=self.session,
20
             session=self.session,
21
             config=self.config,
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
         nu = api.get_one_by_email('bob@bob')
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
     def test_unit__user_with_email_exists__ok__nominal_case(self):
61
     def test_unit__user_with_email_exists__ok__nominal_case(self):
32
         api = UserApi(
62
         api = UserApi(
34
             session=self.session,
64
             session=self.session,
35
             config=self.config,
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
         transaction.commit()
69
         transaction.commit()
40
 
70
 
41
         eq_(True, api.user_with_email_exists('bibi@bibi'))
71
         eq_(True, api.user_with_email_exists('bibi@bibi'))
42
         eq_(False, api.user_with_email_exists('unknown'))
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
         api = UserApi(
75
         api = UserApi(
46
             current_user=None,
76
             current_user=None,
47
             session=self.session,
77
             session=self.session,
48
             config=self.config,
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
         uid = u.user_id
83
         uid = u.user_id
53
         transaction.commit()
84
         transaction.commit()
54
 
85
 
63
         with pytest.raises(NoResultFound):
94
         with pytest.raises(NoResultFound):
64
             api.get_one_by_email('unknown')
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
     def test_unit__get_one__ok__nominal_case(self):
109
     def test_unit__get_one__ok__nominal_case(self):
79
         api = UserApi(
110
         api = UserApi(
81
             session=self.session,
112
             session=self.session,
82
             config=self.config,
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
         one = api.get_one(u.user_id)
117
         one = api.get_one(u.user_id)
87
         eq_(u.user_id, one.user_id)
118
         eq_(u.user_id, one.user_id)
88
 
119
 

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

52
             current_user=admin,
52
             current_user=admin,
53
             config=self.config
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
         eq_([], wapi.get_notifiable_roles(workspace=w))
56
         eq_([], wapi.get_notifiable_roles(workspace=w))
57
         rapi = RoleApi(
57
         rapi = RoleApi(
58
             session=self.session,
58
             session=self.session,
88
             session=self.session,
88
             session=self.session,
89
             current_user=None,
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
         wapi = WorkspaceApi(
92
         wapi = WorkspaceApi(
93
             session=self.session,
93
             session=self.session,
94
             current_user=u
94
             current_user=u