Browse Source

merge with upstream

Guénaël Muller 6 years ago
parent
commit
aa0accc736
38 changed files with 1873 additions and 274 deletions
  1. 7 0
      .travis.yml
  2. 11 0
      README.md
  3. 9 1
      development.ini.sample
  4. 9 2
      setup.py
  5. 57 0
      tests_configs.ini
  6. 23 6
      tracim/__init__.py
  7. 21 6
      tracim/command/user.py
  8. 2 4
      tracim/command/webdav.py
  9. 111 108
      tracim/config.py
  10. 6 1
      tracim/exceptions.py
  11. 11 0
      tracim/fixtures/content.py
  12. 5 4
      tracim/lib/core/content.py
  13. 23 11
      tracim/lib/core/notifications.py
  14. 48 4
      tracim/lib/core/user.py
  15. 61 0
      tracim/lib/mail_notifier/daemon.py
  16. 571 0
      tracim/lib/mail_notifier/notifier.py
  17. 114 0
      tracim/lib/mail_notifier/sender.py
  18. 33 0
      tracim/lib/mail_notifier/utils.py
  19. 25 0
      tracim/lib/utils/utils.py
  20. 21 14
      tracim/lib/webdav/__init__.py
  21. 4 1
      tracim/lib/webdav/middlewares.py
  22. 10 8
      tracim/models/data.py
  23. 0 0
      tracim/templates/mail/__init__.py
  24. 73 0
      tracim/templates/mail/content_update_body_html.mak
  25. 31 0
      tracim/templates/mail/content_update_body_text.mak
  26. 88 0
      tracim/templates/mail/created_account_body_html.mak
  27. 25 0
      tracim/templates/mail/created_account_body_text.mak
  28. 25 10
      tracim/tests/__init__.py
  29. 267 0
      tracim/tests/functional/test_mail_notification.py
  30. 0 2
      tracim/tests/functional/test_session.py
  31. 50 52
      tracim/tests/library/test_content_api.py
  32. 4 2
      tracim/tests/library/test_notification.py
  33. 55 24
      tracim/tests/library/test_user_api.py
  34. 50 1
      tracim/tests/library/test_webdav.py
  35. 2 2
      tracim/tests/library/test_workspace.py
  36. 17 0
      wsgi/__init__.py
  37. 2 4
      wsgi/web.py
  38. 2 7
      wsgi/webdav.py

+ 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]"

+ 11 - 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
 
112
 
113
 
113
 ## Run Tests and others checks ##
114
 ## Run Tests and others checks ##
114
 
115
 
116
+Before running some functional test related to email, you need a local working *MailHog*
117
+see here : https://github.com/mailhog/MailHog
118
+
119
+You can run it this way with docker :
120
+
121
+    docker pull mailhog/mailhog
122
+    docker run -d -p 1025:1025 -p 8025:8025 mailhog/mailhog
123
+
115
 Run your project's tests:
124
 Run your project's tests:
116
 
125
 
117
     pytest
126
     pytest
118
 
127
 
128
+### Lints and others checks ###
129
+
119
 Run mypy checks:
130
 Run mypy checks:
120
 
131
 
121
     mypy --ignore-missing-imports --disallow-untyped-defs tracim
132
     mypy --ignore-missing-imports --disallow-untyped-defs tracim

+ 9 - 1
development.ini.sample View File

2
 # app configuration
2
 # app configuration
3
 # https://docs.pylonsproject.org/projects/pyramid/en/latest/narr/environment.html
3
 # https://docs.pylonsproject.org/projects/pyramid/en/latest/narr/environment.html
4
 ###
4
 ###
5
-[app:main]
5
+[pipeline:main]
6
+pipeline = tracim_web
7
+[app:tracim_web]
6
 use = egg:tracim_backend
8
 use = egg:tracim_backend
7
 
9
 
8
 pyramid.reload_templates = true
10
 pyramid.reload_templates = true
13
 pyramid.includes =
15
 pyramid.includes =
14
     pyramid_debugtoolbar
16
     pyramid_debugtoolbar
15
 
17
 
18
+[pipeline:webdav]
19
+pipeline = tracim_webdav
20
+[app:tracim_webdav]
21
+use = egg:tracim_backend#webdav
22
+
23
+[DEFAULT]
16
 sqlalchemy.url = sqlite:///%(here)s/tracim.sqlite
24
 sqlalchemy.url = sqlite:///%(here)s/tracim.sqlite
17
 
25
 
18
 retry.attempts = 3
26
 retry.attempts = 3

+ 9 - 2
setup.py View File

24
     'zope.sqlalchemy',
24
     'zope.sqlalchemy',
25
     'alembic',
25
     'alembic',
26
     # API
26
     # API
27
-    'hapic',
27
+    'hapic>=0.41',
28
     'marshmallow <3.0.0a1,>2.0.0',
28
     'marshmallow <3.0.0a1,>2.0.0',
29
     # CLI
29
     # CLI
30
     'cliff',
30
     'cliff',
35
     'filedepot',
35
     'filedepot',
36
     'babel',
36
     'babel',
37
     'python-slugify',
37
     'python-slugify',
38
+    # mail-notifier
39
+    'mako',
40
+    'lxml',
41
+    'redis',
42
+    'rq',
38
 ]
43
 ]
39
 
44
 
40
 tests_require = [
45
 tests_require = [
43
     'pytest-cov',
48
     'pytest-cov',
44
     'pep8',
49
     'pep8',
45
     'mypy',
50
     'mypy',
51
+    'requests'
46
 ]
52
 ]
47
 
53
 
