Sfoglia il codice sorgente

merge with upstream

Guénaël Muller 6 anni fa
parent
commit
aa0accc736
38 ha cambiato i file con 1873 aggiunte e 274 eliminazioni
  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 Vedi File

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

+ 11 - 0
README.md Vedi File

@@ -19,6 +19,7 @@ on Debian Stretch (9) with sudo:
19 19
     sudo apt update
20 20
     sudo apt install git
21 21
     sudo apt install python3 python3-venv python3-dev python3-pip
22
+    sudo apt install redis-server
22 23
 
23 24
 ### Get the source ###
24 25
 
@@ -112,10 +113,20 @@ run wsgidav server:
112 113
 
113 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 124
 Run your project's tests:
116 125
 
117 126
     pytest
118 127
 
128
+### Lints and others checks ###
129
+
119 130
 Run mypy checks:
120 131
 
121 132
     mypy --ignore-missing-imports --disallow-untyped-defs tracim

+ 9 - 1
development.ini.sample Vedi File

@@ -2,7 +2,9 @@
2 2
 # app configuration
3 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 8
 use = egg:tracim_backend
7 9
 
8 10
 pyramid.reload_templates = true
@@ -13,6 +15,12 @@ pyramid.default_locale_name = en
13 15
 pyramid.includes =
14 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 24
 sqlalchemy.url = sqlite:///%(here)s/tracim.sqlite
17 25
 
18 26
 retry.attempts = 3

+ 9 - 2
setup.py Vedi File

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

+ 57 - 0
tests_configs.ini Vedi File

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

+ 23 - 6
tracim/__init__.py Vedi File

@@ -5,6 +5,8 @@ import time
5 5
 from pyramid.config import Configurator
6 6
 from pyramid.authentication import BasicAuthAuthenticationPolicy
7 7
 from hapic.ext.pyramid import PyramidContext
8
+from pyramid.exceptions import NotFound
9
+from sqlalchemy.exc import OperationalError
8 10
 
9 11
 from tracim.extensions import hapic
10 12
 from tracim.config import CFG
@@ -13,6 +15,7 @@ from tracim.lib.utils.authentification import basic_auth_check_credentials
13 15
 from tracim.lib.utils.authentification import BASIC_AUTH_WEBUI_REALM
14 16
 from tracim.lib.utils.authorization import AcceptAllAuthorizationPolicy
15 17
 from tracim.lib.utils.authorization import TRACIM_DEFAULT_PERM
18
+from tracim.lib.webdav import WebdavAppFactory
16 19
 from tracim.views import BASE_API_V2
17 20
 from tracim.views.core_api.session_controller import SessionController
18 21
 from tracim.views.core_api.system_controller import SystemController
@@ -22,9 +25,11 @@ from tracim.views.errors import ErrorSchema
22 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 29
     """ This function returns a Pyramid WSGI application.
27 30
     """
31
+    settings = global_config
32
+    settings.update(local_settings)
28 33
     # set CFG object
29 34
     app_config = CFG(settings)
30 35
     app_config.configure_filedepot()
@@ -52,12 +57,15 @@ def main(global_config, **settings):
52 57
     # Add SqlAlchemy DB
53 58
     configurator.include('.models')
54 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 69
     # Add controllers
62 70
     session_controller = SessionController()
63 71
     system_controller = SystemController()
@@ -73,3 +81,12 @@ def main(global_config, **settings):
73 81
         'API of Tracim v2',
74 82
     )
75 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 Vedi File

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

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

@@ -6,6 +6,7 @@ from waitress import serve
6 6
 
7 7
 from tracim.command import AppContextCommand
8 8
 from tracim.lib.webdav import WebdavAppFactory
9
+from wsgi import webdav_app
9 10
 
10 11
 
11 12
 class WebdavRunnerCommand(AppContextCommand):
