Browse Source

Fix post-merge failing tests

Guénaël Muller 6 years ago
parent
commit
0245979ab1
64 changed files with 3008 additions and 405 deletions
  1. 7 0
      .travis.yml
  2. 14 0
      README.md
  3. 259 0
      development.ini.old
  4. 275 0
      development.ini.oloool
  5. 9 1
      development.ini.sample
  6. 9 2
      setup.py
  7. 65 0
      tests_configs.ini
  8. 23 6
      tracim/__init__.py
  9. 2 1
      tracim/command/__init__.py
  10. 38 13
      tracim/command/user.py
  11. 2 4
      tracim/command/webdav.py
  12. 111 108
      tracim/config.py
  13. 11 7
      tracim/exceptions.py
  14. 6 1
      tracim/fixtures/__init__.py
  15. 11 0
      tracim/fixtures/content.py
  16. 5 4
      tracim/lib/core/content.py
  17. 17 2
      tracim/lib/core/group.py
  18. 23 11
      tracim/lib/core/notifications.py
  19. 48 4
      tracim/lib/core/user.py
  20. 3 1
      tracim/lib/core/userworkspace.py
  21. 61 0
      tracim/lib/mail_notifier/daemon.py
  22. 571 0
      tracim/lib/mail_notifier/notifier.py
  23. 114 0
      tracim/lib/mail_notifier/sender.py
  24. 33 0
      tracim/lib/mail_notifier/utils.py
  25. 1 1
      tracim/lib/utils/authentification.py
  26. 3 3
      tracim/lib/utils/authorization.py
  27. 6 3
      tracim/lib/utils/request.py
  28. 25 0
      tracim/lib/utils/utils.py
  29. 21 14
      tracim/lib/webdav/__init__.py
  30. 2 2
      tracim/lib/webdav/design.py
  31. 4 1
      tracim/lib/webdav/middlewares.py
  32. 21 9
      tracim/models/applications.py
  33. 17 17
      tracim/models/contents.py
  34. 17 15
      tracim/models/data.py
  35. 5 5
      tracim/models/workspace_menu_entries.py
  36. 0 0
      tracim/templates/mail/__init__.py
  37. 73 0
      tracim/templates/mail/content_update_body_html.mak
  38. 31 0
      tracim/templates/mail/content_update_body_text.mak
  39. 88 0
      tracim/templates/mail/created_account_body_html.mak
  40. 25 0
      tracim/templates/mail/created_account_body_text.mak
  41. 46 18
      tracim/tests/__init__.py
  42. 181 3
      tracim/tests/commands/test_commands.py
  43. 267 0
      tracim/tests/functional/test_mail_notification.py
  44. 0 2
      tracim/tests/functional/test_session.py
  45. 9 9
      tracim/tests/functional/test_system.py
  46. 7 7
      tracim/tests/functional/test_user.py
  47. 7 7
      tracim/tests/functional/test_workspaces.py
  48. 48 51
      tracim/tests/library/test_content_api.py
  49. 74 0
      tracim/tests/library/test_group_api.py
  50. 4 2
      tracim/tests/library/test_notification.py
  51. 55 24
      tracim/tests/library/test_user_api.py
  52. 50 1
      tracim/tests/library/test_webdav.py
  53. 2 2
      tracim/tests/library/test_workspace.py
  54. 16 0
      tracim/tests/models/test_controller.py
  55. 51 0
      tracim/tests/models/test_permission.py
  56. 92 11
      tracim/tests/models/test_user.py
  57. 4 4
      tracim/views/core_api/schemas.py
  58. 2 2
      tracim/views/core_api/session_controller.py
  59. 3 3
      tracim/views/core_api/system_controller.py
  60. 2 2
      tracim/views/core_api/user_controller.py
  61. 11 11
      tracim/views/core_api/workspace_controller.py
  62. 17 0
      wsgi/__init__.py
  63. 2 4
      wsgi/web.py
  64. 2 7
      wsgi/webdav.py

+ 7 - 0
.travis.yml View 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]"

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

+ 259 - 0
development.ini.old View File

@@ -0,0 +1,259 @@
1
+###
2
+# app configuration
3
+# https://docs.pylonsproject.org/projects/pyramid/en/latest/narr/environment.html
4
+###
5
+[app:main]
6
+use = egg:tracim_backend
7
+
8
+pyramid.reload_templates = true
9
+pyramid.debug_authorization = false
10
+pyramid.debug_notfound = false
11
+pyramid.debug_routematch = false
12
+pyramid.default_locale_name = en
13
+pyramid.includes =
14
+    pyramid_debugtoolbar
15
+
16
+sqlalchemy.url = sqlite:///%(here)s/tracim.sqlite
17
+
18
+retry.attempts = 3
19
+
20
+# By default, the toolbar only appears for clients from IP addresses
21
+# '127.0.0.1' and '::1'.
22
+# debugtoolbar.hosts = 127.0.0.1 ::1
23
+
24
+###
25
+# TRACIM SPECIFIC CONF
26
+###
27
+
28
+### Global
29
+debug = false
30
+cache_dir = %(here)s/data
31
+# preview generator cache directory
32
+preview_cache_dir = /tmp/tracim/preview/
33
+# file depot storage
34
+depot_storage_name = tracim
35
+depot_storage_dir = %(here)s/depot/
36
+
37
+# The following parameters allow to personalize the home page
38
+# They are html ready (you can put html tags they will be interpreted)
39
+website.title = TRACIM
40
+website.title.color = #555
41
+website.home.subtitle = Default login: email: admin@admin.admin (password: admin@admin.admin)
42
+website.home.tag_line = <div class="text-center" style="font-weight: bold;">Collaboration, versionning and traceability</div>
43
+website.home.below_login_form = in case of problem, please contact the administrator.
44
+# Values may be 'all' or 'folders'
45
+website.treeview.content = all
46
+# The following base_url is used for links and icons
47
+# integrated in the email notifcations
48
+website.base_url = http://127.0.0.1:8080
49
+# If config not provided, it will be extracted from website.base_url
50
+website.server_name = 127.0.0.1
51
+
52
+# Specifies if the update of comments and attached files is allowed (by the owner only).
53
+# Examples:
54
+#    600 means 10 minutes (ie 600 seconds)
55
+#   3600 means 1 hour (60x60 seconds)
56
+#
57
+# Allowed values:
58
+#  -1 means that content update is allowed for ever
59
+#   0 means that content update is not allowed
60
+#   x means that content update is allowed for x seconds (with x>0)
61
+content.update.allowed.duration = 3600
62
+
63
+# Auth type (internal or ldap)
64
+auth_type = internal
65
+# If auth_type is ldap, uncomment following ldap_* parameters
66
+# LDAP server address
67
+# ldap_url = ldap://localhost:389
68
+# Base dn to make queries
69
+# ldap_base_dn = dc=directory,dc=fsf,dc=org
70
+# Bind dn to identify the search
71
+# ldap_bind_dn = cn=admin,dc=directory,dc=fsf,dc=org
72
+# The bind password
73
+# ldap_bind_pass = toor
74
+# Attribute name of user record who contain user login (email)
75
+# ldap_ldap_naming_attribute = uid
76
+# Matching between ldap attribute and ldap user field (ldap_attr1=user_field1,ldap_attr2=user_field2,...)
77
+# ldap_user_attributes = mail=email
78
+# TLS usage to communicate with your LDAP server
79
+# ldap_tls = False
80
+# If True, LDAP own tracim group managment (not available for now!)
81
+# ldap_group_enabled = False
82
+# User auth token validity in seconds (used to interfaces like web calendars)
83
+user.auth_token.validity = 604800
84
+
85
+### Mail
86
+
87
+# Reset password through email related configuration.
88
+# These emails will be sent through SMTP
89
+#
90
+resetpassword.email_sender = email@sender.com
91
+resetpassword.smtp_host = smtp.sender
92
+resetpassword.smtp_port = 25
93
+resetpassword.smtp_login = smtp.login
94
+resetpassword.smtp_passwd = smtp.password
95
+
96
+email.notification.activated = False
97
+# email.notification.log_file_path = /tmp/mail-notifications.log
98
+# email notifications can be sent with the user_id added as an identifier
99
+# this way email clients like Thunderbird will be able to distinguish
100
+# notifications generated by a user or another one
101
+email.notification.from.email = noreply+{user_id}@trac.im
102
+email.notification.from.default_label = Tracim Notifications
103
+email.notification.reply_to.email = reply+{content_id}@trac.im
104
+email.notification.references.email = thread+{content_id}@trac.im
105
+email.notification.content_update.template.html = %(here)s/tracim/templates/mail/content_update_body_html.mak
106
+email.notification.content_update.template.text = %(here)s/tracim/templates/mail/content_update_body_text.mak
107
+email.notification.created_account.template.html = %(here)s/tracim/templates/mail/created_account_body_html.mak
108
+email.notification.created_account.template.text = %(here)s/tracim/templates/mail/created_account_body_text.mak
109
+# Note: items between { and } are variable names. Do not remove / rename them
110
+email.notification.content_update.subject = [{website_title}] [{workspace_label}] {content_label} ({content_status_label})
111
+email.notification.created_account.subject = [{website_title}] Created account
112
+# processing_mode may be sync or async
113
+email.notification.processing_mode = sync
114
+email.notification.smtp.server = your_smtp_server
115
+email.notification.smtp.port = 25
116
+email.notification.smtp.user = your_smtp_user
117
+email.notification.smtp.password = your_smtp_password
118
+
119
+## Email sending configuration
120
+# processing_mode may be sync or async,
121
+# with async, please configure redis below
122
+email.processing_mode = sync
123
+# email.async.redis.host = localhost
124
+# email.async.redis.port = 6379
125
+# email.async.redis.db = 0
126
+
127
+# Email reply configuration
128
+email.reply.activated = False
129
+email.reply.imap.server = your_imap_server
130
+email.reply.imap.port = 993
131
+email.reply.imap.user = your_imap_user
132
+email.reply.imap.password = your_imap_password
133
+email.reply.imap.folder = INBOX
134
+email.reply.imap.use_ssl = true
135
+email.reply.imap.use_idle = true
136
+# Re-new connection each 10 minutes
137
+email.reply.connection.max_lifetime = 600
138
+# Token for communication between mail fetcher and tracim controller
139
+email.reply.token = mysecuretoken
140
+# Delay in seconds between each check
141
+email.reply.check.heartbeat = 60
142
+email.reply.use_html_parsing = true
143
+email.reply.use_txt_parsing = true
144
+# Lockfile path is required for email_reply feature,
145
+# it's just an empty file use to prevent concurrent access to imap unseen mail
146
+email.reply.lockfile_path = %(here)s/email_fetcher.lock
147
+
148
+### Radical (CalDav server) configuration
149
+
150
+# radicale.server.host = 0.0.0.0
151
+# radicale.server.port = 5232
152
+# radicale.server.ssl = false
153
+radicale.server.filesystem.folder = %(here)s/radicale/collections/
154
+# radicale.server.allow_origin = *
155
+# radicale.server.realm_message = Tracim Calendar - Password Required
156
+## url can be extended like http://127.0.0.1:5232/calendar
157
+## in this case, you have to create your own proxy behind this url.
158
+## and update following parameters
159
+# radicale.client.base_url.host = http://127.0.0.1:5232
160
+# radicale.client.base_url.prefix = /
161
+
162
+### WSGIDAV
163
+
164
+wsgidav.config_path = %(here)s/wsgidav.conf
165
+## url can be extended like 127.0.0.1/webdav
166
+## in this case, you have to create your own proxy behind this url.
167
+## Do not set http:// prefix.
168
+# wsgidav.client.base_url = 127.0.0.1:<WSGIDAV_PORT>
169
+
170
+###
171
+# wsgi server configuration
172
+###
173
+
174
+[server:main]
175
+use = egg:waitress#main
176
+listen = localhost:6543
177
+
178
+[alembic]
179
+# path to migration scripts
180
+script_location = tracim/migration
181
+
182
+# template used to generate migration files
183
+# file_template = %%(rev)s_%%(slug)s
184
+
185
+# timezone to use when rendering the date
186
+# within the migration file as well as the filename.
187
+# string value is passed to dateutil.tz.gettz()
188
+# leave blank for localtime
189
+# timezone =
190
+
191
+# max length of characters to apply to the
192
+# "slug" field
193
+#truncate_slug_length = 40
194
+
195
+# set to 'true' to run the environment during
196
+# the 'revision' command, regardless of autogenerate
197
+# revision_environment = false
198
+
199
+# set to 'true' to allow .pyc and .pyo files without
200
+# a source .py file to be detected as revisions in the
201
+# versions/ directory
202
+# sourceless = false
203
+
204
+# version location specification; this defaults
205
+# to migrate/versions.  When using multiple version
206
+# directories, initial revisions must be specified with --version-path
207
+# version_locations = %(here)s/bar %(here)s/bat migrate/versions
208
+
209
+# the output encoding used when revision files
210
+# are written from script.py.mako
211
+# output_encoding = utf-8
212
+
213
+sqlalchemy.url = sqlite:///%(here)s/tracim.sqlite
214
+
215
+###
216
+# logging configuration
217
+# https://docs.pylonsproject.org/projects/pyramid/en/latest/narr/logging.html
218
+###
219
+
220
+[loggers]
221
+keys = root, tracim, sqlalchemy, alembic
222
+
223
+[handlers]
224
+keys = console
225
+
226
+[formatters]
227
+keys = generic
228
+
229
+[logger_root]
230
+level = INFO
231
+handlers = console
232
+
233
+[logger_tracim]
234
+level = DEBUG
235
+handlers =
236
+qualname = tracim
237
+
238
+[logger_sqlalchemy]
239
+level = INFO
240
+handlers =
241
+qualname = sqlalchemy.engine
242
+# "level = INFO" logs SQL queries.
243
+# "level = DEBUG" logs SQL queries and results.
244
+# "level = WARN" logs neither.  (Recommended for production systems.)
245
+
246
+[logger_alembic]
247
+level = INFO
248
+handlers =
249
+qualname = alembic
250
+
251
+[handler_console]
252
+class = StreamHandler
253
+args = (sys.stderr,)
254
+level = NOTSET
255
+formatter = generic
256
+
257
+[formatter_generic]
258
+format = %(asctime)s %(levelname)-5.5s [%(name)s:%(lineno)s][%(threadName)s] %(message)s
259
+datefmt = %H:%M:%S