48
 mysql_require = [
54
 mysql_require = [
90
     install_requires=requires,
96
     install_requires=requires,
91
     entry_points={
97
     entry_points={
92
         'paste.app_factory': [
98
         'paste.app_factory': [
93
-            'main = tracim:main',
99
+            'main = tracim:web',
100
+            'webdav = tracim:webdav'
94
         ],
101
         ],
95
         'console_scripts': [
102
         'console_scripts': [
96
             'tracimcli = tracim.command:main',
103
             'tracimcli = tracim.command:main',

+ 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

+ 23 - 6
tracim/__init__.py View File

5
 from pyramid.config import Configurator
5
 from pyramid.config import Configurator
6
 from pyramid.authentication import BasicAuthAuthenticationPolicy
6
 from pyramid.authentication import BasicAuthAuthenticationPolicy
7
 from hapic.ext.pyramid import PyramidContext
7
 from hapic.ext.pyramid import PyramidContext
8
+from pyramid.exceptions import NotFound
9
+from sqlalchemy.exc import OperationalError
8
 
10
 
9
 from tracim.extensions import hapic
11
 from tracim.extensions import hapic
10
 from tracim.config import CFG
12
 from tracim.config import CFG
13
 from tracim.lib.utils.authentification import BASIC_AUTH_WEBUI_REALM
15
 from tracim.lib.utils.authentification import BASIC_AUTH_WEBUI_REALM
14
 from tracim.lib.utils.authorization import AcceptAllAuthorizationPolicy
16
 from tracim.lib.utils.authorization import AcceptAllAuthorizationPolicy
15
 from tracim.lib.utils.authorization import TRACIM_DEFAULT_PERM
17
 from tracim.lib.utils.authorization import TRACIM_DEFAULT_PERM
18
+from tracim.lib.webdav import WebdavAppFactory
16
 from tracim.views import BASE_API_V2
19
 from tracim.views import BASE_API_V2
17
 from tracim.views.core_api.session_controller import SessionController
20
 from tracim.views.core_api.session_controller import SessionController
18
 from tracim.views.core_api.system_controller import SystemController
21
 from tracim.views.core_api.system_controller import SystemController
22
 from tracim.lib.utils.cors import add_cors_support
25
 from tracim.lib.utils.cors import add_cors_support
23
 
26
 
24
 
27
 
25
-def main(global_config, **settings):
28
+def web(global_config, **local_settings):
26
     """ This function returns a Pyramid WSGI application.
29
     """ This function returns a Pyramid WSGI application.
27
     """
30
     """
31
+    settings = global_config
32
+    settings.update(local_settings)
28
     # set CFG object
33
     # set CFG object
29
     app_config = CFG(settings)
34
     app_config = CFG(settings)
30
     app_config.configure_filedepot()
35
     app_config.configure_filedepot()
52
     # Add SqlAlchemy DB
57
     # Add SqlAlchemy DB
53
     configurator.include('.models')
58
     configurator.include('.models')
54
     # set Hapic
59
     # set Hapic
55
-    hapic.set_context(
56
-        PyramidContext(
57
-            configurator=configurator,
58
-            default_error_builder=ErrorSchema()
59
-        )
60
+    context = PyramidContext(
61
+        configurator=configurator,
62
+        default_error_builder=ErrorSchema(),
63
+        debug=app_config.DEBUG,
60
     )
64
     )
65
+    hapic.set_context(context)
66
+    context.handle_exception(NotFound, 404)
67
+    context.handle_exception(OperationalError, 500)
68
+    context.handle_exception(Exception, 500)
61
     # Add controllers
69
     # Add controllers
62
     session_controller = SessionController()
70
     session_controller = SessionController()
63
     system_controller = SystemController()
71
     system_controller = SystemController()
73
         'API of Tracim v2',
81
         'API of Tracim v2',
74
     )
82
     )
75
     return configurator.make_wsgi_app()
83
     return configurator.make_wsgi_app()
84
+
85
+
86
+def webdav(global_config, **local_settings):
87
+    settings = global_config
88
+    settings.update(local_settings)
89
+    app_factory = WebdavAppFactory(
90
+        tracim_config_file_path=settings['__file__'],
91
+    )
92
+    return app_factory.get_wsgi_app()

+ 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
 
167
             try:
179
             try:
168
                 user = self._create_user(
180
                 user = self._create_user(
169
                     login=parsed_args.login,
181
                     login=parsed_args.login,
170
-                    password=parsed_args.password
182
+                    password=parsed_args.password,
183
+                    do_notify=parsed_args.send_email,
171
                 )
184
                 )
172
             except AlreadyExistError:
185
             except AlreadyExistError:
173
                 raise CommandAbortedError("Error: User already exist (use `user update` command instead)")
186
                 raise CommandAbortedError("Error: User already exist (use `user update` command instead)")
187
+            except NotificationNotSend:
188
+                raise CommandAbortedError("Error: Cannot send email notification, user not created.")
174
             # TODO - G.M - 04-04-2018 - [Email] Check this code
189
             # TODO - G.M - 04-04-2018 - [Email] Check this code
175
             # if parsed_args.send_email:
190
             # if parsed_args.send_email:
176
             #     email_manager = get_email_manager()
191
             #     email_manager = get_email_manager()

+ 2 - 4
tracim/command/webdav.py View File

6
 
6
 
7
 from tracim.command import AppContextCommand
7
 from tracim.command import AppContextCommand
8
 from tracim.lib.webdav import WebdavAppFactory
8
 from tracim.lib.webdav import WebdavAppFactory
9
+from wsgi import webdav_app
9
 
10
 
10
 
11
 
11
 class WebdavRunnerCommand(AppContextCommand):
12
 class WebdavRunnerCommand(AppContextCommand):
22
         super(WebdavRunnerCommand, self).take_action(parsed_args)
23
         super(WebdavRunnerCommand, self).take_action(parsed_args)
23
         tracim_config = parsed_args.config_file
24
         tracim_config = parsed_args.config_file
24
         # TODO - G.M - 16-04-2018 - Allow specific webdav config file
25
         # TODO - G.M - 16-04-2018 - Allow specific webdav config file
25
-        app_factory = WebdavAppFactory(
26
-            tracim_config_file_path=tracim_config,
27
-        )
28
-        app = app_factory.get_wsgi_app()
26
+        app = webdav_app(tracim_config)
29
         serve(app, port=app.config['port'], host=app.config['host'])
27
         serve(app, port=app.config['port'], host=app.config['host'])

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

+ 6 - 1
tracim/exceptions.py View File

96
 class UserDoesNotExist(TracimException):
96
 class UserDoesNotExist(TracimException):
97
     pass
97
     pass
98
 
98
 
99
+
99
 class UserNotFoundInTracimRequest(TracimException):
100
 class UserNotFoundInTracimRequest(TracimException):
100
-    pass
101
+    pass
102
+
103
+
104
+class NotificationNotSend(TracimException):
105
+    pass

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

73
             workspace=w1,
73
             workspace=w1,
74
             label='w1f1',
74
             label='w1f1',
75
             do_save=True,
75
             do_save=True,
76
+            do_notify=False,
76
         )
77
         )
77
         w1f2 = content_api.create(
78
         w1f2 = content_api.create(
78
             content_type=ContentType.Folder,
79
             content_type=ContentType.Folder,
79
             workspace=w1,
80
             workspace=w1,
80
             label='w1f2',
81
             label='w1f2',
81
             do_save=True,
82
             do_save=True,
83
+            do_notify=False,
82
         )
84
         )
83
 
85
 
84
         w2f1 = content_api.create(
86
         w2f1 = content_api.create(
86
             workspace=w2,
88
             workspace=w2,
87
             label='w2f1',
89
             label='w2f1',
88
             do_save=True,
90
             do_save=True,
91
+            do_notify=False,
89
         )
92
         )
90
         w2f2 = content_api.create(
93
         w2f2 = content_api.create(
91
             content_type=ContentType.Folder,
94
             content_type=ContentType.Folder,
92
             workspace=w2,
95
             workspace=w2,
93
             label='w2f2',
96
             label='w2f2',
94
             do_save=True,
97
             do_save=True,
98
+            do_notify=False,
95
         )
99
         )
96
 
100
 
97
         w3f1 = content_api.create(
101
         w3f1 = content_api.create(
99
             workspace=w3,
103
             workspace=w3,
100
             label='w3f3',
104
             label='w3f3',
101
             do_save=True,
105
             do_save=True,
106
+            do_notify=False,
102
         )
107
         )
103
 
108
 
104
         # Pages, threads, ..
109
         # Pages, threads, ..
108
             parent=w1f1,
113
             parent=w1f1,
109
             label='w1f1p1',
114
             label='w1f1p1',
110
             do_save=True,
115
             do_save=True,
116
+            do_notify=False,
111
         )
117
         )
112
         w1f1t1 = content_api.create(
118
         w1f1t1 = content_api.create(
113
             content_type=ContentType.Thread,
119
             content_type=ContentType.Thread,
115
             parent=w1f1,
121
             parent=w1f1,
116
             label='w1f1t1',
122
             label='w1f1t1',
117
             do_save=False,
123
             do_save=False,
124
+            do_notify=False,
118
         )
125
         )
119
         w1f1t1.description = 'w1f1t1 description'
126
         w1f1t1.description = 'w1f1t1 description'
120
         self._session.add(w1f1t1)
127
         self._session.add(w1f1t1)
124
             parent=w1f1,
131
             parent=w1f1,
125
             label='w1f1d1',
132
             label='w1f1d1',
126
             do_save=False,
133
             do_save=False,
134
+            do_notify=False,
127
         )
135
         )
128
         w1f1d1_txt.file_extension = '.txt'
136
         w1f1d1_txt.file_extension = '.txt'
129
         w1f1d1_txt.depot_file = FileIntent(
137
         w1f1d1_txt.depot_file = FileIntent(
138
             parent=w1f1,
146
             parent=w1f1,
139
             label='w1f1d2',
147
             label='w1f1d2',
140
             do_save=False,
148
             do_save=False,
149
+            do_notify=False,
141
         )
150
         )
142
         w1f1d2_html.file_extension = '.html'
151
         w1f1d2_html.file_extension = '.html'
143
         w1f1d2_html.depot_file = FileIntent(
152
         w1f1d2_html.depot_file = FileIntent(
152
             label='w1f1f1',
161
             label='w1f1f1',
153
             parent=w1f1,
162
             parent=w1f1,
154
             do_save=True,
163
             do_save=True,
164
+            do_notify=False,
155
         )
165
         )
156
 
166
 
157
         w2f1p1 = content_api.create(
167
         w2f1p1 = content_api.create(
160
             parent=w2f1,
170
             parent=w2f1,
161
             label='w2f1p1',
171
             label='w2f1p1',
162
             do_save=True,
172
             do_save=True,
173
+            do_notify=False,
163
         )
174
         )
164
         self._session.flush()
175
         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
114
             user: User,
117
             user: User,
115
             name: str=None,
118
             name: str=None,
116
             email: str=None,
119
             email: str=None,
117
-            do_save=True,
120
+            password: str=None,
118
             timezone: str='',
121
             timezone: str='',
122
+            do_save=True,
119
     ) -> None:
123
     ) -> None:
120
         if name is not None:
124
         if name is not None:
121
             user.display_name = name
125
             user.display_name = name
123
         if email is not None:
127
         if email is not None:
124
             user.email = email
128
             user.email = email
125
 
129
 
130
+        if password is not None:
131
+            user.password = password
132
+
126
         user.timezone = timezone
133
         user.timezone = timezone
127
 
134
 
128
         if do_save:
135
         if do_save:
129
             self.save(user)
136
             self.save(user)
130
 
137
 
131
-    def create_user(self, email=None, groups=[], save_now=False) -> User:
138
+    def create_user(
139
+        self,
140
+        email,
141
+        password: str = None,
142
+        name: str = None,
143
+        timezone: str = '',
144
+        groups=[],
145
+        do_save: bool=True,
146
+        do_notify: bool=True,
147
+    ) -> User:
148
+        new_user = self.create_minimal_user(email, groups, save_now=False)
149
+        self.update(
150
+            user=new_user,
151
+            name=name,
152
+            email=email,
153
+            password=password,
154
+            timezone=timezone,
155
+            do_save=False,
156
+        )
157
+        if do_notify:
158
+            try:
159
+                email_manager = get_email_manager(self._config, self._session)
160
+                email_manager.notify_created_account(
161
+                    new_user,
162
+                    password=password
163
+                )
164
+            except SMTPException as e:
165
+                raise NotificationNotSend()
166
+        if do_save:
167
+            self.save(new_user)
168
+        return new_user
169
+
170
+    def create_minimal_user(
171
+            self,
172
+            email,
173
+            groups=[],
174
+            save_now=False
175
+    ) -> User:
176
+        """Previous create_user method"""
132
         user = User()