@@ -22,8 +23,5 @@ class WebdavRunnerCommand(AppContextCommand):
22 23
         super(WebdavRunnerCommand, self).take_action(parsed_args)
23 24
         tracim_config = parsed_args.config_file
24 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 27
         serve(app, port=app.config['port'], host=app.config['host'])

+ 111 - 108
tracim/config.py Vedi File

@@ -4,6 +4,8 @@ from paste.deploy.converters import asbool
4 4
 from tracim.lib.utils.logger import logger
5 5
 from depot.manager import DepotManager
6 6
 
7
+from tracim.models.data import ActionDescription, ContentType
8
+
7 9
 
8 10
 class CFG(object):
9 11
     """Object used for easy access to config file parameters."""
@@ -128,90 +130,91 @@ class CFG(object):
128 130
             '604800',
129 131
         ))
130 132
 
133
+        self.DEBUG = asbool(settings.get('debug', False))
131 134
         # TODO - G.M - 27-03-2018 - [Email] Restore email config
132 135
         ###
133 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 198
         self.EMAIL_NOTIFICATION_ACTIVATED = asbool(settings.get(
196 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 218
         # self.EMAIL_REPLY_ACTIVATED = asbool(settings.get(
216 219
         #     'email.reply.activated',
217 220
         #     False,
@@ -267,36 +270,36 @@ class CFG(object):
267 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 305
         # WSGIDAV (Webdav server)

+ 6 - 1
tracim/exceptions.py Vedi File

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

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

@@ -73,12 +73,14 @@ class Content(Fixture):
73 73
             workspace=w1,
74 74
             label='w1f1',
75 75
             do_save=True,
76
+            do_notify=False,
76 77
         )
77 78
         w1f2 = content_api.create(
78 79
             content_type=ContentType.Folder,
79 80
             workspace=w1,
80 81
             label='w1f2',
81 82
             do_save=True,
83
+            do_notify=False,
82 84
         )
83 85
 
84 86
         w2f1 = content_api.create(
@@ -86,12 +88,14 @@ class Content(Fixture):
86 88
             workspace=w2,
87 89
             label='w2f1',
88 90
             do_save=True,
91
+            do_notify=False,
89 92
         )
90 93
         w2f2 = content_api.create(
91 94
             content_type=ContentType.Folder,
92 95
             workspace=w2,
93 96
             label='w2f2',
94 97
             do_save=True,
98
+            do_notify=False,
95 99
         )
96 100
 
97 101
         w3f1 = content_api.create(
@@ -99,6 +103,7 @@ class Content(Fixture):
99 103
             workspace=w3,
100 104
             label='w3f3',
101 105
             do_save=True,
106
+            do_notify=False,
102 107
         )
103 108
 
104 109
         # Pages, threads, ..
@@ -108,6 +113,7 @@ class Content(Fixture):
108 113
             parent=w1f1,
109 114
             label='w1f1p1',
110 115
             do_save=True,
116
+            do_notify=False,
111 117
         )
112 118
         w1f1t1 = content_api.create(
113 119
             content_type=ContentType.Thread,
@@ -115,6 +121,7 @@ class Content(Fixture):
115 121
             parent=w1f1,
116 122
             label='w1f1t1',
117 123
             do_save=False,
124
+            do_notify=False,
118 125
         )
119 126
         w1f1t1.description = 'w1f1t1 description'
120 127
         self._session.add(w1f1t1)
@@ -124,6 +131,7 @@ class Content(Fixture):
124 131
             parent=w1f1,
125 132
             label='w1f1d1',
126 133
             do_save=False,
134
+            do_notify=False,
127 135
         )
128 136
         w1f1d1_txt.file_extension = '.txt'
129 137
         w1f1d1_txt.depot_file = FileIntent(
@@ -138,6 +146,7 @@ class Content(Fixture):
138 146
             parent=w1f1,
139 147
             label='w1f1d2',
140 148
             do_save=False,
149
+            do_notify=False,
141 150
         )
