Przeglądaj źródła

dirty misc commit including:

Damien ACCORSI 10 lat temu
rodzic
commit
0636050fa1

+ 2 - 1
doc/database/tracim-init-database.new.sql Wyświetl plik

@@ -152,7 +152,8 @@ ALTER SEQUENCE seq__users__user_id OWNED BY users.user_id;
152 152
 CREATE TABLE user_workspace (
153 153
     user_id integer NOT NULL,
154 154
     workspace_id integer NOT NULL,
155
-    role integer
155
+    role integer,
156
+    do_notify boolean DEFAULT FALSE NOT NULL
156 157
 );
157 158
 
158 159
 CREATE TABLE workspaces (

+ 2 - 0
doc/database/tracim-migrate-to-first-stable-release.sql Wyświetl plik

@@ -65,6 +65,8 @@ CREATE TABLE pod_user_workspace
65 65
   user_id integer NOT NULL,
66 66
   workspace_id integer NOT NULL,
67 67
   role integer,
68
+  do_notify boolean DEFAULT FALSE NOT NULL,
69
+
68 70
   CONSTRAINT pk__pod_user_workspace__user_id__workspace_id PRIMARY KEY (user_id , workspace_id ),
69 71
   CONSTRAINT fk__pod_user_workspace__user_id FOREIGN KEY (user_id)
70 72
       REFERENCES pod_user (user_id) MATCH SIMPLE

+ 67 - 3
tracim/tracim/config/app_cfg.py Wyświetl plik

@@ -13,13 +13,16 @@ convert them into boolean, for example, you should use the
13 13
  
14 14
 """
15 15
 
16
+import tg
16 17
 from tg.configuration import AppConfig
17
-from tgext.pluggable import plug, replace_template
18
+from tgext.pluggable import plug
19
+from tgext.pluggable import replace_template
20
+
18 21
 from tg.i18n import lazy_ugettext as l_
19 22
 
20 23
 import tracim
21 24
 from tracim import model
22
-from tracim.lib import app_globals, helpers
25
+from tracim.lib.base import logger
23 26
 
24 27
 base_config = AppConfig()
25 28
 base_config.renderers = []
@@ -131,6 +134,12 @@ Please click this link to reset your password:
131 134
 If you no longer wish to make the above change, or if you did not initiate this request, please disregard and/or delete this e-mail.
132 135
 ''')
133 136
 
137
+#######
138
+#
139
+# INFO - D.A. - 2014-10-31
140
+# The following code is a dirty way to integrate translation for resetpassword tgapp in tracim
141
+# TODO - Integrate these translations into tgapp-resetpassword
142
+#
134 143
 
135 144
 l_('New password')
136 145
 l_('Confirm new password')
@@ -152,4 +161,59 @@ Please click this link to reset your password:
152 161
 %(password_reset_link)s
153 162
 
154 163
 If you no longer wish to make the above change, or if you did not initiate this request, please disregard and/or delete this e-mail.
155
-''')
164
+''')
165
+
166
+class CFG(object):
167
+    """
168
+    Singleton used for easy access to config file parameters
169
+    """
170
+
171
+    _instance = None
172
+
173
+    @classmethod
174
+    def get_instance(cls) -> 'CFG':
175
+        if not CFG._instance:
176
+            CFG._instance = CFG()
177
+        return CFG._instance
178
+
179
+    def __init__(self):
180
+        self.WEBSITE_TITLE = tg.config.get('website.title', 'TRACIM')
181
+        self.WEBSITE_HOME_TITLE_COLOR = tg.config.get('website.title.color', '#555')
182
+        self.WEBSITE_HOME_IMAGE_URL = tg.lurl('/assets/img/home_illustration.jpg')
183
+        self.WEBSITE_HOME_BACKGROUND_IMAGE_URL = tg.lurl('/assets/img/bg.jpg')
184
+        self.WEBSITE_BASE_URL = tg.config.get('website.base_url')
185
+
186
+        self.EMAIL_NOTIFICATION_FROM = tg.config.get('email.notification.from')
187
+        self.EMAIL_NOTIFICATION_CONTENT_UPDATE_TEMPLATE_HTML = tg.config.get('email.notification.content_update.template.html')
188
+        self.EMAIL_NOTIFICATION_CONTENT_UPDATE_TEMPLATE_TEXT = tg.config.get('email.notification.content_update.template.text')
189
+        self.EMAIL_NOTIFICATION_CONTENT_UPDATE_SUBJECT = tg.config.get('email.notification.content_update.subject')
190
+        self.EMAIL_NOTIFICATION_PROCESSING_MODE = tg.config.get('email.notification.processing_mode')
191
+
192
+
193
+        self.EMAIL_NOTIFICATION_SMTP_SERVER = tg.config.get('email.notification.smtp.server')
194
+        self.EMAIL_NOTIFICATION_SMTP_PORT = tg.config.get('email.notification.smtp.port')
195
+        self.EMAIL_NOTIFICATION_SMTP_USER = tg.config.get('email.notification.smtp.user')
196
+        self.EMAIL_NOTIFICATION_SMTP_PASSWORD = tg.config.get('email.notification.smtp.password')
197
+
198
+
199
+    class CST(object):
200
+        ASYNC = 'ASYNC'
201
+        SYNC = 'SYNC'
202
+
203
+#######
204
+#
205
+# INFO - D.A. - 2014-11-05
206
+# Allow to process asynchronous tasks
207
+# This is used for email notifications
208
+#
209
+
210
+# import tgext.asyncjob
211
+# tgext.asyncjob.plugme(base_config)
212
+#
213
+# OR
214
+#
215
+# plug(base_config, 'tgext.asyncjob', app_globals=base_config)
216
+#
217
+# OR
218
+#
219
+plug(base_config, 'tgext.asyncjob')

+ 1 - 2
tracim/tracim/controllers/admin/__init__.py Wyświetl plik

@@ -1,8 +1,6 @@
1 1
 # -*- coding: utf-8 -*-
2 2
 
3
-from tg.i18n import lazy_ugettext as l_
4 3
 from tg.predicates import in_any_group
5
-from tg.predicates import in_group
6 4
 
7 5
 from tracim.controllers import StandardController
8 6
 from tracim.controllers.admin.workspace import WorkspaceRestController
@@ -10,6 +8,7 @@ from tracim.controllers.admin.user import UserRestController
10 8
 
11 9
 from tracim.model.auth import Group
12 10
 
11
+
13 12
 class AdminController(StandardController):
14 13
 
15 14
     allow_only = in_any_group(Group.TIM_MANAGER_GROUPNAME, Group.TIM_ADMIN_GROUPNAME)

+ 0 - 1
tracim/tracim/controllers/content.py Wyświetl plik

@@ -293,7 +293,6 @@ class UserWorkspaceFolderPageRestController(TIMWorkspaceContentRestController):
293 293
     @tg.require(current_user_is_contributor())
294 294
     @tg.expose()
295 295
     def post(self, label='', content=''):
296
-        # TODO - SECURE THIS
297 296
         workspace = tmpl_context.workspace
298 297
 
299 298
         api = ContentApi(tmpl_context.current_user)

+ 39 - 0
tracim/tracim/controllers/user.py Wyświetl plik

@@ -28,6 +28,43 @@ from tracim.model import DBSession
28 28
 from tracim.model.auth import Group, User
29 29
 from tracim.model.serializers import Context, CTX, DictLikeClass
30 30
 
31
+
32
+class UserWorkspaceRestController(TIMRestController):
33
+
34
+    def _before(self, *args, **kw):
35
+        """
36
+        Instantiate the current workspace in tg.tmpl_context
37
+        :param args:
38
+        :param kw:
39
+        :return:
40
+        """
41
+        super(self.__class__, self)._before(args, kw)
42
+
43
+        api = UserApi(tg.tmpl_context.current_user)
44
+        user_id = tmpl_context.current_user_id
45
+        user = tmpl_context.current_user
46
+
47
+    @tg.expose()
48
+    def enable_notifications(self, workspace_id):
49
+        workspace_id = int(workspace_id)
50
+        api = WorkspaceApi(tg.tmpl_context.current_user)
51
+
52
+        workspace = api.get_one(workspace_id)
53
+        api.enable_notifications(tg.tmpl_context.current_user, workspace)
54
+        tg.flash(_('Notification enabled for workspace {}').format(workspace.label))
55
+        tg.redirect(self.parent_controller.url(None, 'me'))
56
+
57
+    @tg.expose()
58
+    def disable_notifications(self, workspace_id):
59
+        workspace_id = int(workspace_id)
60
+        api = WorkspaceApi(tg.tmpl_context.current_user)
61
+
62
+        workspace = api.get_one(workspace_id)
63
+        api.disable_notifications(tg.tmpl_context.current_user, workspace)
64
+        tg.flash(_('Notification disabled for workspace {}').format(workspace.label))
65
+        tg.redirect(self.parent_controller.url(None, 'me'))
66
+
67
+
31 68
 class UserPasswordRestController(TIMRestController):
32 69
     """
33 70
      CRUD Controller allowing to manage password of a given user
@@ -85,6 +122,7 @@ class UserRestController(TIMRestController):
85 122
     """
86 123
 
87 124
     password = UserPasswordRestController()
125
+    workspaces = UserWorkspaceRestController()
88 126
 
89 127
     @classmethod
90 128
     def current_item_id_key_in_context(cls):
@@ -106,6 +144,7 @@ class UserRestController(TIMRestController):
106 144
         current_user = tmpl_context.current_user
107 145
         assert user_id==current_user.user_id
108 146
         api = UserApi(current_user)
147
+        current_user = api.get_one(current_user.user_id)
109 148
         dictified_user = Context(CTX.USER).toDict(current_user, 'user')
110 149
         current_user_content = Context(CTX.CURRENT_USER).toDict(tmpl_context.current_user)
111 150
         fake_api_content = DictLikeClass(current_user=current_user_content)

+ 4 - 8
tracim/tracim/controllers/workspace.py Wyświetl plik

@@ -3,16 +3,12 @@
3 3
 import tg
4 4
 from tg import tmpl_context
5 5
 from tg.i18n import ugettext as _
6
+from tg.predicates import not_anonymous
6 7
 
7 8
 from tracim.controllers import TIMRestController
8
-from tracim.controllers import TIMRestPathContextSetup
9
-
9
+from tracim.controllers.content import UserWorkspaceFolderRestController
10 10
 
11
-from tracim.lib import CST
12 11
 from tracim.lib.helpers import convert_id_into_instances
13
-from tracim.lib.base import BaseController
14
-from tracim.lib.user import UserApi
15
-from tracim.lib.userworkspace import RoleApi
16 12
 from tracim.lib.content import ContentApi
17 13
 from tracim.lib.workspace import WorkspaceApi
18 14
 
@@ -20,16 +16,16 @@ from tracim.model.data import NodeTreeItem
20 16
 from tracim.model.data import Content
21 17
 from tracim.model.data import ContentType
22 18
 from tracim.model.data import Workspace
23
-from tracim.model.data import UserRoleInWorkspace
24 19
 
25 20
 from tracim.model.serializers import Context, CTX, DictLikeClass
26 21
 
27
-from tracim.controllers.content import UserWorkspaceFolderRestController
28 22
 
29 23
 
30 24
 
31 25
 class UserWorkspaceRestController(TIMRestController):
32 26
 
27
+    allow_only = not_anonymous()
28
+
33 29
     folders = UserWorkspaceFolderRestController()
34 30
 
35 31
     @property

BIN
tracim/tracim/i18n/fr/LC_MESSAGES/tracim.mo Wyświetl plik


+ 161 - 84
tracim/tracim/i18n/fr/LC_MESSAGES/tracim.po Wyświetl plik

@@ -7,8 +7,8 @@ msgid ""
7 7
 msgstr ""
8 8
 "Project-Id-Version: pod 0.1\n"
9 9
 "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
10
-"POT-Creation-Date: 2014-10-30 17:34+0100\n"
11
-"PO-Revision-Date: 2014-10-30 17:37+0100\n"
10
+"POT-Creation-Date: 2014-11-06 14:37+0100\n"
11
+"PO-Revision-Date: 2014-11-06 14:38+0100\n"
12 12
 "Last-Translator: Damien Accorsi <damien.accorsi@free.fr>\n"
13 13
 "Language-Team: fr_FR <LL@li.org>\n"
14 14
 "Plural-Forms: nplurals=2; plural=(n > 1)\n"
@@ -17,12 +17,12 @@ msgstr ""
17 17
 "Content-Transfer-Encoding: 8bit\n"
18 18
 "Generated-By: Babel 1.3\n"
19 19
 
20
-#: tracim/config/app_cfg.py:124
20
+#: tracim/config/app_cfg.py:130
21 21
 msgid "Password reset request"
22 22
 msgstr "Réinitialisation du mot de passe"
23 23
 
24
-#: tracim/config/app_cfg.py:125
25
-#: tracim/config/app_cfg.py:148
24
+#: tracim/config/app_cfg.py:131
25
+#: tracim/config/app_cfg.py:160
26 26
 #, python-format
27 27
 msgid ""
28 28
 "\n"
@@ -41,43 +41,43 @@ msgstr ""
41 41
 "\n"
42 42
 "Si vous ne souhaitez plus procéder à ce changement, ou si vous n'êtes pas à l'originie de cette requête, merci d'ignorer et/ou supprimer cet e-mail.\n"
43 43
 
44
-#: tracim/config/app_cfg.py:135
44
+#: tracim/config/app_cfg.py:147
45 45
 #: tracim/templates/user_workspace_forms.mak:251
46 46
 #: tracim/templates/user_workspace_forms.mak:252
47 47
 msgid "New password"
48 48
 msgstr "Nouveau mot de passe"
49 49
 
50
-#: tracim/config/app_cfg.py:136
50
+#: tracim/config/app_cfg.py:148
51 51
 msgid "Confirm new password"
52 52
 msgstr "Confirmer le nouveau mot de passe"
53 53
 
54
-#: tracim/config/app_cfg.py:137
54
+#: tracim/config/app_cfg.py:149
55 55
 msgid "Save new password"
56 56
 msgstr "Enregistrer le nouveau mot de passe"
57 57
 
58
-#: tracim/config/app_cfg.py:138
58
+#: tracim/config/app_cfg.py:150
59 59
 #: tracim/templates/user_get_all.mak:34
60 60
 msgid "Email address"
61 61
 msgstr "Adresse email"
62 62
 
63
-#: tracim/config/app_cfg.py:139
63
+#: tracim/config/app_cfg.py:151
64 64
 msgid "Send Request"
65 65
 msgstr "Valider"
66 66
 
67
-#: tracim/config/app_cfg.py:142
67
+#: tracim/config/app_cfg.py:154
68 68
 msgid "Password reset request sent"
69 69
 msgstr "Requête de réinitialisation du mot de passe envoyée"
70 70
 
71
-#: tracim/config/app_cfg.py:143
72
-#: tracim/config/app_cfg.py:145
71
+#: tracim/config/app_cfg.py:155
72
+#: tracim/config/app_cfg.py:157
73 73
 msgid "Invalid password reset request"
74 74
 msgstr "Requête de réinitialisation du mot de passe invalide"
75 75
 
76
-#: tracim/config/app_cfg.py:144
76
+#: tracim/config/app_cfg.py:156
77 77
 msgid "Password reset request timed out"
78 78
 msgstr "Echec de la requête de réinitialisation du mot de passe"
79 79
 
80
-#: tracim/config/app_cfg.py:146
80
+#: tracim/config/app_cfg.py:158
81 81
 msgid "Password changed successfully"
82 82
 msgstr "Mot de passe changé"
83 83
 
@@ -162,39 +162,39 @@ msgstr "Fichier créé"
162 162
 msgid "Page"
163 163
 msgstr "Page"
164 164
 
165
-#: tracim/controllers/content.py:305
165
+#: tracim/controllers/content.py:304
166 166
 msgid "Page created"
167 167
 msgstr "Page créée"
168 168
 
169
-#: tracim/controllers/content.py:345
169
+#: tracim/controllers/content.py:344
170 170
 msgid "Thread"
171
-msgstr "Discussion"
171
+msgstr "Sujet"
172 172
 
173
-#: tracim/controllers/content.py:386
173
+#: tracim/controllers/content.py:385
174 174
 msgid "Thread created"
175
-msgstr "Discussion créée"
175
+msgstr "Sujet créé"
176 176
 
177
-#: tracim/controllers/content.py:465
177
+#: tracim/controllers/content.py:464
178 178
 msgid "Item moved to {}"
179 179
 msgstr "Element déplacé vers {}"
180 180
 
181
-#: tracim/controllers/content.py:467
181
+#: tracim/controllers/content.py:466
182 182
 msgid "Item moved to workspace root"
183 183
 msgstr "Element déplacé à la racine de l'espace de travail"
184 184
 
185
-#: tracim/controllers/content.py:587
185
+#: tracim/controllers/content.py:586
186 186
 msgid "Folder created"
187 187
 msgstr "Dossier créé"
188 188
 
189
-#: tracim/controllers/content.py:593
189
+#: tracim/controllers/content.py:592
190 190
 msgid "Folder not created: {}"
191 191
 msgstr "Dossier non créé : {}"
192 192
 
193
-#: tracim/controllers/content.py:628
193
+#: tracim/controllers/content.py:627
194 194
 msgid "Folder updated"
195 195
 msgstr "Dossier mis à jour"
196 196
 
197
-#: tracim/controllers/content.py:633
197
+#: tracim/controllers/content.py:632
198 198
 msgid "Folder not updated: {}"
199 199
 msgstr "Dossier non mis à jour : {}"
200 200
 
@@ -211,27 +211,35 @@ msgstr "Bonjour %s. Ravis de vous revoir !"
211 211
 msgid "Successfully logged out. We hope to see you soon!"
212 212
 msgstr "Déconnexion réussie. Nous espérons vous revoir bientôt !"
213 213
 
214
-#: tracim/controllers/user.py:64
214
+#: tracim/controllers/user.py:53
215
+msgid "Notification enabled for workspace {}"
216
+msgstr "Notifications activées pour l'espace de travail {}"
217
+
218
+#: tracim/controllers/user.py:63
219
+msgid "Notification disabled for workspace {}"
220
+msgstr "Notifications désactivées pour l'espace de travail {}"
221
+
222
+#: tracim/controllers/user.py:100
215 223
 #: tracim/controllers/admin/user.py:201
216 224
 msgid "Empty password is not allowed."
217 225
 msgstr "Le mot de passe ne doit pas être vide"
218 226
 
219
-#: tracim/controllers/user.py:68
227
+#: tracim/controllers/user.py:104
220 228
 #: tracim/controllers/admin/user.py:205
221 229
 msgid "The current password you typed is wrong"
222 230
 msgstr "Le mot de passe que vous avez tapé est erroné"
223 231
 
224
-#: tracim/controllers/user.py:72
232
+#: tracim/controllers/user.py:108
225 233
 #: tracim/controllers/admin/user.py:209
226 234
 msgid "New passwords do not match."
227 235
 msgstr "Les mots de passe ne concordent pas"
228 236
 
229
-#: tracim/controllers/user.py:78
237
+#: tracim/controllers/user.py:114
230 238
 #: tracim/controllers/admin/user.py:215
231 239
 msgid "Your password has been changed"
232 240
 msgstr "Votre mot de passe a été changé"
233 241
 
234
-#: tracim/controllers/user.py:133
242
+#: tracim/controllers/user.py:171
235 243
 msgid "profile updated."
236 244
 msgstr "Profil mis à jour"
237 245
 
@@ -356,98 +364,98 @@ msgstr "Managers globaux"
356 364
 msgid "Administrators"
357 365
 msgstr "Administrateurs"
358 366
 
359
-#: tracim/model/data.py:79
367
+#: tracim/model/data.py:80
360 368
 msgid "N/A"
361 369
 msgstr "N/A"
362 370
 
363
-#: tracim/model/data.py:80
371
+#: tracim/model/data.py:81
364 372
 #: tracim/templates/help/page-user-role-definition.mak:15
365 373
 msgid "Reader"
366 374
 msgstr "Lecteur"
367 375
 
368
-#: tracim/model/data.py:81
376
+#: tracim/model/data.py:82
369 377
 #: tracim/templates/help/page-user-role-definition.mak:19
370 378
 msgid "Contributor"
371 379
 msgstr "Contributeurs"
372 380
 
373
-#: tracim/model/data.py:82
381
+#: tracim/model/data.py:83
374 382
 #: tracim/templates/help/page-user-role-definition.mak:23
375 383
 msgid "Content Manager"
376 384
 msgstr "Gestionnaire de contenu"
377 385
 
378
-#: tracim/model/data.py:83
386
+#: tracim/model/data.py:84
379 387
 #: tracim/templates/help/page-user-role-definition.mak:27
380 388
 msgid "Workspace Manager"
381 389
 msgstr "Responsable"
382 390
 
383
-#: tracim/model/data.py:153
391
+#: tracim/model/data.py:154
384 392
 msgid "Item archived"
385 393
 msgstr "Element archivé"
386 394
 
387
-#: tracim/model/data.py:154
395
+#: tracim/model/data.py:155
388 396
 msgid "Item commented"
389 397
 msgstr "Element commenté"
390 398
 
391
-#: tracim/model/data.py:155
399
+#: tracim/model/data.py:156
392 400
 msgid "Item created"
393 401
 msgstr "Elément créé"
394 402
 
395
-#: tracim/model/data.py:156
403
+#: tracim/model/data.py:157
396 404
 msgid "Item deleted"
397 405
 msgstr "Element supprimé"
398 406
 
399
-#: tracim/model/data.py:157
407
+#: tracim/model/data.py:158
400 408
 msgid "Item modified"
401 409
 msgstr "Elément modifié"
402 410
 
403
-#: tracim/model/data.py:158
411
+#: tracim/model/data.py:159
404 412
 msgid "New revision"
405 413
 msgstr "Nouvelle version"
406 414
 
407
-#: tracim/model/data.py:159
415
+#: tracim/model/data.py:160
408 416
 msgid "Status modified"
409 417
 msgstr "Statut modifié"
410 418
 
411
-#: tracim/model/data.py:160
419
+#: tracim/model/data.py:161
412 420
 msgid "Item un-archived"
413 421
 msgstr "Elément désarchivé"
414 422
 
415
-#: tracim/model/data.py:161
423
+#: tracim/model/data.py:162
416 424
 msgid "Item undeleted"
417 425
 msgstr "Elément restauré"
418 426
 
419
-#: tracim/model/data.py:198
420
-#: tracim/model/data.py:208
427
+#: tracim/model/data.py:199
428
+#: tracim/model/data.py:209
421 429
 msgid "work in progress"
422 430
 msgstr "travail en cours"
423 431
 
424
-#: tracim/model/data.py:199
425
-#: tracim/model/data.py:209
432
+#: tracim/model/data.py:200
433
+#: tracim/model/data.py:210
426 434
 msgid "closed — validated"
427 435
 msgstr "clos(e) — validé(e)"
428 436
 
429
-#: tracim/model/data.py:200
430
-#: tracim/model/data.py:210
437
+#: tracim/model/data.py:201
438
+#: tracim/model/data.py:211
431 439
 msgid "closed — cancelled"
432 440
 msgstr "clos(e) — annulé(e)"
433 441
 
434
-#: tracim/model/data.py:201
435
-#: tracim/model/data.py:206
436
-#: tracim/model/data.py:211
442
+#: tracim/model/data.py:202
443
+#: tracim/model/data.py:207
444
+#: tracim/model/data.py:212
437 445
 msgid "deprecated"
438 446
 msgstr "obsolète"
439 447
 
440
-#: tracim/model/data.py:203
448
+#: tracim/model/data.py:204
441 449
 msgid "subject in progress"
442
-msgstr "discussion en cours"
450
+msgstr "sujet en cours"
443 451
 
444
-#: tracim/model/data.py:204
452
+#: tracim/model/data.py:205
445 453
 msgid "subject closed — resolved"
446
-msgstr "discussion close — résolue"
454
+msgstr "sujet close — résolu"
447 455
 
448
-#: tracim/model/data.py:205
456
+#: tracim/model/data.py:206
449 457
 msgid "subject closed — cancelled"
450
-msgstr "discussion close — annulée"
458
+msgstr "sujet clos — annulé"
451 459
 
452 460
 #: tracim/templates/create_account.mak:5
453 461
 msgid "Create account"
@@ -539,7 +547,7 @@ msgstr "Déplacer le dossier courant"
539 547
 
540 548
 #: tracim/templates/index.mak:25
541 549
 #: tracim/templates/index.mak:46
542
-#: tracim/templates/master_authenticated.mak:139
550
+#: tracim/templates/master_authenticated.mak:141
543 551
 msgid "Login"
544 552
 msgstr "Login"
545 553
 
@@ -579,12 +587,16 @@ msgstr "Espace de travail"
579 587
 msgid "Admin"
580 588
 msgstr "Admin"
581 589
 
582
-#: tracim/templates/master_authenticated.mak:129
590
+#: tracim/templates/master_authenticated.mak:100
591
+msgid "you MUST desactivate debug in production"
592
+msgstr "vous DEVEZ désactiver le mode \"debug\" en production"
593
+
594
+#: tracim/templates/master_authenticated.mak:131
583 595
 #: tracim/templates/master_no_toolbar_no_login.mak:144
584 596
 msgid "My account"
585 597
 msgstr "Mon compte"
586 598
 
587
-#: tracim/templates/master_authenticated.mak:134
599
+#: tracim/templates/master_authenticated.mak:136
588 600
 #: tracim/templates/master_no_toolbar_no_login.mak:149
589 601
 msgid "Logout"
590 602
 msgstr "Fermer la session"
@@ -654,15 +666,15 @@ msgstr "Requête de réinitialisation du mot de passe"
654 666
 
655 667
 #: tracim/templates/thread_toolbars.mak:8
656 668
 msgid "Edit current thread"
657
-msgstr "Modifier la discussion"
669
+msgstr "Modifier le sujet"
658 670
 
659 671
 #: tracim/templates/thread_toolbars.mak:22
660 672
 msgid "Archive thread"
661
-msgstr "Archiver la discussion"
673
+msgstr "Archiver le sujet"
662 674
 
663 675
 #: tracim/templates/thread_toolbars.mak:23
664 676
 msgid "Delete thread"
665
-msgstr "Supprimer la discussion"
677
+msgstr "Supprimer le sujet"
666 678
 
667 679
 #: tracim/templates/user_get_all.mak:24
668 680
 msgid "Create a user account..."
@@ -759,14 +771,13 @@ msgid "My profile"
759 771
 msgstr "Mon profil"
760 772
 
761 773
 #: tracim/templates/user_get_me.mak:26
762
-#: tracim/templates/user_get_one.mak:26
763
-msgid "This user can create workspaces."
764
-msgstr "Cet utilisateur peut créer des espaces de travail."
774
+#: tracim/templates/user_profile.mak:26
775
+msgid "I can create workspaces."
776
+msgstr "je peux créer des espaces de travail"
765 777
 
766 778
 #: tracim/templates/user_get_me.mak:29
767
-#: tracim/templates/user_get_one.mak:29
768
-msgid "This user is an administrator."
769
-msgstr "Cet utilisateur est un administrateur"
779
+msgid "I am an administrator."
780
+msgstr "Je suis un administrateur."
770 781
 
771 782
 #: tracim/templates/user_get_me.mak:39
772 783
 #: tracim/templates/user_workspace_get_all.mak:8
@@ -775,8 +786,28 @@ msgid "My workspaces"
775 786
 msgstr "Mes espaces de travail"
776 787
 
777 788
 #: tracim/templates/user_get_me.mak:42
778
-msgid "You are not member of any workspace."
779
-msgstr "Vous n'êtes membre d'aucun espace de travail"
789
+msgid "I'm not member of any workspace."
790
+msgstr "Je ne suis membre d'aucun espace de travail"
791
+
792
+#: tracim/templates/user_get_me.mak:52
793
+msgid "Email notifications subscribed. Click to stop notifications."
794
+msgstr "Vous êtes abonné(e) aux notifications par email. Cliquez pour pour les désactiver"
795
+
796
+#: tracim/templates/user_get_me.mak:56
797
+msgid "Email notifications desactivated. Click to subscribe."
798
+msgstr "Vous n'êtes pas abonné(e) aux notifications par email. Cliquez pour pour les activer"
799
+
800
+#: tracim/templates/user_get_me.mak:66
801
+msgid "You can configure your email notifications by clicking on the email icons above"
802
+msgstr "Vous pouvez configurer vos notifications par email en cliquant sur l'icône \"email\" ci-dessus"
803
+
804
+#: tracim/templates/user_get_one.mak:26
805
+msgid "This user can create workspaces."
806
+msgstr "Cet utilisateur peut créer des espaces de travail."
807
+
808
+#: tracim/templates/user_get_one.mak:29
809
+msgid "This user is an administrator."
810
+msgstr "Cet utilisateur est un administrateur"
780 811
 
781 812
 #: tracim/templates/user_get_one.mak:42
782 813
 #: tracim/templates/user_profile.mak:42
@@ -787,10 +818,6 @@ msgstr "Cet utilisateur n'est membre d'aucun espace de travail"
787 818
 msgid "User profile"
788 819
 msgstr "Profil utilisateur"
789 820
 
790
-#: tracim/templates/user_profile.mak:26
791
-msgid "I can create workspaces."
792
-msgstr "je peux créer des espaces de travail"
793
-
794 821
 #: tracim/templates/user_profile.mak:29
795 822
 msgid "I'm an administrator."
796 823
 msgstr "Je suis un administrateur"
@@ -942,7 +969,7 @@ msgstr "sous-dossiers"
942 969
 #: tracim/templates/user_workspace_forms.mak:18
943 970
 #: tracim/templates/user_workspace_forms.mak:55
944 971
 msgid "threads"
945
-msgstr "discussions"
972
+msgstr "sujets"
946 973
 
947 974
 #: tracim/templates/user_workspace_folder_get_one.mak:51
948 975
 #: tracim/templates/user_workspace_forms.mak:19
@@ -965,11 +992,11 @@ msgstr "créer un nouveau dossier..."
965 992
 
966 993
 #: tracim/templates/user_workspace_folder_get_one.mak:73
967 994
 msgid "Threads"
968
-msgstr "Discussions"
995
+msgstr "Sujets"
969 996
 
970 997
 #: tracim/templates/user_workspace_folder_get_one.mak:73
971 998
 msgid "start new thread..."
972
-msgstr "lancer une nouvelle discussion..."
999
+msgstr "lancer un nouveau sujet..."
973 1000
 
974 1001
 #: tracim/templates/user_workspace_folder_get_one.mak:83
975 1002
 msgid "Files"
@@ -1028,7 +1055,7 @@ msgstr "Vous pouvez décrire le sujet (facultatif)"
1028 1055
 
1029 1056
 #: tracim/templates/user_workspace_folder_thread_get_one.mak:64
1030 1057
 msgid "<b>Note</b>: In case you'd like to post a reply, you must first open again the thread"
1031
-msgstr "<b>Note</b> : si vous souhaitez commenter cette discussion, vous devez tout d'abord en modifier le statut pour la ré-ouvrir"
1058
+msgstr "<b>Note</b> : si vous souhaitez commenter ce sujet, vous devez tout d'abord en modifier le statut pour la ré-ouvrir"
1032 1059
 
1033 1060
 #: tracim/templates/user_workspace_folder_thread_get_one.mak:69
1034 1061
 msgid "Post a reply..."
@@ -1161,7 +1188,7 @@ msgstr "{nb_total} sous-dossier(s)"
1161 1188
 
1162 1189
 #: tracim/templates/user_workspace_widgets.mak:50
1163 1190
 msgid "{nb_total} thread(s) &mdash; {nb_open} open"
1164
-msgstr "{nb_total} discussion(s) &mdash; {nb_open} ouverte(s)"
1191
+msgstr "{nb_total} sujet(s) &mdash; {nb_open} ouvert(s)"
1165 1192
 
1166 1193
 #: tracim/templates/user_workspace_widgets.mak:55
1167 1194
 msgid "{nb_total} file(s) &mdash; {nb_open} open"
@@ -1181,7 +1208,7 @@ msgstr "Aucun fichier."
1181 1208
 
1182 1209
 #: tracim/templates/user_workspace_widgets.mak:108
1183 1210
 msgid "No thread found."
1184
-msgstr "Aucune discussion."
1211
+msgstr "Aucun sujet."
1185 1212
 
1186 1213
 #: tracim/templates/user_workspace_widgets.mak:115
1187 1214
 msgid "{} message(s)"
@@ -1310,6 +1337,50 @@ msgstr "Comme <span style=\"color: #759ac5;\">contributeur</span> + gestion du c
1310 1337
 msgid "Same as <span style=\"color: #ea983d;\">content manager</span> + workspace management rights: edit workspace, invite users, revoke them."
1311 1338
 msgstr "Comme <span style=\"color: #ea983d;\">gestionnaire de contenu</span> + modification de l'espace de travail."
1312 1339
 
1340
+#: tracim/templates/mail/content_update_body_html.mak:51
1341
+#, fuzzy
1342
+#| msgid "Some activity has been detected on the item above"
1343
+msgid "Some activity has been detected"
1344
+msgstr "Il se passe des choses"
1345
+
1346
+#: tracim/templates/mail/content_update_body_html.mak:59
1347
+msgid "<span style=\"{style}\">&mdash; by {actor_name}</span>"
1348
+msgstr "<span style=\"{style}\">&mdash; par {actor_name}</span>"
1349
+
1350
+#: tracim/templates/mail/content_update_body_html.mak:64
1351
+#: tracim/templates/mail/content_update_body_text.mak:16
1352
+msgid "This item has been deleted."
1353
+msgstr "Cet élément a été supprimé"
1354
+
1355
+#: tracim/templates/mail/content_update_body_html.mak:67
1356
+#: tracim/templates/mail/content_update_body_text.mak:18
1357
+msgid "This item has been archived."
1358
+msgstr "Cet élément a été archivé."
1359
+
1360
+#: tracim/templates/mail/content_update_body_html.mak:71
1361
+msgid "Go to information"
1362
+msgstr "Voir en ligne"
1363
+
1364
+#: tracim/templates/mail/content_update_body_html.mak:80
1365
+msgid "{user_display_name}, you receive this email because you are <b>{user_role_label}</b> in the workspace <a href=\"{workspace_url}\">{workspace_label}</a>"
1366
+msgstr "{user_display_name}, vous recevez cet email car vous êtes <b>{user_role_label}</b> dans l'espace de travail <a href=\"{workspace_url}\">{workspace_label}</a>"
1367
+
1368
+#: tracim/templates/mail/content_update_body_text.mak:8
1369
+msgid "Some activity has been detected on the item above"
1370
+msgstr "Il se passe des choses"
1371
+
1372
+#: tracim/templates/mail/content_update_body_text.mak:8
1373
+msgid "-- by {actor_name}"
1374
+msgstr "-- par {actor_name}"
1375
+
1376
+#: tracim/templates/mail/content_update_body_text.mak:20
1377
+msgid "Go to information:"
1378
+msgstr "Voir en ligne :"
1379
+
1380
+#: tracim/templates/mail/content_update_body_text.mak:25
1381
+msgid "*{user_display_name}*, you receive this email because you are *{user_role_label}* in the workspace {workspace_label} - {workspace_url}"
1382
+msgstr "*{user_display_name}*, vous recevez cet email car vous êtes *{user_role_label}* dans l'espace de travail {workspace_label} - {workspace_url}"
1383
+
1313 1384
 #~ msgid "You have no document yet."
1314 1385
 #~ msgstr "Vous n'avez pas de document pour le moment."
1315 1386
 
@@ -1852,3 +1923,9 @@ msgstr "Comme <span style=\"color: #ea983d;\">gestionnaire de contenu</span> + m
1852 1923
 
1853 1924
 #~ msgid "Titleless Document"
1854 1925
 #~ msgstr "Document sans titre"
1926
+
1927
+#~ msgid "You can create workspaces."
1928
+#~ msgstr "je peux créer des espaces de travail"
1929
+
1930
+#~ msgid "You are an administrator."
1931
+#~ msgstr "Je suis un administrateur"

+ 6 - 3
tracim/tracim/lib/content.py Wyświetl plik

@@ -5,6 +5,7 @@ __author__ = 'damien'
5 5
 import tg
6 6
 
7 7
 from sqlalchemy.orm.attributes import get_history
8
+from tracim.lib.notifications import Notifier
8 9
 from tracim.model import DBSession
9 10
 from tracim.model.auth import User
10 11
 from tracim.model.data import ContentStatus, ContentRevisionRO, ActionDescription
@@ -64,7 +65,7 @@ class ContentApi(object):
64 65
         content.revision_type = ActionDescription.CREATION
65 66
 
66 67
         if do_save:
67
-            self.save(content)
68
+            self.save(content, ActionDescription.CREATION)
68 69
         return content
69 70
 
70 71
 
@@ -80,7 +81,7 @@ class ContentApi(object):
80 81
         item.revision_type = ActionDescription.COMMENT
81 82
 
82 83
         if do_save:
83
-            self.save(item)
84
+            self.save(item, ActionDescription.COMMENT)
84 85
         return content
85 86
 
86 87
 
@@ -203,7 +204,7 @@ class ContentApi(object):
203 204
         content.is_deleted = False
204 205
         content.revision_type = ActionDescription.UNDELETION
205 206
 
206
-    def save(self, content: Content, action_description: str=None, do_flush=True):
207
+    def save(self, content: Content, action_description: str=None, do_flush=True, do_notify=True):
207 208
         """
208 209
         Save an object, flush the session and set the revision_type property
209 210
         :param content:
@@ -223,3 +224,5 @@ class ContentApi(object):
223 224
         if do_flush:
224 225
             DBSession.flush()
225 226
 
227
+        if do_notify:
228
+            Notifier(self._user).notify_content_update(content)

+ 49 - 0
tracim/tracim/lib/email.py Wyświetl plik

@@ -0,0 +1,49 @@
1
+# -*- coding: utf-8 -*-
2
+
3
+from email.mime.multipart import MIMEMultipart
4
+import smtplib
5
+
6
+from tracim.lib.base import logger
7
+
8
+
9
+class SmtpConfiguration(object):
10
+    """
11
+    Container class for SMTP configuration used in Tracim
12
+    """
13
+
14
+    def __init__(self, server: str, port: int, login: str, password: str):
15
+        self.server = server
16
+        self.port = port
17
+        self.login = login
18
+        self.password = password
19
+
20
+
21
+
22
+class EmailSender(object):
23
+    """
24
+    this class allow to send emails and has no relations with SQLAlchemy and other tg HTTP request environment
25
+    This means that it can be used in any thread (even through a asyncjob_perform() call
26
+    """
27
+    def __init__(self, config: SmtpConfiguration):
28
+        self._smtp_config = config
29
+        self._smtp_connection = None
30
+
31
+    def connect(self):
32
+        if not self._smtp_connection:
33
+            logger.info(self, 'Connecting from SMTP server {}'.format(self._smtp_config.server))
34
+            self._smtp_connection = smtplib.SMTP(self._smtp_config.server, self._smtp_config.port)
35
+            self._smtp_connection.ehlo()
36
+            self._smtp_connection.login(self._smtp_config.login, self._smtp_config.password)
37
+            logger.info(self, 'Connection OK')
38
+
39
+    def disconnect(self):
40
+        if self._smtp_connection:
41
+            logger.info(self, 'Disconnecting from SMTP server {}'.format(self._smtp_config.server))
42
+            self._smtp_connection.quit()
43
+            logger.info(self, 'Connection closed.')
44
+
45
+
46
+    def send_mail(self, message: MIMEMultipart):
47
+        self.connect() # Acutally, this connects to SMTP only if required
48
+        logger.info(self, 'Sending email to {}'.format(message['To']))
49
+        self._smtp_connection.send_message(message)

+ 3 - 15
tracim/tracim/lib/helpers.py Wyświetl plik

@@ -96,8 +96,7 @@ def AllStatus(type=''):
96 96
 
97 97
 
98 98
 def is_debug_mode():
99
-    # return tg.config.get('debug')
100
-    return False
99
+    return tg.config.get('debug')
101 100
 
102 101
 def on_off_to_boolean(on_or_off: str) -> bool:
103 102
     return True if on_or_off=='on' else False
@@ -151,16 +150,5 @@ def user_role(user, workspace) -> int:
151 150
 
152 151
     return 0
153 152
 
154
-# SDAEP RELATED DAtA
155
-WEBSITE_TITLE = 'SDAEP22'
156
-WEBSITE_HOME_TITLE_COLOR = '#555'
157
-WEBSITE_HOME_IMAGE_URL = 'http://t0.gstatic.com/images?q=tbn:ANd9GcSGwpT9eJn4jSrQQuYyd6mxj9f59ZfHWf9m4FcWpinPV7eFXHaosDv4bynJ'
158
-WEBSITE_HOME_BACKGROUND_IMAGE_URL = tg.lurl('/assets/img/eau.jpg')
159
-
160
-
161
-
162
-WEBSITE_TITLE = 'Tracim'
163
-WEBSITE_TITLE_COLOR = '#555'
164
-WEBSITE_HOME_IMAGE_URL = tg.lurl('/assets/img/home_illustration.jpg')
165
-WEBSITE_HOME_BACKGROUND_IMAGE_URL = tg.lurl('/assets/img/bg.jpg')
166
-
153
+from tracim.config.app_cfg import CFG as CFG_ORI
154
+CFG = CFG_ORI.get_instance() # local CFG var is an instance of CFG class found in app_cfg

+ 199 - 0
tracim/tracim/lib/notifications.py Wyświetl plik

@@ -0,0 +1,199 @@
1
+# -*- coding: utf-8 -*-
2
+
3
+from email.mime.multipart import MIMEMultipart
4
+from email.mime.text import MIMEText
5
+
6
+
7
+from mako.template import Template
8
+
9
+from tg.i18n import lazy_ugettext as l_
10
+from tg.i18n import ugettext as _
11
+
12
+from tracim.config.app_cfg import CFG
13
+
14
+from tracim.lib.base import logger
15
+from tracim.lib.email import SmtpConfiguration
16
+from tracim.lib.email import EmailSender
17
+from tracim.lib.user import UserApi
18
+from tracim.lib.workspace import WorkspaceApi
19
+
20
+from tracim.model.serializers import Context
21
+from tracim.model.serializers import CTX
22
+from tracim.model.serializers import DictLikeClass
23
+
24
+from tracim.model.data import Content, UserRoleInWorkspace, ContentType
25
+from tracim.model.auth import User
26
+
27
+
28
+from tgext.asyncjob import asyncjob_perform
29
+
30
+class Notifier(object):
31
+
32
+    def __init__(self, current_user: User=None):
33
+        """
34
+
35
+        :param current_user: the user that has triggered the notification
36
+        :return:
37
+        """
38
+        cfg = CFG.get_instance()
39
+
40
+        self._user = current_user
41
+        self._smtp_config = SmtpConfiguration(cfg.EMAIL_NOTIFICATION_SMTP_SERVER,
42
+                                       cfg.EMAIL_NOTIFICATION_SMTP_PORT,
43
+                                       cfg.EMAIL_NOTIFICATION_SMTP_USER,
44
+                                       cfg.EMAIL_NOTIFICATION_SMTP_PASSWORD)
45
+
46
+    def notify_content_update(self, content: Content):
47
+        logger.info(self, 'About to email-notify update of content {} by user {}'.format(content.content_id, self._user.user_id))
48
+
49
+        global_config = CFG.get_instance()
50
+
51
+        ####
52
+        #
53
+        # INFO - D.A. - 2014-11-05 - Emails are sent through asynchronous jobs. For that reason, we do not
54
+        # give SQLAlchemy objects but ids only (SQLA objects are related to a given thread/session)
55
+        #
56
+        if global_config.EMAIL_NOTIFICATION_PROCESSING_MODE.lower()==global_config.CST.ASYNC.lower():
57
+            logger.info(self, 'Sending email in ASYNC mode')
58
+            # TODO - D.A - 2014-11-06
59
+            # This feature must be implemented in order to be able to scale to large communities
60
+            raise NotImplementedError('Sending emails through ASYNC mode is not working yet')
61
+            asyncjob_perform(EmailNotifier(self._smtp_config, global_config).notify_content_update, self._user.user_id, content.content_id)
62
+        else:
63
+            logger.info(self, 'Sending email in SYNC mode')
64
+            EmailNotifier(self._smtp_config, global_config).notify_content_update(self._user.user_id, content.content_id)
65
+
66
+class EST(object):
67
+    """
68
+    EST = Email Subject Tags - this is a convenient class - no business logic here
69
+    This class is intended to agregate all dynamic content that may be included in email subjects
70
+    """
71
+
72
+    WEBSITE_TITLE = '{website_title}'
73
+    WORKSPACE_LABEL = '{workspace_label}'
74
+    CONTENT_LABEL = '{content_label}'
75
+    CONTENT_STATUS_LABEL = '{content_status_label}'
76
+
77
+    @classmethod
78
+    def all(cls):
79
+        return [
80
+            cls.CONTENT_LABEL,
81
+            cls.CONTENT_STATUS_LABEL,
82
+            cls.WEBSITE_TITLE,
83
+            cls.WORKSPACE_LABEL
84
+        ]
85
+
86
+class EmailNotifier(object):
87
+
88
+    """
89
+    Compared to Notifier, this class is independant from the HTTP request thread
90
+
91
+    TODO: Do this class really independant (but it means to get as parameter the user language
92
+    and other stuff related to the turbogears environment)
93
+    """
94
+
95
+    def __init__(self, smtp_config: SmtpConfiguration, global_config: CFG):
96
+        self._smtp_config = smtp_config
97
+        self._global_config = global_config
98
+
99
+
100
+    def notify_content_update(self, event_actor_id: int, event_content_id: int):
101
+        """
102
+        Look for all users to be notified about the new content and send them an individual email
103
+        :param event_actor_id: id of the user that has triggered the event
104
+        :param event_content_id: related content_id
105
+        :return:
106
+        """
107
+        # FIXME - D.A. - 2014-11-05
108
+        # Dirty import. It's here in order to avoid circular import
109
+        from tracim.lib.content import ContentApi
110
+
111
+        user = UserApi(None).get_one(event_actor_id)
112
+        logger.debug(self, 'Content: {}'.format(event_content_id))
113
+
114
+        content = ContentApi(user, show_archived=True, show_deleted=True).get_one(event_content_id, ContentType.Any) # TODO - use a system user instead of the user that has triggered the event
115
+        content = content.parent if content.type==ContentType.Comment else content
116
+        notifiable_roles = WorkspaceApi(user).get_notifiable_roles(content.workspace)
117
+
118
+        if len(notifiable_roles)<=0:
119
+            logger.info(self, 'Skipping notification as nobody subscribed to in workspace {}'.format(content.workspace.label))
120
+            return
121
+
122
+        logger.info(self, 'Sending asynchronous emails to {} user(s)'.format(len(notifiable_roles)))
123
+        # INFO - D.A. - 2014-11-06
124
+        # The following email sender will send emails in the async task queue
125
+        # This allow to build all mails through current thread but really send them (including SMTP connection)
126
+        # In the other thread.
127
+        #
128
+        # This way, the webserver will return sooner (actually before notification emails are sent
129
+        async_email_sender = EmailSender(self._smtp_config)
130
+
131
+        for role in notifiable_roles:
132
+            logger.info(self, 'Sending email to {}'.format(role.user.email))
133
+            to_addr = '{name} <{email}>'.format(name=role.user.display_name, email=role.user.email)
134
+
135
+            #
136
+            #  INFO - D.A. - 2014-11-06
137
+            # We do not use .format() here because the subject defined in the .ini file
138
+            # may not include all required labels. In order to avoid partial format() (which result in an exception)
139
+            # we do use replace and force the use of .__str__() in order to process LazyString objects
140
+            #
141
+            subject = self._global_config.EMAIL_NOTIFICATION_CONTENT_UPDATE_SUBJECT
142
+            subject = subject.replace(EST.WEBSITE_TITLE, self._global_config.WEBSITE_TITLE.__str__())
143
+            subject = subject.replace(EST.WORKSPACE_LABEL, content.workspace.label.__str__())
144
+            subject = subject.replace(EST.CONTENT_LABEL, content.label.__str__())
145
+            subject = subject.replace(EST.CONTENT_STATUS_LABEL, content.get_status().label.__str__())
146
+
147
+            message = MIMEMultipart('alternative')
148
+            message['Subject'] = subject
149
+            message['From'] = self._global_config.EMAIL_NOTIFICATION_FROM
150
+            message['To'] = to_addr
151
+
152
+            body_text = self._build_email_body(self._global_config.EMAIL_NOTIFICATION_CONTENT_UPDATE_TEMPLATE_TEXT, role, content, user)
153
+            body_html = self._build_email_body(self._global_config.EMAIL_NOTIFICATION_CONTENT_UPDATE_TEMPLATE_HTML, role, content, user)
154
+            part1 = MIMEText(body_text, 'plain', 'utf-8')
155
+            part2 = MIMEText(body_html, 'html', 'utf-8')
156
+            # Attach parts into message container.
157
+            # According to RFC 2046, the last part of a multipart message, in this case
158
+            # the HTML message, is best and preferred.
159
+            message.attach(part1)
160
+            message.attach(part2)
161
+
162
+            message_str = message.as_string()
163
+            asyncjob_perform(async_email_sender.send_mail, message)
164
+            # s.send_message(message)
165
+
166
+        # Note: The following action allow to close the SMTP connection.
167
+        # This will work only if the async jobs are done in the right order
168
+        asyncjob_perform(async_email_sender.disconnect)
169
+
170
+
171
+    def _build_email_body(self, mako_template_filepath: str, role: UserRoleInWorkspace, content: Content, actor: User) -> str:
172
+        """
173
+        Build an email body and return it as a string
174
+        :param mako_template_filepath: the absolute path to the mako template to be used for email body building
175
+        :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
176
+        :param content: the content item related to the notification
177
+        :param actor: the user at the origin of the action / notification (for example the one who wrote a comment
178
+        :param config: the global configuration
179
+        :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
180
+        """
181
+        logger.debug(self, 'Building email content from MAKO template {}'.format(mako_template_filepath))
182
+
183
+        template = Template(filename=mako_template_filepath)
184
+        # TODO - D.A. - 2014-11-06 - move this
185
+        # Import is here for circular import problem
186
+        import tracim.lib.helpers as helpers
187
+
188
+        dictified_item = Context(CTX.EMAIL_NOTIFICATION, self._global_config.WEBSITE_BASE_URL).toDict(content)
189
+        dictified_actor = Context(CTX.DEFAULT).toDict(actor)
190
+
191
+        body_content = template.render(base_url=self._global_config.WEBSITE_BASE_URL,
192
+                               _=_,
193
+                               h=helpers,
194
+                               user_display_name=role.user.display_name,
195
+                               user_role_label=role.role_as_label(),
196
+                               workspace_label=role.workspace.label,
197
+                               result = DictLikeClass(item=dictified_item, actor=dictified_actor))
198
+
199
+        return body_content

+ 18 - 0
tracim/tracim/lib/workspace.py Wyświetl plik

@@ -73,6 +73,24 @@ class WorkspaceApi(object):
73 73
         workspaces.sort(key=lambda workspace: workspace.label.lower())
74 74
         return workspaces
75 75
 
76
+    def disable_notifications(self, user: User, workspace: Workspace):
77
+        for role in user.roles:
78
+            if role.workspace==workspace:
79
+                role.do_notify = False
80
+
81
+    def enable_notifications(self, user: User, workspace: Workspace):
82
+        for role in user.roles:
83
+            if role.workspace==workspace:
84
+                role.do_notify = True
85
+
86
+    def get_notifiable_roles(self, workspace: Workspace) -> [UserRoleInWorkspace]:
87
+        roles = []
88
+        for role in workspace.roles:
89
+            print(role.user.email)
90
+            if role.do_notify==True and role.user!=self._user:
91
+                roles.append(role)
92
+        return roles
93
+
76 94
     def save(self, workspace: Workspace):
77 95
         DBSession.flush()
78 96
 

+ 1 - 0
tracim/tracim/model/data.py Wyświetl plik

@@ -65,6 +65,7 @@ class UserRoleInWorkspace(DeclarativeBase):
65 65
     user_id = Column(Integer, ForeignKey('users.user_id'), nullable=False, default=None, primary_key=True)
66 66
     workspace_id = Column(Integer, ForeignKey('workspaces.workspace_id'), nullable=False, default=None, primary_key=True)
67 67
     role = Column(Integer, nullable=False, default=0, primary_key=False)
68
+    do_notify = Column(Boolean, unique=False, nullable=False, default=False)
68 69
 
69 70
     workspace = relationship('Workspace', remote_side=[Workspace.workspace_id], backref='roles', lazy='joined')
70 71
     user = relationship('User', remote_side=[User.user_id], backref='roles')

+ 41 - 25
tracim/tracim/model/serializers.py Wyświetl plik

@@ -55,30 +55,24 @@ class ContextConverterNotFoundException(Exception):
55 55
 
56 56
 class CTX(object):
57 57
     """ constants that are used for serialization / dictification of models"""
58
-    DEFAULT = 'DEFAULT' # default context. This will allow to define a serialization method to be used by default
59
-
60
-    CURRENT_USER = 'CURRENT_USER'
61
-
62
-    USER = 'USER'
63
-    USERS = 'USERS'
64 58
     ADMIN_WORKSPACE = 'ADMIN_WORKSPACE'
65 59
     ADMIN_WORKSPACES = 'ADMIN_WORKSPACES'
66
-
67
-    WORKSPACE = 'WORKSPACE'
68
-    FOLDER = 'FOLDER'
69
-    FOLDERS = 'FOLDERS'
70
-
60
+    CURRENT_USER = 'CURRENT_USER'
61
+    DEFAULT = 'DEFAULT' # default context. This will allow to define a serialization method to be used by default
62
+    EMAIL_NOTIFICATION = 'EMAIL_NOTIFICATION'
71 63
     FILE = 'FILE'
72 64
     FILES = 'FILES'
73
-
65
+    FOLDER = 'FOLDER'
66
+    FOLDERS = 'FOLDERS'
67
+    MENU_API = 'MENU_API'
68
+    MENU_API_BUILD_FROM_TREE_ITEM = 'MENU_API_BUILD_FROM_TREE_ITEM'
74 69
     PAGE = 'PAGE'
75 70
     PAGES = 'PAGES'
76
-
77 71
     THREAD = 'THREAD'
78 72
     THREADS = 'THREADS'
79
-
80
-    MENU_API = 'MENU_API'
81
-    MENU_API_BUILD_FROM_TREE_ITEM = 'MENU_API_BUILD_FROM_TREE_ITEM'
73
+    USER = 'USER'
74
+    USERS = 'USERS'
75
+    WORKSPACE = 'WORKSPACE'
82 76
 
83 77
 
84 78
 class DictLikeClass(dict):
@@ -93,7 +87,6 @@ class DictLikeClass(dict):
93 87
     __setattr__ = dict.__setitem__
94 88
 
95 89
 
96
-
97 90
 class Context(object):
98 91
     """
99 92
     Convert a series of mapped objects into ClassLikeDict (a dictionnary which can be accessed through properties)
@@ -139,10 +132,18 @@ class Context(object):
139 132
 
140 133
             raise ContextConverterNotFoundException(context_string,model_class)
141 134
 
142
-    def __init__(self, context_string):
135
+    def __init__(self, context_string, base_url=''):
143 136
         """
144 137
         """
145 138
         self.context_string = context_string
139
+        self._base_url = base_url # real root url like http://mydomain.com:8080
140
+
141
+    def url(self, base_url='/', params=None, qualified=False) -> str:
142
+        url = tg.url(base_url, params)
143
+
144
+        if self._base_url:
145
+            url = '{}{}'.format(self._base_url, url)
146
+        return  url
146 147
 
147 148
     def toDict(self, serializableObject, key_value_for_a_list_object='', key_value_for_list_item_nb=''):
148 149
         """
@@ -206,6 +207,7 @@ class Context(object):
206 207
         assert isinstance(result, DictLikeClass)
207 208
         return result
208 209
 
210
+
209 211
 ########################################################################################################################
210 212
 ## ActionDescription
211 213
 
@@ -253,6 +255,19 @@ def serialize_breadcrumb_item(content: Content, context: Context):
253 255
         workspace = context.toDict(content.workspace)
254 256
     )