177
         user = User()
133
 
178
 
134
-        if email:
135
-            user.email = email
179
+        user.email = email
136
 
180
 
137
         for group in groups:
181
         for group in groups:
138
             user.groups.append(group)
182
             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)

+ 571 - 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
+            config=self.config,
242
+        ).get_notifiable_roles(content.workspace)
243
+
244
+        if len(notifiable_roles) <= 0:
245
+            logger.info(self, 'Skipping notification as nobody subscribed to in workspace {}'.format(content.workspace.label))
246
+            return
247
+
248
+
249
+        logger.info(self, 'Sending asynchronous emails to {} user(s)'.format(len(notifiable_roles)))
250
+        # INFO - D.A. - 2014-11-06
251
+        # The following email sender will send emails in the async task queue
252
+        # This allow to build all mails through current thread but really send them (including SMTP connection)
253
+        # In the other thread.
254
+        #
255
+        # This way, the webserver will return sooner (actually before notification emails are sent
256
+        async_email_sender = EmailSender(
257
+            self.config,
258
+            self._smtp_config,
259
+            self.config.EMAIL_NOTIFICATION_ACTIVATED
260
+        )
261
+        for role in notifiable_roles:
262
+            logger.info(self, 'Sending email to {}'.format(role.user.email))
263
+            to_addr = formataddr((role.user.display_name, role.user.email))
264
+            #
265
+            # INFO - G.M - 2017-11-15 - set content_id in header to permit reply
266
+            # references can have multiple values, but only one in this case.
267
+            replyto_addr = self.config.EMAIL_NOTIFICATION_REPLY_TO_EMAIL.replace( # nopep8
268
+                '{content_id}',str(content.content_id)
269
+            )
270
+
271
+            reference_addr = self.config.EMAIL_NOTIFICATION_REFERENCES_EMAIL.replace( #nopep8
272
+                '{content_id}',str(content.content_id)
273
+             )
274
+            #
275
+            #  INFO - D.A. - 2014-11-06
276
+            # We do not use .format() here because the subject defined in the .ini file
277
+            # may not include all required labels. In order to avoid partial format() (which result in an exception)
278
+            # we do use replace and force the use of .__str__() in order to process LazyString objects
279
+            #
280
+            subject = self.config.EMAIL_NOTIFICATION_CONTENT_UPDATE_SUBJECT
281
+            subject = subject.replace(EST.WEBSITE_TITLE, self.config.WEBSITE_TITLE.__str__())
282
+            subject = subject.replace(EST.WORKSPACE_LABEL, main_content.workspace.label.__str__())
283
+            subject = subject.replace(EST.CONTENT_LABEL, main_content.label.__str__())
284
+            subject = subject.replace(EST.CONTENT_STATUS_LABEL, main_content.get_status().label.__str__())
285
+            reply_to_label = l_('{username} & all members of {workspace}').format(
286
+                username=user.display_name,
287
+                workspace=main_content.workspace.label)
288
+
289
+            message = MIMEMultipart('alternative')
290
+            message['Subject'] = subject
291
+            message['From'] = self._get_sender(user)
292
+            message['To'] = to_addr
293
+            message['Reply-to'] = formataddr((reply_to_label, replyto_addr))
294
+            # INFO - G.M - 2017-11-15
295
+            # References can theorically have label, but in pratice, references
296
+            # contains only message_id from parents post in thread.
297
+            # To link this email to a content we create a virtual parent
298
+            # in reference who contain the content_id.
299
+            message['References'] = formataddr(('',reference_addr))
300
+            body_text = self._build_email_body_for_content(self.config.EMAIL_NOTIFICATION_CONTENT_UPDATE_TEMPLATE_TEXT, role, content, user)
301
+            body_html = self._build_email_body_for_content(self.config.EMAIL_NOTIFICATION_CONTENT_UPDATE_TEMPLATE_HTML, role, content, user)
302
+
303
+            part1 = MIMEText(body_text, 'plain', 'utf-8')
304
+            part2 = MIMEText(body_html, 'html', 'utf-8')
305
+            # Attach parts into message container.
306
+            # According to RFC 2046, the last part of a multipart message, in this case
307
+            # the HTML message, is best and preferred.
308
+            message.attach(part1)
309
+            message.attach(part2)
310
+
311
+            self.log_notification(
312
+                action='CREATED',
313
+                recipient=message['To'],
314
+                subject=message['Subject'],
315
+                config=self.config,
316
+            )
317
+
318
+            send_email_through(
319
+                self.config,
320
+                async_email_sender.send_mail,
321
+                message
322
+            )
323
+
324
+    def notify_created_account(
325
+            self,
326
+            user: User,
327
+            password: str,
328
+    ) -> None:
329
+        """
330
+        Send created account email to given user.
331
+
332
+        :param password: choosed password
333
+        :param user: user to notify
334
+        """
335
+        # TODO BS 20160712: Cyclic import
336
+        logger.debug(self, 'user: {}'.format(user.user_id))
337
+        logger.info(self, 'Sending asynchronous email to 1 user ({0})'.format(
338
+            user.email,
339
+        ))
340
+
341
+        async_email_sender = EmailSender(
342
+            self.config,
343
+            self._smtp_config,
344
+            self.config.EMAIL_NOTIFICATION_ACTIVATED
345
+        )
346
+
347
+        subject = \
348
+            self.config.EMAIL_NOTIFICATION_CREATED_ACCOUNT_SUBJECT \
349
+            .replace(
350
+                EST.WEBSITE_TITLE,
351
+                self.config.WEBSITE_TITLE.__str__()
352
+            )
353
+        message = MIMEMultipart('alternative')
354
+        message['Subject'] = subject
355
+        message['From'] = self._get_sender()
356
+        message['To'] = formataddr((user.get_display_name(), user.email))
357
+
358
+        text_template_file_path = self.config.EMAIL_NOTIFICATION_CREATED_ACCOUNT_TEMPLATE_TEXT  # nopep8
359
+        html_template_file_path = self.config.EMAIL_NOTIFICATION_CREATED_ACCOUNT_TEMPLATE_HTML  # nopep8
360
+
361
+        context = {
362
+            'user': user,
363
+            'password': password,
364
+            # TODO - G.M - 11-06-2018 - [emailTemplateURL] correct value for logo_url  # nopep8
365
+            'logo_url': '',
366
+            # TODO - G.M - 11-06-2018 - [emailTemplateURL] correct value for login_url  # nopep8
367
+            'login_url': self.config.WEBSITE_BASE_URL,
368
+        }
369
+        body_text = self._render_template(
370
+            mako_template_filepath=text_template_file_path,
371
+            context=context
372
+        )
373
+
374
+        body_html = self._render_template(
375
+            mako_template_filepath=html_template_file_path,
376
+            context=context,
377
+        )
378
+
379
+        part1 = MIMEText(body_text, 'plain', 'utf-8')
380
+        part2 = MIMEText(body_html, 'html', 'utf-8')
381
+
382
+        # Attach parts into message container.
383
+        # According to RFC 2046, the last part of a multipart message,
384
+        # in this case the HTML message, is best and preferred.
385
+        message.attach(part1)
386
+        message.attach(part2)
387
+
388
+        send_email_through(
389
+            config=self.config,
390
+            sendmail_callable=async_email_sender.send_mail,
391
+            message=message
392
+        )
393
+
394
+    def _render_template(
395
+            self,
396
+            mako_template_filepath: str,
397
+            context: dict
398
+    ) -> str:
399
+        """
400
+        Render mako template with all needed current variables.
401
+
402
+        :param mako_template_filepath: file path of mako template
403
+        :param context: dict with template context
404
+        :return: template rendered string
405
+        """
406
+
407
+        template = Template(filename=mako_template_filepath)
408
+        return template.render(
409
+            _=_,
410
+            config=self.config,
411
+            **context
412
+        )
413
+
414
+    def _build_email_body_for_content(
415
+            self,
416
+            mako_template_filepath: str,
417
+            role: UserRoleInWorkspace,
418
+            content: Content,
419
+            actor: User
420
+    ) -> str:
421
+        """
422
+        Build an email body and return it as a string
423
+        :param mako_template_filepath: the absolute path to the mako template to be used for email body building
424
+        :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
425
+        :param content: the content item related to the notification
426
+        :param actor: the user at the origin of the action / notification (for example the one who wrote a comment
427
+        :param config: the global configuration
428
+        :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
429
+        """
430
+        logger.debug(self, 'Building email content from MAKO template {}'.format(mako_template_filepath))
431
+
432
+        main_title = content.label
433
+        content_intro = ''
434
+        content_text = ''
435
+        call_to_action_text = ''
436
+
437
+        # TODO - G.M - 11-06-2018 - [emailTemplateURL] correct value for call_to_action_url  # nopep8
438
+        call_to_action_url =''
439
+        # TODO - G.M - 11-06-2018 - [emailTemplateURL] correct value for status_icon_url  # nopep8
440
+        status_icon_url = ''
441
+        # TODO - G.M - 11-06-2018 - [emailTemplateURL] correct value for workspace_url  # nopep8
442
+        workspace_url = ''
443
+        # TODO - G.M - 11-06-2018 - [emailTemplateURL] correct value for logo_url  # nopep8
444
+        logo_url = ''
445
+
446
+        action = content.get_last_action().id
447
+        if ActionDescription.COMMENT == action:
448
+            content_intro = l_('<span id="content-intro-username">{}</span> added a comment:').format(actor.display_name)
449
+            content_text = content.description
450
+            call_to_action_text = l_('Answer')
451
+
452
+        elif ActionDescription.CREATION == action:
453
+
454
+            # Default values (if not overriden)
455
+            content_text = content.description
456
+            call_to_action_text = l_('View online')
457
+
458
+            if ContentType.Thread == content.type:
459
+                call_to_action_text = l_('Answer')
460
+                content_intro = l_('<span id="content-intro-username">{}</span> started a thread entitled:').format(actor.display_name)
461
+                content_text = '<p id="content-body-intro">{}</p>'.format(content.label) + \
462
+                               content.get_last_comment_from(actor).description
463
+
464
+            elif ContentType.File == content.type:
465
+                content_intro = l_('<span id="content-intro-username">{}</span> added a file entitled:').format(actor.display_name)
466
+                if content.description:
467
+                    content_text = content.description
468
+                else:
469
+                    content_text = '<span id="content-body-only-title">{}</span>'.format(content.label)
470
+
471
+            elif ContentType.Page == content.type:
472
+                content_intro = l_('<span id="content-intro-username">{}</span> added a page entitled:').format(actor.display_name)
473
+                content_text = '<span id="content-body-only-title">{}</span>'.format(content.label)
474
+
475
+        elif ActionDescription.REVISION == action:
476
+            content_text = content.description
477
+            call_to_action_text = l_('View online')
478
+
479
+            if ContentType.File == content.type:
480
+                content_intro = l_('<span id="content-intro-username">{}</span> uploaded a new revision.').format(actor.display_name)
481
+                content_text = ''
482
+
483
+            elif ContentType.Page == content.type:
484
+                content_intro = l_('<span id="content-intro-username">{}</span> updated this page.').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 ContentType.Thread == content.type:
494
+                content_intro = l_('<span id="content-intro-username">{}</span> updated the thread description.').format(actor.display_name)
495
+                previous_revision = content.get_previous_revision()
496
+                title_diff = ''
497
+                if previous_revision.label != content.label:
498
+                    title_diff = htmldiff(previous_revision.label, content.label)
499
+                content_text = str(l_('<p id="content-body-intro">Here is an overview of the changes:</p>'))+ \
500
+                    title_diff + \
501
+                    htmldiff(previous_revision.description, content.description)
502
+
503
+        elif ActionDescription.EDITION == action:
504
+            call_to_action_text = l_('View online')
505
+
506
+            if ContentType.File == content.type:
507
+                content_intro = l_('<span id="content-intro-username">{}</span> updated the file description.').format(actor.display_name)
508
+                content_text = '<p id="content-body-intro">{}</p>'.format(content.get_label()) + \
509
+                    content.description
510
+
511
+        elif ActionDescription.STATUS_UPDATE == action:
512
+            call_to_action_text = l_('View online')
513
+            intro_user_msg = l_(
514
+                '<span id="content-intro-username">{}</span> '
515
+                'updated the following status:'
516
+            )
517
+            content_intro = intro_user_msg.format(actor.display_name)
518
+            intro_body_msg = '<p id="content-body-intro">{}: {}</p>'
519
+            content_text = intro_body_msg.format(
520
+                content.get_label(),
521
+                content.get_status().label,
522
+            )
523
+
524
+        if '' == content_intro and content_text == '':
525
+            # Skip notification, but it's not normal
526
+            logger.error(
527
+                self, 'A notification is being sent but no content. '
528
+                      'Here are some debug informations: [content_id: {cid}]'
529
+                      '[action: {act}][author: {actor}]'.format(
530
+                    cid=content.content_id, act=action, actor=actor
531
+                )
532
+            )
533
+            raise ValueError('Unexpected empty notification')
534
+
535
+        context = {
536
+            'user': role.user,
537
+            'workspace': role.workspace,
538
+            'workspace_url': workspace_url,
539
+            'main_title': main_title,
540
+            'status_label': content.get_status().label,
541
+            'status_icon_url': status_icon_url,
542
+            'role_label': role.role_as_label(),
543
+            'content_intro': content_intro,
544
+            'content_text': content_text,
545
+            'call_to_action_text': call_to_action_text,
546
+            'call_to_action_url': call_to_action_url,
547
+            'logo_url': logo_url,
548
+        }
549
+        user = role.user
550
+        workspace = role.workspace
551
+        body_content = self._render_template(
552
+            mako_template_filepath=mako_template_filepath,
553
+            context=context,
554
+        )
555
+        return body_content
556
+
557
+
558
+def get_email_manager(config: CFG, session: Session):
559
+    """
560
+    :return: EmailManager instance
561
+    """
562
+    #  TODO: Find a way to import properly without cyclic import
563
+
564
+    smtp_config = SmtpConfiguration(
565
+        config.EMAIL_NOTIFICATION_SMTP_SERVER,
566
+        config.EMAIL_NOTIFICATION_SMTP_PORT,
567
+        config.EMAIL_NOTIFICATION_SMTP_USER,
568
+        config.EMAIL_NOTIFICATION_SMTP_PASSWORD
569
+    )
570
+
571
+    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