142 151
         w1f1d2_html.file_extension = '.html'
143 152
         w1f1d2_html.depot_file = FileIntent(
@@ -152,6 +161,7 @@ class Content(Fixture):
152 161
             label='w1f1f1',
153 162
             parent=w1f1,
154 163
             do_save=True,
164
+            do_notify=False,
155 165
         )
156 166
 
157 167
         w2f1p1 = content_api.create(
@@ -160,5 +170,6 @@ class Content(Fixture):
160 170
             parent=w2f1,
161 171
             label='w2f1p1',
162 172
             do_save=True,
173
+            do_notify=False,
163 174
         )
164 175
         self._session.flush()

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

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

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

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

+ 48 - 4
tracim/lib/core/user.py Vedi File

@@ -1,9 +1,12 @@
1 1
 # -*- coding: utf-8 -*-
2 2
 import threading
3
+from smtplib import SMTPException
3 4
 
4 5
 import transaction
5 6
 import typing as typing
6 7
 
8
+from tracim.exceptions import NotificationNotSend
9
+from tracim.lib.mail_notifier.notifier import get_email_manager
7 10
 from sqlalchemy.orm import Session
8 11
 
9 12
 from tracim import CFG
@@ -114,8 +117,9 @@ class UserApi(object):
114 117
             user: User,
115 118
             name: str=None,
116 119
             email: str=None,
117
-            do_save=True,
120
+            password: str=None,
118 121
             timezone: str='',
122
+            do_save=True,
119 123
     ) -> None:
120 124
         if name is not None:
121 125
             user.display_name = name
@@ -123,16 +127,56 @@ class UserApi(object):
123 127
         if email is not None:
124 128
             user.email = email
125 129
 
130
+        if password is not None:
131
+            user.password = password
132
+
126 133
         user.timezone = timezone
127 134
 
128 135
         if do_save:
129 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 177
         user = User()
133 178
 
134
-        if email:
135
-            user.email = email
179
+        user.email = email
136 180
 
137 181
         for group in groups:
138 182
             user.groups.append(group)

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

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

+ 571 - 0
tracim/lib/mail_notifier/notifier.py Vedi File

@@ -0,0 +1,571 @@
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 Vedi File

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

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

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

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

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

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

@@ -27,43 +27,37 @@ from tracim.models import get_engine, get_session_factory
27 27
 class WebdavAppFactory(object):
28 28
 
29 29
     def __init__(self,
30
-                 webdav_config_file_path: str = None,
31 30
                  tracim_config_file_path: str = None,
32 31
                  ):
33 32
         self.config = self._initConfig(
34
-            webdav_config_file_path,
35 33
             tracim_config_file_path
36 34
         )
37 35
 
38 36
     def _initConfig(self,
39
-                    webdav_config_file_path: str = None,
40 37
                     tracim_config_file_path: str = None
41 38
                     ):
42 39
         """Setup configuration dictionary from default,
43 40
          command line and configuration file."""
44
-        if not webdav_config_file_path:
45
-            webdav_config_file_path = DEFAULT_WEBDAV_CONFIG_FILE
46 41
         if not tracim_config_file_path:
47 42
             tracim_config_file_path = DEFAULT_TRACIM_CONFIG_FILE
48 43
 
49 44
         # Set config defaults
50 45
         config = DEFAULT_CONFIG.copy()
51 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 54
         webdav_config_file = self._readConfigFile(
55
-            webdav_config_file_path,
55
+            default_config_file,
56 56
             temp_verbose
57 57
             )
58 58
         # Configuration file overrides defaults
59 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 61
         if not useLxml and config["verbose"] >= 1:
68 62
             print(
69 63
                 "WARNING: Could not import lxml: using xml instead (slower). "
@@ -93,10 +87,23 @@ class WebdavAppFactory(object):
93 87
         config['domaincontroller'] = TracimDomainController(
94 88
             presetdomain=None,
95 89
             presetserver=None,
96
-            app_config = app_config,
90
+            app_config=app_config,
97 91
         )
98 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 107
     # INFO - G.M - 13-04-2018 - Copy from
101 108
     # wsgidav.server.run_server._readConfigFile
102 109
     def _readConfigFile(self, config_file, verbose):

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

@@ -256,7 +256,10 @@ class TracimEnv(BaseMiddleware):
256 256
         super().__init__(application, config)
257 257
         self._application = application
258 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 263
         self.engine = get_engine(self.settings)
261 264
         self.session_factory = get_session_factory(self.engine)
262 265
         self.app_config = CFG(self.settings)

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

@@ -139,12 +139,12 @@ class UserRoleInWorkspace(DeclarativeBase):
139 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 149
     # STYLE = dict()
150 150
     # STYLE[0] = ''
@@ -169,8 +169,9 @@ class UserRoleInWorkspace(DeclarativeBase):
169 169
     # def style(self):
170 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 176
     @classmethod
176 177
     def get_all_role_values(cls) -> typing.List[int]:
@@ -340,6 +341,7 @@ class ContentStatus(object):
340 341
                  # type=''
341 342
     ):
342 343
         self.id = id
344
+        self.label = self.id
343 345
         # TODO - G.M - 10-04-2018 - [Cleanup] Drop this
344 346
         # self.icon = ContentStatus._ICONS[id]
345 347
         # self.css = ContentStatus._CSS[id]

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


+ 73 - 0
tracim/templates/mail/content_update_body_html.mak Vedi File

@@ -0,0 +1,73 @@
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 Vedi File