+ 275 - 0
development.ini.oloool View File

@@ -0,0 +1,275 @@
1
+###
2
+# app configuration
3
+# https://docs.pylonsproject.org/projects/pyramid/en/latest/narr/environment.html
4
+###
5
+;[pipeline:main]
6
+;pipeline = tracim_web
7
+
8
+[app:main]
9
+use = egg:tracim_backend
10
+
11
+pyramid.reload_templates = true
12
+pyramid.debug_authorization = false
13
+pyramid.debug_notfound = false
14
+pyramid.debug_routematch = false
15
+pyramid.default_locale_name = en
16
+;pyramid.includes =
17
+;    pyramid_debugtoolbar
18
+
19
+retry.attempts = 3
20
+
21
+sqlalchemy.url = sqlite:///%(here)s/tracim.sqlite
22
+
23
+# By default, the toolbar only appears for clients from IP addresses
24
+# '127.0.0.1' and '::1'.
25
+# debugtoolbar.hosts = 127.0.0.1 ::1
26
+
27
+###
28
+# TRACIM SPECIFIC CONF
29
+###
30
+
31
+### Global
32
+
33
+cache_dir = %(here)s/data
34
+# preview generator cache directory
35
+preview_cache_dir = /tmp/tracim/preview/
36
+# file depot storage
37
+depot_storage_name = tracim
38
+depot_storage_dir = %(here)s/depot/
39
+
40
+# The following parameters allow to personalize the home page
41
+# They are html ready (you can put html tags they will be interpreted)
42
+website.title = TRACIM
43
+website.title.color = #555
44
+website.home.subtitle = Default login: email: admin@admin.admin (password: admin@admin.admin)
45
+website.home.tag_line = <div class="text-center" style="font-weight: bold;">Collaboration, versionning and traceability</div>
46
+website.home.below_login_form = in case of problem, please contact the administrator.
47
+# Values may be 'all' or 'folders'
48
+website.treeview.content = all
49
+# The following base_url is used for links and icons
50
+# integrated in the email notifcations
51
+website.base_url = http://127.0.0.1:8080
52
+# If config not provided, it will be extracted from website.base_url
53
+website.server_name = 127.0.0.1
54
+
55
+# Specifies if the update of comments and attached files is allowed (by the owner only).
56
+# Examples:
57
+#    600 means 10 minutes (ie 600 seconds)
58
+#   3600 means 1 hour (60x60 seconds)
59
+#
60
+# Allowed values:
61
+#  -1 means that content update is allowed for ever
62
+#   0 means that content update is not allowed
63
+#   x means that content update is allowed for x seconds (with x>0)
64
+content.update.allowed.duration = 3600
65
+
66
+# Auth type (internal or ldap)
67
+auth_type = internal
68
+# If auth_type is ldap, uncomment following ldap_* parameters
69
+# LDAP server address
70
+# ldap_url = ldap://localhost:389
71
+# Base dn to make queries
72
+# ldap_base_dn = dc=directory,dc=fsf,dc=org
73
+# Bind dn to identify the search
74
+# ldap_bind_dn = cn=admin,dc=directory,dc=fsf,dc=org
75
+# The bind password
76
+# ldap_bind_pass = toor
77
+# Attribute name of user record who contain user login (email)
78
+# ldap_ldap_naming_attribute = uid
79
+# Matching between ldap attribute and ldap user field (ldap_attr1=user_field1,ldap_attr2=user_field2,...)
80
+# ldap_user_attributes = mail=email
81
+# TLS usage to communicate with your LDAP server
82
+# ldap_tls = False
83
+# If True, LDAP own tracim group managment (not available for now!)
84
+# ldap_group_enabled = False
85
+# User auth token validity in seconds (used to interfaces like web calendars)
86
+user.auth_token.validity = 604800
87
+
88
+### Mail
89
+
90
+# Reset password through email related configuration.
91
+# These emails will be sent through SMTP
92
+#
93
+resetpassword.email_sender = email@sender.com
94
+resetpassword.smtp_host = smtp.sender
95
+resetpassword.smtp_port = 25
96
+resetpassword.smtp_login = smtp.login
97
+resetpassword.smtp_passwd = smtp.password
98
+
99
+email.notification.activated = false
100
+# email.notification.log_file_path = /tmp/mail-notifications.log
101
+# email notifications can be sent with the user_id added as an identifier
102
+# this way email clients like Thunderbird will be able to distinguish
103
+# notifications generated by a user or another one
104
+email.notification.from.email = dev.tracim.maildaemon+{user_id}@algoo.fr
105
+email.notification.from.default_label = Tracim Notifications
106
+email.notification.reply_to.email = dev.tracim.maildaemon+{content_id}@algoo.fr
107
+email.notification.references.email = dev.tracim.maildaemon+{content_id}@algoo.fr
108
+email.notification.content_update.template.html = %(here)s/tracim/templates/mail/content_update_body_html.mak
109
+email.notification.content_update.template.text = %(here)s/tracim/templates/mail/content_update_body_text.mak
110
+email.notification.created_account.template.html = %(here)s/tracim/templates/mail/created_account_body_html.mak
111
+email.notification.created_account.template.text = %(here)s/tracim/templates/mail/created_account_body_text.mak
112
+# Note: items between { and } are variable names. Do not remove / rename them
113
+email.notification.content_update.subject = [{website_title}] [{workspace_label}] {content_label} ({content_status_label})
114
+email.notification.created_account.subject = [{website_title}] Created account
115
+# processing_mode may be sync or async
116
+email.notification.processing_mode = async
117
+email.notification.smtp.server = mail.gandi.net
118
+email.notification.smtp.port = 25
119
+email.notification.smtp.user = dev.tracim.maildaemon@algoo.fr
120
+email.notification.smtp.password = dev.tracim.maildaemon
121
+
122
+## Email sending configuration
123
+# processing_mode may be sync or async,
124
+# with async, please configure redis below
125
+email.processing_mode = sync
126
+# email.async.redis.host = localhost
127
+# email.async.redis.port = 6379
128
+# email.async.redis.db = 0
129
+
130
+# Email reply configuration
131
+email.reply.activated = false
132
+email.reply.imap.server = your_imap_server
133
+email.reply.imap.port = 993
134
+email.reply.imap.user = your_imap_user
135
+email.reply.imap.password = your_imap_password
136
+email.reply.imap.folder = INBOX
137
+email.reply.imap.use_ssl = true
138
+email.reply.imap.use_idle = true
139
+# Re-new connection each 10 minutes
140
+email.reply.connection.max_lifetime = 600
141
+# Token for communication between mail fetcher and tracim controller
142
+email.reply.token = mysecuretoken
143
+# Delay in seconds between each check
144
+email.reply.check.heartbeat = 60
145
+email.reply.use_html_parsing = true
146
+email.reply.use_txt_parsing = true
147
+# Lockfile path is required for email_reply feature,
148
+# it's just an empty file use to prevent concurrent access to imap unseen mail
149
+email.reply.lockfile_path = %(here)s/email_fetcher.lock
150
+
151
+### Radical (CalDav server) configuration
152
+
153
+# radicale.server.host = 0.0.0.0
154
+# radicale.server.port = 5232
155
+# radicale.server.ssl = false
156
+radicale.server.filesystem.folder = %(here)s/radicale/collections/
157
+# radicale.server.allow_origin = *
158
+# radicale.server.realm_message = Tracim Calendar - Password Required
159
+## url can be extended like http://127.0.0.1:5232/calendar
160
+## in this case, you have to create your own proxy behind this url.
161
+## and update following parameters
162
+# radicale.client.base_url.host = http://127.0.0.1:5232
163
+# radicale.client.base_url.prefix = /
164
+
165
+### WSGIDAV
166
+
167
+wsgidav.config_path = %(here)s/wsgidav.conf
168
+## url can be extended like 127.0.0.1/webdav
169
+## in this case, you have to create your own proxy behind this url.
170
+## Do not set http:// prefix.
171
+# wsgidav.client.base_url = 127.0.0.1:<WSGIDAV_PORT>
172
+
173
+###
174
+# wsgi server configuration
175
+###
176
+[uwsgi]
177
+# Legacy server config (waitress)
178
+[server:main]
179
+use = egg:waitress#main
180
+listen = localhost:6543
181
+
182
+[alembic]
183
+# path to migration scripts
184
+script_location = tracim/migration
185
+
186
+# template used to generate migration files
187
+# file_template = %%(rev)s_%%(slug)s
188
+
189
+# timezone to use when rendering the date
190
+# within the migration file as well as the filename.
191
+# string value is passed to dateutil.tz.gettz()
192
+# leave blank for localtime
193
+# timezone =
194
+
195
+# max length of characters to apply to the
196
+# "slug" field
197
+#truncate_slug_length = 40
198
+
199
+# set to 'true' to run the environment during
200
+# the 'revision' command, regardless of autogenerate
201
+# revision_environment = false
202
+
203
+# set to 'true' to allow .pyc and .pyo files without
204
+# a source .py file to be detected as revisions in the
205
+# versions/ directory
206
+# sourceless = false
207
+
208
+# version location specification; this defaults
209
+# to migrate/versions.  When using multiple version
210
+# directories, initial revisions must be specified with --version-path
211
+# version_locations = %(here)s/bar %(here)s/bat migrate/versions
212
+
213
+# the output encoding used when revision files
214
+# are written from script.py.mako
215
+# output_encoding = utf-8
216
+
217
+sqlalchemy.url = sqlite:///%(here)s/tracim.sqlite
218
+
219
+###
220
+# logging configuration
221
+# https://docs.pylonsproject.org/projects/pyramid/en/latest/narr/logging.html
222
+###
223
+
224
+[loggers]
225
+keys = root, tracim, sqlalchemy, alembic, sentry
226
+
227
+[handlers]
228
+keys = console, sentry
229
+
230
+[formatters]
231
+keys = generic
232
+
233
+[logger_root]
234
+level = INFO
235
+handlers = console, sentry
236
+
237
+[logger_sentry]
238
+level = WARN
239
+handlers = console
240
+qualname = sentry.errors
241
+propagate = 0
242
+
243
+[logger_tracim]
244
+level = DEBUG
245
+handlers =
246
+qualname = tracim
247
+
248
+[logger_sqlalchemy]
249
+level = INFO
250
+handlers =
251
+qualname = sqlalchemy.engine
252
+# "level = INFO" logs SQL queries.
253
+# "level = DEBUG" logs SQL queries and results.
254
+# "level = WARN" logs neither.  (Recommended for production systems.)
255
+
256
+[logger_alembic]
257
+level = INFO
258
+handlers =
259
+qualname = alembic
260
+
261
+[handler_console]
262
+class = StreamHandler
263
+args = (sys.stderr,)
264
+level = NOTSET
265
+formatter = generic
266
+
267
+[handler_sentry]
268
+class = raven.handlers.logging.SentryHandler
269
+args = ('http://1dbab0942cca4fbb97f3dae62cbc965d:4d1deecd8abc41e38c02b37ed4954f58@127.0.0.1:9000/4',)
270
+level = WARNING
271
+formatter = generic
272
+
273
+[formatter_generic]
274
+format = %(asctime)s %(levelname)-5.5s [%(name)s:%(lineno)s][%(threadName)s] %(message)s
275
+datefmt = %H:%M:%S

+ 9 - 1
development.ini.sample View 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 View 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',

+ 65 - 0
tests_configs.ini View File

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

+ 23 - 6
tracim/__init__.py View 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()

+ 2 - 1
tracim/command/__init__.py View File

@@ -9,7 +9,7 @@ from cliff.commandmanager import CommandManager
9 9
 
10 10
 from pyramid.paster import bootstrap
11 11
 from pyramid.scripting import AppEnvironment
12
-from tracim.exceptions import CommandAbortedError
12
+from tracim.exceptions import BadCommandError
13 13
 from tracim.lib.utils.utils import DEFAULT_TRACIM_CONFIG_FILE
14 14
 
15 15
 
@@ -56,6 +56,7 @@ class AppContextCommand(Command):
56 56
                 with app_context['request'].tm:
57 57
                     self.take_app_action(parsed_args, app_context)
58 58
 
59
+
59 60
     def get_parser(self, prog_name: str) -> argparse.ArgumentParser:
60 61
         parser = super(AppContextCommand, self).get_parser(prog_name)
61 62
 

+ 38 - 13
tracim/command/user.py View File

@@ -3,6 +3,7 @@ import argparse
3 3
 from pyramid.scripting import AppEnvironment
4 4
 import transaction
5 5
 from sqlalchemy.exc import IntegrityError
6
+from sqlalchemy.orm.exc import NoResultFound
6 7
 
7 8
 from tracim import CFG
8 9
 from tracim.command import AppContextCommand
@@ -11,8 +12,9 @@ from tracim.command import Extender
11 12
 #from tracim.lib.daemons import DaemonsManager
12 13
 #from tracim.lib.daemons import RadicaleDaemon
13 14
 #from tracim.lib.email import get_email_manager
14
-from tracim.exceptions import AlreadyExistError
15
-from tracim.exceptions import CommandAbortedError
15
+from tracim.exceptions import UserAlreadyExistError, GroupDoesNotExist
16
+from tracim.exceptions import NotificationNotSend
17
+from tracim.exceptions import BadCommandError
16 18
 from tracim.lib.core.group import GroupApi
17 19
 from tracim.lib.core.user import UserApi
18 20
 from tracim.models import User
@@ -84,6 +86,14 @@ class UserCommand(AppContextCommand):
84 86
         return self._user_api.user_with_email_exists(login)
85 87
 
86 88
     def _get_group(self, name: str) -> Group:
89
+        groups_availables = [group.group_name
90
+                             for group in self._group_api.get_all()]
91
+        if name not in groups_availables:
92
+            msg = "Group '{}' does not exist, choose a group name in : ".format(name)  # nopep8
93
+            for group in groups_availables:
94
+                msg+= "'{}',".format(group)
95
+            self._session.rollback()
96
+            raise GroupDoesNotExist(msg)
87 97
         return self._group_api.get_one_with_name(name)
88 98
 
89 99
     def _add_user_to_named_group(
@@ -91,6 +101,7 @@ class UserCommand(AppContextCommand):
91 101
             user: str,
92 102
             group_name: str
93 103
     ) -> None:
104
+
94 105
         group = self._get_group(group_name)
95 106
         if user not in group.users:
96 107
             group.users.append(user)
@@ -106,27 +117,38 @@ class UserCommand(AppContextCommand):
106 117
             group.users.remove(user)
107 118
         self._session.flush()
108 119
 
109
-    def _create_user(self, login: str, password: str, **kwargs) -> User:
120
+    def _create_user(
121
+            self,
122
+            login: str,
123
+            password: str,
124
+            do_notify: bool,
125
+            **kwargs
126
+    ) -> User:
110 127
         if not password:
111 128
             if self._password_required():
112
-                raise CommandAbortedError(
129
+                raise BadCommandError(
113 130
                     "You must provide -p/--password parameter"
114 131
                 )
115 132
             password = ''
116 133
 
117 134
         try:
118
-            user = self._user_api.create_user(email=login)
119
-            user.password = password
120
-            self._user_api.save(user)
135
+            user = self._user_api.create_user(
136
+                email=login,
137
+                password=password,
138
+                do_save=True,
139
+                do_notify=do_notify,
140
+            )
121 141
             # TODO - G.M - 04-04-2018 - [Caldav] Check this code
122 142
             # # We need to enable radicale if it not already done
123 143
             # daemons = DaemonsManager()
124 144
             # daemons.run('radicale', RadicaleDaemon)
125
-
126 145
             self._user_api.execute_created_user_actions(user)
127 146
         except IntegrityError:
128 147
             self._session.rollback()
129
-            raise AlreadyExistError()
148
+            raise UserAlreadyExistError()
149
+        except NotificationNotSend as exception:
150
+            self._session.rollback()
151
+            raise exception
130 152
 
131 153
         return user
132 154
 
@@ -167,10 +189,13 @@ class UserCommand(AppContextCommand):
167 189
             try:
168 190
                 user = self._create_user(
169 191
                     login=parsed_args.login,
170
-                    password=parsed_args.password
192
+                    password=parsed_args.password,
193
+                    do_notify=parsed_args.send_email,
171 194
                 )
172
-            except AlreadyExistError:
173
-                raise CommandAbortedError("Error: User already exist (use `user update` command instead)")
195
+            except UserAlreadyExistError:
196
+                raise UserAlreadyExistError("Error: User already exist (use `user update` command instead)")
197
+            except NotificationNotSend:
198
+                raise NotificationNotSend("Error: Cannot send email notification, user not created.")
174 199
             # TODO - G.M - 04-04-2018 - [Email] Check this code
175 200
             # if parsed_args.send_email:
176 201
             #     email_manager = get_email_manager()
@@ -228,5 +253,5 @@ class UpdateUserCommand(UserCommand):
228 253
     action = UserCommand.ACTION_UPDATE
229 254
 
230 255
 
231
-class LDAPUserUnknown(CommandAbortedError):
256
+class LDAPUserUnknown(BadCommandError):
232 257
     pass

+ 2 - 4
tracim/command/webdav.py View 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 View File

@@ -4,6 +4,8 @@ from paste.deploy.converters import asbool
4 4
 from tracim.lib.utils.logger import logger
5 5
 from depot.manager import DepotManager
6 6
 
7
+from tracim.models.data import ActionDescription, ContentType
8
+
7 9
 
8 10
 class CFG(object):
9 11
     """Object used for easy access to config file parameters."""
@@ -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)

+ 11 - 7
tracim/exceptions.py View File

@@ -25,15 +25,11 @@ class ConfigurationError(TracimError):
25 25
     pass
26 26
 
27 27
 
28
-class AlreadyExistError(TracimError):
28
+class UserAlreadyExistError(TracimError):
29 29
     pass
30 30
 
31 31
 
32
-class CommandError(TracimError):
33
-    pass
34
-
35
-
36
-class CommandAbortedError(CommandError):
32
+class BadCommandError(TracimError):
37 33
     pass
38 34
 
39 35
 
@@ -61,7 +57,7 @@ class SameValueError(ValueError):
61 57
     pass
62 58
 
63 59
 
64
-class NotAuthentificated(TracimException):
60
+class NotAuthenticated(TracimException):
65 61
     pass
66 62
 
67 63
 
@@ -93,6 +89,14 @@ class WrongUserPassword(TracimException):
93 89
     pass
94 90
 
95 91
 
92
+class NotificationNotSend(TracimException):
93
+    pass
94
+
95
+
96
+class GroupDoesNotExist(TracimError):
97
+    pass
98
+
99
+
96 100
 class ContentStatusNotExist(TracimError):
97 101
     pass
98 102
 

+ 6 - 1
tracim/fixtures/__init__.py View File

@@ -1,3 +1,4 @@
1
+import copy
1 2
 import transaction
2 3
 
3 4
 
@@ -23,7 +24,11 @@ class FixturesLoader(object):
23 24
         loaded = [] if loaded is None else loaded
24 25
         self._loaded = loaded
25 26
         self._session = session
26
-        self._config = config
27
+        # FIXME - G.M - 2018-06-169 - Fixture failed with email_notification
28
+        # activated, disable it there now. Find better way to fix this
29
+        # later
30
+        self._config = copy.copy(config)
31
+        self._config.EMAIL_NOTIFICATION_ACTIVATED = False
27 32
 
28 33
     def loads(self, fixtures_classes):
29 34
         for fixture_class in fixtures_classes:

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

@@ -75,12 +75,14 @@ class Content(Fixture):
75 75
             workspace=business_workspace,
76 76
             label='Tools',
77 77
             do_save=True,
78
+            do_notify=False,
78 79
         )
79 80
         menu_workspace = content_api.create(
80 81
             content_type=ContentType.Folder,
81 82
             workspace=business_workspace,
82 83
             label='Menus',
83 84
             do_save=True,
85
+            do_notify=False,
84 86
         )
85 87
 
86 88
         dessert_folder = content_api.create(
@@ -88,18 +90,21 @@ class Content(Fixture):
88 90
             workspace=recipe_workspace,
89 91
             label='Desserts',
90 92
             do_save=True,
93
+            do_notify=False,
91 94
         )
92 95
         salads_folder = content_api.create(
93 96
             content_type=ContentType.Folder,
94 97
             workspace=recipe_workspace,
95 98
             label='Salads',
96 99
             do_save=True,
100
+            do_notify=False,
97 101
         )
98 102
         other_folder = content_api.create(
99 103
             content_type=ContentType.Folder,
100 104
             workspace=other_workspace,
101 105
             label='Infos',
102 106
             do_save=True,
107
+            do_notify=False,
103 108
         )
104 109
 
105 110
         # Pages, threads, ..
@@ -109,6 +114,7 @@ class Content(Fixture):
109 114
             parent=dessert_folder,
110 115
             label='Tiramisu Recipe',
111 116
             do_save=True,
117
+            do_notify=False,
112 118
         )
113 119
         best_cake_thread = content_api.create(
114 120
             content_type=ContentType.Thread,
@@ -116,6 +122,7 @@ class Content(Fixture):
116 122
             parent=dessert_folder,
117 123
             label='Best Cakes ?',
118 124
             do_save=False,
125
+            do_notify=False,
119 126
         )
120 127
         best_cake_thread.description = 'What is the best cake ?'
121 128
         self._session.add(best_cake_thread)
@@ -125,6 +132,7 @@ class Content(Fixture):
125 132
             parent=dessert_folder,
126 133
             label='Apple_Pie',
127 134
             do_save=False,
135
+            do_notify=False,
128 136
         )
129 137
         apple_pie_recipe.file_extension = '.txt'
130 138
         apple_pie_recipe.depot_file = FileIntent(
@@ -139,6 +147,7 @@ class Content(Fixture):
139 147
             parent=dessert_folder,
140 148
             label='Brownie Recipe',
141 149
             do_save=False,
150
+            do_notify=False,
142 151
         )
143 152
         Brownie_recipe.file_extension = '.html'
144 153
         Brownie_recipe.depot_file = FileIntent(
@@ -176,6 +185,7 @@ class Content(Fixture):
176 185
             parent=fruits_desserts_folder,
177 186
             label='Fruit Salad',
178 187
             do_save=True,
188
+            do_notify=False,
179 189
         )
180 190
         with new_revision(
181 191
                 session=self._session,
@@ -191,6 +201,7 @@ class Content(Fixture):
191 201
             parent=fruits_desserts_folder,
192 202
             label='Bad Fruit Salad',
193 203
             do_save=True,
204
+            do_notify=False,
194 205
         )