+ 21 - 14
tracim/lib/webdav/__init__.py View File

27
 class WebdavAppFactory(object):
27
 class WebdavAppFactory(object):
28
 
28
 
29
     def __init__(self,
29
     def __init__(self,
30
-                 webdav_config_file_path: str = None,
31
                  tracim_config_file_path: str = None,
30
                  tracim_config_file_path: str = None,
32
                  ):
31
                  ):
33
         self.config = self._initConfig(
32
         self.config = self._initConfig(
34
-            webdav_config_file_path,
35
             tracim_config_file_path
33
             tracim_config_file_path
36
         )
34
         )
37
 
35
 
38
     def _initConfig(self,
36
     def _initConfig(self,
39
-                    webdav_config_file_path: str = None,
40
                     tracim_config_file_path: str = None
37
                     tracim_config_file_path: str = None
41
                     ):
38
                     ):
42
         """Setup configuration dictionary from default,
39
         """Setup configuration dictionary from default,
43
          command line and configuration file."""
40
          command line and configuration file."""
44
-        if not webdav_config_file_path:
45
-            webdav_config_file_path = DEFAULT_WEBDAV_CONFIG_FILE
46
         if not tracim_config_file_path:
41
         if not tracim_config_file_path:
47
             tracim_config_file_path = DEFAULT_TRACIM_CONFIG_FILE
42
             tracim_config_file_path = DEFAULT_TRACIM_CONFIG_FILE
48
 
43
 
49
         # Set config defaults
44
         # Set config defaults
50
         config = DEFAULT_CONFIG.copy()
45
         config = DEFAULT_CONFIG.copy()
51
         temp_verbose = config["verbose"]
46
         temp_verbose = config["verbose"]
47
+        # Get pyramid Env
48
+        tracim_config_file_path = os.path.abspath(tracim_config_file_path)
49
+        config['tracim_config'] = tracim_config_file_path
50
+        settings = self._get_tracim_settings(config)
51
+        app_config = CFG(settings)
52
 
52
 
53
-        default_config_file = os.path.abspath(webdav_config_file_path)
53
+        default_config_file = os.path.abspath(settings['wsgidav.config_path'])
54
         webdav_config_file = self._readConfigFile(
54
         webdav_config_file = self._readConfigFile(
55
-            webdav_config_file_path,
55
+            default_config_file,
56
             temp_verbose
56
             temp_verbose
57
             )
57
             )
58
         # Configuration file overrides defaults
58
         # Configuration file overrides defaults
59
         config.update(webdav_config_file)
59
         config.update(webdav_config_file)
60
 
60
 
61
-        # Get pyramid Env
62
-        tracim_config_file_path = os.path.abspath(tracim_config_file_path)
63
-        config['tracim_config'] = tracim_config_file_path
64
-        settings = get_appsettings(config['tracim_config'])
65
-        app_config = CFG(settings)
66
-
67
         if not useLxml and config["verbose"] >= 1:
61
         if not useLxml and config["verbose"] >= 1:
68
             print(
62
             print(
69
                 "WARNING: Could not import lxml: using xml instead (slower). "
63
                 "WARNING: Could not import lxml: using xml instead (slower). "
93
         config['domaincontroller'] = TracimDomainController(
87
         config['domaincontroller'] = TracimDomainController(
94
             presetdomain=None,
88
             presetdomain=None,
95
             presetserver=None,
89
             presetserver=None,
96
-            app_config = app_config,
90
+            app_config=app_config,
97
         )
91
         )
98
         return config
92
         return config
99
 
93
 
94
+    def _get_tracim_settings(
95
+            self,
96
+            default_config,
97
+    ):
98
+        """
99
+        Get tracim settings
100
+        """
101
+        global_conf = get_appsettings(default_config['tracim_config']).global_conf
102
+        local_conf = get_appsettings(default_config['tracim_config'], 'tracim_web')  # nopep8
103
+        settings = global_conf
104
+        settings.update(local_conf)
105
+        return settings
106
+
100
     # INFO - G.M - 13-04-2018 - Copy from
107
     # INFO - G.M - 13-04-2018 - Copy from
101
     # wsgidav.server.run_server._readConfigFile
108
     # wsgidav.server.run_server._readConfigFile
102
     def _readConfigFile(self, config_file, verbose):
109
     def _readConfigFile(self, config_file, verbose):

+ 4 - 1
tracim/lib/webdav/middlewares.py View File

256
         super().__init__(application, config)
256
         super().__init__(application, config)
257
         self._application = application
257
         self._application = application
258
         self._config = config
258
         self._config = config
259
-        self.settings = get_appsettings(config['tracim_config'])
259
+        global_conf = get_appsettings(config['tracim_config']).global_conf
260
+        local_conf = get_appsettings(config['tracim_config'], 'tracim_web')
261
+        self.settings = global_conf
262
+        self.settings.update(local_conf)
260
         self.engine = get_engine(self.settings)
263
         self.engine = get_engine(self.settings)
261
         self.session_factory = get_session_factory(self.engine)
264
         self.session_factory = get_session_factory(self.engine)
262
         self.app_config = CFG(self.settings)
265
         self.app_config = CFG(self.settings)

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

139
         WORKSPACE_MANAGER: 'workspace_manager',
139
         WORKSPACE_MANAGER: 'workspace_manager',
140
     }
140
     }
141
 
141
 
142
-    # LABEL = dict()
143
-    # LABEL[0] = l_('N/A')
144
-    # LABEL[1] = l_('Reader')
145
-    # LABEL[2] = l_('Contributor')
146
-    # LABEL[4] = l_('Content Manager')
147
-    # LABEL[8] = l_('Workspace Manager')
142
+    LABEL = dict()
143
+    LABEL[0] = l_('N/A')
144
+    LABEL[1] = l_('Reader')
145
+    LABEL[2] = l_('Contributor')
146
+    LABEL[4] = l_('Content Manager')
147
+    LABEL[8] = l_('Workspace Manager')
148
     #
148
     #
149
     # STYLE = dict()
149
     # STYLE = dict()
150
     # STYLE[0] = ''
150
     # STYLE[0] = ''
169
     # def style(self):
169
     # def style(self):
170
     #     return UserRoleInWorkspace.STYLE[self.role]
170
     #     return UserRoleInWorkspace.STYLE[self.role]
171
     #
171
     #
172
-    # def role_as_label(self):
173
-    #     return UserRoleInWorkspace.LABEL[self.role]
172
+
173
+    def role_as_label(self):
174
+        return UserRoleInWorkspace.LABEL[self.role]
174
 
175
 
175
     @classmethod
176
     @classmethod
176
     def get_all_role_values(cls) -> typing.List[int]:
177
     def get_all_role_values(cls) -> typing.List[int]:
340
                  # type=''
341
                  # type=''
341
     ):
342
     ):
342
         self.id = id
343
         self.id = id
344
+        self.label = self.id
343
         # TODO - G.M - 10-04-2018 - [Cleanup] Drop this
345
         # TODO - G.M - 10-04-2018 - [Cleanup] Drop this
344
         # self.icon = ContentStatus._ICONS[id]
346
         # self.icon = ContentStatus._ICONS[id]
345
         # self.css = ContentStatus._CSS[id]
347
         # self.css = ContentStatus._CSS[id]

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


+ 73 - 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
+            <img alt="logo" src="${logo_url}" style="vertical-align: middle;">
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_label|n}
48
+              <img alt="status_icon" src="${status_icon_url}" style="vertical-align: middle;">
49
+            </span>
50
+        </td>
51
+      </tr>
52
+    </table>
53
+
54
+    <p id="content-intro">${content_intro|n}</p>
55
+    <div id="content-body">
56
+        <div>${content_text|n}</div>
57
+        <div href='' id="call-to-action-container">
58
+        </div>
59
+    </div>
60
+    
61
+    <div id="footer">
62
+        <p>
63
+
64
+            ${_('{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_label, workspace_url=workspace_url, workspace_label=workspace.label, website_title=config.WEBSITE_TITLE)|n}
65
+        </p>
66
+        <hr/>
67
+        <p>
68
+            ${_('This email was automatically sent by <i>Tracim</i>, a collaborative software developped by Algoo.')}<br/>
69
+            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>
70
+        </p>
71
+    </div>
72
+  </body>
73
+</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_label}/ 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
+            <img alt="logo" src="${logo_url}" style="vertical-align: middle;">
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

+ 25 - 10
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
17
 from tracim.fixtures.users_and_groups import Base as BaseFixture
20
 from tracim.fixtures.users_and_groups import Base as BaseFixture
18
 from tracim.config import CFG
21
 from tracim.config import CFG
19
 from tracim.extensions import hapic
22
 from tracim.extensions import hapic
20
-from tracim import main
23
+from tracim import web
21
 from webtest import TestApp
24
 from webtest import TestApp
22
 
25
 
23
 
26
 
42
 
45
 
43
         }
46
         }
44
         hapic.reset_context()
47
         hapic.reset_context()
45
-        app = main({}, **settings)
48
+        app = web({}, **settings)
46
         self.init_database(settings)
49
         self.init_database(settings)
47
         self.testapp = TestApp(app)
50
         self.testapp = TestApp(app)
48
 
51
 
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(
226
             owner=user
229
             owner=user
227
         )
230
         )
228
         return thread
231
         return thread
232
+
233
+
234
+class MailHogTest(DefaultTest):
235
+    """
236
+    Theses test need a working mailhog
237
+    """
238
+
239
+    config_section = 'mail_test'
240
+
241
+    def tearDown(self):
242
+        logger.debug(self, 'Cleanup MailHog list...')
243
+        requests.delete('http://127.0.0.1:8025/api/v1/messages')

+ 267 - 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
+            config=self.app_config,
131
+        )
132
+        workspace = wapi.get_one_by_label('w1')
133
+        user = uapi.get_one_by_email('bob@fsf.local')
134
+        wapi.enable_notifications(user, workspace)
135
+
136
+        api = ContentApi(
137
+            current_user=user,
138
+            session=self.session,
139
+            config=self.app_config,
140
+        )
141
+        item = api.create(
142
+            ContentType.Folder,
143
+            workspace,
144
+            None,
145
+            'parent',
146
+            do_save=True,
147
+            do_notify=False,
148
+        )
149
+        item2 = api.create(
150
+            ContentType.File,
151
+            workspace,
152
+            item,
153
+            'file1',
154
+            do_save=True,
155
+            do_notify=True,
156
+        )
157
+
158
+        # check mail received
159
+        response = requests.get('http://127.0.0.1:8025/api/v1/messages')
160
+        response = response.json()
161
+        headers = response[0]['Content']['Headers']
162
+        assert headers['From'][0] == '"Bob i. via Tracim" <test_user_from+3@localhost>'  # nopep8
163
+        assert headers['To'][0] == 'Global manager <admin@admin.admin>'
164
+        assert headers['Subject'][0] == '[TRACIM] [w1] file1 (open)'
165
+        assert headers['References'][0] == 'test_user_refs+13@localhost'
166
+        assert headers['Reply-to'][0] == '"Bob i. & all members of w1" <test_user_reply+13@localhost>'  # nopep8
167
+
168
+
169
+class TestNotificationsAsync(MailHogTest):
170
+    fixtures = [BaseFixture, ContentFixture]
171
+    config_section = 'mail_test_async'
172
+
173
+    def test_func__create_user_with_mail_notification__ok__nominal_case(self):
174
+        api = UserApi(
175
+            current_user=None,
176
+            session=self.session,
177
+            config=self.app_config,
178
+        )
179
+        u = api.create_user(
180
+            email='bob@bob',
181
+            password='pass',
182
+            name='bob',
183
+            timezone='+2',
184
+            do_save=True,
185
+            do_notify=True,
186
+        )
187
+        assert u is not None
188
+        assert u.email == "bob@bob"
189
+        assert u.validate_password('pass')
190
+        assert u.display_name == 'bob'
191
+        assert u.timezone == '+2'
192
+
193
+        # Send mail async from redis queue
194
+        redis = get_redis_connection(
195
+            self.app_config
196
+        )
197
+        queue = get_rq_queue(
198
+            redis,
199
+            'mail_sender',
200
+        )
201
+        worker = SimpleWorker([queue], connection=queue.connection)
202
+        worker.work(burst=True)
203
+        # check mail received
204
+        response = requests.get('http://127.0.0.1:8025/api/v1/messages')
205
+        response = response.json()
206
+        headers = response[0]['Content']['Headers']
207
+        assert headers['From'][0] == 'Tracim Notifications <test_user_from+0@localhost>'  # nopep8
208
+        assert headers['To'][0] == 'bob <bob@bob>'
209
+        assert headers['Subject'][0] == '[TRACIM] Created account'
210
+
211
+    def test_func__create_new_content_with_notification__ok__nominal_case(self):
212
+        uapi = UserApi(
213
+            current_user=None,
214
+            session=self.session,
215
+            config=self.app_config,
216
+        )
217
+        current_user = uapi.get_one_by_email('admin@admin.admin')
218
+        # Create new user with notification enabled on w1 workspace
219
+        wapi = WorkspaceApi(
220
+            current_user=current_user,
221
+            session=self.session,
222
+            config=self.app_config,
223
+        )
224
+        workspace = wapi.get_one_by_label('w1')
225
+        user = uapi.get_one_by_email('bob@fsf.local')
226
+        wapi.enable_notifications(user, workspace)
227
+
228
+        api = ContentApi(
229
+            current_user=user,
230
+            session=self.session,
231
+            config=self.app_config,
232
+        )
233
+        item = api.create(
234
+            ContentType.Folder,
235
+            workspace,
236
+            None,
237
+            'parent',
238
+            do_save=True,
239
+            do_notify=False,
240
+        )
241
+        item2 = api.create(
242
+            ContentType.File,
243
+            workspace,
244
+            item,
245
+            'file1',
246
+            do_save=True,
247
+            do_notify=True,
248
+        )
249
+        # Send mail async from redis queue
250
+        redis = get_redis_connection(
251
+            self.app_config
252
+        )
253
+        queue = get_rq_queue(
254
+            redis,
255
+            'mail_sender',
256
+        )
257
+        worker = SimpleWorker([queue], connection=queue.connection)
258
+        worker.work(burst=True)
259
+        # check mail received
260
+        response = requests.get('http://127.0.0.1:8025/api/v1/messages')
261
+        response = response.json()
262
+        headers = response[0]['Content']['Headers']
263
+        assert headers['From'][0] == '"Bob i. via Tracim" <test_user_from+3@localhost>'  # nopep8
264
+        assert headers['To'][0] == 'Global manager <admin@admin.admin>'
265
+        assert headers['Subject'][0] == '[TRACIM] [w1] file1 (open)'
266
+        assert headers['References'][0] == 'test_user_refs+13@localhost'
267
+        assert headers['Reply-to'][0] == '"Bob i. & all members of w1" <test_user_reply+13@localhost>'  # nopep8