@@ -0,0 +1,31 @@
1
+## -*- coding: utf-8 -*-
2
+
3
+Dear ${user.display_name},
4
+
5
+This email is intended to be read as HTML content.
6
+Please configure your email client to get the best of Tracim notifications.
7
+
8
+We understand that Email was originally intended to carry raw text only.
9
+And you probably understand on your own that we are a decades after email
10
+was created ;)
11
+
12
+Hope you'll switch your mail client configuration and enjoy Tracim :)
13
+
14
+
15
+--------------------------------------------------------------------------------
16
+
17
+
18
+You receive this email because you are registered on /${config.WEBSITE_TITLE}/
19
+and you are /${role_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 Vedi File

@@ -0,0 +1,88 @@
1
+## -*- coding: utf-8 -*-
2
+<html>
3
+  <head>
4
+    <style>
5
+      a { color: #3465af;}
6
+      a.call-to-action {
7
+        background: #3465af;
8
+        padding: 3px 4px 5px 4px;
9
+        border: 1px solid #12438d;
10
+        font-weight: bold;
11
+        color: #FFF;
12
+        text-decoration: none;
13
+        margin-left: 5px;
14
+      }
15
+      a.call-to-action img { vertical-align: middle;}
16
+      th { vertical-align: top;}
17
+      
18
+      #content-intro-username { font-size: 1.5em; color: #666; font-weight: bold; }
19
+      #content-intro { margin: 0; border: 1em solid #DDD; border-width: 0 0 0 0em; padding: 1em 1em 1em 1em; }
20
+      #content-body { margin: 0em; border: 2em solid #DDD; border-width: 0 0 0 4em; padding: 0.5em 2em 1em 1em; }
21
+      #content-body-intro { font-size: 2em; color: #666; }
22
+      #content-body-only-title { font-size: 1.5em; }
23
+
24
+      #content-body ins { background-color: #AFA; }
25
+      #content-body del { background-color: #FAA; }
26
+
27
+
28
+      #call-to-action-button { background-color: #5CB85C; border: 1px solid #4CAE4C; color: #FFF; text-decoration: none; font-weight: bold; border-radius: 3px; font-size: 2em; padding: 4px 0.3em;}
29
+      #call-to-action-container { text-align: right; margin-top: 2em; }
30
+
31
+      #footer hr { border: 0px solid #CCC; border-top-width: 1px; width: 8em; max-width:25%; margin-left: 0;}
32
+      #footer { color: #999; margin: 4em auto auto 0.5em; }
33
+      #footer a { color: #999; }
34
+    </style>
35
+  </head>
36
+  <body style="font-family: Arial; font-size: 12px; max-width: 600px; margin: 0; padding: 0;">
37
+
38
+    <table style="width: 100%; cell-padding: 0; border-collapse: collapse; margin: 0">
39
+      <tr style="background-color: F5F5F5; border-bottom: 1px solid #CCC;" >
40
+        <td style="background-color: #666;">
41
+            <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 Vedi File

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

+ 25 - 10
tracim/tests/__init__.py Vedi File

@@ -1,5 +1,8 @@
1 1
 # -*- coding: utf-8 -*-
2 2
 import unittest
3
+
4
+import plaster
5
+import requests
3 6
 import transaction
4 7
 from depot.manager import DepotManager
5 8
 from pyramid import testing
@@ -17,7 +20,7 @@ from tracim.fixtures import FixturesLoader
17 20
 from tracim.fixtures.users_and_groups import Base as BaseFixture
18 21
 from tracim.config import CFG
19 22
 from tracim.extensions import hapic
20
-from tracim import main
23
+from tracim import web
21 24
 from webtest import TestApp
22 25
 
23 26
 
@@ -42,7 +45,7 @@ class FunctionalTest(unittest.TestCase):
42 45
 
43 46
         }
44 47
         hapic.reset_context()
45
-        app = main({}, **settings)
48
+        app = web({}, **settings)
46 49
         self.init_database(settings)
47 50
         self.testapp = TestApp(app)
48 51
 
@@ -92,16 +95,16 @@ class BaseTest(unittest.TestCase):
92 95
     Pyramid default test.
93 96
     """
94 97
 
98
+    config_uri = 'tests_configs.ini'
99
+    config_section = 'base_test'
100
+
95 101
     def setUp(self):
96 102
         logger.debug(self, 'Setup Test...')
97
-        self.config = testing.setUp(settings={
98
-            'sqlalchemy.url': 'sqlite:///:memory:',
99
-            'user.auth_token.validity': '604800',
100
-            'depot_storage_dir': '/tmp/test/depot',
101
-            'depot_storage_name': 'test',
102
-            'preview_cache_dir': '/tmp/test/preview_cache',
103
-
104
-        })
103
+        self.settings = plaster.get_settings(
104
+            self.config_uri,
105
+            self.config_section
106
+        )
107
+        self.config = testing.setUp(settings = self.settings)
105 108
         self.config.include('tracim.models')
106 109
         DepotManager._clear()
107 110
         DepotManager.configure(
@@ -226,3 +229,15 @@ class DefaultTest(StandardTest):
226 229
             owner=user
227 230
         )
228 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 Vedi File

@@ -0,0 +1,267 @@
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 Vedi File

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

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

@@ -116,8 +116,8 @@ class TestContentApi(DefaultTest):
116 116
                   group_api.get_one(Group.TIM_MANAGER),
117 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 121
         workspace = WorkspaceApi(
122 122
             current_user=user,
123 123
             session=self.session,
@@ -210,13 +210,17 @@ class TestContentApi(DefaultTest):
210 210
                   group_api.get_one(Group.TIM_MANAGER),
211 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 218
         workspace_api = WorkspaceApi(
216 219
             current_user=user,
217 220
             session=self.session,
218
-            config=self.app_config,
221
+            config=self.app_config
219 222
         )
223
+
220 224
         workspace = workspace_api.create_workspace(
221 225
             'test workspace',
222 226
             save_now=True
@@ -315,7 +319,7 @@ class TestContentApi(DefaultTest):
315 319
                   group_api.get_one(Group.TIM_MANAGER),
316 320
                   group_api.get_one(Group.TIM_ADMIN)]
317 321
 
318
-        user = uapi.create_user(
322
+        user = uapi.create_minimal_user(
319 323
             email='this.is@user',
320 324
             groups=groups,
321 325
             save_now=True
@@ -381,8 +385,8 @@ class TestContentApi(DefaultTest):
381 385
                   group_api.get_one(Group.TIM_MANAGER),
382 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 390
         workspace = WorkspaceApi(
387 391
             current_user=user,
388 392
             session=self.session,
@@ -456,8 +460,8 @@ class TestContentApi(DefaultTest):
456 460
                   group_api.get_one(Group.TIM_MANAGER),
457 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 466
         workspace = WorkspaceApi(
463 467
             current_user=user,
@@ -496,8 +500,8 @@ class TestContentApi(DefaultTest):
496 500
                   group_api.get_one(Group.TIM_MANAGER),
497 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 506
         workspace = WorkspaceApi(
503 507
             current_user=user,
@@ -540,8 +544,8 @@ class TestContentApi(DefaultTest):
540 544
                   group_api.get_one(Group.TIM_MANAGER),
541 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 550
         workspace = WorkspaceApi(
547 551
             current_user=user,
@@ -584,12 +588,12 @@ class TestContentApi(DefaultTest):
584 588
                   group_api.get_one(Group.TIM_MANAGER),
585 589
                   group_api.get_one(Group.TIM_ADMIN)]
586 590
 
587
-        user = uapi.create_user(
591
+        user = uapi.create_minimal_user(
588 592
             email='user1@user',
589 593
             groups=groups,
590 594
             save_now=True
591 595
         )
592
-        user2 = uapi.create_user(
596
+        user2 = uapi.create_minimal_user(
593 597
             email='user2@user',
594 598
             groups=groups,
595 599
             save_now=True
@@ -703,12 +707,12 @@ class TestContentApi(DefaultTest):
703 707
                   group_api.get_one(Group.TIM_MANAGER),
704 708
                   group_api.get_one(Group.TIM_ADMIN)]
705 709
 
706
-        user = uapi.create_user(
710
+        user = uapi.create_minimal_user(
707 711
             email='user1@user',
708 712
             groups=groups,
709 713
             save_now=True
710 714
         )
711
-        user2 = uapi.create_user(
715
+        user2 = uapi.create_minimal_user(
712 716
             email='user2@user',
713 717
             groups=groups,
714 718
             save_now=True
@@ -820,12 +824,12 @@ class TestContentApi(DefaultTest):
820 824
                   group_api.get_one(Group.TIM_MANAGER),
821 825
                   group_api.get_one(Group.TIM_ADMIN)]
822 826
 
823
-        user = uapi.create_user(
827
+        user = uapi.create_minimal_user(
824 828
             email='user1@user',
825 829
             groups=groups,
826 830
             save_now=True,
827 831
         )
828
-        user2 = uapi.create_user(
832
+        user2 = uapi.create_minimal_user(
829 833
             email='user2@user',
830 834
             groups=groups,
831 835
             save_now=True
@@ -925,10 +929,10 @@ class TestContentApi(DefaultTest):
925 929
                   group_api.get_one(Group.TIM_MANAGER),
926 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 937
         wapi = WorkspaceApi(
934 938
             current_user=user_a,
@@ -1032,12 +1036,12 @@ class TestContentApi(DefaultTest):
1032 1036
                   group_api.get_one(Group.TIM_MANAGER),
1033 1037
                   group_api.get_one(Group.TIM_ADMIN)]
1034 1038
 
1035
-        user_a = uapi.create_user(
1039
+        user_a = uapi.create_minimal_user(
1036 1040
             email='this.is@user',
1037 1041
             groups=groups,
1038 1042
             save_now=True
1039 1043
         )
1040
-        user_b = uapi.create_user(
1044
+        user_b = uapi.create_minimal_user(
1041 1045
             email='this.is@another.user',
1042 1046
             groups=groups,
1043 1047
             save_now=True
@@ -1105,12 +1109,12 @@ class TestContentApi(DefaultTest):
1105 1109
                   group_api.get_one(Group.TIM_MANAGER),
1106 1110
                   group_api.get_one(Group.TIM_ADMIN)]
1107 1111
 
1108
-        user_a = uapi.create_user(
1112
+        user_a = uapi.create_minimal_user(
1109 1113
             email='this.is@user',
1110 1114
             groups=groups,
1111 1115
             save_now=True
1112 1116
         )
1113
-        user_b = uapi.create_user(
1117
+        user_b = uapi.create_minimal_user(
1114 1118
             email='this.is@another.user',
1115 1119
             groups=groups,
1116 1120
             save_now=True
@@ -1204,7 +1208,7 @@ class TestContentApi(DefaultTest):
1204 1208
                   group_api.get_one(Group.TIM_MANAGER),
1205 1209
                   group_api.get_one(Group.TIM_ADMIN)]
1206 1210
 
1207
-        user1 = uapi.create_user(
1211
+        user1 = uapi.create_minimal_user(
1208 1212
             email='this.is@user',
1209 1213
             groups=groups,
1210 1214
             save_now=True
@@ -1222,8 +1226,7 @@ class TestContentApi(DefaultTest):
1222 1226
         
1223 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 1230
         uapi.save(user2)
1228 1231
 
1229 1232
         RoleApi(
@@ -1333,7 +1336,7 @@ class TestContentApi(DefaultTest):
1333 1336
                   group_api.get_one(Group.TIM_MANAGER),
1334 1337
                   group_api.get_one(Group.TIM_ADMIN)]
1335 1338
 
1336
-        user1 = uapi.create_user(
1339
+        user1 = uapi.create_minimal_user(
1337 1340
             email='this.is@user',
1338 1341
             groups=groups,
1339 1342
             save_now=True,
@@ -1348,8 +1351,7 @@ class TestContentApi(DefaultTest):
1348 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 1355
         uapi.save(user2)
1354 1356
 
1355 1357
         RoleApi(
@@ -1414,7 +1416,7 @@ class TestContentApi(DefaultTest):
1414 1416
                   group_api.get_one(Group.TIM_MANAGER),
1415 1417
                   group_api.get_one(Group.TIM_ADMIN)]
1416 1418
 
1417
-        user1 = uapi.create_user(
1419
+        user1 = uapi.create_minimal_user(
1418 1420
             email='this.is@user',
1419 1421
             groups=groups,
1420 1422
             save_now=True
@@ -1431,8 +1433,7 @@ class TestContentApi(DefaultTest):
1431 1433
         )
1432 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 1437
         uapi.save(user2)
1437 1438
 
1438 1439
         RoleApi(
@@ -1539,7 +1540,7 @@ class TestContentApi(DefaultTest):
1539 1540
                   group_api.get_one(Group.TIM_MANAGER),
1540 1541
                   group_api.get_one(Group.TIM_ADMIN)]
1541 1542
 
1542
-        user1 = uapi.create_user(
1543
+        user1 = uapi.create_minimal_user(
1543 1544
             email='this.is@user',
1544 1545
             groups=groups,
1545 1546
             save_now=True,
@@ -1555,8 +1556,7 @@ class TestContentApi(DefaultTest):
1555 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 1560
         uapi.save(user2)
1561 1561
 
1562 1562
         RoleApi(
@@ -1627,7 +1627,7 @@ class TestContentApi(DefaultTest):
1627 1627
                   group_api.get_one(Group.TIM_MANAGER),
1628 1628
                   group_api.get_one(Group.TIM_ADMIN)]
1629 1629
 
1630
-        user1 = uapi.create_user(
1630
+        user1 = uapi.create_minimal_user(
1631 1631
             email='this.is@user',
1632 1632
             groups=groups,
1633 1633
             save_now=True
@@ -1645,8 +1645,7 @@ class TestContentApi(DefaultTest):
1645 1645
         )
1646 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 1649
         uapi.save(user2)
1651 1650
 
1652 1651
         RoleApi(
@@ -1784,7 +1783,7 @@ class TestContentApi(DefaultTest):
1784 1783
                   group_api.get_one(Group.TIM_MANAGER),
1785 1784
                   group_api.get_one(Group.TIM_ADMIN)]
1786 1785
 
1787
-        user1 = uapi.create_user(
1786
+        user1 = uapi.create_minimal_user(
1788 1787
             email='this.is@user',
1789 1788
             groups=groups,
1790 1789
             save_now=True
@@ -1802,8 +1801,7 @@ class TestContentApi(DefaultTest):
1802 1801
         )
1803 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 1805
         uapi.save(user2)
1808 1806
 
1809 1807
         RoleApi(
@@ -1942,8 +1940,8 @@ class TestContentApi(DefaultTest):
1942 1940
                   group_api.get_one(Group.TIM_MANAGER),
1943 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 1946
         workspace = WorkspaceApi(
1949 1947
             current_user=user,
@@ -1998,8 +1996,8 @@ class TestContentApi(DefaultTest):
1998 1996
                   group_api.get_one(Group.TIM_MANAGER),
1999 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 2002
         workspace = WorkspaceApi(
2005 2003
             current_user=user,
@@ -2054,8 +2052,8 @@ class TestContentApi(DefaultTest):
2054 2052
                   group_api.get_one(Group.TIM_MANAGER),
2055 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 2058
         workspace = WorkspaceApi(
2061 2059
             current_user=user,

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

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

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

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

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

@@ -3,8 +3,10 @@ import io
3 3
 
4 4
 import pytest
5 5
 from sqlalchemy.exc import InvalidRequestError
6
-
6
+from wsgidav.wsgidav_app import DEFAULT_CONFIG
7
+from tracim import WebdavAppFactory
7 8
 from tracim.lib.core.user import UserApi
9
+from tracim.lib.webdav import TracimDomainController
8 10
 from tracim.tests import eq_
9 11
 from tracim.lib.core.notifications import DummyNotifier
10 12
 from tracim.lib.webdav.dav_provider import Provider
@@ -15,6 +17,53 @@ from tracim.tests import StandardTest
15 17
 from tracim.fixtures.content import Content as ContentFixtures
16 18
 from tracim.fixtures.users_and_groups import Base as BaseFixture
17 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 69
 class TestWebDav(StandardTest):

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

@@ -53,7 +53,7 @@ class TestThread(DefaultTest):
53 53
             current_user=admin,
54 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 57
         eq_([], wapi.get_notifiable_roles(workspace=w))
58 58
         rapi = RoleApi(
59 59
             session=self.session,
@@ -92,7 +92,7 @@ class TestThread(DefaultTest):
92 92
             current_user=None,
93 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 96
         wapi = WorkspaceApi(
97 97
             session=self.session,
98 98
             current_user=u,

+ 17 - 0
wsgi/__init__.py Vedi File

@@ -0,0 +1,17 @@
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 Vedi File

@@ -1,9 +1,7 @@
1 1
 # coding=utf-8
2 2
 # Runner for uwsgi
3 3
 import os
4
-import pyramid.paster
4
+from wsgi import web_app
5 5
 
6 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 Vedi File

@@ -1,12 +1,7 @@
1 1
 # coding=utf-8
2 2
 # Runner for uwsgi
3
-from tracim.lib.webdav import WebdavAppFactory
4 3
 import os
4
+from wsgi import webdav_app
5 5
 
6 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)