195 206
         with new_revision(
196 207
                 session=self._session,

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

@@ -389,7 +389,7 @@ class ContentApi(object):
389 389
 
390 390
         return result
391 391
 
392
-    def create(self, content_type: str, workspace: Workspace, parent: Content=None, label:str ='', do_save=False, is_temporary: bool=False) -> Content:
392
+    def create(self, content_type: str, workspace: Workspace, parent: Content=None, label:str ='', do_save=False, is_temporary: bool=False, do_notify=True) -> Content:
393 393
         assert content_type in ContentType.allowed_types()
394 394
 
395 395
         if content_type == ContentType.Folder and not label:
@@ -412,7 +412,7 @@ class ContentApi(object):
412 412
 
413 413
         if do_save:
414 414
             self._session.add(content)
415
-            self.save(content, ActionDescription.CREATION)
415
+            self.save(content, ActionDescription.CREATION, do_notify=do_notify)
416 416
         return content
417 417
 
418 418
 
@@ -1141,8 +1141,9 @@ class ContentApi(object):
1141 1141
         :return:
1142 1142
         """
1143 1143
         NotifierFactory.create(
1144
-            self._config,
1145
-            self._user
1144
+            config=self._config,
1145
+            current_user=self._user,
1146
+            session=self._session,
1146 1147
         ).notify_content_update(content)
1147 1148
 
1148 1149
     def get_keywords(self, search_string, search_string_separators=None) -> [str]:

+ 17 - 2
tracim/lib/core/group.py View File

@@ -1,8 +1,12 @@
1 1
 # -*- coding: utf-8 -*-
2 2
 import typing
3 3
 
4
+from sqlalchemy.orm.exc import NoResultFound
5
+
6
+from tracim.exceptions import GroupDoesNotExist
4 7
 from tracim import CFG
5 8
 
9
+
6 10
 __author__ = 'damien'
7 11
 
8 12
 from tracim.models.auth import Group, User
@@ -26,7 +30,18 @@ class GroupApi(object):
26 30
         return self._session.query(Group)
27 31
 
28 32
     def get_one(self, group_id) -> Group:
29
-        return self._base_query().filter(Group.group_id == group_id).one()
33
+        try:
34
+            group = self._base_query().filter(Group.group_id == group_id).one()
35
+            return group
36
+        except NoResultFound:
37
+            raise GroupDoesNotExist()
30 38
 
31 39
     def get_one_with_name(self, group_name) -> Group:
32
-        return self._base_query().filter(Group.group_name == group_name).one()
40
+        try:
41
+            group = self._base_query().filter(Group.group_name == group_name).one()
42
+            return group
43
+        except NoResultFound:
44
+            raise GroupDoesNotExist()
45
+
46
+    def get_all(self):
47
+        return self._base_query().order_by(Group.group_id).all()

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

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

+ 48 - 4
tracim/lib/core/user.py View 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)

+ 3 - 1
tracim/lib/core/userworkspace.py View File

@@ -49,6 +49,7 @@ class RoleApi(object):
49 49
         """
50 50
         Return WorkspaceInContext object from Workspace
51 51
         """
52
+        assert self._config
52 53
         workspace = UserRoleWorkspaceInContext(
53 54
             user_role=user_role,
54 55
             dbsession=self._session,
@@ -138,7 +139,8 @@ class RoleApi(object):
138 139
         workspace:Workspace
139 140
     ) -> typing.List[UserRoleInWorkspace]:
140 141
         return self._session.query(UserRoleInWorkspace)\
141
-            .filter(UserRoleInWorkspace.workspace_id == workspace.workspace_id).all()  # nopep8
142
+            .filter(UserRoleInWorkspace.workspace_id==workspace.workspace_id)\
143
+            .all()
142 144
 
143 145
     def save(self, role: UserRoleInWorkspace) -> None:
144 146
         self._session.flush()

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

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

+ 571 - 0
tracim/lib/mail_notifier/notifier.py View 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 View File

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

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

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

+ 1 - 1
tracim/lib/utils/authentification.py View File

@@ -51,6 +51,6 @@ def _get_basic_auth_unsafe_user(
51 51
         if not login:
52 52
             return None
53 53
         user = uapi.get_one_by_email(login)
54
-    except (NoResultFound, UserDoesNotExist):
54
+    except UserDoesNotExist:
55 55
         return None
56 56
     return user

+ 3 - 3
tracim/lib/utils/authorization.py View File

@@ -44,7 +44,7 @@ class AcceptAllAuthorizationPolicy(object):
44 44
 # We prefer to use decorators
45 45
 
46 46
 
47
-def require_same_user_or_profile(group):
47
+def require_same_user_or_profile(group: int):
48 48
     """
49 49
     Decorator for view to restrict access of tracim request if candidate user
50 50
     is distinct from authenticated user and not with high enough profile.
@@ -64,7 +64,7 @@ def require_same_user_or_profile(group):
64 64
     return decorator
65 65
 
66 66
 
67
-def require_profile(group):
67
+def require_profile(group: int):
68 68
     """
69 69
     Decorator for view to restrict access of tracim request if profile is
70 70
     not high enough
@@ -82,7 +82,7 @@ def require_profile(group):
82 82
     return decorator
83 83
 
84 84
 
85
-def require_workspace_role(minimal_required_role):
85
+def require_workspace_role(minimal_required_role: int):
86 86
     """
87 87
     Decorator for view to restrict access of tracim request if role
88 88
     is not high enough

+ 6 - 3
tracim/lib/utils/request.py View File

@@ -6,7 +6,7 @@ from pyramid.request import Request
6 6
 from sqlalchemy.orm.exc import NoResultFound
7 7
 
8 8
 
9
-from tracim.exceptions import NotAuthentificated
9
+from tracim.exceptions import NotAuthenticated
10 10
 from tracim.exceptions import UserNotFoundInTracimRequest
11 11
 from tracim.exceptions import UserDoesNotExist
12 12
 from tracim.exceptions import WorkspaceNotFound
@@ -40,11 +40,14 @@ class TracimRequest(Request):
40 40
         )
41 41
         # Current workspace, found by request headers or content
42 42
         self._current_workspace = None  # type: Workspace
43
+
43 44
         # Authenticated user
44 45
         self._current_user = None  # type: User
46
+
45 47
         # User found from request headers, content, distinct from authenticated
46 48
         # user
47 49
         self._user_candidate = None  # type: User
50
+
48 51
         # INFO - G.M - 18-05-2018 - Close db at the end of the request
49 52
         self.add_finished_callback(self._cleanup)
50 53
 