+ 0 - 2
tracim/tests/functional/test_session.py View File

17
 
17
 
18
 class TestLoginEndpointUnititedDB(FunctionalTestNoDB):
18
 class TestLoginEndpointUnititedDB(FunctionalTestNoDB):
19
 
19
 
20
-    @pytest.mark.xfail(raises=OperationalError,
21
-                       reason='Not supported yet by hapic')
22
     def test_api__try_login_enpoint__err_500__no_inited_db(self):
20
     def test_api__try_login_enpoint__err_500__no_inited_db(self):
23
         params = {
21
         params = {
24
             'email': 'admin@admin.admin',
22
             'email': 'admin@admin.admin',

+ 50 - 52
tracim/tests/library/test_content_api.py View File

116
                   group_api.get_one(Group.TIM_MANAGER),
116
                   group_api.get_one(Group.TIM_MANAGER),
117
                   group_api.get_one(Group.TIM_ADMIN)]
117
                   group_api.get_one(Group.TIM_ADMIN)]
118
 
118
 
119
-        user = uapi.create_user(email='this.is@user',
120
-                                groups=groups, save_now=True)
119
+        user = uapi.create_minimal_user(email='this.is@user',
120
+                                        groups=groups, save_now=True)
121
         workspace = WorkspaceApi(
121
         workspace = WorkspaceApi(
122
             current_user=user,
122
             current_user=user,
123
             session=self.session,
123
             session=self.session,
210
                   group_api.get_one(Group.TIM_MANAGER),
210
                   group_api.get_one(Group.TIM_MANAGER),
211
                   group_api.get_one(Group.TIM_ADMIN)]
211
                   group_api.get_one(Group.TIM_ADMIN)]
212
 
212
 
213
-        user = uapi.create_user(email='this.is@user',
214
-                                groups=groups, save_now=True)
213
+        user = uapi.create_minimal_user(
214
+            email='this.is@user',
215
+            groups=groups,
216
+            save_now=True
217
+        )
215
         workspace_api = WorkspaceApi(
218
         workspace_api = WorkspaceApi(
216
             current_user=user,
219
             current_user=user,
217
             session=self.session,
220
             session=self.session,
218
-            config=self.app_config,
221
+            config=self.app_config
219
         )
222
         )
223
+
220
         workspace = workspace_api.create_workspace(
224
         workspace = workspace_api.create_workspace(
221
             'test workspace',
225
             'test workspace',
222
             save_now=True
226
             save_now=True
315
                   group_api.get_one(Group.TIM_MANAGER),
319
                   group_api.get_one(Group.TIM_MANAGER),
316
                   group_api.get_one(Group.TIM_ADMIN)]
320
                   group_api.get_one(Group.TIM_ADMIN)]
317
 
321
 
318
-        user = uapi.create_user(
322
+        user = uapi.create_minimal_user(
319
             email='this.is@user',
323
             email='this.is@user',
320
             groups=groups,
324
             groups=groups,
321
             save_now=True
325
             save_now=True
381
                   group_api.get_one(Group.TIM_MANAGER),
385
                   group_api.get_one(Group.TIM_MANAGER),
382
                   group_api.get_one(Group.TIM_ADMIN)]
386
                   group_api.get_one(Group.TIM_ADMIN)]
383
 
387
 
384
-        user = uapi.create_user(email='this.is@user',
385
-                                groups=groups, save_now=True)
388
+        user = uapi.create_minimal_user(email='this.is@user',
389
+                                        groups=groups, save_now=True)
386
         workspace = WorkspaceApi(
390
         workspace = WorkspaceApi(
387
             current_user=user,
391
             current_user=user,
388
             session=self.session,
392
             session=self.session,
456
                   group_api.get_one(Group.TIM_MANAGER),
460
                   group_api.get_one(Group.TIM_MANAGER),
457
                   group_api.get_one(Group.TIM_ADMIN)]
461
                   group_api.get_one(Group.TIM_ADMIN)]
458
 
462
 
459
-        user = uapi.create_user(email='this.is@user',
460
-                                groups=groups, save_now=True)
463
+        user = uapi.create_minimal_user(email='this.is@user',
464
+                                        groups=groups, save_now=True)
461
 
465
 
462
         workspace = WorkspaceApi(
466
         workspace = WorkspaceApi(
463
             current_user=user,
467
             current_user=user,
496
                   group_api.get_one(Group.TIM_MANAGER),
500
                   group_api.get_one(Group.TIM_MANAGER),
497
                   group_api.get_one(Group.TIM_ADMIN)]
501
                   group_api.get_one(Group.TIM_ADMIN)]
498
 
502
 
499
-        user = uapi.create_user(email='this.is@user',
500
-                                groups=groups, save_now=True)
503
+        user = uapi.create_minimal_user(email='this.is@user',
504
+                                        groups=groups, save_now=True)
501
 
505
 
502
         workspace = WorkspaceApi(
506
         workspace = WorkspaceApi(
503
             current_user=user,
507
             current_user=user,
540
                   group_api.get_one(Group.TIM_MANAGER),
544
                   group_api.get_one(Group.TIM_MANAGER),
541
                   group_api.get_one(Group.TIM_ADMIN)]
545
                   group_api.get_one(Group.TIM_ADMIN)]
542
 
546
 
543
-        user = uapi.create_user(email='this.is@user',
544
-                                groups=groups, save_now=True)
547
+        user = uapi.create_minimal_user(email='this.is@user',
548
+                                        groups=groups, save_now=True)
545
 
549
 
546
         workspace = WorkspaceApi(
550
         workspace = WorkspaceApi(
547
             current_user=user,
551
             current_user=user,
584
                   group_api.get_one(Group.TIM_MANAGER),
588
                   group_api.get_one(Group.TIM_MANAGER),
585
                   group_api.get_one(Group.TIM_ADMIN)]
589
                   group_api.get_one(Group.TIM_ADMIN)]
586
 
590
 
587
-        user = uapi.create_user(
591
+        user = uapi.create_minimal_user(
588
             email='user1@user',
592
             email='user1@user',
589
             groups=groups,
593
             groups=groups,
590
             save_now=True
594
             save_now=True
591
         )
595
         )
592
-        user2 = uapi.create_user(
596
+        user2 = uapi.create_minimal_user(
593
             email='user2@user',
597
             email='user2@user',
594
             groups=groups,
598
             groups=groups,
595
             save_now=True
599
             save_now=True
703
                   group_api.get_one(Group.TIM_MANAGER),
707
                   group_api.get_one(Group.TIM_MANAGER),
704
                   group_api.get_one(Group.TIM_ADMIN)]
708
                   group_api.get_one(Group.TIM_ADMIN)]
705
 
709
 
706
-        user = uapi.create_user(
710
+        user = uapi.create_minimal_user(
707
             email='user1@user',
711
             email='user1@user',
708
             groups=groups,
712
             groups=groups,
709
             save_now=True
713
             save_now=True
710
         )
714
         )