255 257
 
258
+@pod_serializer(Content, CTX.EMAIL_NOTIFICATION)
259
+def serialize_item(content: Content, context: Context):
260
+    return DictLikeClass(
261
+        id = content.content_id,
262
+        label = content.label,
263
+        status = context.toDict(content.get_status()),
264
+        folder = context.toDict(DictLikeClass(id = content.parent.content_id if content.parent else None)),
265
+        workspace = context.toDict(content.workspace),
266
+        is_deleted = content.is_deleted,
267
+        is_archived = content.is_archived,
268
+        url = context.url('/workspaces/{wid}/folders/{fid}/{ctype}/{cid}'.format(wid = content.workspace_id, fid=content.parent_id, ctype=content.type+'s', cid=content.content_id))
269
+    )
270
+
256 271
 
257 272
 @pod_serializer(Content, CTX.MENU_API)
258 273
 def serialize_content_for_menu_api(content: Content, context: Context):
@@ -263,7 +278,7 @@ def serialize_content_for_menu_api(content: Content, context: Context):
263 278
         id = CST.TREEVIEW_MENU.ID_TEMPLATE__FULL.format(workspace_id, content_id),
264 279
         children = True, # TODO: make this dynamic
265 280
         text = content.label,
266
-        a_attr = { 'href' : tg.url('/workspaces/{}/folders/{}'.format(workspace_id, content_id)) },
281
+        a_attr = { 'href' : context.url('/workspaces/{}/folders/{}'.format(workspace_id, content_id)) },
267 282
         li_attr = { 'title': content.label, 'class': 'tracim-tree-item-is-a-folder' },