@@ -168,7 +171,7 @@ def get_auth_safe_user(
168 171
             raise UserNotFoundInTracimRequest('You request a current user but the context not permit to found one')  # nopep8
169 172
         user = uapi.get_one_by_email(login)
170 173
     except (UserDoesNotExist, UserNotFoundInTracimRequest) as exc:
171
-        raise NotAuthentificated('User {} not found'.format(login)) from exc
174
+        raise NotAuthenticated('User {} not found'.format(login)) from exc
172 175
     return user
173 176
 
174 177
 
@@ -187,7 +190,7 @@ def get_workspace(
187 190
         if 'workspace_id' in request.matchdict:
188 191
             workspace_id = request.matchdict['workspace_id']
189 192
         if not workspace_id:
190
-            raise WorkspaceNotFound('No workspace_id param')
193
+            raise WorkspaceNotFound('No workspace_id property found in request')
191 194
         wapi = WorkspaceApi(
192 195
             current_user=user,
193 196
             session=request.dbsession,

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

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

+ 21 - 14
tracim/lib/webdav/__init__.py View 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):

+ 2 - 2
tracim/lib/webdav/design.py View File

@@ -164,7 +164,7 @@ def designPage(content: data.Content, content_revision: data.ContentRevisionRO)
164 164
                     <td>%s</td>
165 165
                 </tr>
166 166
                 ''' % ('warning' if event.id == content_revision.revision_id else '',
167
-                       event.type.icon,
167
+                       event.type.fa_icon,
168 168
                        label,
169 169
                        date,
170 170
                        event.owner.display_name,
@@ -282,7 +282,7 @@ def designThread(content: data.Content, content_revision: data.ContentRevisionRO
282 282
                         </div>
283 283
                     </div>
284 284
                     ''' % ('warning' if t.id == content_revision.revision_id else '',
285
-                           t.type.icon,
285
+                           t.type.fa_icon,
286 286
                            t.owner.display_name,
287 287
                            t.create_readable_date(),
288 288
                            label,

+ 4 - 1
tracim/lib/webdav/middlewares.py View 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)

+ 21 - 9
tracim/models/applications.py View File

@@ -10,15 +10,27 @@ class Application(object):
10 10
             self,
11 11
             label: str,
12 12
             slug: str,
13
-            icon: str,
13
+            fa_icon: str,
14 14
             hexcolor: str,
15 15
             is_active: bool,
16 16
             config: typing.Dict[str, str],
17 17
             main_route: str,
18 18
     ) -> None:
19
+        """
20
+        @param label: public label of application
21
+        @param slug: identifier of application
22
+        @param icon: font awesome icon class
23
+        @param hexcolor: hexa color of application main color
24
+        @param is_active: True if application enable, False if inactive
25
+        @param config: a dict with eventual application config
26
+        @param main_route: the route of the frontend "home" screen of
27
+        the application. For exemple, if you have an application
28
+        called "calendar", the main route will be something
29
+        like /#/workspace/{wid}/calendar.
30
+        """
19 31
         self.label = label
20 32
         self.slug = slug
21
-        self.icon = icon
33
+        self.fa_icon = fa_icon
22 34
         self.hexcolor = hexcolor
23 35
         self.is_active = is_active
24 36
         self.config = config
@@ -29,7 +41,7 @@ class Application(object):
29 41
 calendar = Application(
30 42
     label='Calendar',
31 43
     slug='calendar',
32
-    icon='calendar-alt',
44
+    fa_icon='calendar-alt',
33 45
     hexcolor='#757575',
34 46
     is_active=True,
35 47
     config={},
@@ -39,7 +51,7 @@ calendar = Application(
39 51
 thread = Application(
40 52
     label='Threads',
41 53
     slug='contents/threads',
42
-    icon='comments-o',
54
+    fa_icon='comments-o',
43 55
     hexcolor='#ad4cf9',
44 56
     is_active=True,
45 57
     config={},
@@ -47,10 +59,10 @@ thread = Application(
47 59
 
48 60
 )
49 61
 
50
-file = Application(
62
+_file = Application(
51 63
     label='Files',
52 64
     slug='contents/files',
53
-    icon='paperclip',
65
+    fa_icon='paperclip',
54 66
     hexcolor='#FF9900',
55 67
     is_active=True,
56 68
     config={},
@@ -60,7 +72,7 @@ file = Application(
60 72
 markdownpluspage = Application(
61 73
     label='Markdown Plus Documents',  # TODO - G.M - 24-05-2018 - Check label
62 74
     slug='contents/markdownpluspage',
63
-    icon='file-code',
75
+    fa_icon='file-code',
64 76
     hexcolor='#f12d2d',
65 77
     is_active=True,
66 78
     config={},
@@ -70,7 +82,7 @@ markdownpluspage = Application(
70 82
 htmlpage = Application(
71 83
     label='Text Documents',  # TODO - G.M - 24-05-2018 - Check label
72 84
     slug='contents/htmlpage',
73
-    icon='file-text-o',
85
+    fa_icon='file-text-o',
74 86
     hexcolor='#3f52e3',
75 87
     is_active=True,
76 88
     config={},
@@ -81,7 +93,7 @@ htmlpage = Application(
81 93
 applications = [
82 94
     htmlpage,
83 95
     markdownpluspage,
84
-    file,
96
+    _file,
85 97
     thread,
86 98
     calendar,
87 99
 ]

+ 17 - 17
tracim/models/contents.py View File

@@ -3,7 +3,7 @@ import typing
3 3
 from enum import Enum
4 4
 
5 5
 from tracim.exceptions import ContentStatusNotExist, ContentTypeNotExist
6
-from tracim.models.applications import htmlpage, file, thread, markdownpluspage
6
+from tracim.models.applications import htmlpage, _file, thread, markdownpluspage
7 7
 
8 8
 
9 9
 ####
@@ -24,13 +24,13 @@ class NewContentStatus(object):
24 24
             slug: str,
25 25
             global_status: str,
26 26
             label: str,
27
-            icon: str,
27
+            fa_icon: str,
28 28
             hexcolor: str,
29 29
     ):
30 30
         self.slug = slug
31 31
         self.global_status = global_status
32 32
         self.label = label
33
-        self.icon = icon
33
+        self.fa_icon = fa_icon
34 34
         self.hexcolor = hexcolor
35 35
 
36 36
 
@@ -38,7 +38,7 @@ open_status = NewContentStatus(
38 38
     slug='open',
39 39
     global_status=GlobalStatus.OPEN.value,
40 40
     label='Open',
41
-    icon='fa-square-o',
41
+    fa_icon='fa-square-o',
42 42
     hexcolor='#000FF',
43 43
 )
44 44
 
@@ -46,7 +46,7 @@ closed_validated_status = NewContentStatus(
46 46
     slug='closed-validated',
47 47
     global_status=GlobalStatus.CLOSED.value,
48 48
     label='Validated',
49
-    icon='fa-check-square-o',
49
+    fa_icon='fa-check-square-o',
50 50
     hexcolor='#000FF',
51 51
 )
52 52
 
@@ -54,7 +54,7 @@ closed_unvalidated_status = NewContentStatus(
54 54
     slug='closed-unvalidated',
55 55
     global_status=GlobalStatus.CLOSED.value,
56 56
     label='Cancelled',
57
-    icon='fa-close',
57
+    fa_icon='fa-close',
58 58
     hexcolor='#000FF',
59 59
 )
60 60
 
@@ -62,7 +62,7 @@ closed_deprecated_status = NewContentStatus(
62 62
     slug='closed-deprecated',
63 63
     global_status=GlobalStatus.CLOSED.value,
64 64
     label='Deprecated',
65
-    icon='fa-warning',
65
+    fa_icon='fa-warning',
66 66
     hexcolor='#000FF',
67 67
 )
68 68
 
@@ -91,7 +91,7 @@ class ContentStatusLegacy(NewContentStatus):
91 91
                     slug=status.slug,
92 92
                     global_status=status.global_status,
93 93
                     label=status.label,
94
-                    icon=status.icon,
94
+                    fa_icon=status.fa_icon,
95 95
                     hexcolor=status.hexcolor,
96 96
                 )
97 97
                 return
@@ -117,7 +117,7 @@ class NewContentType(object):
117 117
     def __init__(
118 118
             self,
119 119
             slug: str,
120
-            icon: str,
120
+            fa_icon: str,
121 121
             hexcolor: str,
122 122
             label: str,
123 123
             creation_label: str,
@@ -125,7 +125,7 @@ class NewContentType(object):
125 125
 
126 126
     ):
127 127
         self.slug = slug
128
-        self.icon = icon
128
+        self.fa_icon = fa_icon
129 129
         self.hexcolor = hexcolor
130 130
         self.label = label
131 131
         self.creation_label = creation_label
@@ -134,7 +134,7 @@ class NewContentType(object):
134 134
 
135 135
 thread_type = NewContentType(
136 136
     slug='thread',
137
-    icon=thread.icon,
137
+    fa_icon=thread.fa_icon,
138 138
     hexcolor=thread.hexcolor,
139 139
     label='Thread',
140 140
     creation_label='Discuss about a topic',
@@ -143,8 +143,8 @@ thread_type = NewContentType(
143 143
 
144 144
 file_type = NewContentType(
145 145
     slug='file',
146
-    icon=file.icon,
147
-    hexcolor=file.hexcolor,
146
+    fa_icon=_file.fa_icon,
147
+    hexcolor=_file.hexcolor,
148 148
     label='File',
149 149
     creation_label='Upload a file',
150 150
     available_statuses=CONTENT_DEFAULT_STATUS,
@@ -152,7 +152,7 @@ file_type = NewContentType(
152 152
 
153 153
 markdownpluspage_type = NewContentType(
154 154
     slug='markdownpage',
155
-    icon=markdownpluspage.icon,
155
+    fa_icon=markdownpluspage.fa_icon,
156 156
     hexcolor=markdownpluspage.hexcolor,
157 157
     label='Rich Markdown File',
158 158
     creation_label='Create a Markdown document',
@@ -161,7 +161,7 @@ markdownpluspage_type = NewContentType(
161 161
 
162 162
 htmlpage_type = NewContentType(
163 163
     slug='page',
164
-    icon=htmlpage.icon,
164
+    fa_icon=htmlpage.fa_icon,
165 165
     hexcolor=htmlpage.hexcolor,
166 166
     label='Text Document',
167 167
     creation_label='Write a document',
@@ -171,7 +171,7 @@ htmlpage_type = NewContentType(
171 171
 # TODO - G.M - 31-05-2018 - Set Better folder params
172 172
 folder_type = NewContentType(
173 173
     slug='folder',
174
-    icon=thread.icon,
174
+    fa_icon=thread.fa_icon,
175 175
     hexcolor=thread.hexcolor,
176 176
     label='Folder',
177 177
     creation_label='Create collection of any documents',
@@ -208,7 +208,7 @@ class ContentTypeLegacy(NewContentType):
208 208
             if slug == content_type.slug:
209 209
                 super(ContentTypeLegacy, self).__init__(
210 210
                     slug=content_type.slug,
211
-                    icon=content_type.icon,
211
+                    fa_icon=content_type.fa_icon,
212 212
                     hexcolor=content_type.hexcolor,
213 213
                     label=content_type.label,
214 214
                     creation_label=content_type.creation_label,

+ 17 - 15
tracim/models/data.py View 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] = ''
@@ -162,15 +162,16 @@ class UserRoleInWorkspace(DeclarativeBase):
162 162
     #
163 163
     #
164 164
     # @property
165
-    # def icon(self):
165
+    # def fa_icon(self):
166 166
     #     return UserRoleInWorkspace.ICON[self.role]
167 167
     #
168 168
     # @property
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]:
@@ -199,7 +200,7 @@ class RoleType(object):
199 200
     def __init__(self, role_id):
200 201
         self.role_type_id = role_id
201 202
         # TODO - G.M - 10-04-2018 - [Cleanup] Drop this
202
-        # self.icon = UserRoleInWorkspace.ICON[role_id]
203
+        # self.fa_icon = UserRoleInWorkspace.ICON[role_id]
203 204
         # self.role_label = UserRoleInWorkspace.LABEL[role_id]
204 205
         # self.css_style = UserRoleInWorkspace.STYLE[role_id]
205 206
 
@@ -264,11 +265,12 @@ class ActionDescription(object):
264 265
     def __init__(self, id):
265 266
         assert id in ActionDescription.allowed_values()
266 267
         self.id = id
267
-        # FIXME - G.M - 17-04-2018 - Label and icon needed for webdav
268
+        # FIXME - G.M - 17-04-2018 - Label and fa_icon needed for webdav
268 269
         #  design template,
269 270
         # find a way to not rely on this.
270 271
         self.label = self.id
271
-        self.icon = ActionDescription._ICONS[id]
272
+        self.fa_icon = ActionDescription._ICONS[id]
273
+        #self.icon = self.fa_icon
272 274
         # TODO - G.M - 10-04-2018 - [Cleanup] Drop this
273 275
         # self.label = ActionDescription._LABELS[id]
274 276
 
@@ -290,8 +292,8 @@ class ActionDescription(object):
290 292
                 ]
291 293
 
292 294
 
293
-from .contents import ContentStatusLegacy as ContentStatus
294
-from .contents import ContentTypeLegacy as ContentType
295
+from tracim.models.contents import ContentStatusLegacy as ContentStatus
296
+from tracim.models.contents import ContentTypeLegacy as ContentType
295 297
 # TODO - G.M - 30-05-2018 - Drop this old code when whe are sure nothing
296 298
 # is lost .
297 299
 
@@ -1464,7 +1466,7 @@ class VirtualEvent(object):
1464 1466
         assert hasattr(type, 'id')
1465 1467
         # TODO - G.M - 10-04-2018 - [Cleanup] Drop this
1466 1468
         # assert hasattr(type, 'css')
1467
-        # assert hasattr(type, 'icon')
1469
+        # assert hasattr(type, 'fa_icon')
1468 1470
         # assert hasattr(type, 'label')
1469 1471
 
1470 1472
     def created_as_delta(self, delta_from_datetime:datetime=None):

+ 5 - 5
tracim/models/workspace_menu_entries.py View File

@@ -14,7 +14,7 @@ class WorkspaceMenuEntry(object):
14 14
             self,
15 15
             label: str,
16 16
             slug: str,
17
-            icon: str,
17
+            fa_icon: str,
18 18
             hexcolor: str,
19 19
             route: str,
20 20
     ) -> None:
@@ -22,21 +22,21 @@ class WorkspaceMenuEntry(object):
22 22
         self.label = label
23 23
         self.route = route
24 24
         self.hexcolor = hexcolor
25
-        self.icon = icon
25
+        self.fa_icon = fa_icon
26 26
 
27 27
 dashboard_menu_entry = WorkspaceMenuEntry(
28 28
   slug='dashboard',
29 29
   label='Dashboard',
30 30
   route='/#/workspaces/{workspace_id}/dashboard',
31 31
   hexcolor='#252525',
32
-  icon="",
32
+  fa_icon="",
33 33
 )
34 34
 all_content_menu_entry = WorkspaceMenuEntry(
35 35
   slug="contents/all",
36 36
   label="All Contents",
37 37
   route="/#/workspaces/{workspace_id}/contents",
38 38
   hexcolor="#fdfdfd",
39
-  icon="",
39
+  fa_icon="",
40 40
 )
41 41
 
42 42
 # TODO - G.M - 08-06-2018 - This is hardcoded default menu entry,
@@ -57,7 +57,7 @@ def default_workspace_menu_entry(
57 57
                 slug=app.slug,
58 58
                 label=app.label,
59 59
                 hexcolor=app.hexcolor,
60
-                icon=app.icon,
60
+                fa_icon=app.fa_icon,
61 61
                 route=app.main_route
62 62
             )
63 63
             menu_entries.append(new_entry)

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


+ 73 - 0
tracim/templates/mail/content_update_body_html.mak View 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 View 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 View 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 View 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

+ 46 - 18
tracim/tests/__init__.py View 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
 
@@ -32,8 +35,9 @@ class FunctionalTest(unittest.TestCase):
32 35
     sqlalchemy_url = 'sqlite:///tracim_test.sqlite'
33 36
 
34 37
     def setUp(self):
38
+        logger._logger.setLevel('WARNING')
35 39
         DepotManager._clear()
36
-        settings = {
40
+        self.settings = {
37 41
             'sqlalchemy.url': self.sqlalchemy_url,
38 42
             'user.auth_token.validity': '604800',
39 43
             'depot_storage_dir': '/tmp/test/depot',
@@ -42,19 +46,24 @@ class FunctionalTest(unittest.TestCase):
42 46
 
43 47
         }
44 48
         hapic.reset_context()
45
-        app = main({}, **settings)
46
-        self.init_database(settings)
49
+        self.engine = get_engine(self.settings)
50
+        DeclarativeBase.metadata.create_all(self.engine)
51
+        self.session_factory = get_session_factory(self.engine)
52
+        self.app_config = CFG(self.settings)
53
+        self.app_config.configure_filedepot()
54
+        self.init_database(self.settings)
55
+        DepotManager._clear()
56
+        self.run_app()
57
+
58
+    def run_app(self):
59
+        app = web({}, **self.settings)
47 60
         self.testapp = TestApp(app)
48 61
 
49 62
     def init_database(self, settings):
50
-        self.engine = get_engine(settings)
51
-        DeclarativeBase.metadata.create_all(self.engine)
52
-        session_factory = get_session_factory(self.engine)
53
-        app_config = CFG(settings)
54 63
         with transaction.manager:
55
-            dbsession = get_tm_session(session_factory, transaction.manager)
64
+            dbsession = get_tm_session(self.session_factory, transaction.manager)
56 65
             try:
57
-                fixtures_loader = FixturesLoader(dbsession, app_config)
66
+                fixtures_loader = FixturesLoader(dbsession, self.app_config)
58 67
                 fixtures_loader.loads(self.fixtures)
59 68
                 transaction.commit()
60 69
                 print("Database initialized.")
@@ -87,21 +96,28 @@ class FunctionalTestNoDB(FunctionalTest):
87 96
         self.engine = get_engine(settings)
88 97
 
89 98
 
99
+class CommandFunctionalTest(FunctionalTest):
100
+
101
+    def run_app(self):
102
+        self.session = get_tm_session(self.session_factory, transaction.manager)
103
+
104
+
90 105
 class BaseTest(unittest.TestCase):
91 106
     """
92 107
     Pyramid default test.
93 108
     """
94 109
 
110
+    config_uri = 'tests_configs.ini'
111
+    config_section = 'base_test'
112
+
95 113
     def setUp(self):
114
+        logger._logger.setLevel('WARNING')
96 115
         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
-        })
116
+        self.settings = plaster.get_settings(
117
+            self.config_uri,
118
+            self.config_section
119
+        )
120
+        self.config = testing.setUp(settings = self.settings)
105 121
         self.config.include('tracim.models')
106 122
         DepotManager._clear()
107 123
         DepotManager.configure(
@@ -226,3 +242,15 @@ class DefaultTest(StandardTest):
226 242
             owner=user
227 243
         )
228 244
         return thread
245
+
246
+
247
+class MailHogTest(DefaultTest):
248
+    """
249
+    Theses test need a working mailhog
250
+    """
251
+
252
+    config_section = 'mail_test'
253
+
254
+    def tearDown(self):
255
+        logger.debug(self, 'Cleanup MailHog list...')
256
+        requests.delete('http://127.0.0.1:8025/api/v1/messages')

+ 181 - 3
tracim/tests/commands/test_commands.py View File

@@ -1,12 +1,25 @@
1 1
 # -*- coding: utf-8 -*-
2 2
 import os
3 3
 import subprocess
4
-
4
+import pytest
5 5
 import tracim
6
+from tracim.command import TracimCLI
7
+from tracim.exceptions import UserAlreadyExistError
8
+from tracim.exceptions import BadCommandError
9
+from tracim.exceptions import GroupDoesNotExist
10
+from tracim.exceptions import UserDoesNotExist
11
+from tracim.lib.core.user import UserApi
12
+from tracim.tests import CommandFunctionalTest
13
+
6 14
 
15
+class TestCommands(CommandFunctionalTest):
16
+    """
17
+    Test tracimcli command line ui.
18
+    """
7 19
 
8
-class TestCommands(object):
9
-    def test_commands(self):
20
+    config_section = 'app:command_test'
21
+
22
+    def test_func__check_commands_list__ok__nominal_case(self) -> None:
10 23
         """
11 24
         Test listing of tracimcli command: Tracim commands must be listed
12 25
         :return:
@@ -20,3 +33,168 @@ class TestCommands(object):
20 33
         assert output.find('user update') > 0
21 34
         assert output.find('db init') > 0
22 35
         assert output.find('db delete') > 0
36
+        assert output.find('webdav start') > 0
37
+
38
+    def test_func__user_create_command__ok__nominal_case(self) -> None:
39
+        """
40
+        Test User creation
41
+        """
42
+        api = UserApi(
43
+            current_user=None,
44
+            session=self.session,
45
+            config=self.app_config,
46
+        )
47
+        with pytest.raises(UserDoesNotExist):
48
+            api.get_one_by_email('command_test@user')
49
+        app = TracimCLI()
50
+        result = app.run([
51
+            'user', 'create',
52
+            '-c', 'tests_configs.ini#command_test',
53
+            '-l', 'command_test@user',
54
+            '-p', 'new_password',
55
+            '--debug',
56
+        ])
57
+        new_user = api.get_one_by_email('command_test@user')
58
+        assert new_user.email == 'command_test@user'
59
+        assert new_user.validate_password('new_password')
60
+        assert new_user.profile.name == 'users'
61
+
62
+    def test_func__user_create_command__ok__in_admin_group(self) -> None:
63
+        """
64
+        Test User creation with admin as group
65
+        """
66
+        api = UserApi(
67
+            current_user=None,
68
+            session=self.session,
69
+            config=self.app_config,
70
+        )
71
+        with pytest.raises(UserDoesNotExist):
72
+            api.get_one_by_email('command_test@user')
73
+        app = TracimCLI()
74
+        result = app.run([
75
+            'user', 'create',
76
+            '-c', 'tests_configs.ini#command_test',
77
+            '-l', 'command_test@user',
78
+            '-p', 'new_password',
79
+            '-g', 'administrators',
80
+            '--debug',
81
+        ])
82
+        new_user = api.get_one_by_email('command_test@user')
83
+        assert new_user.email == 'command_test@user'
84
+        assert new_user.validate_password('new_password')
85
+        assert new_user.profile.name == 'administrators'
86
+
87
+    def test_func__user_create_command__err__in_unknown_group(self) -> None:
88
+        """
89
+        Test User creation with an unknown group
90
+        """
91
+        api = UserApi(
92
+            current_user=None,
93
+            session=self.session,
94
+            config=self.app_config,
95
+        )
96
+        app = TracimCLI()
97
+        with pytest.raises(GroupDoesNotExist):
98
+            result = app.run([
99
+                'user', 'create',
100
+                '-c', 'tests_configs.ini#command_test',
101
+                '-l', 'command_test@user',
102
+                '-p', 'new_password',
103
+                '-g', 'unknown',
104
+                '--debug',
105
+            ])
106
+
107
+    def test_func__user_create_command__err_user_already_exist(self) -> None:
108
+        """
109
+        Test User creation with existing user login
110
+        """
111
+        api = UserApi(
112
+            current_user=None,
113
+            session=self.session,
114
+            config=self.app_config,
115
+        )
116
+        app = TracimCLI()
117
+        with pytest.raises(UserAlreadyExistError):
118
+            result = app.run([
119
+                '--debug',
120
+                'user', 'create',
121
+                '-c', 'tests_configs.ini#command_test',
122
+                '-l', 'admin@admin.admin',
123
+                '-p', 'new_password',
124
+                '--debug',
125
+            ])
126
+
127
+    def test_func__user_create_command__err__password_required(self) -> None:
128
+        """
129
+        Test User creation without filling password
130
+        """
131
+        api = UserApi(
132
+            current_user=None,
133
+            session=self.session,
134
+            config=self.app_config,
135
+        )
136
+        app = TracimCLI()
137
+        with pytest.raises(BadCommandError):
138
+            result = app.run([
139
+                '--debug',
140
+                'user', 'create',
141
+                '-c', 'tests_configs.ini#command_test',
142
+                '-l', 'admin@admin.admin',
143
+                '--debug',
144
+            ])
145
+
146
+    def test_func__user_update_command__ok__nominal_case(self) -> None:
147
+        """
148
+        Test user password update
149
+        """
150
+        api = UserApi(
151
+            current_user=None,
152
+            session=self.session,
153
+            config=self.app_config,
154
+        )
155
+        user = api.get_one_by_email('admin@admin.admin')
156
+        assert user.email == 'admin@admin.admin'
157
+        assert user.validate_password('admin@admin.admin')
158
+        assert not user.validate_password('new_password')
159
+
160
+        app = TracimCLI()
161
+        result = app.run([
162
+            'user', 'update',
163
+            '-c', 'tests_configs.ini#command_test',
164
+            '-l', 'admin@admin.admin',
165
+            '-p', 'new_password',
166
+            '--debug',
167
+        ])
168
+        new_user = api.get_one_by_email('admin@admin.admin')
169
+        assert new_user.email == 'admin@admin.admin'
170
+        assert new_user.validate_password('new_password')
171
+        assert not new_user.validate_password('admin@admin.admin')
172
+
173
+    def test_func__user_update_command__ok__remove_group(self) -> None:
174
+        """
175
+        Test user password update
176
+        """
177
+        api = UserApi(
178
+            current_user=None,
179
+            session=self.session,
180
+            config=self.app_config,
181
+        )
182
+        user = api.get_one_by_email('admin@admin.admin')
183
+        assert user.email == 'admin@admin.admin'
184
+        assert user.validate_password('admin@admin.admin')
185
+        assert not user.validate_password('new_password')
186
+        assert user.profile.name == 'administrators'
187
+        app = TracimCLI()
188
+        result = app.run([
189
+            'user', 'update',
190
+            '-c', 'tests_configs.ini#command_test',
191
+            '-l', 'admin@admin.admin',
192
+            '-p', 'new_password',
193
+            '-rmg', 'administrators',
194
+            '--debug',
195
+        ])
196
+        new_user = api.get_one_by_email('admin@admin.admin')
197
+        assert new_user.email == 'admin@admin.admin'
198
+        assert new_user.validate_password('new_password')
199
+        assert not new_user.validate_password('admin@admin.admin')
200
+        assert new_user.profile.name == 'managers'

+ 267 - 0
tracim/tests/functional/test_mail_notification.py View 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('Recipes')
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] [Recipes] file1 (Open)'
165
+        assert headers['References'][0] == 'test_user_refs+19@localhost'
166
+        assert headers['Reply-to'][0] == '"Bob i. & all members of Recipes" <test_user_reply+19@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('Recipes')
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] [Recipes] file1 (Open)'
266
+        assert headers['References'][0] == 'test_user_refs+19@localhost'
267
+        assert headers['Reply-to'][0] == '"Bob i. & all members of Recipes" <test_user_reply+19@localhost>'  # nopep8

+ 0 - 2
tracim/tests/functional/test_session.py View 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',

+ 9 - 9
tracim/tests/functional/test_system.py View File

@@ -26,35 +26,35 @@ class TestApplicationsEndpoint(FunctionalTest):
26 26
         application = res[0]
27 27
         assert application['label'] == "Text Documents"
28 28
         assert application['slug'] == 'contents/htmlpage'
29
-        assert application['icon'] == 'file-text-o'
29
+        assert application['fa_icon'] == 'file-text-o'
30 30
         assert application['hexcolor'] == '#3f52e3'
31 31
         assert application['is_active'] is True
32 32
         assert 'config' in application
33 33
         application = res[1]
34 34
         assert application['label'] == "Markdown Plus Documents"
35 35
         assert application['slug'] == 'contents/markdownpluspage'
36
-        assert application['icon'] == 'file-code'
36
+        assert application['fa_icon'] == 'file-code'
37 37
         assert application['hexcolor'] == '#f12d2d'
38 38
         assert application['is_active'] is True
39 39
         assert 'config' in application
40 40
         application = res[2]
41 41
         assert application['label'] == "Files"
42 42
         assert application['slug'] == 'contents/files'
43
-        assert application['icon'] == 'paperclip'
43
+        assert application['fa_icon'] == 'paperclip'
44 44
         assert application['hexcolor'] == '#FF9900'
45 45
         assert application['is_active'] is True
46 46
         assert 'config' in application
47 47
         application = res[3]
48 48
         assert application['label'] == "Threads"
49 49
         assert application['slug'] == 'contents/threads'
50
-        assert application['icon'] == 'comments-o'
50
+        assert application['fa_icon'] == 'comments-o'
51 51
         assert application['hexcolor'] == '#ad4cf9'
52 52
         assert application['is_active'] is True
53 53
         assert 'config' in application
54 54
         application = res[4]
55 55
         assert application['label'] == "Calendar"
56 56
         assert application['slug'] == 'calendar'
57
-        assert application['icon'] == 'calendar-alt'
57
+        assert application['fa_icon'] == 'calendar-alt'
58 58
         assert application['hexcolor'] == '#757575'
59 59
         assert application['is_active'] is True
60 60
         assert 'config' in application
@@ -98,7 +98,7 @@ class TestContentsTypesEndpoint(FunctionalTest):
98 98
 
99 99
         content_type = res[0]
100 100
         assert content_type['slug'] == 'thread'
101
-        assert content_type['icon'] == 'comments-o'
101
+        assert content_type['fa_icon'] == 'comments-o'
102 102
         assert content_type['hexcolor'] == '#ad4cf9'
103 103
         assert content_type['label'] == 'Thread'
104 104
         assert content_type['creation_label'] == 'Discuss about a topic'
@@ -107,7 +107,7 @@ class TestContentsTypesEndpoint(FunctionalTest):
107 107
 
108 108
         content_type = res[1]
109 109
         assert content_type['slug'] == 'file'
110
-        assert content_type['icon'] == 'paperclip'
110
+        assert content_type['fa_icon'] == 'paperclip'
111 111
         assert content_type['hexcolor'] == '#FF9900'
112 112
         assert content_type['label'] == 'File'
113 113
         assert content_type['creation_label'] == 'Upload a file'
@@ -116,7 +116,7 @@ class TestContentsTypesEndpoint(FunctionalTest):
116 116
 
117 117
         content_type = res[2]
118 118
         assert content_type['slug'] == 'markdownpage'
119
-        assert content_type['icon'] == 'file-code'
119
+        assert content_type['fa_icon'] == 'file-code'
120 120
         assert content_type['hexcolor'] == '#f12d2d'
121 121
         assert content_type['label'] == 'Rich Markdown File'
122 122
         assert content_type['creation_label'] == 'Create a Markdown document'
@@ -125,7 +125,7 @@ class TestContentsTypesEndpoint(FunctionalTest):
125 125
 
126 126
         content_type = res[3]
127 127
         assert content_type['slug'] == 'page'
128
-        assert content_type['icon'] == 'file-text-o'
128
+        assert content_type['fa_icon'] == 'file-text-o'
129 129
         assert content_type['hexcolor'] == '#3f52e3'
130 130
         assert content_type['label'] == 'Text Document'
131 131
         assert content_type['creation_label'] == 'Write a document'

+ 7 - 7
tracim/tests/functional/test_user.py View File

@@ -37,49 +37,49 @@ class TestUserWorkspaceEndpoint(FunctionalTest):
37 37
         assert sidebar_entry['label'] == 'Dashboard'
38 38
         assert sidebar_entry['route'] == '/#/workspaces/1/dashboard'  # nopep8
39 39
         assert sidebar_entry['hexcolor'] == "#252525"
40
-        assert sidebar_entry['icon'] == ""
40
+        assert sidebar_entry['fa_icon'] == ""
41 41
 
42 42
         sidebar_entry = workspace['sidebar_entries'][1]
43 43
         assert sidebar_entry['slug'] == 'contents/all'
44 44
         assert sidebar_entry['label'] == 'All Contents'
45 45
         assert sidebar_entry['route'] == "/#/workspaces/1/contents"  # nopep8
46 46
         assert sidebar_entry['hexcolor'] == "#fdfdfd"
47
-        assert sidebar_entry['icon'] == ""
47
+        assert sidebar_entry['fa_icon'] == ""
48 48
 
49 49
         sidebar_entry = workspace['sidebar_entries'][2]
50 50
         assert sidebar_entry['slug'] == 'contents/htmlpage'
51 51
         assert sidebar_entry['label'] == 'Text Documents'
52 52
         assert sidebar_entry['route'] == '/#/workspaces/1/contents?type=htmlpage'  # nopep8
53 53
         assert sidebar_entry['hexcolor'] == "#3f52e3"
54
-        assert sidebar_entry['icon'] == "file-text-o"
54
+        assert sidebar_entry['fa_icon'] == "file-text-o"
55 55
 
56 56
         sidebar_entry = workspace['sidebar_entries'][3]
57 57
         assert sidebar_entry['slug'] == 'contents/markdownpluspage'
58 58
         assert sidebar_entry['label'] == 'Markdown Plus Documents'
59 59
         assert sidebar_entry['route'] == "/#/workspaces/1/contents?type=markdownpluspage"    # nopep8
60 60
         assert sidebar_entry['hexcolor'] == "#f12d2d"
61
-        assert sidebar_entry['icon'] == "file-code"
61
+        assert sidebar_entry['fa_icon'] == "file-code"
62 62
 
63 63
         sidebar_entry = workspace['sidebar_entries'][4]
64 64
         assert sidebar_entry['slug'] == 'contents/files'
65 65
         assert sidebar_entry['label'] == 'Files'
66 66
         assert sidebar_entry['route'] == "/#/workspaces/1/contents?type=file"  # nopep8
67 67
         assert sidebar_entry['hexcolor'] == "#FF9900"
68
-        assert sidebar_entry['icon'] == "paperclip"
68
+        assert sidebar_entry['fa_icon'] == "paperclip"
69 69
 
70 70
         sidebar_entry = workspace['sidebar_entries'][5]
71 71
         assert sidebar_entry['slug'] == 'contents/threads'
72 72
         assert sidebar_entry['label'] == 'Threads'
73 73
         assert sidebar_entry['route'] == "/#/workspaces/1/contents?type=thread"  # nopep8
74 74
         assert sidebar_entry['hexcolor'] == "#ad4cf9"
75
-        assert sidebar_entry['icon'] == "comments-o"
75
+        assert sidebar_entry['fa_icon'] == "comments-o"
76 76
 
77 77
         sidebar_entry = workspace['sidebar_entries'][6]
78 78
         assert sidebar_entry['slug'] == 'calendar'
79 79
         assert sidebar_entry['label'] == 'Calendar'
80 80
         assert sidebar_entry['route'] == "/#/workspaces/1/calendar"  # nopep8
81 81
         assert sidebar_entry['hexcolor'] == "#757575"
82
-        assert sidebar_entry['icon'] == "calendar-alt"
82
+        assert sidebar_entry['fa_icon'] == "calendar-alt"
83 83
 
84 84
     def test_api__get_user_workspaces__err_403__unallowed_user(self):
85 85
         """

+ 7 - 7
tracim/tests/functional/test_workspaces.py View File

@@ -38,49 +38,49 @@ class TestWorkspaceEndpoint(FunctionalTest):
38 38
         assert sidebar_entry['label'] == 'Dashboard'
39 39
         assert sidebar_entry['route'] == '/#/workspaces/1/dashboard'  # nopep8
40 40
         assert sidebar_entry['hexcolor'] == "#252525"
41
-        assert sidebar_entry['icon'] == ""
41
+        assert sidebar_entry['fa_icon'] == ""
42 42
 
43 43
         sidebar_entry = workspace['sidebar_entries'][1]
44 44
         assert sidebar_entry['slug'] == 'contents/all'
45 45
         assert sidebar_entry['label'] == 'All Contents'
46 46
         assert sidebar_entry['route'] == "/#/workspaces/1/contents"  # nopep8
47 47
         assert sidebar_entry['hexcolor'] == "#fdfdfd"
48
-        assert sidebar_entry['icon'] == ""
48
+        assert sidebar_entry['fa_icon'] == ""
49 49
 
50 50
         sidebar_entry = workspace['sidebar_entries'][2]
51 51
         assert sidebar_entry['slug'] == 'contents/htmlpage'
52 52
         assert sidebar_entry['label'] == 'Text Documents'
53 53
         assert sidebar_entry['route'] == '/#/workspaces/1/contents?type=htmlpage'  # nopep8
54 54
         assert sidebar_entry['hexcolor'] == "#3f52e3"
55
-        assert sidebar_entry['icon'] == "file-text-o"
55
+        assert sidebar_entry['fa_icon'] == "file-text-o"
56 56
 
57 57
         sidebar_entry = workspace['sidebar_entries'][3]
58 58
         assert sidebar_entry['slug'] == 'contents/markdownpluspage'
59 59
         assert sidebar_entry['label'] == 'Markdown Plus Documents'
60 60
         assert sidebar_entry['route'] == "/#/workspaces/1/contents?type=markdownpluspage"    # nopep8
61 61
         assert sidebar_entry['hexcolor'] == "#f12d2d"
62
-        assert sidebar_entry['icon'] == "file-code"
62
+        assert sidebar_entry['fa_icon'] == "file-code"
63 63
 
64 64
         sidebar_entry = workspace['sidebar_entries'][4]
65 65
         assert sidebar_entry['slug'] == 'contents/files'
66 66
         assert sidebar_entry['label'] == 'Files'
67 67
         assert sidebar_entry['route'] == "/#/workspaces/1/contents?type=file"  # nopep8
68 68
         assert sidebar_entry['hexcolor'] == "#FF9900"
69
-        assert sidebar_entry['icon'] == "paperclip"
69
+        assert sidebar_entry['fa_icon'] == "paperclip"
70 70
 
71 71
         sidebar_entry = workspace['sidebar_entries'][5]
72 72
         assert sidebar_entry['slug'] == 'contents/threads'
73 73
         assert sidebar_entry['label'] == 'Threads'
74 74
         assert sidebar_entry['route'] == "/#/workspaces/1/contents?type=thread"  # nopep8
75 75
         assert sidebar_entry['hexcolor'] == "#ad4cf9"
76
-        assert sidebar_entry['icon'] == "comments-o"
76
+        assert sidebar_entry['fa_icon'] == "comments-o"
77 77
 
78 78
         sidebar_entry = workspace['sidebar_entries'][6]
79 79
         assert sidebar_entry['slug'] == 'calendar'
80 80
         assert sidebar_entry['label'] == 'Calendar'
81 81
         assert sidebar_entry['route'] == "/#/workspaces/1/calendar"  # nopep8
82 82
         assert sidebar_entry['hexcolor'] == "#757575"
83
-        assert sidebar_entry['icon'] == "calendar-alt"
83
+        assert sidebar_entry['fa_icon'] == "calendar-alt"
84 84
 
85 85
     def test_api__get_workspace__err_403__unallowed_user(self) -> None:
86 86
         """

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

+ 74 - 0
tracim/tests/library/test_group_api.py View File

@@ -0,0 +1,74 @@
1
+# coding=utf-8
2
+import pytest
3
+
4
+from tracim.exceptions import GroupDoesNotExist
5
+from tracim.lib.core.group import GroupApi
6
+from tracim.tests import DefaultTest
7
+from tracim.fixtures.users_and_groups import Base as BaseFixture
8
+from tracim.fixtures.content import Content as ContentFixture
9
+
10
+
11
+class TestGroupApi(DefaultTest):
12
+    fixtures = [BaseFixture, ContentFixture]
13
+
14
+    def test_unit__get_one__ok_nominal_case(self) -> None:
15
+        """
16
+        Get one group by id
17
+        """
18
+        api = GroupApi(
19
+            current_user=None,
20
+            session=self.session,
21
+            config=self.app_config,
22
+        )
23
+        group = api.get_one(1)
24
+        assert group.group_id == 1
25
+        assert group.group_name == 'users'
26
+
27
+    def test_unit__get_one__err__group_not_exist(self) -> None:
28
+        """
29
+        Get one group who does not exist by id
30
+        """
31
+        api = GroupApi(
32
+            current_user=None,
33
+            session=self.session,
34
+            config=self.app_config,
35
+        )
36
+        with pytest.raises(GroupDoesNotExist):
37
+            group = api.get_one(10)
38
+
39
+    def test_unit__get_one_group_with_name__nominal_case(self) -> None:
40
+        """
41
+        get one group by name
42
+        """
43
+        api = GroupApi(
44
+            current_user=None,
45
+            session=self.session,
46
+            config=self.app_config,
47
+        )
48
+        group = api.get_one_with_name('administrators')
49
+        assert group.group_id == 3
50
+        assert group.group_name == 'administrators'
51
+
52
+    def test_unit__get_one_with_name__err__group_not_exist(self) -> None:
53
+        """
54
+        get one group by name who does not exist
55
+        """
56
+        api = GroupApi(
57
+            current_user=None,
58
+            session=self.session,
59
+            config=self.app_config,
60
+        )
61
+        with pytest.raises(GroupDoesNotExist):
62
+            group = api.get_one_with_name('unknown_group')
63
+
64
+    def test_unit__get_all__ok__nominal_case(self):
65
+        """
66
+        get all groups
67
+        """
68
+        api = GroupApi(
69
+            current_user=None,
70
+            session=self.session,
71
+            config=self.app_config,
72
+        )
73
+        groups = api.get_all()
74
+        assert ['users', 'managers', 'administrators'] == [group.group_name for group in groups]  # nopep8

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

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

+ 55 - 24
tracim/tests/library/test_user_api.py View 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 View 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 View 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,

+ 16 - 0
tracim/tests/models/test_controller.py View File

@@ -0,0 +1,16 @@
1
+# coding=utf-8
2
+import pytest
3
+from pyramid.config import Configurator
4
+
5
+from tracim.views.controllers import Controller
6
+
7
+
8
+class TestControllerModel(object):
9
+    """
10
+    Test for Controller object
11
+    """
12
+    def test_unit__bind__err__not_implemented(self):
13
+        controller = Controller()
14
+        configurator = Configurator()
15
+        with pytest.raises(NotImplementedError):
16
+            controller.bind(configurator)

+ 51 - 0
tracim/tests/models/test_permission.py View File

@@ -0,0 +1,51 @@
1
+# coding=utf-8
2
+import transaction
3
+from tracim.tests import eq_
4
+from tracim.tests import BaseTest
5
+from tracim.models.auth import Permission
6
+
7
+
8
+class TestPermissionModel(BaseTest):
9
+    """
10
+    Test for permission model
11
+    """
12
+    def test_unit__create__ok__nominal_case(self):
13
+        self.session.flush()
14
+        transaction.commit()
15
+
16
+        name = 'my_permission'
17
+        description = 'my_perm_description'
18
+        permission = Permission()
19
+        permission.permission_name = name
20
+        permission.description = description
21
+
22
+        self.session.add(permission)
23
+        self.session.flush()
24
+        transaction.commit()
25
+
26
+        new_permission = self.session.query(Permission).filter(permission.permission_name == name).one()  # nopep8
27
+
28
+        assert new_permission.permission_name == name
29
+        assert new_permission.description == description
30
+        assert new_permission.permission_id
31
+        assert isinstance(new_permission.permission_id, int)
32
+        # TODO - G.M -24-05-2018 - Do test for groups
33
+
34
+    def test_unit__repr__ok__nominal_case(self):
35
+        name = 'my_permission'
36
+        description = 'my_perm_description'
37
+        permission = Permission()
38
+        permission.permission_name = name
39
+        permission.description = description
40
+
41
+        assert permission.__repr__() == "<Permission: name='my_permission'>"
42
+
43
+    def test_unit__unicode__ok__nominal_case(self):
44
+        name = 'my_permission'
45
+        description = 'my_perm_description'
46
+        permission = Permission()
47
+        permission.permission_name = name
48
+        permission.description = description
49
+
50
+        assert permission.__unicode__() == name
51
+

+ 92 - 11
tracim/tests/models/test_user.py View File

@@ -1,15 +1,15 @@
1 1
 # -*- coding: utf-8 -*-
2
-import transaction
3 2
 
4
-from tracim.tests import eq_
3
+import transaction
5 4
 from tracim.tests import BaseTest
6
-
7 5
 from tracim.models.auth import User
8 6
 
9 7
 
10 8
 class TestUserModel(BaseTest):
11
-
12
-    def test_create(self):
9
+    """
10
+    Test for User model
11
+    """
12
+    def test_unit__create__ok__nominal_case(self):
13 13
         self.session.flush()
14 14
         transaction.commit()
15 15
         name = 'Damien'
@@ -23,13 +23,49 @@ class TestUserModel(BaseTest):
23 23
         self.session.flush()
24 24
         transaction.commit()
25 25
 
26
-        new_user = self.session.query(User).filter(User.display_name==name).one()
26
+        new_user = self.session.query(User).filter(User.display_name == name).one()  # nopep8
27
+
28
+        assert new_user.display_name == name
29
+        assert new_user.email == email
30
+        assert new_user.email_address == email
31
+
32
+    def test_unit__password__ok__nominal_case(self):
33
+        """
34
+        Check if password can be set and hashed password
35
+        can be retrieve. Verify if hashed password is not
36
+        same as password.
37
+        """
38
+        name = 'Damien'
39
+        email = 'tracim@trac.im'
40
+        password = 'my_secure_password'
41
+
42
+        user = User()
43
+        user.display_name = name
44
+        user.email = email
45
+        assert user._password is None
46
+        user.password = password
47
+        assert user._password is not None
48
+        assert user._password != password
49
+        assert user.password == user._password
50
+
51
+    def test__unit__validate_password__ok__nominal_case(self):
52
+        """
53
+        Check if validate_password can correctly check if password i the correct
54
+        one
55
+        """
27 56
 
28
-        eq_(new_user.display_name, name)
29
-        eq_(new_user.email, email)
30
-        eq_(new_user.email_address, email)
57
+        name = 'Damien'
58
+        email = 'tracim@trac.im'
59
+        password = 'my_secure_password'
60
+
61
+        user = User()
62
+        user.display_name = name
63
+        user.email = email
64
+        user.password = password
65
+
66
+        assert user.validate_password(password) is True
31 67
 
32
-    def test_null_password(self):
68
+    def test_unit__validate_password__false__null_password(self):
33 69
         # Check bug #70 fixed
34 70
         # http://tracim.org/workspaces/4/folders/5/threads/70
35 71
 
@@ -40,4 +76,49 @@ class TestUserModel(BaseTest):
40 76
         user.display_name = name
41 77
         user.email = email
42 78
 
43
-        eq_(False, user.validate_password(None))
79
+        assert user.validate_password('') is False
80
+
81
+    def test_unit__validate_password__false__bad_password(self):
82
+        """
83
+        Check if validate_password can correctly check if password is
84
+        an uncorrect correct one
85
+        """
86
+        name = 'Damien'
87
+        email = 'tracim@trac.im'
88
+        password = 'my_secure_password'
89
+
90
+        user = User()
91
+        user.display_name = name
92
+        user.email = email
93
+        user.password = password
94
+
95
+        assert user.validate_password('uncorrect_password') is False
96
+
97
+    def test_unit__repr__ok__nominal_case(self):
98
+        name = 'Damien'
99
+        email = 'tracim@trac.im'
100
+
101
+        user = User()
102
+        user.display_name = name
103
+        user.email = email
104
+
105
+        assert user.__repr__() == "<User: email='tracim@trac.im', display='Damien'>"  # nopep8
106
+
107
+    def test_unit__unicode__ok__nominal_case(self):
108
+        name = 'Damien'
109
+        email = 'tracim@trac.im'
110
+
111
+        user = User()
112
+        user.display_name = name
113
+        user.email = email
114
+
115
+        assert user.__unicode__() == name
116
+
117
+    def test__unit__unicode__ok__no_display_name(self):
118
+
119
+        email = 'tracim@trac.im'
120
+
121
+        user = User()
122
+        user.email = email
123
+
124
+        assert user.__unicode__() == email

+ 4 - 4
tracim/views/core_api/schemas.py View File

@@ -173,7 +173,7 @@ class WorkspaceMenuEntrySchema(marshmallow.Schema):
173 173
                     'which must be replaced on backend size '
174 174
                     '(the route must be ready-to-use)'
175 175
     )
176
-    icon = marshmallow.fields.String(
176
+    fa_icon = marshmallow.fields.String(
177 177
         example='file-text-o',
178 178
         description='CSS class of the icon. Example: file-o for using Fontawesome file-text-o icon',  # nopep8
179 179
     )
@@ -229,7 +229,7 @@ class ApplicationConfigSchema(marshmallow.Schema):
229 229
 class ApplicationSchema(marshmallow.Schema):
230 230
     label = marshmallow.fields.String(example='Calendar')
231 231
     slug = marshmallow.fields.String(example='calendar')
232
-    icon = marshmallow.fields.String(
232
+    fa_icon = marshmallow.fields.String(
233 233
         example='file-o',
234 234
         description='CSS class of the icon. Example: file-o for using Fontawesome file-o icon',  # nopep8
235 235
     )
@@ -261,7 +261,7 @@ class StatusSchema(marshmallow.Schema):
261 261
         validate=OneOf([status.value for status in GlobalStatus]),
262 262
     )
263 263
     label = marshmallow.fields.String(example='Open')
264
-    icon = marshmallow.fields.String(example='fa-check')
264
+    fa_icon = marshmallow.fields.String(example='fa-check')
265 265
     hexcolor = marshmallow.fields.String(example='#0000FF')
266 266
 
267 267
 
@@ -270,7 +270,7 @@ class ContentTypeSchema(marshmallow.Schema):
270 270
         example='pagehtml',
271 271
         validate=OneOf([content.slug for content in CONTENT_DEFAULT_TYPE]),
272 272
     )
273
-    icon = marshmallow.fields.String(
273
+    fa_icon = marshmallow.fields.String(
274 274
         example='fa-file-text-o',
275 275
         description='CSS class of the icon. Example: file-o for using Fontawesome file-o icon',  # nopep8
276 276
     )

+ 2 - 2
tracim/views/core_api/session_controller.py View File

@@ -13,7 +13,7 @@ from tracim.views.core_api.schemas import UserSchema
13 13
 from tracim.views.core_api.schemas import NoContentSchema
14 14
 from tracim.views.core_api.schemas import LoginOutputHeaders
15 15
 from tracim.views.core_api.schemas import BasicAuthSchema
16
-from tracim.exceptions import NotAuthentificated
16
+from tracim.exceptions import NotAuthenticated
17 17
 from tracim.exceptions import AuthenticationFailed
18 18
 
19 19
 
@@ -52,7 +52,7 @@ class SessionController(Controller):
52 52
         return
53 53
 
54 54
     @hapic.with_api_doc()
55
-    @hapic.handle_exception(NotAuthentificated, HTTPStatus.UNAUTHORIZED)
55
+    @hapic.handle_exception(NotAuthenticated, HTTPStatus.UNAUTHORIZED)
56 56
     @hapic.output_body(UserSchema(),)
57 57
     def whoami(self, context, request: TracimRequest, hapic_data=None):
58 58
         """

+ 3 - 3
tracim/views/core_api/system_controller.py View File

@@ -1,7 +1,7 @@
1 1
 # coding=utf-8
2 2
 from pyramid.config import Configurator
3 3
 
4
-from tracim.exceptions import NotAuthentificated, InsufficientUserProfile
4
+from tracim.exceptions import NotAuthenticated, InsufficientUserProfile
5 5
 from tracim.lib.utils.authorization import require_profile
6 6
 from tracim.models import Group
7 7
 from tracim.models.applications import applications
@@ -21,7 +21,7 @@ from tracim.views.core_api.schemas import ApplicationSchema, ContentTypeSchema
21 21
 class SystemController(Controller):
22 22
 
23 23
     @hapic.with_api_doc()
24
-    @hapic.handle_exception(NotAuthentificated, HTTPStatus.UNAUTHORIZED)
24
+    @hapic.handle_exception(NotAuthenticated, HTTPStatus.UNAUTHORIZED)
25 25
     @hapic.handle_exception(InsufficientUserProfile, HTTPStatus.FORBIDDEN)
26 26
     @require_profile(Group.TIM_USER)
27 27
     @hapic.output_body(ApplicationSchema(many=True),)
@@ -32,7 +32,7 @@ class SystemController(Controller):
32 32
         return applications
33 33
 
34 34
     @hapic.with_api_doc()
35
-    @hapic.handle_exception(NotAuthentificated, HTTPStatus.UNAUTHORIZED)
35
+    @hapic.handle_exception(NotAuthenticated, HTTPStatus.UNAUTHORIZED)
36 36
     @hapic.handle_exception(InsufficientUserProfile, HTTPStatus.FORBIDDEN)
37 37
     @require_profile(Group.TIM_USER)
38 38
     @hapic.output_body(ContentTypeSchema(many=True),)

+ 2 - 2
tracim/views/core_api/user_controller.py View File

@@ -12,7 +12,7 @@ except ImportError:
12 12
 
13 13
 from tracim import hapic, TracimRequest
14 14
 
15
-from tracim.exceptions import NotAuthentificated
15
+from tracim.exceptions import NotAuthenticated
16 16
 from tracim.exceptions import InsufficientUserProfile
17 17
 from tracim.exceptions import UserDoesNotExist
18 18
 from tracim.lib.core.workspace import WorkspaceApi
@@ -24,7 +24,7 @@ from tracim.views.core_api.schemas import WorkspaceDigestSchema
24 24
 class UserController(Controller):
25 25
 
26 26
     @hapic.with_api_doc()
27
-    @hapic.handle_exception(NotAuthentificated, HTTPStatus.UNAUTHORIZED)
27
+    @hapic.handle_exception(NotAuthenticated, HTTPStatus.UNAUTHORIZED)
28 28
     @hapic.handle_exception(InsufficientUserProfile, HTTPStatus.FORBIDDEN)
29 29
     @hapic.handle_exception(UserDoesNotExist, HTTPStatus.NOT_FOUND)
30 30
     @require_same_user_or_profile(Group.TIM_ADMIN)

+ 11 - 11
tracim/views/core_api/workspace_controller.py View File

@@ -15,7 +15,7 @@ from tracim.lib.utils.authorization import require_workspace_role
15 15
 from tracim.models.data import UserRoleInWorkspace, ActionDescription
16 16
 from tracim.models.context_models import UserRoleWorkspaceInContext
17 17
 from tracim.models.context_models import ContentInContext
18
-from tracim.exceptions import NotAuthentificated
18
+from tracim.exceptions import NotAuthenticated
19 19
 from tracim.exceptions import InsufficientUserProfile
20 20
 from tracim.exceptions import WorkspaceNotFound
21 21
 from tracim.views.controllers import Controller
@@ -28,14 +28,14 @@ from tracim.views.core_api.schemas import ContentDigestSchema
28 28
 from tracim.views.core_api.schemas import WorkspaceSchema
29 29
 from tracim.views.core_api.schemas import WorkspaceIdPathSchema
30 30
 from tracim.views.core_api.schemas import WorkspaceMemberSchema
31
-from tracim.models.data import ContentType
31
+from tracim.models.contents import ContentTypeLegacy as ContentType
32 32
 from tracim.models.revision_protection import new_revision
33 33
 
34 34
 
35 35
 class WorkspaceController(Controller):
36 36
 
37 37
     @hapic.with_api_doc()
38
-    @hapic.handle_exception(NotAuthentificated, HTTPStatus.UNAUTHORIZED)
38
+    @hapic.handle_exception(NotAuthenticated, HTTPStatus.UNAUTHORIZED)
39 39
     @hapic.handle_exception(InsufficientUserProfile, HTTPStatus.FORBIDDEN)
40 40
     @hapic.handle_exception(WorkspaceNotFound, HTTPStatus.FORBIDDEN)
41 41
     @require_workspace_role(UserRoleInWorkspace.READER)
@@ -55,7 +55,7 @@ class WorkspaceController(Controller):
55 55
         return wapi.get_workspace_with_context(request.current_workspace)
56 56
 
57 57
     @hapic.with_api_doc()
58
-    @hapic.handle_exception(NotAuthentificated, HTTPStatus.UNAUTHORIZED)
58
+    @hapic.handle_exception(NotAuthenticated, HTTPStatus.UNAUTHORIZED)
59 59
     @hapic.handle_exception(InsufficientUserProfile, HTTPStatus.FORBIDDEN)
60 60
     @hapic.handle_exception(WorkspaceNotFound, HTTPStatus.FORBIDDEN)
61 61
     @require_workspace_role(UserRoleInWorkspace.READER)
@@ -84,7 +84,7 @@ class WorkspaceController(Controller):
84 84
         ]
85 85
 
86 86
     @hapic.with_api_doc()
87
-    @hapic.handle_exception(NotAuthentificated, HTTPStatus.UNAUTHORIZED)
87
+    @hapic.handle_exception(NotAuthenticated, HTTPStatus.UNAUTHORIZED)
88 88
     @hapic.handle_exception(InsufficientUserProfile, HTTPStatus.FORBIDDEN)
89 89
     @hapic.handle_exception(WorkspaceNotFound, HTTPStatus.FORBIDDEN)
90 90
     @require_workspace_role(UserRoleInWorkspace.READER)
@@ -120,7 +120,7 @@ class WorkspaceController(Controller):
120 120
         return contents
121 121
 
122 122
     @hapic.with_api_doc()
123
-    @hapic.handle_exception(NotAuthentificated, HTTPStatus.UNAUTHORIZED)
123
+    @hapic.handle_exception(NotAuthenticated, HTTPStatus.UNAUTHORIZED)
124 124
     @hapic.handle_exception(InsufficientUserProfile, HTTPStatus.FORBIDDEN)
125 125
     @hapic.handle_exception(WorkspaceNotFound, HTTPStatus.FORBIDDEN)
126 126
     @require_workspace_role(UserRoleInWorkspace.CONTRIBUTOR)
@@ -153,7 +153,7 @@ class WorkspaceController(Controller):
153 153
         return content
154 154
 
155 155
     @hapic.with_api_doc()
156
-    @hapic.handle_exception(NotAuthentificated, HTTPStatus.UNAUTHORIZED)
156
+    @hapic.handle_exception(NotAuthenticated, HTTPStatus.UNAUTHORIZED)
157 157
     @hapic.handle_exception(InsufficientUserProfile, HTTPStatus.FORBIDDEN)
158 158
     @hapic.handle_exception(WorkspaceNotFound, HTTPStatus.FORBIDDEN)
159 159
     @require_workspace_role(UserRoleInWorkspace.CONTRIBUTOR)
@@ -193,7 +193,7 @@ class WorkspaceController(Controller):
193 193
         return
194 194
 
195 195
     @hapic.with_api_doc()
196
-    @hapic.handle_exception(NotAuthentificated, HTTPStatus.UNAUTHORIZED)
196
+    @hapic.handle_exception(NotAuthenticated, HTTPStatus.UNAUTHORIZED)
197 197
     @hapic.handle_exception(InsufficientUserProfile, HTTPStatus.FORBIDDEN)
198 198
     @hapic.handle_exception(WorkspaceNotFound, HTTPStatus.FORBIDDEN)
199 199
     @require_workspace_role(UserRoleInWorkspace.CONTRIBUTOR)
@@ -228,7 +228,7 @@ class WorkspaceController(Controller):
228 228
         return
229 229
 
230 230
     @hapic.with_api_doc()
231
-    @hapic.handle_exception(NotAuthentificated, HTTPStatus.UNAUTHORIZED)
231
+    @hapic.handle_exception(NotAuthenticated, HTTPStatus.UNAUTHORIZED)
232 232
     @hapic.handle_exception(InsufficientUserProfile, HTTPStatus.FORBIDDEN)
233 233
     @hapic.handle_exception(WorkspaceNotFound, HTTPStatus.FORBIDDEN)
234 234
     @require_workspace_role(UserRoleInWorkspace.CONTRIBUTOR)
@@ -264,7 +264,7 @@ class WorkspaceController(Controller):
264 264
         return
265 265
 
266 266
     @hapic.with_api_doc()
267
-    @hapic.handle_exception(NotAuthentificated, HTTPStatus.UNAUTHORIZED)
267
+    @hapic.handle_exception(NotAuthenticated, HTTPStatus.UNAUTHORIZED)
268 268
     @hapic.handle_exception(InsufficientUserProfile, HTTPStatus.FORBIDDEN)
269 269
     @hapic.handle_exception(WorkspaceNotFound, HTTPStatus.FORBIDDEN)
270 270
     @require_workspace_role(UserRoleInWorkspace.CONTRIBUTOR)
@@ -296,7 +296,7 @@ class WorkspaceController(Controller):
296 296
         return
297 297
 
298 298
     @hapic.with_api_doc()
299
-    @hapic.handle_exception(NotAuthentificated, HTTPStatus.UNAUTHORIZED)
299
+    @hapic.handle_exception(NotAuthenticated, HTTPStatus.UNAUTHORIZED)
300 300
     @hapic.handle_exception(InsufficientUserProfile, HTTPStatus.FORBIDDEN)
301 301
     @hapic.handle_exception(WorkspaceNotFound, HTTPStatus.FORBIDDEN)
302 302
     @require_workspace_role(UserRoleInWorkspace.CONTRIBUTOR)

+ 17 - 0
wsgi/__init__.py View 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 View 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 View 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)