711
-        user2 = uapi.create_user(
715
+        user2 = uapi.create_minimal_user(
712
             email='user2@user',
716
             email='user2@user',
713
             groups=groups,
717
             groups=groups,
714
             save_now=True
718
             save_now=True
820
                   group_api.get_one(Group.TIM_MANAGER),
824
                   group_api.get_one(Group.TIM_MANAGER),
821
                   group_api.get_one(Group.TIM_ADMIN)]
825
                   group_api.get_one(Group.TIM_ADMIN)]
822
 
826
 
823
-        user = uapi.create_user(
827
+        user = uapi.create_minimal_user(
824
             email='user1@user',
828
             email='user1@user',
825
             groups=groups,
829
             groups=groups,
826
             save_now=True,
830
             save_now=True,
827
         )
831
         )
828
-        user2 = uapi.create_user(
832
+        user2 = uapi.create_minimal_user(
829
             email='user2@user',
833
             email='user2@user',
830
             groups=groups,
834
             groups=groups,
831
             save_now=True
835
             save_now=True
925
                   group_api.get_one(Group.TIM_MANAGER),
929
                   group_api.get_one(Group.TIM_MANAGER),
926
                   group_api.get_one(Group.TIM_ADMIN)]
930
                   group_api.get_one(Group.TIM_ADMIN)]
927
 
931
 
928
-        user_a = uapi.create_user(email='this.is@user',
929
-                                  groups=groups, save_now=True)
930
-        user_b = uapi.create_user(email='this.is@another.user',
931
-                                  groups=groups, save_now=True)
932
+        user_a = uapi.create_minimal_user(email='this.is@user',
933
+                                          groups=groups, save_now=True)
934
+        user_b = uapi.create_minimal_user(email='this.is@another.user',
935
+                                          groups=groups, save_now=True)
932
 
936
 
933
         wapi = WorkspaceApi(
937
         wapi = WorkspaceApi(
934
             current_user=user_a,
938
             current_user=user_a,
1032
                   group_api.get_one(Group.TIM_MANAGER),
1036
                   group_api.get_one(Group.TIM_MANAGER),
1033
                   group_api.get_one(Group.TIM_ADMIN)]
1037
                   group_api.get_one(Group.TIM_ADMIN)]
1034
 
1038
 
1035
-        user_a = uapi.create_user(
1039
+        user_a = uapi.create_minimal_user(
1036
             email='this.is@user',
1040
             email='this.is@user',
1037
             groups=groups,
1041
             groups=groups,
1038
             save_now=True
1042
             save_now=True
1039
         )
1043
         )
1040
-        user_b = uapi.create_user(
1044
+        user_b = uapi.create_minimal_user(
1041
             email='this.is@another.user',
1045
             email='this.is@another.user',
1042
             groups=groups,
1046
             groups=groups,
1043
             save_now=True
1047
             save_now=True
1105
                   group_api.get_one(Group.TIM_MANAGER),
1109
                   group_api.get_one(Group.TIM_MANAGER),
1106
                   group_api.get_one(Group.TIM_ADMIN)]
1110
                   group_api.get_one(Group.TIM_ADMIN)]
1107
 
1111
 
1108
-        user_a = uapi.create_user(
1112
+        user_a = uapi.create_minimal_user(
1109
             email='this.is@user',
1113
             email='this.is@user',
1110
             groups=groups,
1114
             groups=groups,
1111
             save_now=True
1115
             save_now=True
1112
         )
1116
         )
1113
-        user_b = uapi.create_user(
1117
+        user_b = uapi.create_minimal_user(
1114
             email='this.is@another.user',
1118
             email='this.is@another.user',
1115
             groups=groups,
1119
             groups=groups,
1116
             save_now=True
1120
             save_now=True
1204
                   group_api.get_one(Group.TIM_MANAGER),
1208
                   group_api.get_one(Group.TIM_MANAGER),
1205
                   group_api.get_one(Group.TIM_ADMIN)]
1209
                   group_api.get_one(Group.TIM_ADMIN)]
1206
 
1210
 