268 283
         type = content.type,
269 284
         state = { 'opened': False, 'selected': False }
@@ -679,6 +694,8 @@ def serialize_role_in_list_for_user(role: UserRoleInWorkspace, context: Context)
679 694
     result['label'] = role.role_as_label()
680 695
     result['style'] = RoleType(role.role).css_style
681 696
     result['workspace'] =  context.toDict(role.workspace)
697
+    result['notifications_subscribed'] = role.do_notify
698
+
682 699
     # result['workspace_name'] = role.workspace.label
683 700
 
684 701
     return result
@@ -690,7 +707,8 @@ def serialize_role_in_list_for_user(role: UserRoleInWorkspace, context: Context)
690 707
 def serialize_workspace_default(workspace: Workspace, context: Context):
691 708
     result = DictLikeClass(
692 709
         id = workspace.workspace_id,
693
-        label = workspace.label
710
+        label = workspace.label,
711
+        url = context.url('/workspaces/{}'.format(workspace.workspace_id))
694 712
     )
695 713
     return result
696 714
 
@@ -741,7 +759,7 @@ def serialize_workspace_for_menu_api(workspace: Workspace, context: Context):
741 759
         id = CST.TREEVIEW_MENU.ID_TEMPLATE__WORKSPACE_ONLY.format(workspace.workspace_id),
