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

+ 14 - 0
README.md View File

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

+ 259 - 0
development.ini.old View File

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

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

+ 9 - 2
setup.py View File

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

+ 65 - 0
tests_configs.ini View File

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

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

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

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

3
 from pyramid.scripting import AppEnvironment
3
 from pyramid.scripting import AppEnvironment
4
 import transaction
4
 import transaction
5
 from sqlalchemy.exc import IntegrityError
5
 from sqlalchemy.exc import IntegrityError
6
+from sqlalchemy.orm.exc import NoResultFound
6
 
7
 
7
 from tracim import CFG
8
 from tracim import CFG
8
 from tracim.command import AppContextCommand
9
 from tracim.command import AppContextCommand
11
 #from tracim.lib.daemons import DaemonsManager
12
 #from tracim.lib.daemons import DaemonsManager
12
 #from tracim.lib.daemons import RadicaleDaemon
13
 #from tracim.lib.daemons import RadicaleDaemon
13
 #from tracim.lib.email import get_email_manager
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
 from tracim.lib.core.group import GroupApi
18
 from tracim.lib.core.group import GroupApi
17
 from tracim.lib.core.user import UserApi
19
 from tracim.lib.core.user import UserApi
18
 from tracim.models import User
20
 from tracim.models import User
84
         return self._user_api.user_with_email_exists(login)
86
         return self._user_api.user_with_email_exists(login)
85
 
87
 
86
     def _get_group(self, name: str) -> Group:
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
         return self._group_api.get_one_with_name(name)
97
         return self._group_api.get_one_with_name(name)
88
 
98
 
89
     def _add_user_to_named_group(
99
     def _add_user_to_named_group(
91
             user: str,
101
             user: str,
92
             group_name: str
102
             group_name: str
93
     ) -> None:
103
     ) -> None:
104
+
94
         group = self._get_group(group_name)
105
         group = self._get_group(group_name)
95
         if user not in group.users:
106
         if user not in group.users:
96
             group.users.append(user)
107
             group.users.append(user)
106
             group.users.remove(user)
117
             group.users.remove(user)
107
         self._session.flush()
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
         if not password:
127
         if not password:
111
             if self._password_required():
128
             if self._password_required():
112
-                raise CommandAbortedError(
129
+                raise BadCommandError(
113
                     "You must provide -p/--password parameter"
130
                     "You must provide -p/--password parameter"
114
                 )
131
                 )
115
             password = ''
132
             password = ''
116
 
133
 
117
         try:
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
             # TODO - G.M - 04-04-2018 - [Caldav] Check this code
141
             # TODO - G.M - 04-04-2018 - [Caldav] Check this code
122
             # # We need to enable radicale if it not already done
142
             # # We need to enable radicale if it not already done
123
             # daemons = DaemonsManager()
143
             # daemons = DaemonsManager()
124
             # daemons.run('radicale', RadicaleDaemon)
144
             # daemons.run('radicale', RadicaleDaemon)
125
-
126
             self._user_api.execute_created_user_actions(user)
145
             self._user_api.execute_created_user_actions(user)
127
         except IntegrityError:
146
         except IntegrityError:
128
             self._session.rollback()
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
         return user
153
         return user
132
 
154
 
167
             try:
189
             try:
168
                 user = self._create_user(
190
                 user = self._create_user(
169
                     login=parsed_args.login,
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
             # TODO - G.M - 04-04-2018 - [Email] Check this code
199
             # TODO - G.M - 04-04-2018 - [Email] Check this code
175
             # if parsed_args.send_email:
200
             # if parsed_args.send_email:
176
             #     email_manager = get_email_manager()
201
             #     email_manager = get_email_manager()
228
     action = UserCommand.ACTION_UPDATE
253
     action = UserCommand.ACTION_UPDATE
229
 
254
 
230
 
255
 
231
-class LDAPUserUnknown(CommandAbortedError):
256
+class LDAPUserUnknown(BadCommandError):
232
     pass
257
     pass

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

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

+ 111 - 108
tracim/config.py View File

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

+ 11 - 7
tracim/exceptions.py View File

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

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

1
+import copy
1
 import transaction
2
 import transaction
2
 
3
 
3
 
4
 
23
         loaded = [] if loaded is None else loaded
24
         loaded = [] if loaded is None else loaded
24
         self._loaded = loaded
25
         self._loaded = loaded
25
         self._session = session
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
     def loads(self, fixtures_classes):
33
     def loads(self, fixtures_classes):
29
         for fixture_class in fixtures_classes:
34
         for fixture_class in fixtures_classes:

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

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

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

389
 
389
 
390
         return result
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
         assert content_type in ContentType.allowed_types()
393
         assert content_type in ContentType.allowed_types()
394
 
394
 
395
         if content_type == ContentType.Folder and not label:
395
         if content_type == ContentType.Folder and not label:
412
 
412
 
413
         if do_save:
413
         if do_save:
414
             self._session.add(content)
414
             self._session.add(content)
415
-            self.save(content, ActionDescription.CREATION)
415
+            self.save(content, ActionDescription.CREATION, do_notify=do_notify)
416
         return content
416
         return content
417
 
417
 
418
 
418
 
1141
         :return:
1141
         :return:
1142
         """
1142
         """
1143
         NotifierFactory.create(
1143
         NotifierFactory.create(
1144
-            self._config,
1145
-            self._user
1144
+            config=self._config,
1145
+            current_user=self._user,
1146
+            session=self._session,
1146
         ).notify_content_update(content)
1147
         ).notify_content_update(content)
1147
 
1148
 
1148
     def get_keywords(self, search_string, search_string_separators=None) -> [str]:
1149
     def get_keywords(self, search_string, search_string_separators=None) -> [str]:

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

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

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

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

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

49
         """
49
         """
50
         Return WorkspaceInContext object from Workspace
50
         Return WorkspaceInContext object from Workspace
51
         """
51
         """
52
+        assert self._config
52
         workspace = UserRoleWorkspaceInContext(
53
         workspace = UserRoleWorkspaceInContext(
53
             user_role=user_role,
54
             user_role=user_role,
54
             dbsession=self._session,
55
             dbsession=self._session,
138
         workspace:Workspace
139
         workspace:Workspace
139
     ) -> typing.List[UserRoleInWorkspace]:
140
     ) -> typing.List[UserRoleInWorkspace]:
140
         return self._session.query(UserRoleInWorkspace)\
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
     def save(self, role: UserRoleInWorkspace) -> None:
145
     def save(self, role: UserRoleInWorkspace) -> None:
144
         self._session.flush()
146
         self._session.flush()

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

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

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

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

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

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

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

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

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

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

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

44
 # We prefer to use decorators
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
     Decorator for view to restrict access of tracim request if candidate user
49
     Decorator for view to restrict access of tracim request if candidate user
50
     is distinct from authenticated user and not with high enough profile.
50
     is distinct from authenticated user and not with high enough profile.
64
     return decorator
64
     return decorator
65
 
65
 
66
 
66
 
67
-def require_profile(group):
67
+def require_profile(group: int):
68
     """
68
     """
69
     Decorator for view to restrict access of tracim request if profile is
69
     Decorator for view to restrict access of tracim request if profile is
70
     not high enough
70
     not high enough
82
     return decorator
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
     Decorator for view to restrict access of tracim request if role
87
     Decorator for view to restrict access of tracim request if role
88
     is not high enough
88
     is not high enough

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

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

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

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

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

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

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

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

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

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

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

10
             self,
10
             self,
11
             label: str,
11
             label: str,
12
             slug: str,
12
             slug: str,
13
-            icon: str,
13
+            fa_icon: str,
14
             hexcolor: str,
14
             hexcolor: str,
15
             is_active: bool,
15
             is_active: bool,
16
             config: typing.Dict[str, str],
16
             config: typing.Dict[str, str],
17
             main_route: str,
17
             main_route: str,
18
     ) -> None:
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
         self.label = label
31
         self.label = label
20
         self.slug = slug
32
         self.slug = slug
21
-        self.icon = icon
33
+        self.fa_icon = fa_icon
22
         self.hexcolor = hexcolor
34
         self.hexcolor = hexcolor
23
         self.is_active = is_active
35
         self.is_active = is_active
24
         self.config = config
36
         self.config = config
29
 calendar = Application(
41
 calendar = Application(
30
     label='Calendar',
42
     label='Calendar',
31
     slug='calendar',
43
     slug='calendar',
32
-    icon='calendar-alt',
44
+    fa_icon='calendar-alt',
33
     hexcolor='#757575',
45
     hexcolor='#757575',
34
     is_active=True,
46
     is_active=True,
35
     config={},
47
     config={},
39
 thread = Application(
51
 thread = Application(
40
     label='Threads',
52
     label='Threads',
41
     slug='contents/threads',
53
     slug='contents/threads',
42
-    icon='comments-o',
54
+    fa_icon='comments-o',
43
     hexcolor='#ad4cf9',
55
     hexcolor='#ad4cf9',
44
     is_active=True,
56
     is_active=True,
45
     config={},
57
     config={},
47
 
59
 
48
 )
60
 )
49
 
61
 
50
-file = Application(
62
+_file = Application(
51
     label='Files',
63
     label='Files',
52
     slug='contents/files',
64
     slug='contents/files',
53
-    icon='paperclip',
65
+    fa_icon='paperclip',
54
     hexcolor='#FF9900',
66
     hexcolor='#FF9900',
55
     is_active=True,
67
     is_active=True,
56
     config={},
68
     config={},
60
 markdownpluspage = Application(
72
 markdownpluspage = Application(
61
     label='Markdown Plus Documents',  # TODO - G.M - 24-05-2018 - Check label
73
     label='Markdown Plus Documents',  # TODO - G.M - 24-05-2018 - Check label
62
     slug='contents/markdownpluspage',
74
     slug='contents/markdownpluspage',
63
-    icon='file-code',
75
+    fa_icon='file-code',
64
     hexcolor='#f12d2d',
76
     hexcolor='#f12d2d',
65
     is_active=True,
77
     is_active=True,
66
     config={},
78
     config={},
70
 htmlpage = Application(
82
 htmlpage = Application(
71
     label='Text Documents',  # TODO - G.M - 24-05-2018 - Check label
83
     label='Text Documents',  # TODO - G.M - 24-05-2018 - Check label
72
     slug='contents/htmlpage',
84
     slug='contents/htmlpage',
73
-    icon='file-text-o',
85
+    fa_icon='file-text-o',
74
     hexcolor='#3f52e3',
86
     hexcolor='#3f52e3',
75
     is_active=True,
87
     is_active=True,
76
     config={},
88
     config={},
81
 applications = [
93
 applications = [
82
     htmlpage,
94
     htmlpage,
83
     markdownpluspage,
95
     markdownpluspage,
84
-    file,
96
+    _file,
85
     thread,
97
     thread,
86
     calendar,
98
     calendar,
87
 ]
99
 ]

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

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

+ 17 - 15
tracim/models/data.py View File

139
         WORKSPACE_MANAGER: 'workspace_manager',
139
         WORKSPACE_MANAGER: 'workspace_manager',
140
     }
140
     }
141
 
141
 
142
-    # LABEL = dict()
143
-    # LABEL[0] = l_('N/A')
144
-    # LABEL[1] = l_('Reader')
145
-    # LABEL[2] = l_('Contributor')
146
-    # LABEL[4] = l_('Content Manager')
147
-    # LABEL[8] = l_('Workspace Manager')
142
+    LABEL = dict()
143
+    LABEL[0] = l_('N/A')
144
+    LABEL[1] = l_('Reader')
145
+    LABEL[2] = l_('Contributor')
146
+    LABEL[4] = l_('Content Manager')
147
+    LABEL[8] = l_('Workspace Manager')
148
     #
148
     #
149
     # STYLE = dict()
149
     # STYLE = dict()
150
     # STYLE[0] = ''
150
     # STYLE[0] = ''
162
     #
162
     #
163
     #
163
     #
164
     # @property
164
     # @property
165
-    # def icon(self):
165
+    # def fa_icon(self):
166
     #     return UserRoleInWorkspace.ICON[self.role]
166
     #     return UserRoleInWorkspace.ICON[self.role]
167
     #
167
     #
168
     # @property
168
     # @property
169
     # def style(self):
169
     # def style(self):
170
     #     return UserRoleInWorkspace.STYLE[self.role]
170
     #     return UserRoleInWorkspace.STYLE[self.role]
171
     #
171
     #
172
-    # def role_as_label(self):
173
-    #     return UserRoleInWorkspace.LABEL[self.role]
172
+
173
+    def role_as_label(self):
174
+        return UserRoleInWorkspace.LABEL[self.role]
174
 
175
 
175
     @classmethod
176
     @classmethod
176
     def get_all_role_values(cls) -> typing.List[int]:
177
     def get_all_role_values(cls) -> typing.List[int]:
199
     def __init__(self, role_id):
200
     def __init__(self, role_id):
200
         self.role_type_id = role_id
201
         self.role_type_id = role_id
201
         # TODO - G.M - 10-04-2018 - [Cleanup] Drop this
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
         # self.role_label = UserRoleInWorkspace.LABEL[role_id]
204
         # self.role_label = UserRoleInWorkspace.LABEL[role_id]
204
         # self.css_style = UserRoleInWorkspace.STYLE[role_id]
205
         # self.css_style = UserRoleInWorkspace.STYLE[role_id]
205
 
206
 
264
     def __init__(self, id):
265
     def __init__(self, id):
265
         assert id in ActionDescription.allowed_values()
266
         assert id in ActionDescription.allowed_values()
266
         self.id = id
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
         #  design template,
269
         #  design template,
269
         # find a way to not rely on this.
270
         # find a way to not rely on this.
270
         self.label = self.id
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
         # TODO - G.M - 10-04-2018 - [Cleanup] Drop this
274
         # TODO - G.M - 10-04-2018 - [Cleanup] Drop this
273
         # self.label = ActionDescription._LABELS[id]
275
         # self.label = ActionDescription._LABELS[id]
274
 
276
 
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
 # TODO - G.M - 30-05-2018 - Drop this old code when whe are sure nothing
297
 # TODO - G.M - 30-05-2018 - Drop this old code when whe are sure nothing
296
 # is lost .
298
 # is lost .
297
 
299
 
1464
         assert hasattr(type, 'id')
1466
         assert hasattr(type, 'id')
1465
         # TODO - G.M - 10-04-2018 - [Cleanup] Drop this
1467
         # TODO - G.M - 10-04-2018 - [Cleanup] Drop this
1466
         # assert hasattr(type, 'css')
1468
         # assert hasattr(type, 'css')
1467
-        # assert hasattr(type, 'icon')
1469
+        # assert hasattr(type, 'fa_icon')
1468
         # assert hasattr(type, 'label')
1470
         # assert hasattr(type, 'label')
1469
 
1471
 
1470
     def created_as_delta(self, delta_from_datetime:datetime=None):
1472
     def created_as_delta(self, delta_from_datetime:datetime=None):

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

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

1
+## -*- coding: utf-8 -*-
2
+<html>
3
+  <head>
4
+    <style>
5
+      a { color: #3465af;}
6
+      a.call-to-action {
7
+        background: #3465af;
8
+        padding: 3px 4px 5px 4px;
9
+        border: 1px solid #12438d;
10
+        font-weight: bold;
11
+        color: #FFF;
12
+        text-decoration: none;
13
+        margin-left: 5px;
14
+      }
15
+      a.call-to-action img { vertical-align: middle;}
16
+      th { vertical-align: top;}
17
+      
18
+      #content-intro-username { font-size: 1.5em; color: #666; font-weight: bold; }
19
+      #content-intro { margin: 0; border: 1em solid #DDD; border-width: 0 0 0 0em; padding: 1em 1em 1em 1em; }
20
+      #content-body { margin: 0em; border: 2em solid #DDD; border-width: 0 0 0 4em; padding: 0.5em 2em 1em 1em; }
21
+      #content-body-intro { font-size: 2em; color: #666; }
22
+      #content-body-only-title { font-size: 1.5em; }
23
+
24
+      #content-body ins { background-color: #AFA; }
25
+      #content-body del { background-color: #FAA; }
26
+
27
+
28
+      #call-to-action-button { background-color: #5CB85C; border: 1px solid #4CAE4C; color: #FFF; text-decoration: none; font-weight: bold; border-radius: 3px; font-size: 2em; padding: 4px 0.3em;}
29
+      #call-to-action-container { text-align: right; margin-top: 2em; }
30
+
31
+      #footer hr { border: 0px solid #CCC; border-top-width: 1px; width: 8em; max-width:25%; margin-left: 0;}
32
+      #footer { color: #999; margin: 4em auto auto 0.5em; }
33
+      #footer a { color: #999; }
34
+    </style>
35
+  </head>
36
+  <body style="font-family: Arial; font-size: 12px; max-width: 600px; margin: 0; padding: 0;">
37
+
38
+    <table style="width: 100%; cell-padding: 0; border-collapse: collapse; margin: 0">
39
+      <tr style="background-color: F5F5F5; border-bottom: 1px solid #CCC;" >
40
+        <td style="background-color: #666;">
41
+            <img alt="logo" src="${logo_url}" style="vertical-align: middle;">
42
+        </td>
43
+        <td style="padding: 0.5em; background-color: #666; text-align: left;">
44
+          <span style="font-size: 1.3em; color: #FFF; font-weight: bold;">
45
+            ${main_title}
46
+            &mdash;&nbsp;<span style="font-weight: bold; color: #999; font-weight: bold;">
47
+              ${status_label|n}
48
+              <img alt="status_icon" src="${status_icon_url}" style="vertical-align: middle;">
49
+            </span>
50
+        </td>
51
+      </tr>
52
+    </table>
53
+
54
+    <p id="content-intro">${content_intro|n}</p>
55
+    <div id="content-body">
56
+        <div>${content_text|n}</div>
57
+        <div href='' id="call-to-action-container">
58
+        </div>
59
+    </div>
60
+    
61
+    <div id="footer">
62
+        <p>
63
+
64
+            ${_('{user_display_name}, you receive this email because you are registered on <i>{website_title}</i> and you are <i>{user_role_label}</i> in the workspace <a href="{workspace_url}">{workspace_label}</a>.').format(user_display_name=user.display_name, user_role_label=role_label, workspace_url=workspace_url, workspace_label=workspace.label, website_title=config.WEBSITE_TITLE)|n}
65
+        </p>
66
+        <hr/>
67
+        <p>
68
+            ${_('This email was automatically sent by <i>Tracim</i>, a collaborative software developped by Algoo.')}<br/>
69
+            Algoo SAS &mdash; 340 Rue de l'Eygala, 38430 Moirans, France &mdash; <a style="text-decoration: none;" href="http://algoo.fr">www.algoo.fr</a>
70
+        </p>
71
+    </div>
72
+  </body>
73
+</html>

+ 31 - 0
tracim/templates/mail/content_update_body_text.mak View File

1
+## -*- coding: utf-8 -*-
2
+
3
+Dear ${user.display_name},
4
+
5
+This email is intended to be read as HTML content.
6
+Please configure your email client to get the best of Tracim notifications.
7
+
8
+We understand that Email was originally intended to carry raw text only.
9
+And you probably understand on your own that we are a decades after email
10
+was created ;)
11
+
12
+Hope you'll switch your mail client configuration and enjoy Tracim :)
13
+
14
+
15
+--------------------------------------------------------------------------------
16
+
17
+
18
+You receive this email because you are registered on /${config.WEBSITE_TITLE}/
19
+and you are /${role_label}/ in the workspace /${workspace.label}/
20
+
21
+----
22
+
23
+This email was automatically sent by *Tracim*,
24
+a collaborative software developped by Algoo.
25
+
26
+**Algoo SAS**
27
+340 Rue de l'Eygala
28
+38430 Moirans
29
+France
30
+http://algoo.fr
31
+

+ 88 - 0
tracim/templates/mail/created_account_body_html.mak View File

1
+## -*- coding: utf-8 -*-
2
+<html>
3
+  <head>
4
+    <style>
5
+      a { color: #3465af;}
6
+      a.call-to-action {
7
+        background: #3465af;
8
+        padding: 3px 4px 5px 4px;
9
+        border: 1px solid #12438d;
10
+        font-weight: bold;
11
+        color: #FFF;
12
+        text-decoration: none;
13
+        margin-left: 5px;
14
+      }
15
+      a.call-to-action img { vertical-align: middle;}
16
+      th { vertical-align: top;}
17
+      
18
+      #content-intro-username { font-size: 1.5em; color: #666; font-weight: bold; }
19
+      #content-intro { margin: 0; border: 1em solid #DDD; border-width: 0 0 0 0em; padding: 1em 1em 1em 1em; }
20
+      #content-body { margin: 0em; border: 2em solid #DDD; border-width: 0 0 0 4em; padding: 0.5em 2em 1em 1em; }
21
+      #content-body-intro { font-size: 2em; color: #666; }
22
+      #content-body-only-title { font-size: 1.5em; }
23
+
24
+      #content-body ins { background-color: #AFA; }
25
+      #content-body del { background-color: #FAA; }
26
+
27
+
28
+      #call-to-action-button { background-color: #5CB85C; border: 1px solid #4CAE4C; color: #FFF; text-decoration: none; font-weight: bold; border-radius: 3px; font-size: 2em; padding: 4px 0.3em;}
29
+      #call-to-action-container { text-align: right; margin-top: 2em; }
30
+
31
+      #footer hr { border: 0px solid #CCC; border-top-width: 1px; width: 8em; max-width:25%; margin-left: 0;}
32
+      #footer { color: #999; margin: 4em auto auto 0.5em; }
33
+      #footer a { color: #999; }
34
+    </style>
35
+  </head>
36
+  <body style="font-family: Arial; font-size: 12px; max-width: 600px; margin: 0; padding: 0;">
37
+
38
+    <table style="width: 100%; cell-padding: 0; border-collapse: collapse; margin: 0">
39
+      <tr style="background-color: F5F5F5; border-bottom: 1px solid #CCC;" >
40
+        <td style="background-color: #666;">
41
+            <img alt="logo" src="${logo_url}" style="vertical-align: middle;">
42
+        </td>
43
+        <td style="padding: 0.5em; background-color: #666; text-align: left;">
44
+          <span style="font-size: 1.3em; color: #FFF; font-weight: bold;">
45
+
46
+            ${config.WEBSITE_TITLE}: ${_('Created account')}
47
+
48
+          </span>
49
+        </td>
50
+      </tr>
51
+    </table>
52
+
53
+    <div id="content-body">
54
+        <div>
55
+            ${_('An administrator just create account for you on {website_title}'.format(
56
+                website_title=config.WEBSITE_TITLE
57
+            ))}
58
+
59
+            <ul>
60
+                <li>
61
+                    <b>${_('Login')}</b>: ${user.email}
62
+                </li>
63
+                <li>
64
+                    <b>${_('Password')}</b>: ${password}
65
+                </li>
66
+            </ul>
67
+
68
+        </div>
69
+        <div id="call-to-action-container">
70
+
71
+            ${_('To go to {website_title}, please click on following link'.format(
72
+                website_title=config.WEBSITE_TITLE
73
+            ))}
74
+
75
+            <span style="">
76
+                <a href="${login_url}" id='call-to-action-button'>${login_url}</a>
77
+            </span>
78
+        </div>
79
+    </div>
80
+    
81
+    <div id="footer">
82
+        <p>
83
+            ${_('This email was sent by <i>Tracim</i>, a collaborative software developped by Algoo.')}<br/>
84
+            Algoo SAS &mdash; 340 Rue de l'Eygala, 38430 Moirans, France &mdash; <a style="text-decoration: none;" href="http://algoo.fr">www.algoo.fr</a>
85
+        </p>
86
+    </div>
87
+  </body>
88
+</html>

+ 25 - 0
tracim/templates/mail/created_account_body_text.mak View File

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

+ 46 - 18
tracim/tests/__init__.py View File

1
 # -*- coding: utf-8 -*-
1
 # -*- coding: utf-8 -*-
2
 import unittest
2
 import unittest
3
+
4
+import plaster
5
+import requests
3
 import transaction
6
 import transaction
4
 from depot.manager import DepotManager
7
 from depot.manager import DepotManager
5
 from pyramid import testing
8
 from pyramid import testing
17
 from tracim.fixtures.users_and_groups import Base as BaseFixture
20
 from tracim.fixtures.users_and_groups import Base as BaseFixture
18
 from tracim.config import CFG
21
 from tracim.config import CFG
19
 from tracim.extensions import hapic
22
 from tracim.extensions import hapic
20
-from tracim import main
23
+from tracim import web
21
 from webtest import TestApp
24
 from webtest import TestApp
22
 
25
 
23
 
26
 
32
     sqlalchemy_url = 'sqlite:///tracim_test.sqlite'
35
     sqlalchemy_url = 'sqlite:///tracim_test.sqlite'
33
 
36
 
34
     def setUp(self):
37
     def setUp(self):
38
+        logger._logger.setLevel('WARNING')
35
         DepotManager._clear()
39
         DepotManager._clear()
36
-        settings = {
40
+        self.settings = {
37
             'sqlalchemy.url': self.sqlalchemy_url,
41
             'sqlalchemy.url': self.sqlalchemy_url,
38
             'user.auth_token.validity': '604800',
42
             'user.auth_token.validity': '604800',
39
             'depot_storage_dir': '/tmp/test/depot',
43
             'depot_storage_dir': '/tmp/test/depot',
42
 
46
 
43
         }
47
         }
44
         hapic.reset_context()
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
         self.testapp = TestApp(app)
60
         self.testapp = TestApp(app)
48
 
61
 
49
     def init_database(self, settings):
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
         with transaction.manager:
63
         with transaction.manager:
55
-            dbsession = get_tm_session(session_factory, transaction.manager)
64
+            dbsession = get_tm_session(self.session_factory, transaction.manager)
56
             try:
65
             try:
57
-                fixtures_loader = FixturesLoader(dbsession, app_config)
66
+                fixtures_loader = FixturesLoader(dbsession, self.app_config)
58
                 fixtures_loader.loads(self.fixtures)
67
                 fixtures_loader.loads(self.fixtures)
59
                 transaction.commit()
68
                 transaction.commit()
60
                 print("Database initialized.")
69
                 print("Database initialized.")
87
         self.engine = get_engine(settings)
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
 class BaseTest(unittest.TestCase):
105
 class BaseTest(unittest.TestCase):
91
     """
106
     """
92
     Pyramid default test.
107
     Pyramid default test.
93
     """
108
     """
94
 
109
 
110
+    config_uri = 'tests_configs.ini'
111
+    config_section = 'base_test'
112
+
95
     def setUp(self):
113
     def setUp(self):
114
+        logger._logger.setLevel('WARNING')
96
         logger.debug(self, 'Setup Test...')
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
         self.config.include('tracim.models')
121
         self.config.include('tracim.models')
106
         DepotManager._clear()
122
         DepotManager._clear()
107
         DepotManager.configure(
123
         DepotManager.configure(
226
             owner=user
242
             owner=user
227
         )
243
         )
228
         return thread
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
 # -*- coding: utf-8 -*-
1
 # -*- coding: utf-8 -*-
2
 import os
2
 import os
3
 import subprocess
3
 import subprocess
4
-
4
+import pytest
5
 import tracim
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
         Test listing of tracimcli command: Tracim commands must be listed
24
         Test listing of tracimcli command: Tracim commands must be listed
12
         :return:
25
         :return:
20
         assert output.find('user update') > 0
33
         assert output.find('user update') > 0
21
         assert output.find('db init') > 0
34
         assert output.find('db init') > 0
22
         assert output.find('db delete') > 0
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

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

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

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

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

37
         assert sidebar_entry['label'] == 'Dashboard'
37
         assert sidebar_entry['label'] == 'Dashboard'
38
         assert sidebar_entry['route'] == '/#/workspaces/1/dashboard'  # nopep8
38
         assert sidebar_entry['route'] == '/#/workspaces/1/dashboard'  # nopep8
39
         assert sidebar_entry['hexcolor'] == "#252525"
39
         assert sidebar_entry['hexcolor'] == "#252525"
40
-        assert sidebar_entry['icon'] == ""
40
+        assert sidebar_entry['fa_icon'] == ""
41
 
41
 
42
         sidebar_entry = workspace['sidebar_entries'][1]
42
         sidebar_entry = workspace['sidebar_entries'][1]
43
         assert sidebar_entry['slug'] == 'contents/all'
43
         assert sidebar_entry['slug'] == 'contents/all'
44
         assert sidebar_entry['label'] == 'All Contents'
44
         assert sidebar_entry['label'] == 'All Contents'
45
         assert sidebar_entry['route'] == "/#/workspaces/1/contents"  # nopep8
45
         assert sidebar_entry['route'] == "/#/workspaces/1/contents"  # nopep8
46
         assert sidebar_entry['hexcolor'] == "#fdfdfd"
46
         assert sidebar_entry['hexcolor'] == "#fdfdfd"
47
-        assert sidebar_entry['icon'] == ""
47
+        assert sidebar_entry['fa_icon'] == ""
48
 
48
 
49
         sidebar_entry = workspace['sidebar_entries'][2]
49
         sidebar_entry = workspace['sidebar_entries'][2]
50
         assert sidebar_entry['slug'] == 'contents/htmlpage'
50
         assert sidebar_entry['slug'] == 'contents/htmlpage'
51
         assert sidebar_entry['label'] == 'Text Documents'
51
         assert sidebar_entry['label'] == 'Text Documents'
52
         assert sidebar_entry['route'] == '/#/workspaces/1/contents?type=htmlpage'  # nopep8
52
         assert sidebar_entry['route'] == '/#/workspaces/1/contents?type=htmlpage'  # nopep8
53
         assert sidebar_entry['hexcolor'] == "#3f52e3"
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
         sidebar_entry = workspace['sidebar_entries'][3]
56
         sidebar_entry = workspace['sidebar_entries'][3]
57
         assert sidebar_entry['slug'] == 'contents/markdownpluspage'
57
         assert sidebar_entry['slug'] == 'contents/markdownpluspage'
58
         assert sidebar_entry['label'] == 'Markdown Plus Documents'
58
         assert sidebar_entry['label'] == 'Markdown Plus Documents'
59
         assert sidebar_entry['route'] == "/#/workspaces/1/contents?type=markdownpluspage"    # nopep8
59
         assert sidebar_entry['route'] == "/#/workspaces/1/contents?type=markdownpluspage"    # nopep8
60
         assert sidebar_entry['hexcolor'] == "#f12d2d"
60
         assert sidebar_entry['hexcolor'] == "#f12d2d"
61
-        assert sidebar_entry['icon'] == "file-code"
61
+        assert sidebar_entry['fa_icon'] == "file-code"
62
 
62
 
63
         sidebar_entry = workspace['sidebar_entries'][4]
63
         sidebar_entry = workspace['sidebar_entries'][4]
64
         assert sidebar_entry['slug'] == 'contents/files'
64
         assert sidebar_entry['slug'] == 'contents/files'
65
         assert sidebar_entry['label'] == 'Files'
65
         assert sidebar_entry['label'] == 'Files'
66
         assert sidebar_entry['route'] == "/#/workspaces/1/contents?type=file"  # nopep8
66
         assert sidebar_entry['route'] == "/#/workspaces/1/contents?type=file"  # nopep8
67
         assert sidebar_entry['hexcolor'] == "#FF9900"
67
         assert sidebar_entry['hexcolor'] == "#FF9900"
68
-        assert sidebar_entry['icon'] == "paperclip"
68
+        assert sidebar_entry['fa_icon'] == "paperclip"
69
 
69
 
70
         sidebar_entry = workspace['sidebar_entries'][5]
70
         sidebar_entry = workspace['sidebar_entries'][5]
71
         assert sidebar_entry['slug'] == 'contents/threads'
71
         assert sidebar_entry['slug'] == 'contents/threads'
72
         assert sidebar_entry['label'] == 'Threads'
72
         assert sidebar_entry['label'] == 'Threads'
73
         assert sidebar_entry['route'] == "/#/workspaces/1/contents?type=thread"  # nopep8
73
         assert sidebar_entry['route'] == "/#/workspaces/1/contents?type=thread"  # nopep8
74
         assert sidebar_entry['hexcolor'] == "#ad4cf9"
74
         assert sidebar_entry['hexcolor'] == "#ad4cf9"
75
-        assert sidebar_entry['icon'] == "comments-o"
75
+        assert sidebar_entry['fa_icon'] == "comments-o"
76
 
76
 
77
         sidebar_entry = workspace['sidebar_entries'][6]
77
         sidebar_entry = workspace['sidebar_entries'][6]
78
         assert sidebar_entry['slug'] == 'calendar'
78
         assert sidebar_entry['slug'] == 'calendar'
79
         assert sidebar_entry['label'] == 'Calendar'
79
         assert sidebar_entry['label'] == 'Calendar'
80
         assert sidebar_entry['route'] == "/#/workspaces/1/calendar"  # nopep8
80
         assert sidebar_entry['route'] == "/#/workspaces/1/calendar"  # nopep8
81
         assert sidebar_entry['hexcolor'] == "#757575"
81
         assert sidebar_entry['hexcolor'] == "#757575"
82
-        assert sidebar_entry['icon'] == "calendar-alt"
82
+        assert sidebar_entry['fa_icon'] == "calendar-alt"
83
 
83
 
84
     def test_api__get_user_workspaces__err_403__unallowed_user(self):
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
         assert sidebar_entry['label'] == 'Dashboard'
38
         assert sidebar_entry['label'] == 'Dashboard'
39
         assert sidebar_entry['route'] == '/#/workspaces/1/dashboard'  # nopep8
39
         assert sidebar_entry['route'] == '/#/workspaces/1/dashboard'  # nopep8
40
         assert sidebar_entry['hexcolor'] == "#252525"
40
         assert sidebar_entry['hexcolor'] == "#252525"
41
-        assert sidebar_entry['icon'] == ""
41
+        assert sidebar_entry['fa_icon'] == ""
42
 
42
 
43
         sidebar_entry = workspace['sidebar_entries'][1]
43
         sidebar_entry = workspace['sidebar_entries'][1]
44
         assert sidebar_entry['slug'] == 'contents/all'
44
         assert sidebar_entry['slug'] == 'contents/all'
45
         assert sidebar_entry['label'] == 'All Contents'
45
         assert sidebar_entry['label'] == 'All Contents'
46
         assert sidebar_entry['route'] == "/#/workspaces/1/contents"  # nopep8
46
         assert sidebar_entry['route'] == "/#/workspaces/1/contents"  # nopep8
47
         assert sidebar_entry['hexcolor'] == "#fdfdfd"
47
         assert sidebar_entry['hexcolor'] == "#fdfdfd"
48
-        assert sidebar_entry['icon'] == ""
48
+        assert sidebar_entry['fa_icon'] == ""
49
 
49
 
50
         sidebar_entry = workspace['sidebar_entries'][2]
50
         sidebar_entry = workspace['sidebar_entries'][2]
51
         assert sidebar_entry['slug'] == 'contents/htmlpage'
51
         assert sidebar_entry['slug'] == 'contents/htmlpage'
52
         assert sidebar_entry['label'] == 'Text Documents'
52
         assert sidebar_entry['label'] == 'Text Documents'
53
         assert sidebar_entry['route'] == '/#/workspaces/1/contents?type=htmlpage'  # nopep8
53
         assert sidebar_entry['route'] == '/#/workspaces/1/contents?type=htmlpage'  # nopep8
54
         assert sidebar_entry['hexcolor'] == "#3f52e3"
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
         sidebar_entry = workspace['sidebar_entries'][3]
57
         sidebar_entry = workspace['sidebar_entries'][3]
58
         assert sidebar_entry['slug'] == 'contents/markdownpluspage'
58
         assert sidebar_entry['slug'] == 'contents/markdownpluspage'
59
         assert sidebar_entry['label'] == 'Markdown Plus Documents'
59
         assert sidebar_entry['label'] == 'Markdown Plus Documents'
60
         assert sidebar_entry['route'] == "/#/workspaces/1/contents?type=markdownpluspage"    # nopep8
60
         assert sidebar_entry['route'] == "/#/workspaces/1/contents?type=markdownpluspage"    # nopep8
61
         assert sidebar_entry['hexcolor'] == "#f12d2d"
61
         assert sidebar_entry['hexcolor'] == "#f12d2d"
62
-        assert sidebar_entry['icon'] == "file-code"
62
+        assert sidebar_entry['fa_icon'] == "file-code"
63
 
63
 
64
         sidebar_entry = workspace['sidebar_entries'][4]
64
         sidebar_entry = workspace['sidebar_entries'][4]
65
         assert sidebar_entry['slug'] == 'contents/files'
65
         assert sidebar_entry['slug'] == 'contents/files'
66
         assert sidebar_entry['label'] == 'Files'
66
         assert sidebar_entry['label'] == 'Files'
67
         assert sidebar_entry['route'] == "/#/workspaces/1/contents?type=file"  # nopep8
67
         assert sidebar_entry['route'] == "/#/workspaces/1/contents?type=file"  # nopep8
68
         assert sidebar_entry['hexcolor'] == "#FF9900"
68
         assert sidebar_entry['hexcolor'] == "#FF9900"
69
-        assert sidebar_entry['icon'] == "paperclip"
69
+        assert sidebar_entry['fa_icon'] == "paperclip"
70
 
70
 
71
         sidebar_entry = workspace['sidebar_entries'][5]
71
         sidebar_entry = workspace['sidebar_entries'][5]
72
         assert sidebar_entry['slug'] == 'contents/threads'
72
         assert sidebar_entry['slug'] == 'contents/threads'
73
         assert sidebar_entry['label'] == 'Threads'
73
         assert sidebar_entry['label'] == 'Threads'
74
         assert sidebar_entry['route'] == "/#/workspaces/1/contents?type=thread"  # nopep8
74
         assert sidebar_entry['route'] == "/#/workspaces/1/contents?type=thread"  # nopep8
75
         assert sidebar_entry['hexcolor'] == "#ad4cf9"
75
         assert sidebar_entry['hexcolor'] == "#ad4cf9"
76
-        assert sidebar_entry['icon'] == "comments-o"
76
+        assert sidebar_entry['fa_icon'] == "comments-o"
77
 
77
 
78
         sidebar_entry = workspace['sidebar_entries'][6]
78
         sidebar_entry = workspace['sidebar_entries'][6]
79
         assert sidebar_entry['slug'] == 'calendar'
79
         assert sidebar_entry['slug'] == 'calendar'
80
         assert sidebar_entry['label'] == 'Calendar'
80
         assert sidebar_entry['label'] == 'Calendar'
81
         assert sidebar_entry['route'] == "/#/workspaces/1/calendar"  # nopep8
81
         assert sidebar_entry['route'] == "/#/workspaces/1/calendar"  # nopep8
82
         assert sidebar_entry['hexcolor'] == "#757575"
82
         assert sidebar_entry['hexcolor'] == "#757575"
83
-        assert sidebar_entry['icon'] == "calendar-alt"
83
+        assert sidebar_entry['fa_icon'] == "calendar-alt"
84
 
84
 
85
     def test_api__get_workspace__err_403__unallowed_user(self) -> None:
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
                   group_api.get_one(Group.TIM_MANAGER),
116
                   group_api.get_one(Group.TIM_MANAGER),
117
                   group_api.get_one(Group.TIM_ADMIN)]
117
                   group_api.get_one(Group.TIM_ADMIN)]
118
 
118
 
119
-        user = uapi.create_user(email='this.is@user',
120
-                                groups=groups, save_now=True)
119
+        user = uapi.create_minimal_user(email='this.is@user',
120
+                                        groups=groups, save_now=True)
121
         workspace = WorkspaceApi(
121
         workspace = WorkspaceApi(
122
             current_user=user,
122
             current_user=user,
123
             session=self.session,
123
             session=self.session,
210
                   group_api.get_one(Group.TIM_MANAGER),
210
                   group_api.get_one(Group.TIM_MANAGER),
211
                   group_api.get_one(Group.TIM_ADMIN)]
211
                   group_api.get_one(Group.TIM_ADMIN)]
212
 
212
 
213
-        user = uapi.create_user(email='this.is@user',
214
-                                groups=groups, save_now=True)
213
+        user = uapi.create_minimal_user(
214
+            email='this.is@user',
215
+            groups=groups,
216
+            save_now=True,
217
+        )
215
         workspace_api = WorkspaceApi(
218
         workspace_api = WorkspaceApi(
216
             current_user=user,
219
             current_user=user,
217
             session=self.session,
220
             session=self.session,
315
                   group_api.get_one(Group.TIM_MANAGER),
318
                   group_api.get_one(Group.TIM_MANAGER),
316
                   group_api.get_one(Group.TIM_ADMIN)]
319
                   group_api.get_one(Group.TIM_ADMIN)]
317
 
320
 
318
-        user = uapi.create_user(
321
+        user = uapi.create_minimal_user(
319
             email='this.is@user',
322
             email='this.is@user',
320
             groups=groups,
323
             groups=groups,
321
             save_now=True
324
             save_now=True
381
                   group_api.get_one(Group.TIM_MANAGER),
384
                   group_api.get_one(Group.TIM_MANAGER),
382
                   group_api.get_one(Group.TIM_ADMIN)]
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
         workspace = WorkspaceApi(
389
         workspace = WorkspaceApi(
387
             current_user=user,
390
             current_user=user,
388
             session=self.session,
391
             session=self.session,
456
                   group_api.get_one(Group.TIM_MANAGER),
459
                   group_api.get_one(Group.TIM_MANAGER),
457
                   group_api.get_one(Group.TIM_ADMIN)]
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
         workspace = WorkspaceApi(
465
         workspace = WorkspaceApi(
463
             current_user=user,
466
             current_user=user,
496
                   group_api.get_one(Group.TIM_MANAGER),
499
                   group_api.get_one(Group.TIM_MANAGER),
497
                   group_api.get_one(Group.TIM_ADMIN)]
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
         workspace = WorkspaceApi(
505
         workspace = WorkspaceApi(
503
             current_user=user,
506
             current_user=user,
540
                   group_api.get_one(Group.TIM_MANAGER),
543
                   group_api.get_one(Group.TIM_MANAGER),
541
                   group_api.get_one(Group.TIM_ADMIN)]
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
         workspace = WorkspaceApi(
549
         workspace = WorkspaceApi(
547
             current_user=user,
550
             current_user=user,
584
                   group_api.get_one(Group.TIM_MANAGER),
587
                   group_api.get_one(Group.TIM_MANAGER),
585
                   group_api.get_one(Group.TIM_ADMIN)]
588
                   group_api.get_one(Group.TIM_ADMIN)]
586
 
589
 
587
-        user = uapi.create_user(
590
+        user = uapi.create_minimal_user(
588
             email='user1@user',
591
             email='user1@user',
589
             groups=groups,
592
             groups=groups,
590
             save_now=True
593
             save_now=True
591
         )
594
         )
592
-        user2 = uapi.create_user(
595
+        user2 = uapi.create_minimal_user(
593
             email='user2@user',
596
             email='user2@user',
594
             groups=groups,
597
             groups=groups,
595
             save_now=True
598
             save_now=True
703
                   group_api.get_one(Group.TIM_MANAGER),
706
                   group_api.get_one(Group.TIM_MANAGER),
704
                   group_api.get_one(Group.TIM_ADMIN)]
707
                   group_api.get_one(Group.TIM_ADMIN)]
705
 
708
 
706
-        user = uapi.create_user(
709
+        user = uapi.create_minimal_user(
707
             email='user1@user',
710
             email='user1@user',
708
             groups=groups,
711
             groups=groups,
709
             save_now=True
712
             save_now=True
710
         )
713
         )
711
-        user2 = uapi.create_user(
714
+        user2 = uapi.create_minimal_user(
712
             email='user2@user',
715
             email='user2@user',
713
             groups=groups,
716
             groups=groups,
714
             save_now=True
717
             save_now=True
820
                   group_api.get_one(Group.TIM_MANAGER),
823
                   group_api.get_one(Group.TIM_MANAGER),
821
                   group_api.get_one(Group.TIM_ADMIN)]
824
                   group_api.get_one(Group.TIM_ADMIN)]
822
 
825
 
823
-        user = uapi.create_user(
826
+        user = uapi.create_minimal_user(
824
             email='user1@user',
827
             email='user1@user',
825
             groups=groups,
828
             groups=groups,
826
             save_now=True,
829
             save_now=True,
827
         )
830
         )
828
-        user2 = uapi.create_user(
831
+        user2 = uapi.create_minimal_user(
829
             email='user2@user',
832
             email='user2@user',
830
             groups=groups,
833
             groups=groups,
831
             save_now=True
834
             save_now=True
925
                   group_api.get_one(Group.TIM_MANAGER),
928
                   group_api.get_one(Group.TIM_MANAGER),
926
                   group_api.get_one(Group.TIM_ADMIN)]
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
         wapi = WorkspaceApi(
936
         wapi = WorkspaceApi(
934
             current_user=user_a,
937
             current_user=user_a,
1032
                   group_api.get_one(Group.TIM_MANAGER),
1035
                   group_api.get_one(Group.TIM_MANAGER),
1033
                   group_api.get_one(Group.TIM_ADMIN)]
1036
                   group_api.get_one(Group.TIM_ADMIN)]
1034
 
1037
 
1035
-        user_a = uapi.create_user(
1038
+        user_a = uapi.create_minimal_user(
1036
             email='this.is@user',
1039
             email='this.is@user',
1037
             groups=groups,
1040
             groups=groups,
1038
             save_now=True
1041
             save_now=True
1039
         )
1042
         )
1040
-        user_b = uapi.create_user(
1043
+        user_b = uapi.create_minimal_user(
1041
             email='this.is@another.user',
1044
             email='this.is@another.user',
1042
             groups=groups,
1045
             groups=groups,
1043
             save_now=True
1046
             save_now=True
1105
                   group_api.get_one(Group.TIM_MANAGER),
1108
                   group_api.get_one(Group.TIM_MANAGER),
1106
                   group_api.get_one(Group.TIM_ADMIN)]
1109
                   group_api.get_one(Group.TIM_ADMIN)]
1107
 
1110
 
1108
-        user_a = uapi.create_user(
1111
+        user_a = uapi.create_minimal_user(
1109
             email='this.is@user',
1112
             email='this.is@user',
1110
             groups=groups,
1113
             groups=groups,
1111
             save_now=True
1114
             save_now=True
1112
         )
1115
         )
1113
-        user_b = uapi.create_user(
1116
+        user_b = uapi.create_minimal_user(
1114
             email='this.is@another.user',
1117
             email='this.is@another.user',
1115
             groups=groups,
1118
             groups=groups,
1116
             save_now=True
1119
             save_now=True
1204
                   group_api.get_one(Group.TIM_MANAGER),
1207
                   group_api.get_one(Group.TIM_MANAGER),
1205
                   group_api.get_one(Group.TIM_ADMIN)]
1208
                   group_api.get_one(Group.TIM_ADMIN)]
1206
 
1209
 
1207
-        user1 = uapi.create_user(
1210
+        user1 = uapi.create_minimal_user(
1208
             email='this.is@user',
1211
             email='this.is@user',
1209
             groups=groups,
1212
             groups=groups,
1210
             save_now=True
1213
             save_now=True
1222
         
1225
         
1223
         wid = workspace.workspace_id
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
         uapi.save(user2)
1229
         uapi.save(user2)
1228
 
1230
 
1229
         RoleApi(
1231
         RoleApi(
1333
                   group_api.get_one(Group.TIM_MANAGER),
1335
                   group_api.get_one(Group.TIM_MANAGER),
1334
                   group_api.get_one(Group.TIM_ADMIN)]
1336
                   group_api.get_one(Group.TIM_ADMIN)]
1335
 
1337
 
1336
-        user1 = uapi.create_user(
1338
+        user1 = uapi.create_minimal_user(
1337
             email='this.is@user',
1339
             email='this.is@user',
1338
             groups=groups,
1340
             groups=groups,
1339
             save_now=True,
1341
             save_now=True,
1348
             save_now=True
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
         uapi.save(user2)
1354
         uapi.save(user2)
1354
 
1355
 
1355
         RoleApi(
1356
         RoleApi(
1414
                   group_api.get_one(Group.TIM_MANAGER),
1415
                   group_api.get_one(Group.TIM_MANAGER),
1415
                   group_api.get_one(Group.TIM_ADMIN)]
1416
                   group_api.get_one(Group.TIM_ADMIN)]
1416
 
1417
 
1417
-        user1 = uapi.create_user(
1418
+        user1 = uapi.create_minimal_user(
1418
             email='this.is@user',
1419
             email='this.is@user',
1419
             groups=groups,
1420
             groups=groups,
1420
             save_now=True
1421
             save_now=True
1431
         )
1432
         )
1432
         wid = workspace.workspace_id
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
         uapi.save(user2)
1436
         uapi.save(user2)
1437
 
1437
 
1438
         RoleApi(
1438
         RoleApi(
1539
                   group_api.get_one(Group.TIM_MANAGER),
1539
                   group_api.get_one(Group.TIM_MANAGER),
1540
                   group_api.get_one(Group.TIM_ADMIN)]
1540
                   group_api.get_one(Group.TIM_ADMIN)]
1541
 
1541
 
1542
-        user1 = uapi.create_user(
1542
+        user1 = uapi.create_minimal_user(
1543
             email='this.is@user',
1543
             email='this.is@user',
1544
             groups=groups,
1544
             groups=groups,
1545
             save_now=True,
1545
             save_now=True,
1555
             save_now=True
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
         uapi.save(user2)
1559
         uapi.save(user2)
1561
 
1560
 
1562
         RoleApi(
1561
         RoleApi(
1627
                   group_api.get_one(Group.TIM_MANAGER),
1626
                   group_api.get_one(Group.TIM_MANAGER),
1628
                   group_api.get_one(Group.TIM_ADMIN)]
1627
                   group_api.get_one(Group.TIM_ADMIN)]
1629
 
1628
 
1630
-        user1 = uapi.create_user(
1629
+        user1 = uapi.create_minimal_user(
1631
             email='this.is@user',
1630
             email='this.is@user',
1632
             groups=groups,
1631
             groups=groups,
1633
             save_now=True
1632
             save_now=True
1645
         )
1644
         )
1646
         wid = workspace.workspace_id
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
         uapi.save(user2)
1648
         uapi.save(user2)
1651
 
1649
 
1652
         RoleApi(
1650
         RoleApi(
1784
                   group_api.get_one(Group.TIM_MANAGER),
1782
                   group_api.get_one(Group.TIM_MANAGER),
1785
                   group_api.get_one(Group.TIM_ADMIN)]
1783
                   group_api.get_one(Group.TIM_ADMIN)]
1786
 
1784
 
1787
-        user1 = uapi.create_user(
1785
+        user1 = uapi.create_minimal_user(
1788
             email='this.is@user',
1786
             email='this.is@user',
1789
             groups=groups,
1787
             groups=groups,
1790
             save_now=True
1788
             save_now=True
1802
         )
1800
         )
1803
         wid = workspace.workspace_id
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
         uapi.save(user2)
1804
         uapi.save(user2)
1808
 
1805
 
1809
         RoleApi(
1806
         RoleApi(
1942
                   group_api.get_one(Group.TIM_MANAGER),
1939
                   group_api.get_one(Group.TIM_MANAGER),
1943
                   group_api.get_one(Group.TIM_ADMIN)]
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
         workspace = WorkspaceApi(
1945
         workspace = WorkspaceApi(
1949
             current_user=user,
1946
             current_user=user,
1998
                   group_api.get_one(Group.TIM_MANAGER),
1995
                   group_api.get_one(Group.TIM_MANAGER),
1999
                   group_api.get_one(Group.TIM_ADMIN)]
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
         workspace = WorkspaceApi(
2001
         workspace = WorkspaceApi(
2005
             current_user=user,
2002
             current_user=user,
2054
                   group_api.get_one(Group.TIM_MANAGER),
2051
                   group_api.get_one(Group.TIM_MANAGER),
2055
                   group_api.get_one(Group.TIM_ADMIN)]
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
         workspace = WorkspaceApi(
2057
         workspace = WorkspaceApi(
2061
             current_user=user,
2058
             current_user=user,

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

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

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

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

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

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

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

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

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

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

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
 # -*- coding: utf-8 -*-
1
 # -*- coding: utf-8 -*-
2
-import transaction
3
 
2
 
4
-from tracim.tests import eq_
3
+import transaction
5
 from tracim.tests import BaseTest
4
 from tracim.tests import BaseTest
6
-
7
 from tracim.models.auth import User
5
 from tracim.models.auth import User
8
 
6
 
9
 
7
 
10
 class TestUserModel(BaseTest):
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
         self.session.flush()
13
         self.session.flush()
14
         transaction.commit()
14
         transaction.commit()
15
         name = 'Damien'
15
         name = 'Damien'
23
         self.session.flush()
23
         self.session.flush()
24
         transaction.commit()
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
         # Check bug #70 fixed
69
         # Check bug #70 fixed
34
         # http://tracim.org/workspaces/4/folders/5/threads/70
70
         # http://tracim.org/workspaces/4/folders/5/threads/70
35
 
71
 
40
         user.display_name = name
76
         user.display_name = name
41
         user.email = email
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
                     'which must be replaced on backend size '
173
                     'which must be replaced on backend size '
174
                     '(the route must be ready-to-use)'
174
                     '(the route must be ready-to-use)'
175
     )
175
     )
176
-    icon = marshmallow.fields.String(
176
+    fa_icon = marshmallow.fields.String(
177
         example='file-text-o',
177
         example='file-text-o',
178
         description='CSS class of the icon. Example: file-o for using Fontawesome file-text-o icon',  # nopep8
178
         description='CSS class of the icon. Example: file-o for using Fontawesome file-text-o icon',  # nopep8
179
     )
179
     )
229
 class ApplicationSchema(marshmallow.Schema):
229
 class ApplicationSchema(marshmallow.Schema):
230
     label = marshmallow.fields.String(example='Calendar')
230
     label = marshmallow.fields.String(example='Calendar')
231
     slug = marshmallow.fields.String(example='calendar')
231
     slug = marshmallow.fields.String(example='calendar')
232
-    icon = marshmallow.fields.String(
232
+    fa_icon = marshmallow.fields.String(
233
         example='file-o',
233
         example='file-o',
234
         description='CSS class of the icon. Example: file-o for using Fontawesome file-o icon',  # nopep8
234
         description='CSS class of the icon. Example: file-o for using Fontawesome file-o icon',  # nopep8
235
     )
235
     )
261
         validate=OneOf([status.value for status in GlobalStatus]),
261
         validate=OneOf([status.value for status in GlobalStatus]),
262
     )
262
     )
263
     label = marshmallow.fields.String(example='Open')
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
     hexcolor = marshmallow.fields.String(example='#0000FF')
265
     hexcolor = marshmallow.fields.String(example='#0000FF')
266
 
266
 
267
 
267
 
270
         example='pagehtml',
270
         example='pagehtml',
271
         validate=OneOf([content.slug for content in CONTENT_DEFAULT_TYPE]),
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
         example='fa-file-text-o',
274
         example='fa-file-text-o',
275
         description='CSS class of the icon. Example: file-o for using Fontawesome file-o icon',  # nopep8
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
 from tracim.views.core_api.schemas import NoContentSchema
13
 from tracim.views.core_api.schemas import NoContentSchema
14
 from tracim.views.core_api.schemas import LoginOutputHeaders
14
 from tracim.views.core_api.schemas import LoginOutputHeaders
15
 from tracim.views.core_api.schemas import BasicAuthSchema
15
 from tracim.views.core_api.schemas import BasicAuthSchema
16
-from tracim.exceptions import NotAuthentificated
16
+from tracim.exceptions import NotAuthenticated
17
 from tracim.exceptions import AuthenticationFailed
17
 from tracim.exceptions import AuthenticationFailed
18
 
18
 
19
 
19
 
52
         return
52
         return
53
 
53
 
54
     @hapic.with_api_doc()
54
     @hapic.with_api_doc()
55
-    @hapic.handle_exception(NotAuthentificated, HTTPStatus.UNAUTHORIZED)
55
+    @hapic.handle_exception(NotAuthenticated, HTTPStatus.UNAUTHORIZED)
56
     @hapic.output_body(UserSchema(),)
56
     @hapic.output_body(UserSchema(),)
57
     def whoami(self, context, request: TracimRequest, hapic_data=None):
57
     def whoami(self, context, request: TracimRequest, hapic_data=None):
58
         """
58
         """

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

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

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

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

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

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

+ 17 - 0
wsgi/__init__.py View File

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

+ 2 - 4
wsgi/web.py View File

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

+ 2 - 7
wsgi/webdav.py View File

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