1207
-        user1 = uapi.create_user(
1211
+        user1 = uapi.create_minimal_user(
1208
             email='this.is@user',
1212
             email='this.is@user',
1209
             groups=groups,
1213
             groups=groups,
1210
             save_now=True
1214
             save_now=True
1222
         
1226
         
1223
         wid = workspace.workspace_id
1227
         wid = workspace.workspace_id
1224
 
1228
 
1225
-        user2 = uapi.create_user()
1226
-        user2.email = 'this.is@another.user'
1229
+        user2 = uapi.create_minimal_user('this.is@another.user')
1227
         uapi.save(user2)
1230
         uapi.save(user2)
1228
 
1231
 
1229
         RoleApi(
1232
         RoleApi(
1333
                   group_api.get_one(Group.TIM_MANAGER),
1336
                   group_api.get_one(Group.TIM_MANAGER),
1334
                   group_api.get_one(Group.TIM_ADMIN)]
1337
                   group_api.get_one(Group.TIM_ADMIN)]
1335
 
1338
 
1336
-        user1 = uapi.create_user(
1339
+        user1 = uapi.create_minimal_user(
1337
             email='this.is@user',
1340
             email='this.is@user',
1338
             groups=groups,
1341
             groups=groups,
1339
             save_now=True,
1342
             save_now=True,
1348
             save_now=True
1351
             save_now=True
1349
         )
1352
         )
1350
 
1353
 
1351
-        user2 = uapi.create_user()
1352
-        user2.email = 'this.is@another.user'
1354
+        user2 = uapi.create_minimal_user('this.is@another.user')
1353
         uapi.save(user2)
1355
         uapi.save(user2)
1354
 
1356
 
1355
         RoleApi(
1357
         RoleApi(
1414
                   group_api.get_one(Group.TIM_MANAGER),
1416
                   group_api.get_one(Group.TIM_MANAGER),
1415
                   group_api.get_one(Group.TIM_ADMIN)]
1417
                   group_api.get_one(Group.TIM_ADMIN)]
1416
 
1418
 
1417
-        user1 = uapi.create_user(
1419
+        user1 = uapi.create_minimal_user(
1418
             email='this.is@user',
1420
             email='this.is@user',
1419
             groups=groups,
1421
             groups=groups,
1420
             save_now=True
1422
             save_now=True
1431
         )
1433
         )
1432
         wid = workspace.workspace_id
1434
         wid = workspace.workspace_id
1433
 
1435
 
1434
-        user2 = uapi.create_user()
1435
-        user2.email = 'this.is@another.user'
1436
+        user2 = uapi.create_minimal_user('this.is@another.user')
1436
         uapi.save(user2)
1437
         uapi.save(user2)
1437
 
1438
 
1438
         RoleApi(
1439
         RoleApi(
1539
                   group_api.get_one(Group.TIM_MANAGER),
1540
                   group_api.get_one(Group.TIM_MANAGER),
1540
                   group_api.get_one(Group.TIM_ADMIN)]
1541
                   group_api.get_one(Group.TIM_ADMIN)]
1541
 
1542
 
1542
-        user1 = uapi.create_user(
1543
+        user1 = uapi.create_minimal_user(
1543
             email='this.is@user',
1544
             email='this.is@user',
1544
             groups=groups,
1545
             groups=groups,
1545
             save_now=True,
1546
             save_now=True,
1555
             save_now=True
1556
             save_now=True
1556
         )
1557
         )
1557
 
1558
 
1558
-        user2 = uapi.create_user()
1559
-        user2.email = 'this.is@another.user'
1559
+        user2 = uapi.create_minimal_user('this.is@another.user')
1560
         uapi.save(user2)
1560
         uapi.save(user2)
1561
 
1561
 
1562
         RoleApi(
1562
         RoleApi(
1627
                   group_api.get_one(Group.TIM_MANAGER),
1627
                   group_api.get_one(Group.TIM_MANAGER),
1628
                   group_api.get_one(Group.TIM_ADMIN)]
1628
                   group_api.get_one(Group.TIM_ADMIN)]
1629
 
1629
 
1630
-        user1 = uapi.create_user(
1630
+        user1 = uapi.create_minimal_user(
1631
             email='this.is@user',
1631
             email='this.is@user',
1632
             groups=groups,
1632
             groups=groups,
1633
             save_now=True
1633
             save_now=True
1645
         )
1645
         )
1646
         wid = workspace.workspace_id
1646
         wid = workspace.workspace_id
1647
 
1647
 
1648
-        user2 = uapi.create_user()
1649
-        user2.email = 'this.is@another.user'
1648
+        user2 = uapi.create_minimal_user('this.is@another.user')
1650
         uapi.save(user2)
1649
         uapi.save(user2)
1651
 
1650
 
1652
         RoleApi(
1651
         RoleApi(
1784
                   group_api.get_one(Group.TIM_MANAGER),
1783
                   group_api.get_one(Group.TIM_MANAGER),
1785
                   group_api.get_one(Group.TIM_ADMIN)]
1784
                   group_api.get_one(Group.TIM_ADMIN)]
1786
 
1785
 
1787
-        user1 = uapi.create_user(
1786
+        user1 = uapi.create_minimal_user(
1788
             email='this.is@user',
1787
             email='this.is@user',
1789
             groups=groups,
1788
             groups=groups,
1790
             save_now=True
1789
             save_now=True
1802
         )
1801
         )
1803
         wid = workspace.workspace_id
1802
         wid = workspace.workspace_id
1804
 
1803
 
1805
-        user2 = uapi.create_user()
1806
-        user2.email = 'this.is@another.user'
1804
+        user2 = uapi.create_minimal_user('this.is@another.user')
1807
         uapi.save(user2)
1805
         uapi.save(user2)
1808
 
1806
 
1809
         RoleApi(
1807
         RoleApi(
1942
                   group_api.get_one(Group.TIM_MANAGER),
1940
                   group_api.get_one(Group.TIM_MANAGER),
1943
                   group_api.get_one(Group.TIM_ADMIN)]
1941
                   group_api.get_one(Group.TIM_ADMIN)]
1944
 
1942
 
1945
-        user = uapi.create_user(email='this.is@user',
1946
-                                groups=groups, save_now=True)
1943
+        user = uapi.create_minimal_user(email='this.is@user',
1944
+                                        groups=groups, save_now=True)
1947
 
1945
 
1948
         workspace = WorkspaceApi(
1946
         workspace = WorkspaceApi(
1949
             current_user=user,
1947
             current_user=user,
1998
                   group_api.get_one(Group.TIM_MANAGER),
1996
                   group_api.get_one(Group.TIM_MANAGER),
1999
                   group_api.get_one(Group.TIM_ADMIN)]
1997
                   group_api.get_one(Group.TIM_ADMIN)]
2000
 
1998
 
2001
-        user = uapi.create_user(email='this.is@user',
2002
-                                groups=groups, save_now=True)
1999
+        user = uapi.create_minimal_user(email='this.is@user',
2000
+                                        groups=groups, save_now=True)
2003
 
2001
 
2004
         workspace = WorkspaceApi(
2002
         workspace = WorkspaceApi(
2005
             current_user=user,
2003
             current_user=user,
2054
                   group_api.get_one(Group.TIM_MANAGER),
2052
                   group_api.get_one(Group.TIM_MANAGER),
2055
                   group_api.get_one(Group.TIM_ADMIN)]
2053
                   group_api.get_one(Group.TIM_ADMIN)]
2056
 
2054
 
2057
-        user = uapi.create_user(email='this.is@user',
2058
-                                groups=groups, save_now=True)
2055
+        user = uapi.create_minimal_user(email='this.is@user',
2056
+                                        groups=groups, save_now=True)
2059
 
2057
 
2060
         workspace = WorkspaceApi(
2058
         workspace = WorkspaceApi(
2061
             current_user=user,
2059
             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(UserDoesNotExist):
94
         with pytest.raises(UserDoesNotExist):
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
 

+ 50 - 1
tracim/tests/library/test_webdav.py View File

3
 
3
 
4
 import pytest
4
 import pytest
5
 from sqlalchemy.exc import InvalidRequestError
5
 from sqlalchemy.exc import InvalidRequestError
6
-
6
+from wsgidav.wsgidav_app import DEFAULT_CONFIG
7
+from tracim import WebdavAppFactory
7
 from tracim.lib.core.user import UserApi
8
 from tracim.lib.core.user import UserApi
9
+from tracim.lib.webdav import TracimDomainController
8
 from tracim.tests import eq_
10
 from tracim.tests import eq_
9
 from tracim.lib.core.notifications import DummyNotifier
11
 from tracim.lib.core.notifications import DummyNotifier
10
 from tracim.lib.webdav.dav_provider import Provider
12
 from tracim.lib.webdav.dav_provider import Provider
15
 from tracim.fixtures.content import Content as ContentFixtures
17
 from tracim.fixtures.content import Content as ContentFixtures
16
 from tracim.fixtures.users_and_groups import Base as BaseFixture
18
 from tracim.fixtures.users_and_groups import Base as BaseFixture
17
 from wsgidav import util
19
 from wsgidav import util
20
+from unittest.mock import MagicMock
21
+
22
+
23
+class TestWebdavFactory(StandardTest):
24
+
25
+    def test_unit__initConfig__ok__nominal_case(self):
26
+        """
27
+        Check if config is correctly modify for wsgidav using mocked
28
+        wsgidav and tracim conf (as dict)
29
+        :return:
30
+        """
31
+        tracim_settings = {
32
+            'sqlalchemy.url': 'sqlite:///:memory:',
33
+            'user.auth_token.validity': '604800',
34
+            'depot_storage_dir': '/tmp/test/depot',
35
+            'depot_storage_name': 'test',
36
+            'preview_cache_dir': '/tmp/test/preview_cache',
37
+            'wsgidav.config_path': 'development.ini'
38
+
39
+        }
40
+        wsgidav_setting = DEFAULT_CONFIG.copy()
41
+        wsgidav_setting.update(
42
+            {
43
+               'root_path':  '',
44
+               'acceptbasic': True,
45
+               'acceptdigest': False,
46
+               'defaultdigest': False,
47
+            }
48
+        )
49
+        mock = MagicMock()
50
+        mock._initConfig = WebdavAppFactory._initConfig
51
+        mock._readConfigFile.return_value = wsgidav_setting
52
+        mock._get_tracim_settings.return_value = tracim_settings
53
+        config = mock._initConfig(mock)
54
+        assert config
55
+        assert config['acceptbasic'] is True
56
+        assert config['acceptdigest'] is False
57
+        assert config['defaultdigest'] is False
58
+        # TODO - G.M - 25-05-2018 - Better check for middleware stack config
59
+        assert 'middleware_stack' in config
60
+        assert len(config['middleware_stack']) == 7
61
+        assert 'root_path' in config
62
+        assert 'provider_mapping' in config
63
+        assert config['root_path'] in config['provider_mapping']
64
+        assert isinstance(config['provider_mapping'][config['root_path']], Provider)  # nopep8
65
+        assert 'domaincontroller' in config
66
+        assert isinstance(config['domaincontroller'], TracimDomainController)
18
 
67
 
19
 
68
 
20
 class TestWebDav(StandardTest):
69
 class TestWebDav(StandardTest):

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

53
             current_user=admin,
53
             current_user=admin,
54
             config=self.config
54
             config=self.config
55
         )
55
         )
56
-        u = uapi.create_user(email='u.u@u.u', save_now=True)
56
+        u = uapi.create_minimal_user(email='u.u@u.u', save_now=True)
57
         eq_([], wapi.get_notifiable_roles(workspace=w))
57
         eq_([], wapi.get_notifiable_roles(workspace=w))
58
         rapi = RoleApi(
58
         rapi = RoleApi(
59
             session=self.session,
59
             session=self.session,
92
             current_user=None,
92
             current_user=None,
93
             config=self.app_config,
93
             config=self.app_config,
94
         )
94
         )
95
-        u = uapi.create_user('u.s@e.r', [gapi.get_one(Group.TIM_USER)], True)
95
+        u = uapi.create_minimal_user('u.s@e.r', [gapi.get_one(Group.TIM_USER)], True)
96
         wapi = WorkspaceApi(
96
         wapi = WorkspaceApi(
97
             session=self.session,
97
             session=self.session,
98
             current_user=u,
98
             current_user=u,

+ 17 - 0
wsgi/__init__.py View File

1
+# coding=utf-8
2
+import plaster
3
+import pyramid.paster
4
+
5
+from tracim.lib.webdav import WebdavAppFactory
6
+
7
+
8
+def web_app(config_uri):
9
+    pyramid.paster.setup_logging(config_uri)
10
+    return pyramid.paster.get_app(config_uri)
11
+
12
+
13
+def webdav_app(config_uri):
14
+    config_uri = '{}#webdav'.format(config_uri)
15
+    plaster.setup_logging(config_uri)
16
+    loader = plaster.get_loader(config_uri, protocols=['wsgi'])
17
+    return loader.get_wsgi_app()

+ 2 - 4
wsgi/web.py View File

1
 # coding=utf-8
1
 # coding=utf-8
2
 # Runner for uwsgi
2
 # Runner for uwsgi
3
 import os
3
 import os
4
-import pyramid.paster
4
+from wsgi import web_app
5
 
5
 
6
 config_uri = os.environ['TRACIM_CONF_PATH']
6
 config_uri = os.environ['TRACIM_CONF_PATH']
7
-
8
-pyramid.paster.setup_logging(config_uri)
9
-application = pyramid.paster.get_app(config_uri)
7
+application = web_app(config_uri)

+ 2 - 7
wsgi/webdav.py View File

1
 # coding=utf-8
1
 # coding=utf-8
2
 # Runner for uwsgi
2
 # Runner for uwsgi
3
-from tracim.lib.webdav import WebdavAppFactory
4
 import os
3
 import os
4
+from wsgi import webdav_app
5
 
5
 
6
 config_uri = os.environ['TRACIM_CONF_PATH']
6
 config_uri = os.environ['TRACIM_CONF_PATH']
7
-webdav_config_uri = os.environ['TRACIM_WEBDAV_CONF_PATH']
8
-app_factory = WebdavAppFactory(
9
-    tracim_config_file_path=config_uri,
10
-    webdav_config_file_path=webdav_config_uri,
11
-)
12
-application = app_factory.get_wsgi_app()
7
+application = webdav_app(config_uri)