742 760
         children = True, # TODO: make this dynamic
743 761
         text = workspace.label,
744
-        a_attr = { 'href' : tg.url('/workspaces/{}'.format(workspace.workspace_id)) },
762
+        a_attr = { 'href' : context.url('/workspaces/{}'.format(workspace.workspace_id)) },
745 763
         li_attr = { 'title': workspace.label, 'class': 'tracim-tree-item-is-a-workspace' },
746 764
         type = 'workspace',
747 765
         state = { 'opened': False, 'selected': False }
@@ -755,7 +773,7 @@ def serialize_node_tree_item_for_menu_api_tree(item: NodeTreeItem, context: Cont
755 773
             id=CST.TREEVIEW_MENU.ID_TEMPLATE__FULL.format(item.node.workspace_id, item.node.content_id),
756 774
             children=True if len(item.children)<=0 else context.toDict(item.children),
757 775
             text=item.node.label,
758
-            a_attr={'href': tg.url('/workspaces/{}/folders/{}'.format(item.node.workspace_id, item.node.content_id)) },
776
+            a_attr={'href': context.url('/workspaces/{}/folders/{}'.format(item.node.workspace_id, item.node.content_id)) },
759 777
             li_attr={'title': item.node.label, 'class': 'tracim-tree-item-is-a-folder'},
760 778
             type='folder',
761 779
             state={'opened': True if len(item.children)>0 else False, 'selected': item.is_selected}
@@ -765,10 +783,8 @@ def serialize_node_tree_item_for_menu_api_tree(item: NodeTreeItem, context: Cont
765 783
             id=CST.TREEVIEW_MENU.ID_TEMPLATE__WORKSPACE_ONLY.format(item.node.workspace_id),
766 784
             children=True if len(item.children)<=0 else context.toDict(item.children),
767 785
             text=item.node.label,
768
-            a_attr={'href': tg.url('/workspaces/{}'.format(item.node.workspace_id))},
786
+            a_attr={'href': context.url('/workspaces/{}'.format(item.node.workspace_id))},
769 787
             li_attr={'title': item.node.label, 'class': 'tracim-tree-item-is-a-workspace'},
770 788
             type='workspace',
771 789
             state={'opened': True if len(item.children)>0 else False, 'selected': item.is_selected}
772 790
         )
773
-
774
-

BIN
tracim/tracim/public/assets/icons/16x16/actions/mail-notification-none.png Wyświetl plik


+ 3 - 3
tracim/tracim/templates/index.mak Wyświetl plik

@@ -2,7 +2,7 @@
2 2
 <%namespace name="TIM" file="tracim.templates.pod"/>
3 3
 
4 4
 <%def name="title()">
5
-  ${h.WEBSITE_TITLE|n}
5
+  ${h.CFG.WEBSITE_TITLE|n}
6 6
 </%def>
7 7
 
8 8
 
@@ -11,13 +11,13 @@
11 11
         <div>
12 12
             <div class="row">
13 13
                 <div class="col-sm-offset-3 col-sm-5">
14
-                    <h1 class="text-center" style="color: ${h.WEBSITE_HOME_TITLE_COLOR};"><b>${h.WEBSITE_TITLE}</b></h1>
14
+                    <h1 class="text-center" style="color: ${h.CFG.WEBSITE_HOME_TITLE_COLOR};"><b>${h.CFG.WEBSITE_TITLE}</b></h1>
15 15
                 </div>
16 16
             </div>
17 17
             <div class="row">
18 18
                 <div class="col-sm-offset-3 col-sm-2">
19 19
                     <a class="thumbnail">
20
-                        <img src="${h.WEBSITE_HOME_IMAGE_URL}" alt="">
20
+                        <img src="${h.CFG.WEBSITE_HOME_IMAGE_URL}" alt="">
21 21
                     </a>
22 22
                 </div>
23 23
                 <div class="col-sm-3">

+ 0 - 0
tracim/tracim/templates/mail/__init__.py Wyświetl plik


+ 84 - 0
tracim/tracim/templates/mail/content_update_body_html.mak Wyświetl plik

@@ -0,0 +1,84 @@
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
+    </style>
18
+  </head>
19
+  <body style="font-family: Arial; font-size: 12px; width: 600px; margin: 0; padding: 0;">
20
+
21
+    <table style="width: 600px; cell-padding: 0; 	border-collapse: collapse;">
22
+      <tr style="background-color: F5F5F5; border-bottom: 1px solid #CCC;" >
23
+        <td>
24
+          <img src="${base_url+'/assets/img/logo.png'}" style="vertical-align: middle;"/>
25
+          <span style="font-weight: bold; padding-left: 0.5em; font-size: 1.5em; vertical-align: middle;">${h.CFG.WEBSITE_TITLE}</span>
26
+        </td>
27
+      </tr>
28
+      <tr>
29
+        <td style="padding: 5px 5px 5px 2em;">
30
+          <img style="vertical-align: middle;" src="${base_url+'/assets/icons/32x32/places/folder-remote.png'}"/>
31
+          <span style="font-weight: bold; padding-left: 0.5em; font-size: 1.2em; vertical-align: middle;">${result.item.workspace.label}</span>
32
+        </td>
33
+      </tr>
34
+      <tr>
35
+        <td style="padding: 5px 5px 5px 4em;">
36
+          <img style="vertical-align: middle;" src="${base_url+'/assets/icons/32x32/apps/internet-group-chat.png'}"/>
37
+          <span style="font-weight: bold; padding-left: 0.5em; font-size: 1em; vertical-align: middle;">
38
+            ${result.item.label}
39
+            <span style="font-weight: bold; color: #999; font-weight: bold;">
40
+            ${result.item.status.label}
41
+            <img src="${base_url+'/assets/icons/16x16/{}.png'.format(result.item.status.icon)}" style="vertical-align: middle;">
42
+          </span>
43
+        </td>
44
+      </tr>
45
+    </table>
46
+
47
+    <hr style="border: 0px solid #CCC; border-width: 1px 0 0 0;">
48
+
49
+    <div style="margin-left: 0.5em; border: 1em solid #DDD; border-width: 0 0 0 1em; padding-left: 1em;">
50
+      <p>
51
+        ${_('Some activity has been detected')|n}
52
+        ##
53
+        ## TODO - D.A. - Show last action in the notification message
54
+        ##
55
+        ## &mdash;
56
+        ## <img style="vertical-align: middle; " src="${base_url+'/assets/icons/16x16/'+result.item.last_action.icon+'.png'}"/>
57
+        ## ${result.item.last_action.label}
58
+        ##
59
+        ${_('<span style="{style}">&mdash; by {actor_name}</span>').format(style='color: #666; font-weight: bold;', actor_name=result.actor.name)}
60
+      </p>
61
+      <p>
62
+        % if result.item.is_deleted:
63
+            <img style="vertical-align: middle; " src="${base_url+'/assets/icons/16x16/status/user-trash-full.png'}"/>
64
+            ${_('This item has been deleted.')}
65
+        % elif result.item.is_archived:
66
+            <img style="vertical-align: middle; " src="${base_url+'/assets/icons/16x16/mimetypes/package-x-generic.png'}"/>
67
+            ${_('This item has been archived.')}
68
+        % else:
69
+            <a href="${result.item.url}" style="background-color: #5CB85C; border: 1px solid #4CAE4C; color: #FFF; text-decoration: none; font-weight: bold; padding: 4px; border-radius: 3px;">
70
+              <img style="vertical-align: middle; " src="${base_url+'/assets/icons/16x16/actions/system-search.png'}"/>
71
+              ${_('Go to information')}
72
+            </a>
73
+        % endif
74
+        <div style="clear:both;"></div>
75
+      </p>
76
+    </div>
77
+
78
+    <hr style="border: 0px solid #CCC; border-width: 1px 0 0 0;">
79
+
80
+    <p style="color: #999; margin-left: 0.5em;">
81
+      ${_('{user_display_name}, you receive this email because you are <b>{user_role_label}</b> in the workspace <a href="{workspace_url}">{workspace_label}</a>').format(user_display_name=user_display_name, user_role_label=user_role_label, workspace_url=result.item.workspace.url, workspace_label=workspace_label)|n}
82
+    </p>
83
+  </body>
84
+</html>

+ 26 - 0
tracim/tracim/templates/mail/content_update_body_text.mak Wyświetl plik

@@ -0,0 +1,26 @@
1
+## -*- coding: utf-8 -*-
2
+${h.CFG.WEBSITE_TITLE}
3
+-> ${result.item.workspace.label}
4
+-> ${result.item.label} - ${result.item.status.label}
5
+
6
+==============================================================================
7
+ 
8
+${_('Some activity has been detected on the item above')} ${_('-- by {actor_name}').format(actor_name=result.actor.name)}
9
+##
10
+## TODO - D.A. - Show last action in the notification message
11
+##
12
+##${_('{last_action} -- by {actor_name}').format(last_action=result.item.last_action.label, actor_name=result.actor.name)}
13
+##
14
+
15
+% if result.item.is_deleted:
16
+${_('This item has been deleted.')}
17
+% elif result.item.is_archived:
18
+${_('This item has been archived.')}
19
+% else:
20
+${_('Go to information:')} ${result.item.url}
21
+% endif
22
+
23
+==============================================================================
24
+
25
+${_('*{user_display_name}*, you receive this email because you are *{user_role_label}* in the workspace {workspace_label} - {workspace_url}').format(user_display_name=user_display_name, user_role_label=user_role_label, workspace_url=result.item.workspace.url, workspace_label=workspace_label)}
26
+

+ 1 - 1
tracim/tracim/templates/master_anonymous.mak Wyświetl plik

@@ -17,7 +17,7 @@
17 17
 
18 18
     <body class="${self.body_class()}" style="
19 19
     height: 100%;
20
-    background: url(${h.WEBSITE_HOME_BACKGROUND_IMAGE_URL}) no-repeat center bottom scroll;
20
+    background: url(${h.CFG.WEBSITE_HOME_BACKGROUND_IMAGE_URL}) no-repeat center bottom scroll;
21 21
     -webkit-background-size: cover;
22 22
     -moz-background-size: cover;
23 23
     background-size: cover;

+ 16 - 14
tracim/tracim/templates/master_authenticated.mak Wyświetl plik

@@ -21,10 +21,10 @@
21 21
         <div class="container-fluid">
22 22
             ${self.main_menu()}
23 23
             ${self.content_wrapper()}
24
-            <div id="tracim-footer-separator"></div>
25
-        </div>
26
-        ${self.footer()}
27
-
24
+            <div id="tracim-footer-separator"></div>
25
+        </div>
26
+        ${self.footer()}
27
+
28 28
         <script src="${tg.url('/assets/js/bootstrap.min.js')}"></script>
29 29
         ${h.tracker_js()|n}
30 30
     </body>
@@ -94,16 +94,18 @@
94 94
                         % endif
95 95
 
96 96
                         % if h.is_debug_mode():
97
-                          <li class="dropdown">
98
-                              <a href="#" class="dropdown-toggle" data-toggle="dropdown">${TIM.ICO(16, 'categories/applications-system')} Debug <b class="caret"></b></a>
99
-                              <ul class="dropdown-menu">
100
-                                <li><a href="${tg.url('/debug/environ')}">${TIM.ICO(16, 'apps/internet-web-browser')} request.environ</a></li>
101
-                                <li><a href="${tg.url('/debug/identity')}">${TIM.ICO(16, 'actions/contact-new')} request.identity</a></li>
102
-                                <li class="divider" role="presentation"></li>
103
-                                <li><a href="${tg.url('/debug/iconset-fa')}">${TIM.ICO(16, 'mimetypes/image-x-generic')} Icon set - Font Awesome</a></li>
104
-                                <li><a href="${tg.url('/debug/iconset-tango')}">${TIM.ICO(16, 'mimetypes/image-x-generic')} Icon set - Tango Icons</a></li>
105
-                              </ul>
106
-                          </li>
97
+                            <li class="dropdown text-danger" >
98
+                                <a href="#" class="dropdown-toggle" data-toggle="dropdown">${TIM.ICO(16, 'status/dialog-warning')} Debug <b class="caret"></b></a>
99
+                                <ul class="dropdown-menu">
100
+                                    <li><a class="text-danger" href=""><strong>${_('you MUST desactivate debug in production')}</strong></a></li>
101
+                                    <li class="divider" role="presentation"></li>
102
+                                    <li><a href="${tg.url('/debug/environ')}">${TIM.ICO(16, 'apps/internet-web-browser')} request.environ</a></li>
103
+                                    <li><a href="${tg.url('/debug/identity')}">${TIM.ICO(16, 'actions/contact-new')} request.identity</a></li>
104
+                                    <li class="divider" role="presentation"></li>
105
+                                    <li><a href="${tg.url('/debug/iconset-fa')}">${TIM.ICO(16, 'mimetypes/image-x-generic')} Icon set - Font Awesome</a></li>
106
+                                    <li><a href="${tg.url('/debug/iconset-tango')}">${TIM.ICO(16, 'mimetypes/image-x-generic')} Icon set - Tango Icons</a></li>
107
+                                </ul>
108
+                            </li>
107 109
                         % endif
108 110
                     </ul>
109 111
                 % endif

+ 2 - 2
tracim/tracim/templates/reset_password_change_password.mak Wyświetl plik

@@ -1,13 +1,13 @@
1 1
 <%inherit file="local:templates.master_anonymous"/>
2 2
 
3
-<%def name="title()">${h.WEBSITE_TITLE|n} - ${_('Change Password Request')}</%def>
3
+<%def name="title()">${h.CFG.WEBSITE_TITLE|n} - ${_('Change Password Request')}</%def>
4 4
 
5 5
 <div class="container-fluid">
6 6
     <div class="row-fluid">
7 7
         <div>
8 8
             <div class="row">
9 9
                 <div class="col-sm-offset-3 col-sm-5">
10
-                    <h1 class="text-center" style="color: ${h.WEBSITE_HOME_TITLE_COLOR};"><b>${h.WEBSITE_TITLE}</b></h1>
10
+                    <h1 class="text-center" style="color: ${h.CFG.WEBSITE_HOME_TITLE_COLOR};"><b>${h.CFG.WEBSITE_TITLE}</b></h1>
11 11
                 </div>
12 12
             </div>
13 13
             <div class="row">

+ 2 - 2
tracim/tracim/templates/reset_password_index.mak Wyświetl plik

@@ -1,13 +1,13 @@
1 1
 <%inherit file="local:templates.master_anonymous"/>
2 2
 
3
-<%def name="title()">${h.WEBSITE_TITLE|n} - ${_('Password Reset Request')}</%def>
3
+<%def name="title()">${h.CFG.WEBSITE_TITLE|n} - ${_('Password Reset Request')}</%def>
4 4
 
5 5
 <div class="container-fluid">
6 6
     <div class="row-fluid">
7 7
         <div>
8 8
             <div class="row">
9 9
                 <div class="col-sm-offset-3 col-sm-5">
10
-                    <h1 class="text-center" style="color: ${h.WEBSITE_HOME_TITLE_COLOR};"><b>${h.WEBSITE_TITLE}</b></h1>
10
+                    <h1 class="text-center" style="color: ${h.CFG.WEBSITE_HOME_TITLE_COLOR};"><b>${h.CFG.WEBSITE_TITLE}</b></h1>
11 11
                 </div>
12 12
             </div>
13 13
             <div class="row">

+ 21 - 4
tracim/tracim/templates/user_get_me.mak Wyświetl plik

@@ -23,10 +23,10 @@
23 23
                         </p>
24 24
                         <p>
25 25
                             % if result.user.profile.id>=2:
26
-                                <span>${TIM.ICO(16, 'emblems/emblem-checked')} ${_('This user can create workspaces.')}</span><br/>
26
+                                <span>${TIM.ICO(16, 'emblems/emblem-checked')} ${_('I can create workspaces.')}</span><br/>
27 27
                             % endif
28 28
                             % if fake_api.current_user.profile.id>=3:
29
-                                <span>${TIM.ICO(16, 'emblems/emblem-checked')} ${_('This user is an administrator.')}</span><br/>
29
+                                <span>${TIM.ICO(16, 'emblems/emblem-checked')} ${_('I am an administrator.')}</span><br/>
30 30
                             % endif
31 31
                         </p>
32 32
                     </div>
@@ -39,15 +39,32 @@
39 39
                             ${_('My workspaces')}
40 40
                         </h3>
41 41
                         % if len(result.user.roles)<=0:
42
-                            ${WIDGETS.EMPTY_CONTENT(_('You are not member of any workspace.'))}
42
+                            ${WIDGETS.EMPTY_CONTENT(_('I\'m not member of any workspace.'))}
43 43
                         % else:
44 44
                             <table class="table">
45 45
                                 % for role in result.user.roles:
46
-                                    <tr><td>${role.workspace.name}</td><td><span style="${role.style}">${role.label}</span></td></tr>
46
+                                    <tr>
47
+                                        <td>${role.workspace.name}</td>
48
+                                        <td><span style="${role.style}">${role.label}</span></td>
49
+                                        <td>
50
+                                            % if role.notifications_subscribed:
51
+                                                <a href="${tg.url('/user/me/workspaces/{}/disable_notifications').format(role.workspace.id)}">
52
+                                                    ${TIM.ICO_TOOLTIP(16, 'actions/mail-reply-sender', _('Email notifications subscribed. Click to stop notifications.'))}
53
+                                                </a>
54
+                                            % else:
55
+                                                <a href="${tg.url('/user/me/workspaces/{}/enable_notifications').format(role.workspace.id)}">
56
+                                                    ${TIM.ICO_TOOLTIP(16, 'actions/mail-notification-none', _('Email notifications desactivated. Click to subscribe.'))}
57
+                                                </a>
58
+                                            % endif
59
+                                        </td>
60
+                                    </tr>
47 61
                                 % endfor
48 62
                             </table>
49 63
                         % endif
50 64
                     </div>
65
+                    % if len(result.user.roles)>0:
66
+                        <p class="alert alert-info">${_('You can configure your email notifications by clicking on the email icons above')}</p>
67
+                    % endif
51 68
                 </div>
52 69
             </div>
53 70
         </div>