Преглед изворни кода

Merge branch 'lebouquetin-feature/improve-mail-notifications'

Damien ACCORSI пре 10 година
родитељ
комит
2f594cd3de

+ 17 - 0
tracim/tracim/config/app_cfg.py Прегледај датотеку

25
 import tracim
25
 import tracim
26
 from tracim import model
26
 from tracim import model
27
 from tracim.lib.base import logger
27
 from tracim.lib.base import logger
28
+from tracim.model.data import ActionDescription
29
+from tracim.model.data import ContentType
28
 
30
 
29
 base_config = AppConfig()
31
 base_config = AppConfig()
30
 base_config.renderers = []
32
 base_config.renderers = []
235
 
237
 
236
         self.WEBSITE_TREEVIEW_CONTENT = tg.config.get('website.treeview.content')
238
         self.WEBSITE_TREEVIEW_CONTENT = tg.config.get('website.treeview.content')
237
 
239
 
240
+        self.EMAIL_NOTIFICATION_NOTIFIED_EVENTS = [
241
+            ActionDescription.COMMENT,
242
+            ActionDescription.CREATION,
243
+            ActionDescription.EDITION,
244
+            ActionDescription.REVISION
245
+        ]
246
+
247
+        self.EMAIL_NOTIFICATION_NOTIFIED_CONTENTS = [
248
+            ContentType.Page,
249
+            ContentType.Thread,
250
+            ContentType.File,
251
+            ContentType.Comment,
252
+            # ContentType.Folder -- Folder is skipped
253
+        ]
254
+
238
 
255
 
239
     def get_tracker_js_content(self, js_tracker_file_path = None):
256
     def get_tracker_js_content(self, js_tracker_file_path = None):
240
         js_tracker_file_path = tg.config.get('js_tracker_path', None)
257
         js_tracker_file_path = tg.config.get('js_tracker_path', None)

+ 61 - 11
tracim/tracim/controllers/content.py Прегледај датотеку

267
         workspace = tmpl_context.workspace
267
         workspace = tmpl_context.workspace
268
 
268
 
269
         try:
269
         try:
270
+            item_saved = False
270
             api = ContentApi(tmpl_context.current_user)
271
             api = ContentApi(tmpl_context.current_user)
271
             item = api.get_one(int(item_id), self._item_type, workspace)
272
             item = api.get_one(int(item_id), self._item_type, workspace)
272
-            if comment or label:
273
-                api.update_content(item, label if label else item.label, comment if comment else '')
274
-                action_description = ActionDescription.EDITION
275
-                # The action_description is overwritten by ActionDescription.REVISION if the file content is also updated
276
 
273
 
277
-            if isinstance(file_data, FieldStorage):
278
-                api.update_file_data(item, file_data.filename, file_data.type, file_data.file.read())
279
-                action_description = ActionDescription.REVISION
274
+            # TODO - D.A. - 2015-03-19
275
+            # refactor this method in order to make code easier to understand
280
 
276
 
281
-            api.save(item, action_description)
277
+            if comment and label:
278
+                updated_item = api.update_content(
279
+                    item, label if label else item.label,
280
+                    comment if comment else ''
281
+                )
282
+                api.save(updated_item, ActionDescription.EDITION)
283
+
284
+                # This case is the default "file title and description update"
285
+                # In this case the file itself is not revisionned
286
+
287
+            else:
288
+                # So, now we may have a comment and/or a file revision
289
+                if comment and ''==label:
290
+                    comment_item = api.create_comment(workspace,
291
+                                                      item, comment,
292
+                                                      do_save=False)
293
+
294
+                    if not isinstance(file_data, FieldStorage):
295
+                        api.save(comment_item, ActionDescription.COMMENT)
296
+                    else:
297
+                        # The notification is only sent
298
+                        # if the file is NOT updated
299
+                        #
300
+                        #  If the file is also updated,
301
+                        #  then a 'file revision' notification will be sent.
302
+                        api.save(comment_item,
303
+                                 ActionDescription.COMMENT,
304
+                                 do_notify=False)
305
+
306
+                if isinstance(file_data, FieldStorage):
307
+                    api.update_file_data(item, file_data.filename, file_data.type, file_data.file.read())
308
+                    api.save(item, ActionDescription.REVISION)
282
 
309
 
283
             msg = _('{} updated').format(self._item_type_label)
310
             msg = _('{} updated').format(self._item_type_label)
284
             tg.flash(msg, CST.STATUS_OK)
311
             tg.flash(msg, CST.STATUS_OK)
376
 
403
 
377
         page = api.create(ContentType.Page, workspace, tmpl_context.folder, label)
404
         page = api.create(ContentType.Page, workspace, tmpl_context.folder, label)
378
         page.description = content
405
         page.description = content
379
-        api.save(page, ActionDescription.CREATION)
406
+        api.save(page, ActionDescription.CREATION, do_notify=True)
380
 
407
 
381
         tg.flash(_('Page created'), CST.STATUS_OK)
408
         tg.flash(_('Page created'), CST.STATUS_OK)
382
         tg.redirect(tg.url('/workspaces/{}/folders/{}/pages/{}').format(tmpl_context.workspace_id, tmpl_context.folder_id, page.content_id))
409
         tg.redirect(tg.url('/workspaces/{}/folders/{}/pages/{}').format(tmpl_context.workspace_id, tmpl_context.folder_id, page.content_id))
383
 
410
 
384
 
411
 
412
+    @tg.require(current_user_is_contributor())
413
+    @tg.expose()
414
+    def put(self, item_id, label='',content=''):
415
+        # INFO - D.A. This method is a raw copy of
416
+        # TODO - SECURE THIS
417
+        workspace = tmpl_context.workspace
418
+
419
+        try:
420
+            api = ContentApi(tmpl_context.current_user)
421
+            item = api.get_one(int(item_id), self._item_type, workspace)
422
+            api.update_content(item, label, content)
423
+            api.save(item, ActionDescription.REVISION)
424
+
425
+            msg = _('{} updated').format(self._item_type_label)
426
+            tg.flash(msg, CST.STATUS_OK)
427
+            tg.redirect(self._std_url.format(tmpl_context.workspace_id, tmpl_context.folder_id, item.content_id))
428
+
429
+        except ValueError as e:
430
+            msg = _('{} not updated - error: {}').format(self._item_type_label, str(e))
431
+            tg.flash(msg, CST.STATUS_ERROR)
432
+            tg.redirect(self._err_url.format(tmpl_context.workspace_id, tmpl_context.folder_id, item_id))
433
+
385
 
434
 
386
 class UserWorkspaceFolderThreadRestController(TIMWorkspaceContentRestController):
435
 class UserWorkspaceFolderThreadRestController(TIMWorkspaceContentRestController):
387
     """
436
     """
452
 
501
 
453
         thread = api.create(ContentType.Thread, workspace, tmpl_context.folder, label)
502
         thread = api.create(ContentType.Thread, workspace, tmpl_context.folder, label)
454
         # FIXME - DO NOT DUPLCIATE FIRST MESSAGE thread.description = content
503
         # FIXME - DO NOT DUPLCIATE FIRST MESSAGE thread.description = content
455
-        api.save(thread, ActionDescription.CREATION)
504
+        api.save(thread, ActionDescription.CREATION, do_notify=False)
456
 
505
 
457
         comment = api.create(ContentType.Comment, workspace, thread, label)
506
         comment = api.create(ContentType.Comment, workspace, thread, label)
458
         comment.label = ''
507
         comment.label = ''
459
         comment.description = content
508
         comment.description = content
460
-        api.save(comment, ActionDescription.COMMENT)
509
+        api.save(comment, ActionDescription.COMMENT, do_notify=False)
510
+        api.do_notify(thread)
461
 
511
 
462
         tg.flash(_('Thread created'), CST.STATUS_OK)
512
         tg.flash(_('Thread created'), CST.STATUS_OK)
463
         tg.redirect(self._std_url.format(tmpl_context.workspace_id, tmpl_context.folder_id, thread.content_id))
513
         tg.redirect(self._std_url.format(tmpl_context.workspace_id, tmpl_context.folder_id, thread.content_id))

+ 180 - 93
tracim/tracim/i18n/fr/LC_MESSAGES/tracim.po Прегледај датотеку

10
 <<<<<<< HEAD
10
 <<<<<<< HEAD
11
 <<<<<<< HEAD
11
 <<<<<<< HEAD
12
 <<<<<<< HEAD
12
 <<<<<<< HEAD
13
+<<<<<<< HEAD
13
 "POT-Creation-Date: 2015-03-02 22:28+0100\n"
14
 "POT-Creation-Date: 2015-03-02 22:28+0100\n"
14
 "PO-Revision-Date: 2015-03-02 22:32+0100\n"
15
 "PO-Revision-Date: 2015-03-02 22:32+0100\n"
15
 =======
16
 =======
24
 "POT-Creation-Date: 2015-03-10 18:35+0100\n"
25
 "POT-Creation-Date: 2015-03-10 18:35+0100\n"
25
 "PO-Revision-Date: 2015-03-10 18:32+0100\n"
26
 "PO-Revision-Date: 2015-03-10 18:32+0100\n"
26
 >>>>>>> 139e854c54cbb438fb69dda1149d450228cf2e8a
27
 >>>>>>> 139e854c54cbb438fb69dda1149d450228cf2e8a
28
+=======
29
+"POT-Creation-Date: 2015-03-20 00:44+0100\n"
30
+"PO-Revision-Date: 2015-03-20 00:37+0100\n"
31
+>>>>>>> b67ad8598a3cfae087b99a8b7bf905ae8b8ec991
27
 "Last-Translator: Damien Accorsi <damien.accorsi@free.fr>\n"
32
 "Last-Translator: Damien Accorsi <damien.accorsi@free.fr>\n"
28
 "Language-Team: fr_FR <LL@li.org>\n"
33
 "Language-Team: fr_FR <LL@li.org>\n"
29
 "Plural-Forms: nplurals=2; plural=(n > 1)\n"
34
 "Plural-Forms: nplurals=2; plural=(n > 1)\n"
32
 "Content-Transfer-Encoding: 8bit\n"
37
 "Content-Transfer-Encoding: 8bit\n"
33
 "Generated-By: Babel 1.3\n"
38
 "Generated-By: Babel 1.3\n"
34
 
39
 
35
-#: tracim/config/app_cfg.py:135
40
+#: tracim/config/app_cfg.py:137
36
 msgid "Password reset request"
41
 msgid "Password reset request"
37
 msgstr "Réinitialisation du mot de passe"
42
 msgstr "Réinitialisation du mot de passe"
38
 
43
 
39
-#: tracim/config/app_cfg.py:136 tracim/config/app_cfg.py:165
44
+#: tracim/config/app_cfg.py:138 tracim/config/app_cfg.py:167
40
 #, python-format
45
 #, python-format
41
 msgid ""
46
 msgid ""
42
 "\n"
47
 "\n"
59
 " à l'originie de cette requête, merci d'ignorer et/ou supprimer cet "
64
 " à l'originie de cette requête, merci d'ignorer et/ou supprimer cet "
60
 "e-mail.\n"
65
 "e-mail.\n"
61
 
66
 
62
-#: tracim/config/app_cfg.py:152 tracim/templates/user_workspace_forms.mak:251
67
+#: tracim/config/app_cfg.py:154 tracim/templates/user_workspace_forms.mak:251
63
 #: tracim/templates/user_workspace_forms.mak:252
68
 #: tracim/templates/user_workspace_forms.mak:252
64
 msgid "New password"
69
 msgid "New password"
65
 msgstr "Nouveau mot de passe"
70
 msgstr "Nouveau mot de passe"
66
 
71
 
67
-#: tracim/config/app_cfg.py:153
72
+#: tracim/config/app_cfg.py:155
68
 msgid "Confirm new password"
73
 msgid "Confirm new password"
69
 msgstr "Confirmer le nouveau mot de passe"
74
 msgstr "Confirmer le nouveau mot de passe"
70
 
75
 
71
-#: tracim/config/app_cfg.py:154
76
+#: tracim/config/app_cfg.py:156
72
 msgid "Save new password"
77
 msgid "Save new password"
73
 msgstr "Enregistrer le nouveau mot de passe"
78
 msgstr "Enregistrer le nouveau mot de passe"
74
 
79
 
75
-#: tracim/config/app_cfg.py:155 tracim/templates/user_get_all.mak:34
80
+#: tracim/config/app_cfg.py:157 tracim/templates/user_get_all.mak:34
76
 msgid "Email address"
81
 msgid "Email address"
77
 msgstr "Adresse email"
82
 msgstr "Adresse email"
78
 
83
 
79
-#: tracim/config/app_cfg.py:156
84
+#: tracim/config/app_cfg.py:158
80
 msgid "Send Request"
85
 msgid "Send Request"
81
 msgstr "Valider"
86
 msgstr "Valider"
82
 
87
 
83
-#: tracim/config/app_cfg.py:159
88
+#: tracim/config/app_cfg.py:161
84
 msgid "Password reset request sent"
89
 msgid "Password reset request sent"
85
 msgstr "Requête de réinitialisation du mot de passe envoyée"
90
 msgstr "Requête de réinitialisation du mot de passe envoyée"
86
 
91
 
87
-#: tracim/config/app_cfg.py:160 tracim/config/app_cfg.py:162
92
+#: tracim/config/app_cfg.py:162 tracim/config/app_cfg.py:164
88
 msgid "Invalid password reset request"
93
 msgid "Invalid password reset request"
89
 msgstr "Requête de réinitialisation du mot de passe invalide"
94
 msgstr "Requête de réinitialisation du mot de passe invalide"
90
 
95
 
91
-#: tracim/config/app_cfg.py:161
96
+#: tracim/config/app_cfg.py:163
92
 msgid "Password reset request timed out"
97
 msgid "Password reset request timed out"
93
 msgstr "Echec de la requête de réinitialisation du mot de passe"
98
 msgstr "Echec de la requête de réinitialisation du mot de passe"
94
 
99
 
95
-#: tracim/config/app_cfg.py:163
100
+#: tracim/config/app_cfg.py:165
96
 msgid "Password changed successfully"
101
 msgid "Password changed successfully"
97
 msgstr "Mot de passe changé"
102
 msgstr "Mot de passe changé"
98
 
103
 
99
 <<<<<<< HEAD
104
 <<<<<<< HEAD
100
 <<<<<<< HEAD
105
 <<<<<<< HEAD
101
 <<<<<<< HEAD
106
 <<<<<<< HEAD
107
+<<<<<<< HEAD
102
 #: tracim/controllers/__init__.py:139
108
 #: tracim/controllers/__init__.py:139
103
 #: tracim/templates/dashboard.mak:36
109
 #: tracim/templates/dashboard.mak:36
104
 #: tracim/templates/master_authenticated.mak:94
110
 #: tracim/templates/master_authenticated.mak:94
143
 
149
 
144
 #: tracim/controllers/__init__.py:252 tracim/controllers/content.py:288
150
 #: tracim/controllers/__init__.py:252 tracim/controllers/content.py:288
145
 >>>>>>> 139e854c54cbb438fb69dda1149d450228cf2e8a
151
 >>>>>>> 139e854c54cbb438fb69dda1149d450228cf2e8a
152
+=======
153
+#: tracim/controllers/__init__.py:247 tracim/controllers/content.py:310
154
+#: tracim/controllers/content.py:425
155
+msgid "{} updated"
156
+msgstr "{} mis(e) à jour"
157
+
158
+#: tracim/controllers/__init__.py:252 tracim/controllers/content.py:315
159
+#: tracim/controllers/content.py:430
160
+>>>>>>> b67ad8598a3cfae087b99a8b7bf905ae8b8ec991
146
 msgid "{} not updated - error: {}"
161
 msgid "{} not updated - error: {}"
147
 msgstr "{} pas mis(e) à jour - erreur : {}"
162
 msgstr "{} pas mis(e) à jour - erreur : {}"
148
 
163
 
167
 
182
 
168
 <<<<<<< HEAD
183
 <<<<<<< HEAD
169
 <<<<<<< HEAD
184
 <<<<<<< HEAD
185
+<<<<<<< HEAD
170
 #: tracim/controllers/__init__.py:302
186
 #: tracim/controllers/__init__.py:302
171
 >>>>>>> b2e4664ae2ad0cc8fbc3344f6fa37845cf7e344d
187
 >>>>>>> b2e4664ae2ad0cc8fbc3344f6fa37845cf7e344d
172
 #: tracim/controllers/content.py:748
188
 #: tracim/controllers/content.py:748
198
 #: tracim/controllers/content.py:797
214
 #: tracim/controllers/content.py:797
199
 =======
215
 =======
200
 #: tracim/controllers/__init__.py:302 tracim/controllers/content.py:748
216
 #: tracim/controllers/__init__.py:302 tracim/controllers/content.py:748
217
+=======
218
+#: tracim/controllers/__init__.py:302 tracim/controllers/content.py:798
219
+>>>>>>> b67ad8598a3cfae087b99a8b7bf905ae8b8ec991
201
 msgid "{} archived. <a class=\"alert-link\" href=\"{}\">Cancel action</a>"
220
 msgid "{} archived. <a class=\"alert-link\" href=\"{}\">Cancel action</a>"
202
 msgstr "{} archivé(e). <a class=\"alert-link\" href=\"{}\">Annuler l'opération</a>"
221
 msgstr "{} archivé(e). <a class=\"alert-link\" href=\"{}\">Annuler l'opération</a>"
203
 
222
 
204
-#: tracim/controllers/__init__.py:311 tracim/controllers/content.py:757
223
+#: tracim/controllers/__init__.py:311 tracim/controllers/content.py:807
205
 msgid "{} not archived: {}"
224
 msgid "{} not archived: {}"
206
 msgstr "{} non archivé(e) : {}"
225
 msgstr "{} non archivé(e) : {}"
207
 
226
 
208
-#: tracim/controllers/__init__.py:325 tracim/controllers/content.py:771
227
+#: tracim/controllers/__init__.py:325 tracim/controllers/content.py:821
209
 msgid "{} unarchived."
228
 msgid "{} unarchived."
210
 msgstr "{} désarchivé(e)"
229
 msgstr "{} désarchivé(e)"
211
 
230
 
212
-#: tracim/controllers/__init__.py:333 tracim/controllers/content.py:779
231
+#: tracim/controllers/__init__.py:333 tracim/controllers/content.py:829
213
 msgid "{} not un-archived: {}"
232
 msgid "{} not un-archived: {}"
214
 msgstr "{} non désarchivé(e) : {}"
233
 msgstr "{} non désarchivé(e) : {}"
215
 
234
 
216
 #: tracim/controllers/__init__.py:351 tracim/controllers/content.py:88
235
 #: tracim/controllers/__init__.py:351 tracim/controllers/content.py:88
236
+<<<<<<< HEAD
217
 #: tracim/controllers/content.py:796
237
 #: tracim/controllers/content.py:796
218
 >>>>>>> 139e854c54cbb438fb69dda1149d450228cf2e8a
238
 >>>>>>> 139e854c54cbb438fb69dda1149d450228cf2e8a
219
 msgid "{} deleted. <a class=\"alert-link\" href=\"{}\">Cancel action</a>"
239
 msgid "{} deleted. <a class=\"alert-link\" href=\"{}\">Cancel action</a>"
260
 
280
 
261
 #: tracim/controllers/__init__.py:351 tracim/controllers/content.py:88
281
 #: tracim/controllers/__init__.py:351 tracim/controllers/content.py:88
262
 #: tracim/controllers/content.py:796
282
 #: tracim/controllers/content.py:796
283
+=======
284
+#: tracim/controllers/content.py:846
285
+>>>>>>> b67ad8598a3cfae087b99a8b7bf905ae8b8ec991
263
 msgid "{} deleted. <a class=\"alert-link\" href=\"{}\">Cancel action</a>"
286
 msgid "{} deleted. <a class=\"alert-link\" href=\"{}\">Cancel action</a>"
264
 msgstr ""
287
 msgstr ""
265
 "{} supprimé(e). <a class=\"alert-link\" href=\"{}\">Annuler "
288
 "{} supprimé(e). <a class=\"alert-link\" href=\"{}\">Annuler "
266
 "l'opération</a>"
289
 "l'opération</a>"
267
 
290
 
268
 #: tracim/controllers/__init__.py:360 tracim/controllers/content.py:99
291
 #: tracim/controllers/__init__.py:360 tracim/controllers/content.py:99
269
-#: tracim/controllers/content.py:805
292
+#: tracim/controllers/content.py:855
270
 msgid "{} not deleted: {}"
293
 msgid "{} not deleted: {}"
271
 msgstr "{} non supprimé(e) : {}"
294
 msgstr "{} non supprimé(e) : {}"
272
 
295
 
273
 #: tracim/controllers/__init__.py:375 tracim/controllers/content.py:116
296
 #: tracim/controllers/__init__.py:375 tracim/controllers/content.py:116
274
-#: tracim/controllers/content.py:820
297
+#: tracim/controllers/content.py:870
275
 msgid "{} undeleted."
298
 msgid "{} undeleted."
276
 msgstr "{} restauré(e)."
299
 msgstr "{} restauré(e)."
277
 
300
 
288
 
311
 
289
 >>>>>>> 139e854c54cbb438fb69dda1149d450228cf2e8a
312
 >>>>>>> 139e854c54cbb438fb69dda1149d450228cf2e8a
290
 #: tracim/controllers/__init__.py:385 tracim/controllers/content.py:128
313
 #: tracim/controllers/__init__.py:385 tracim/controllers/content.py:128
314
+<<<<<<< HEAD
291
 #: tracim/controllers/content.py:830
315
 #: tracim/controllers/content.py:830
292
 >>>>>>> b2e4664ae2ad0cc8fbc3344f6fa37845cf7e344d
316
 >>>>>>> b2e4664ae2ad0cc8fbc3344f6fa37845cf7e344d
317
+=======
318
+#: tracim/controllers/content.py:880
319
+>>>>>>> b67ad8598a3cfae087b99a8b7bf905ae8b8ec991
293
 msgid "{} not un-deleted: {}"
320
 msgid "{} not un-deleted: {}"
294
 msgstr "{} non restauré(e) : {}"
321
 msgstr "{} non restauré(e) : {}"
295
 
322
 
310
 msgid "File created"
337
 msgid "File created"
311
 msgstr "Fichier créé"
338
 msgstr "Fichier créé"
312
 
339
 
313
-#: tracim/controllers/content.py:313
340
+#: tracim/controllers/content.py:340
314
 msgid "Page"
341
 msgid "Page"
315
 msgstr "Page"
342
 msgstr "Page"
316
 
343
 
317
-#: tracim/controllers/content.py:381
344
+#: tracim/controllers/content.py:408
318
 msgid "Page created"
345
 msgid "Page created"
319
 msgstr "Page créée"
346
 msgstr "Page créée"
320
 
347
 
321
-#: tracim/controllers/content.py:421
348
+#: tracim/controllers/content.py:470
322
 msgid "Thread"
349
 msgid "Thread"
323
 msgstr "Discussion"
350
 msgstr "Discussion"
324
 
351
 
325
-#: tracim/controllers/content.py:462
352
+#: tracim/controllers/content.py:512
326
 msgid "Thread created"
353
 msgid "Thread created"
327
 msgstr "Discussion créée"
354
 msgstr "Discussion créée"
328
 
355
 
329
-#: tracim/controllers/content.py:547
356
+#: tracim/controllers/content.py:597
330
 msgid "Item moved to {}"
357
 msgid "Item moved to {}"
331
 msgstr "Element déplacé vers {}"
358
 msgstr "Element déplacé vers {}"
332
 
359
 
333
-#: tracim/controllers/content.py:549
360
+#: tracim/controllers/content.py:599
334
 msgid "Item moved to workspace root"
361
 msgid "Item moved to workspace root"
335
 msgstr "Element déplacé à la racine de l'espace de travail"
362
 msgstr "Element déplacé à la racine de l'espace de travail"
336
 
363
 
337
-#: tracim/controllers/content.py:669
364
+#: tracim/controllers/content.py:719
338
 msgid "Folder created"
365
 msgid "Folder created"
339
 msgstr "Dossier créé"
366
 msgstr "Dossier créé"
340
 
367
 
341
-#: tracim/controllers/content.py:675
368
+#: tracim/controllers/content.py:725
342
 msgid "Folder not created: {}"
369
 msgid "Folder not created: {}"
343
 msgstr "Dossier non créé : {}"
370
 msgstr "Dossier non créé : {}"
344
 
371
 
345
-#: tracim/controllers/content.py:710
372
+#: tracim/controllers/content.py:760
346
 msgid "Folder updated"
373
 msgid "Folder updated"
347
 msgstr "Dossier mis à jour"
374
 msgstr "Dossier mis à jour"
348
 
375
 
349
-#: tracim/controllers/content.py:715
376
+#: tracim/controllers/content.py:765
350
 msgid "Folder not updated: {}"
377
 msgid "Folder not updated: {}"
351
 msgstr "Dossier non mis à jour : {}"
378
 msgstr "Dossier non mis à jour : {}"
352
 
379
 
353
-#: tracim/controllers/content.py:730
380
+#: tracim/controllers/content.py:780
354
 msgid "Folder"
381
 msgid "Folder"
355
 <<<<<<< HEAD
382
 <<<<<<< HEAD
356
 msgstr "Dossier"
383
 msgstr "Dossier"
505
 <<<<<<< HEAD
532
 <<<<<<< HEAD
506
 <<<<<<< HEAD
533
 <<<<<<< HEAD
507
 <<<<<<< HEAD
534
 <<<<<<< HEAD
535
+<<<<<<< HEAD
508
 =======
536
 =======
509
 #: tracim/lib/content.py:98
537
 #: tracim/lib/content.py:98
510
 #: tracim/templates/dashboard.mak:36
538
 #: tracim/templates/dashboard.mak:36
514
 =======
542
 =======
515
 #: tracim/lib/content.py:98 tracim/templates/dashboard.mak:36
543
 #: tracim/lib/content.py:98 tracim/templates/dashboard.mak:36
516
 >>>>>>> 139e854c54cbb438fb69dda1149d450228cf2e8a
544
 >>>>>>> 139e854c54cbb438fb69dda1149d450228cf2e8a
545
+=======
546
+#: tracim/lib/content.py:100 tracim/templates/dashboard.mak:36
547
+>>>>>>> b67ad8598a3cfae087b99a8b7bf905ae8b8ec991
517
 #: tracim/templates/master_authenticated.mak:94
548
 #: tracim/templates/master_authenticated.mak:94
518
 #: tracim/templates/master_no_toolbar_no_login.mak:112
549
 #: tracim/templates/master_no_toolbar_no_login.mak:112
519
 #: tracim/templates/search.mak:11 tracim/templates/user_get_one.mak:39
550
 #: tracim/templates/search.mak:11 tracim/templates/user_get_one.mak:39
528
 msgid "Workspaces"
559
 msgid "Workspaces"
529
 msgstr "Espaces de travail"
560
 msgstr "Espaces de travail"
530
 
561
 
562
+<<<<<<< HEAD
531
 >>>>>>> b2e4664ae2ad0cc8fbc3344f6fa37845cf7e344d
563
 >>>>>>> b2e4664ae2ad0cc8fbc3344f6fa37845cf7e344d
564
+=======
565
+#: tracim/lib/notifications.py:289
566
+msgid "<span id=\"content-intro-username\">{}</span> added a comment:"
567
+msgstr "<span id=\"content-intro-username\">{}</span> a ajouté un commentaire :"
568
+
569
+#: tracim/lib/notifications.py:291 tracim/lib/notifications.py:300
570
+msgid "Answer"
571
+msgstr "Répondre"
572
+
573
+#: tracim/lib/notifications.py:297 tracim/lib/notifications.py:321
574
+#: tracim/lib/notifications.py:347
575
+msgid "View online"
576
+msgstr "Voir en ligne"
577
+
578
+#: tracim/lib/notifications.py:301
579
+msgid "<span id=\"content-intro-username\">{}</span> started a thread entitled:"
580
+msgstr ""
581
+"<span id=\"content-intro-username\">{}</span> a lancé une discussion "
582
+"intitulée :"
583
+
584
+#: tracim/lib/notifications.py:306
585
+msgid "<span id=\"content-intro-username\">{}</span> added a file entitled:"
586
+msgstr ""
587
+"<span id=\"content-intro-username\">{}</span> a ajouté un fichier "
588
+"intitulé :"
589
+
590
+#: tracim/lib/notifications.py:316
591
+msgid "<span id=\"content-intro-username\">{}</span> added a page entitled:"
592
+msgstr ""
593
+"<span id=\"content-intro-username\">{}</span> a ajouté une page intitulée"
594
+" :"
595
+
596
+#: tracim/lib/notifications.py:324
597
+msgid "<span id=\"content-intro-username\">{}</span> uploaded a new revision."
598
+msgstr ""
599
+"<span id=\"content-intro-username\">{}</span> a téléchargé une nouvelle "
600
+"version."
601
+
602
+#: tracim/lib/notifications.py:328
603
+msgid "<span id=\"content-intro-username\">{}</span> updated this page."
604
+msgstr "<span id=\"content-intro-username\">{}</span> a mis à jour cette page."
605
+
606
+#: tracim/lib/notifications.py:333
607
+msgid "<p id=\"content-body-intro\">Here is an overview of the changes:</p>"
608
+msgstr "<p id=\"content-body-intro\">Voici un aperçu des modifications :</p>"
609
+
610
+#: tracim/lib/notifications.py:338
611
+msgid ""
612
+"<span id=\"content-intro-username\">{}</span> updated the thread "
613
+"description."
614
+msgstr ""
615
+"<span id=\"content-intro-username\">{}</span> a mis à jour la description"
616
+" de la discussion."
617
+
618
+#: tracim/lib/notifications.py:350
619
+msgid ""
620
+"<span id=\"content-intro-username\">{}</span> updated the file "
621
+"description."
622
+msgstr ""
623
+"<span id=\"content-intro-username\">{}</span> a mis à jour la description"
624
+" du fichier."
625
+
626
+>>>>>>> b67ad8598a3cfae087b99a8b7bf905ae8b8ec991
532
 #: tracim/lib/predicates.py:17
627
 #: tracim/lib/predicates.py:17
533
 msgid "You are not authorized to access this resource"
628
 msgid "You are not authorized to access this resource"
534
 msgstr "Vous n'êtes pas autorisé à accéder à cette ressource"
629
 msgstr "Vous n'êtes pas autorisé à accéder à cette ressource"
616
 msgid "Item undeleted"
711
 msgid "Item undeleted"
617
 msgstr "Elément restauré"
712
 msgstr "Elément restauré"
618
 
713
 
619
-#: tracim/model/data.py:202 tracim/model/data.py:212
714
+#: tracim/model/data.py:201 tracim/model/data.py:211
620
 msgid "work in progress"
715
 msgid "work in progress"
621
 msgstr "travail en cours"
716
 msgstr "travail en cours"
622
 
717
 
623
-#: tracim/model/data.py:203 tracim/model/data.py:213
718
+#: tracim/model/data.py:202 tracim/model/data.py:212
624
 msgid "closed — validated"
719
 msgid "closed — validated"
625
 msgstr "clos(e) — validé(e)"
720
 msgstr "clos(e) — validé(e)"
626
 
721
 
627
-#: tracim/model/data.py:204 tracim/model/data.py:214
722
+#: tracim/model/data.py:203 tracim/model/data.py:213
628
 msgid "closed — cancelled"
723
 msgid "closed — cancelled"
629
 msgstr "clos(e) — annulé(e)"
724
 msgstr "clos(e) — annulé(e)"
630
 
725
 
631
-#: tracim/model/data.py:205 tracim/model/data.py:210 tracim/model/data.py:215
726
+#: tracim/model/data.py:204 tracim/model/data.py:209 tracim/model/data.py:214
632
 msgid "deprecated"
727
 msgid "deprecated"
633
 msgstr "obsolète"
728
 msgstr "obsolète"
634
 
729
 
635
-#: tracim/model/data.py:207
730
+#: tracim/model/data.py:206
636
 msgid "subject in progress"
731
 msgid "subject in progress"
637
 msgstr "discussion en cours"
732
 msgstr "discussion en cours"
638
 
733
 
639
-#: tracim/model/data.py:208
734
+#: tracim/model/data.py:207
640
 msgid "subject closed — resolved"
735
 msgid "subject closed — resolved"
641
 msgstr "discussion close — résolue"
736
 msgstr "discussion close — résolue"
642
 
737
 
643
-#: tracim/model/data.py:209
738
+#: tracim/model/data.py:208
644
 msgid "subject closed — cancelled"
739
 msgid "subject closed — cancelled"
645
 msgstr "discussion close — annulée"
740
 msgstr "discussion close — annulée"
646
 
741
 
647
-#: tracim/model/data.py:292
742
+#: tracim/model/data.py:291
648
 msgid "Delete this workspace"
743
 msgid "Delete this workspace"
649
 <<<<<<< HEAD
744
 <<<<<<< HEAD
650
 msgstr "Supprimer cet espace de travail"
745
 msgstr "Supprimer cet espace de travail"
652
 msgstr "Supprimer l'espace de travail"
747
 msgstr "Supprimer l'espace de travail"
653
 >>>>>>> b2e4664ae2ad0cc8fbc3344f6fa37845cf7e344d
748
 >>>>>>> b2e4664ae2ad0cc8fbc3344f6fa37845cf7e344d
654
 
749
 
655
-#: tracim/model/data.py:293
750
+#: tracim/model/data.py:292
656
 msgid "Delete this folder"
751
 msgid "Delete this folder"
657
 msgstr "Supprimer ce dossier"
752
 msgstr "Supprimer ce dossier"
658
 
753
 
659
-#: tracim/model/data.py:294
754
+#: tracim/model/data.py:293
660
 msgid "Delete this file"
755
 msgid "Delete this file"
661
 <<<<<<< HEAD
756
 <<<<<<< HEAD
662
 msgstr "Supprimer ce fichier"
757
 msgstr "Supprimer ce fichier"
664
 msgstr "Supprimer le fichier"
759
 msgstr "Supprimer le fichier"
665
 >>>>>>> b2e4664ae2ad0cc8fbc3344f6fa37845cf7e344d
760
 >>>>>>> b2e4664ae2ad0cc8fbc3344f6fa37845cf7e344d
666
 
761
 
667
-#: tracim/model/data.py:295
762
+#: tracim/model/data.py:294
668
 msgid "Delete this page"
763
 msgid "Delete this page"
669
 msgstr "Supprimer cette page"
764
 msgstr "Supprimer cette page"
670
 
765
 
671
-#: tracim/model/data.py:296
766
+#: tracim/model/data.py:295
672
 msgid "Delete this thread"
767
 msgid "Delete this thread"
673
 msgstr "Supprimer cette discussion"
768
 msgstr "Supprimer cette discussion"
674
 
769
 
675
-#: tracim/model/data.py:297
770
+#: tracim/model/data.py:296
676
 msgid "Delete this comment"
771
 msgid "Delete this comment"
677
 msgstr "Supprimer ce commentaire"
772
 msgstr "Supprimer ce commentaire"
678
 
773
 
1699
 "Comme <span style=\"color: #ea983d;\">gestionnaire de contenu</span> + "
1794
 "Comme <span style=\"color: #ea983d;\">gestionnaire de contenu</span> + "
1700
 "modification de l'espace de travail."
1795
 "modification de l'espace de travail."
1701
 
1796
 
1702
-#: tracim/templates/mail/content_update_body_html.mak:52
1703
-msgid "Some activity has been detected"
1704
-msgstr "Il se passe des choses"
1705
-
1706
-#: tracim/templates/mail/content_update_body_html.mak:60
1707
-msgid "<span style=\"{style}\">&mdash; by {actor_name}</span>"
1708
-msgstr "<span style=\"{style}\">&mdash; par {actor_name}</span>"
1709
-
1710
-#: tracim/templates/mail/content_update_body_html.mak:65
1711
-#: tracim/templates/mail/content_update_body_text.mak:16
1712
-msgid "This item has been deleted."
1713
-msgstr "Cet élément a été supprimé"
1714
-
1715
-#: tracim/templates/mail/content_update_body_html.mak:68
1716
-#: tracim/templates/mail/content_update_body_text.mak:18
1717
-msgid "This item has been archived."
1718
-msgstr "Cet élément a été archivé."
1719
-
1720
-#: tracim/templates/mail/content_update_body_html.mak:72
1721
-msgid "Go to information"
1722
-msgstr "Voir en ligne"
1723
-
1724
-#: tracim/templates/mail/content_update_body_html.mak:82
1797
+#: tracim/templates/mail/content_update_body_html.mak:66
1725
 msgid ""
1798
 msgid ""
1726
-"{user_display_name}, you receive this email because you are "
1727
-"<b>{user_role_label}</b> in the workspace <a "
1728
-"href=\"{workspace_url}\">{workspace_label}</a>"
1799
+"{user_display_name}, you receive this email because you are registered on"
1800
+" <i>{website_title}</i> and you are <i>{user_role_label}</i> in the "
1801
+"workspace <a href=\"{workspace_url}\">{workspace_label}</a>."
1729
 msgstr ""
1802
 msgstr ""
1730
-"{user_display_name}, vous recevez cet email car vous êtes "
1731
-"<b>{user_role_label}</b> dans l'espace de travail <a "
1732
-"href=\"{workspace_url}\">{workspace_label}</a>"
1733
-
1734
-#: tracim/templates/mail/content_update_body_text.mak:8
1735
-msgid "Some activity has been detected on the item above"
1736
-msgstr "Il se passe des choses"
1803
+"{user_display_name}, vous recevez cet email car vous êtes enregistré sur "
1804
+"le site web <i>{website_title}</i> et vous êtes <i>{user_role_label}</i> "
1805
+"dans l'espace de travail <a "
1806
+"href=\"{workspace_url}\">{workspace_label}</a>."
1737
 
1807
 
1738
-#: tracim/templates/mail/content_update_body_text.mak:8
1739
-msgid "-- by {actor_name}"
1740
-msgstr "-- par {actor_name}"
1741
-
1742
-#: tracim/templates/mail/content_update_body_text.mak:20
1743
-msgid "Go to information:"
1744
-msgstr "Voir en ligne :"
1745
-
1746
-#: tracim/templates/mail/content_update_body_text.mak:25
1808
+#: tracim/templates/mail/content_update_body_html.mak:70
1747
 msgid ""
1809
 msgid ""
1748
-"*{user_display_name}*, you receive this email because you are "
1749
-"*{user_role_label}* in the workspace {workspace_label} - {workspace_url}"
1810
+"This email was automatically sent by <i>Tracim</i>, a collaborative "
1811
+"software developped by Algoo."
1750
 msgstr ""
1812
 msgstr ""
1751
-"*{user_display_name}*, vous recevez cet email car vous êtes "
1752
-"*{user_role_label}* dans l'espace de travail {workspace_label} - "
1753
-"{workspace_url}"
1813
+"Cet email a été envoyé automatiquement par <i>Tracim</i>, un logiciel "
1814
+"collaboratif développé par Algoo."
1754
 
1815
 
1755
 #~ msgid "You have no document yet."
1816
 #~ msgid "You have no document yet."
1756
 #~ msgstr "Vous n'avez pas de document pour le moment."
1817
 #~ msgstr "Vous n'avez pas de document pour le moment."
2310
 #~ msgid "Create a topic"
2371
 #~ msgid "Create a topic"
2311
 #~ msgstr "Créer un sujet"
2372
 #~ msgstr "Créer un sujet"
2312
 
2373
 
2374
+#~ msgid "Some activity has been detected"
2375
+#~ msgstr "Il se passe des choses"
2376
+
2377
+#~ msgid "<span style=\"{style}\">&mdash; by {actor_name}</span>"
2378
+#~ msgstr "<span style=\"{style}\">&mdash; par {actor_name}</span>"
2379
+
2380
+#~ msgid "Go to information"
2381
+#~ msgstr "Voir en ligne"
2382
+
2383
+#~ msgid "Some activity has been detected on the item above"
2384
+#~ msgstr "Il se passe des choses"
2385
+
2386
+#~ msgid "-- by {actor_name}"
2387
+#~ msgstr "-- par {actor_name}"
2388
+
2389
+#~ msgid "This item has been deleted."
2390
+#~ msgstr "Cet élément a été supprimé"
2391
+
2392
+#~ msgid "This item has been archived."
2393
+#~ msgstr "Cet élément a été archivé."
2394
+
2395
+#~ msgid "Go to information:"
2396
+#~ msgstr "Voir en ligne :"
2397
+
2313
 #~ msgid ""
2398
 #~ msgid ""
2314
-#~ "This user has no password. Visit "
2315
-#~ "<a href=\"{}\">Reset password</a> page for "
2316
-#~ "init."
2399
+#~ "*{user_display_name}*, you receive this email"
2400
+#~ " because you are *{user_role_label}* in "
2401
+#~ "the workspace {workspace_label} - "
2402
+#~ "{workspace_url}"
2317
 #~ msgstr ""
2403
 #~ msgstr ""
2318
-#~ "Cet utilisateur n'a pas de mot de"
2319
-#~ " passe. Visitez <a href=\"{}\">cette "
2320
-#~ "page</a> pour l'initialiser."
2404
+#~ "*{user_display_name}*, vous recevez cet email"
2405
+#~ " car vous êtes *{user_role_label}* dans "
2406
+#~ "l'espace de travail {workspace_label} - "
2407
+#~ "{workspace_url}"
2321
 
2408
 

+ 31 - 7
tracim/tracim/lib/content.py Прегледај датотеку

25
 from tracim.model.data import ContentType
25
 from tracim.model.data import ContentType
26
 from tracim.model.data import NodeTreeItem
26
 from tracim.model.data import NodeTreeItem
27
 from tracim.model.data import Workspace
27
 from tracim.model.data import Workspace
28
+from tracim.model.data import UserRoleInWorkspace
28
 
29
 
29
-def compare_content_for_sorting_by_type_and_name(content1: Content, content2: Content):
30
+def compare_content_for_sorting_by_type_and_name(content1: Content,
31
+                                                 content2: Content):
30
     """
32
     """
31
     :param content1:
33
     :param content1:
32
     :param content2:
34
     :param content2:
131
         if workspace:
133
         if workspace:
132
             result = result.filter(Content.workspace_id==workspace.workspace_id)
134
             result = result.filter(Content.workspace_id==workspace.workspace_id)
133
 
135
 
136
+        if self._user:
137
+            # Filter according to user workspaces
138
+            workspace_ids = [r.workspace_id for r in self._user.roles \
139
+                             if r.role>=UserRoleInWorkspace.READER]
140
+            result = result.filter(Content.workspace_id.in_(workspace_ids))
141
+
134
         return result
142
         return result
135
 
143
 
136
     def _base_query(self, workspace: Workspace=None):
144
     def _base_query(self, workspace: Workspace=None):
178
         :param workspace:
186
         :param workspace:
179
         :param filter_by_allowed_content_types:
187
         :param filter_by_allowed_content_types:
180
         :param removed_item_ids:
188
         :param removed_item_ids:
181
-        :param allowed_node_types:
189
+        :param allowed_node_types: This parameter allow to hide folders for which the given type of content is not allowed.
190
+               For example, if you want to move a Page from a folder to another, you should show only folders that accept pages
182
         :return:
191
         :return:
183
         """
192
         """
184
         if not allowed_node_types:
193
         if not allowed_node_types:
193
             filter(Content.content_id.notin_(removed_item_ids)).\
202
             filter(Content.content_id.notin_(removed_item_ids)).\
194
             all()
203
             all()
195
 
204
 
196
-        if not filter_by_allowed_content_types or len(filter_by_allowed_content_types)<=0:
197
-            filter_by_allowed_content_types = ContentType.allowed_types_for_folding()
198
-
199
-        # Now, the case is to filter folders by the content that they are allowed to contain
205
+        if not filter_by_allowed_content_types or \
206
+                        len(filter_by_allowed_content_types)<=0:
207
+            # Standard case for the left treeview: we want to show all contents
208
+            # in the left treeview... so we still filter because for example
209
+            # comments must not appear in the treeview
210
+            return [folder for folder in folders \
211
+                    if folder.type in ContentType.allowed_types_for_folding()]
212
+
213
+        # Now this is a case of Folders only (used for moving content)
214
+        # When moving a content, you must get only folders that allow to be filled
215
+        # with the type of content you want to move
200
         result = []
216
         result = []
201
         for folder in folders:
217
         for folder in folders:
202
             for allowed_content_type in filter_by_allowed_content_types:
218
             for allowed_content_type in filter_by_allowed_content_types:
379
             DBSession.flush()
395
             DBSession.flush()
380
 
396
 
381
         if do_notify:
397
         if do_notify:
382
-            NotifierFactory.create(self._user).notify_content_update(content)
398
+            self.do_notify(content)
383
 
399
 
400
+    def do_notify(self, content: Content):
401
+        """
402
+        Allow to force notification for a given content. By default, it is
403
+        called during the .save() operation
404
+        :param content:
405
+        :return:
406
+        """
407
+        NotifierFactory.create(self._user).notify_content_update(content)
384
 
408
 
385
     def get_keywords(self, search_string, search_string_separators=None) -> [str]:
409
     def get_keywords(self, search_string, search_string_separators=None) -> [str]:
386
         """
410
         """

+ 151 - 9
tracim/tracim/lib/notifications.py Прегледај датотеку

3
 from email.mime.multipart import MIMEMultipart
3
 from email.mime.multipart import MIMEMultipart
4
 from email.mime.text import MIMEText
4
 from email.mime.text import MIMEText
5
 
5
 
6
+import lxml
7
+from lxml.html.diff import htmldiff
6
 
8
 
7
 from mako.template import Template
9
 from mako.template import Template
8
 
10
 
21
 from tracim.model.serializers import CTX
23
 from tracim.model.serializers import CTX
22
 from tracim.model.serializers import DictLikeClass
24
 from tracim.model.serializers import DictLikeClass
23
 
25
 
24
-from tracim.model.data import Content, UserRoleInWorkspace, ContentType
26
+from tracim.model.data import Content, UserRoleInWorkspace, ContentType, \
27
+    ActionDescription
25
 from tracim.model.auth import User
28
 from tracim.model.auth import User
26
 
29
 
27
 
30
 
75
                                        cfg.EMAIL_NOTIFICATION_SMTP_PASSWORD)
78
                                        cfg.EMAIL_NOTIFICATION_SMTP_PASSWORD)
76
 
79
 
77
     def notify_content_update(self, content: Content):
80
     def notify_content_update(self, content: Content):
78
-        logger.info(self, 'About to email-notify update of content {} by user {}'.format(content.content_id, self._user.user_id if self._user else 0)) # 0 means "no user"
79
-
80
         global_config = CFG.get_instance()
81
         global_config = CFG.get_instance()
81
 
82
 
83
+        if content.get_last_action().id not \
84
+                in global_config.EMAIL_NOTIFICATION_NOTIFIED_EVENTS:
85
+            logger.info(
86
+                self,
87
+                'Skip email notification for update of content {}'
88
+                'by user {} (the action is {})'.format(
89
+                    content.content_id,
90
+                    # below: 0 means "no user"
91
+                    self._user.user_id if self._user else 0,
92
+                    content.get_last_action().id
93
+                )
94
+            )
95
+            return
96
+
97
+        logger.info(self,
98
+                    'About to email-notify update'
99
+                    'of content {} by user {}'.format(
100
+                        content.content_id,
101
+                        # Below: 0 means "no user"
102
+                        self._user.user_id if self._user else 0
103
+                    )
104
+        )
105
+
106
+        if content.type not \
107
+                in global_config.EMAIL_NOTIFICATION_NOTIFIED_CONTENTS:
108
+            logger.info(
109
+                self,
110
+                'Skip email notification for update of content {}'
111
+                'by user {} (the content type is {})'.format(
112
+                    content.type,
113
+                    # below: 0 means "no user"
114
+                    self._user.user_id if self._user else 0,
115
+                    content.get_last_action().id
116
+                )
117
+            )
118
+            return
119
+
120
+        logger.info(self,
121
+                    'About to email-notify update'
122
+                    'of content {} by user {}'.format(
123
+                        content.content_id,
124
+                        # Below: 0 means "no user"
125
+                        self._user.user_id if self._user else 0
126
+                    )
127
+        )
128
+
82
         ####
129
         ####
83
         #
130
         #
84
-        # INFO - D.A. - 2014-11-05 - Emails are sent through asynchronous jobs. For that reason, we do not
85
-        # give SQLAlchemy objects but ids only (SQLA objects are related to a given thread/session)
131
+        # INFO - D.A. - 2014-11-05 - Emails are sent through asynchronous jobs.
132
+        # For that reason, we do not give SQLAlchemy objects but ids only
133
+        # (SQLA objects are related to a given thread/session)
86
         #
134
         #
87
         try:
135
         try:
88
             if global_config.EMAIL_NOTIFICATION_PROCESSING_MODE.lower()==global_config.CST.ASYNC.lower():
136
             if global_config.EMAIL_NOTIFICATION_PROCESSING_MODE.lower()==global_config.CST.ASYNC.lower():
146
         logger.debug(self, 'Content: {}'.format(event_content_id))
194
         logger.debug(self, 'Content: {}'.format(event_content_id))
147
 
195
 
148
         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
196
         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
149
-        content = content.parent if content.type==ContentType.Comment else content
197
+        main_content = content.parent if content.type==ContentType.Comment else content
150
         notifiable_roles = WorkspaceApi(user).get_notifiable_roles(content.workspace)
198
         notifiable_roles = WorkspaceApi(user).get_notifiable_roles(content.workspace)
151
 
199
 
152
         if len(notifiable_roles)<=0:
200
         if len(notifiable_roles)<=0:
175
             #
223
             #
176
             subject = self._global_config.EMAIL_NOTIFICATION_CONTENT_UPDATE_SUBJECT
224
             subject = self._global_config.EMAIL_NOTIFICATION_CONTENT_UPDATE_SUBJECT
177
             subject = subject.replace(EST.WEBSITE_TITLE, self._global_config.WEBSITE_TITLE.__str__())
225
             subject = subject.replace(EST.WEBSITE_TITLE, self._global_config.WEBSITE_TITLE.__str__())
178
-            subject = subject.replace(EST.WORKSPACE_LABEL, content.workspace.label.__str__())
179
-            subject = subject.replace(EST.CONTENT_LABEL, content.label.__str__() if content.label else content.file_name.__str__())
180
-            subject = subject.replace(EST.CONTENT_STATUS_LABEL, content.get_status().label.__str__())
226
+            subject = subject.replace(EST.WORKSPACE_LABEL, main_content.workspace.label.__str__())
227
+            subject = subject.replace(EST.CONTENT_LABEL, main_content.label.__str__() if main_content.label else main_content.file_name.__str__())
228
+            subject = subject.replace(EST.CONTENT_STATUS_LABEL, main_content.get_status().label.__str__())
181
 
229
 
182
             message = MIMEMultipart('alternative')
230
             message = MIMEMultipart('alternative')
183
             message['Subject'] = subject
231
             message['Subject'] = subject
185
             message['To'] = to_addr
233
             message['To'] = to_addr
186
 
234
 
187
             body_text = self._build_email_body(self._global_config.EMAIL_NOTIFICATION_CONTENT_UPDATE_TEMPLATE_TEXT, role, content, user)
235
             body_text = self._build_email_body(self._global_config.EMAIL_NOTIFICATION_CONTENT_UPDATE_TEMPLATE_TEXT, role, content, user)
236
+
237
+
238
+
188
             body_html = self._build_email_body(self._global_config.EMAIL_NOTIFICATION_CONTENT_UPDATE_TEMPLATE_HTML, role, content, user)
239
             body_html = self._build_email_body(self._global_config.EMAIL_NOTIFICATION_CONTENT_UPDATE_TEMPLATE_HTML, role, content, user)
240
+
189
             part1 = MIMEText(body_text, 'plain', 'utf-8')
241
             part1 = MIMEText(body_text, 'plain', 'utf-8')
190
             part2 = MIMEText(body_html, 'html', 'utf-8')
242
             part2 = MIMEText(body_html, 'html', 'utf-8')
191
             # Attach parts into message container.
243
             # Attach parts into message container.
223
         dictified_item = Context(CTX.EMAIL_NOTIFICATION, self._global_config.WEBSITE_BASE_URL).toDict(content)
275
         dictified_item = Context(CTX.EMAIL_NOTIFICATION, self._global_config.WEBSITE_BASE_URL).toDict(content)
224
         dictified_actor = Context(CTX.DEFAULT).toDict(actor)
276
         dictified_actor = Context(CTX.DEFAULT).toDict(actor)
225
 
277
 
278
+        main_title = dictified_item.label
279
+        content_intro = ''
280
+        content_text = ''
281
+        call_to_action_text = ''
282
+
283
+        action = content.get_last_action().id
284
+        if ActionDescription.COMMENT == action:
285
+            content_intro = _('<span id="content-intro-username">{}</span> added a comment:'.format(actor.display_name))
286
+            content_text = content.description
287
+            call_to_action_text = _('Answer')
288
+
289
+        elif ActionDescription.CREATION == action:
290
+
291
+            # Default values (if not overriden)
292
+            content_text = content.description
293
+            call_to_action_text = _('View online')
294
+
295
+            if ContentType.Thread == content.type:
296
+                call_to_action_text = _('Answer')
297
+                content_intro = _('<span id="content-intro-username">{}</span> started a thread entitled:').format(actor.display_name)
298
+                content_text = '<p id="content-body-intro">{}</p>'.format(content.label) + \
299
+                               content.get_last_comment_from(actor).description
300
+
301
+            elif ContentType.File == content.type:
302
+                content_intro = _('<span id="content-intro-username">{}</span> added a file entitled:').format(actor.display_name)
303
+                if content.description:
304
+                    content_text = content.description
305
+                elif content.label:
306
+                    content_text = '<span id="content-body-only-title">{}</span>'.format(content.label)
307
+                else:
308
+                    content_text = '<span id="content-body-only-title">{}</span>'.format(content.file_name)
309
+
310
+
311
+            elif ContentType.Page == content.type:
312
+                content_intro = _('<span id="content-intro-username">{}</span> added a page entitled:').format(actor.display_name)
313
+                content_text = '<span id="content-body-only-title">{}</span>'.format(content.label)
314
+
315
+        elif ActionDescription.REVISION == action:
316
+            content_text = content.description
317
+            call_to_action_text = _('View online')
318
+
319
+            if ContentType.File == content.type:
320
+                content_intro = _('<span id="content-intro-username">{}</span> uploaded a new revision.').format(actor.display_name)
321
+                content_text = ''
322
+
323
+            elif ContentType.Page == content.type:
324
+                content_intro = _('<span id="content-intro-username">{}</span> updated this page.').format(actor.display_name)
325
+                previous_revision = content.get_previous_revision()
326
+                title_diff = ''
327
+                if previous_revision.label != content.label:
328
+                    title_diff = htmldiff(previous_revision.label, content.label)
329
+                content_text = _('<p id="content-body-intro">Here is an overview of the changes:</p>')+ \
330
+                    title_diff + \
331
+                    htmldiff(previous_revision.description, content.description)
332
+
333
+            elif ContentType.Thread == content.type:
334
+                content_intro = _('<span id="content-intro-username">{}</span> updated the thread description.').format(actor.display_name)
335
+
336
+            # elif ContentType.Thread == content.type:
337
+            #     content_intro = _('<span id="content-intro-username">{}</span> updated this page.').format(actor.display_name)
338
+            #     previous_revision = content.get_previous_revision()
339
+            #     content_text = _('<p id="content-body-intro">Here is an overview of the changes:</p>')+ \
340
+            #         htmldiff(previous_revision.description, content.description)
341
+
342
+        elif ActionDescription.EDITION == action:
343
+            call_to_action_text = _('View online')
344
+
345
+            if ContentType.File == content.type:
346
+                content_intro = _('<span id="content-intro-username">{}</span> updated the file description.').format(actor.display_name)
347
+                content_text = '<p id="content-body-intro">{}</p>'.format(content.get_label()) + \
348
+                    content.description
349
+
350
+
351
+        if '' == content_intro and content_text == '':
352
+            # Skip notification, but it's not normal
353
+            logger.error(
354
+                self, 'A notification is being sent but no content. '
355
+                      'Here are some debug informations: [content_id: {cid}]'
356
+                      '[action: {act}][author: {actor}]'.format(
357
+                    cid=content.content_id, act=action, actor=actor
358
+                )
359
+            )
360
+            raise ValueError('Unexpected empty notification')
361
+
362
+        # Thread - create
363
+        # logger.debug(self, 'This is a NOT comment <--------------------- {}'.format(content.type))
226
         body_content = template.render(base_url=self._global_config.WEBSITE_BASE_URL,
364
         body_content = template.render(base_url=self._global_config.WEBSITE_BASE_URL,
227
                                _=_,
365
                                _=_,
228
                                h=helpers,
366
                                h=helpers,
229
                                user_display_name=role.user.display_name,
367
                                user_display_name=role.user.display_name,
230
                                user_role_label=role.role_as_label(),
368
                                user_role_label=role.role_as_label(),
231
                                workspace_label=role.workspace.label,
369
                                workspace_label=role.workspace.label,
370
+                               content_intro=content_intro,
371
+                               content_text=content_text,
372
+                               main_title=main_title,
373
+                               call_to_action_text=call_to_action_text,
232
                                result = DictLikeClass(item=dictified_item, actor=dictified_actor))
374
                                result = DictLikeClass(item=dictified_item, actor=dictified_actor))
233
 
375
 
234
         return body_content
376
         return body_content

+ 27 - 1
tracim/tracim/model/data.py Прегледај датотеку

165
         'undeletion': l_('Item undeleted'),
165
         'undeletion': l_('Item undeleted'),
166
     }
166
     }
167
 
167
 
168
-
169
     def __init__(self, id):
168
     def __init__(self, id):
170
         assert id in ActionDescription.allowed_values()
169
         assert id in ActionDescription.allowed_values()
171
         self.id = id
170
         self.id = id
448
                 children.append(child)
447
                 children.append(child)
449
         return children
448
         return children
450
 
449
 
450
+    def get_last_comment_from(self, user: User) -> 'Content':
451
+        # TODO - Make this more efficient
452
+        last_comment_updated = None
453
+        last_comment = None
454
+        for comment in self.get_comments():
455
+            if user.user_id==comment.owner.user_id:
456
+                if not last_comment or last_comment_updated<comment.updated:
457
+                    # take only the latest comment !
458
+                    last_comment = comment
459
+                    last_comment_updated = comment.updated
460
+
461
+        return last_comment
462
+
463
+
464
+    def get_previous_revision(self) -> 'ContentRevisionRO':
465
+        rev_ids = [revision.revision_id for revision in self.revisions]
466
+        rev_ids.sort()
467
+
468
+        if len(rev_ids)>=2:
469
+            revision_rev_id = rev_ids[-2]
470
+
471
+            for revision in self.revisions:
472
+                if revision.revision_id == revision_rev_id:
473
+                    return revision
474
+
475
+        return None
476
+
451
 
477
 
452
 
478
 
453
 class ContentChecker(object):
479
 class ContentChecker(object):

+ 8 - 2
tracim/tracim/model/serializers.py Прегледај датотеку

259
 
259
 
260
 @pod_serializer(Content, CTX.EMAIL_NOTIFICATION)
260
 @pod_serializer(Content, CTX.EMAIL_NOTIFICATION)
261
 def serialize_item(content: Content, context: Context):
261
 def serialize_item(content: Content, context: Context):
262
-    return DictLikeClass(
262
+    if ContentType.Comment==content.type:
263
+        content = content.parent
264
+
265
+    result = DictLikeClass(
263
         id = content.content_id,
266
         id = content.content_id,
264
         label = content.label if content.label else content.file_name,
267
         label = content.label if content.label else content.file_name,
265
         icon = ContentType.icon(content.type),
268
         icon = ContentType.icon(content.type),
268
         workspace = context.toDict(content.workspace),
271
         workspace = context.toDict(content.workspace),
269
         is_deleted = content.is_deleted,
272
         is_deleted = content.is_deleted,
270
         is_archived = content.is_archived,
273
         is_archived = content.is_archived,
271
-        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))
274
+        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)),
275
+        last_action = context.toDict(content.get_last_action())
272
     )
276
     )
273
 
277
 
278
+    return result
279
+
274
 
280
 
275
 @pod_serializer(Content, CTX.MENU_API)
281
 @pod_serializer(Content, CTX.MENU_API)
276
 def serialize_content_for_menu_api(content: Content, context: Context):
282
 def serialize_content_for_menu_api(content: Content, context: Context):

+ 45 - 55
tracim/tracim/templates/mail/content_update_body_html.mak Прегледај датотеку

14
       }
14
       }
15
       a.call-to-action img { vertical-align: middle;}
15
       a.call-to-action img { vertical-align: middle;}
16
       th { vertical-align: top;}
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; padding: 4px; border-radius: 3px; font-size: 2em; padding-right: 0.5em;}
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; }
17
     </style>
34
     </style>
18
   </head>
35
   </head>
19
-  <body style="font-family: Arial; font-size: 12px; width: 600px; margin: 0; padding: 0;">
36
+  <body style="font-family: Arial; font-size: 12px; max-width: 600px; margin: 0; padding: 0;">
20
 
37
 
21
-    <table style="width: 600px; cell-padding: 0; 	border-collapse: collapse;">
38
+    <table style="width: 100%; cell-padding: 0; border-collapse: collapse; margin: 0">
22
       <tr style="background-color: F5F5F5; border-bottom: 1px solid #CCC;" >
39
       <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>
40
+        <td style="background-color: #666;">
41
+          <img src="http://team.trac.im/assets/img/logo.png" style="vertical-align: middle;"/>
32
         </td>
42
         </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/'+result.item.icon+'.png'}"/>
37
-          <span style="font-weight: bold; padding-left: 0.5em; font-size: 1em; vertical-align: middle;">
38
-            ${result.item.label} &mdash;
39
-            <span style="font-weight: bold; color: #999; font-weight: bold;">
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;">
40
                 ${result.item.status.label}
47
                 ${result.item.status.label}
41
-                <img src="${base_url+'/assets/icons/16x16/{}.png'.format(result.item.status.icon)}" style="vertical-align: middle;">
48
+                <img alt="" src="${base_url+'/assets/icons/16x16/{}.png'.format(result.item.status.icon)}" style="vertical-align: middle;">
42
             </span>
49
             </span>
43
-          </span>
44
         </td>
50
         </td>
45
       </tr>
51
       </tr>
46
     </table>
52
     </table>
47
 
53
 
48
-    <hr style="border: 0px solid #CCC; border-width: 1px 0 0 0;">
49
-
50
-    <div style="margin-left: 0.5em; border: 1em solid #DDD; border-width: 0 0 0 1em; padding-left: 1em;">
51
-      <p>
52
-        ${_('Some activity has been detected')|n}
53
-        ##
54
-        ## TODO - D.A. - Show last action in the notification message
55
-        ##
56
-        ## &mdash;
57
-        ## <img style="vertical-align: middle; " src="${base_url+'/assets/icons/16x16/'+result.item.last_action.icon+'.png'}"/>
58
-        ## ${result.item.last_action.label}
59
-        ##
60
-        ${_('<span style="{style}">&mdash; by {actor_name}</span>').format(style='color: #666; font-weight: bold;', actor_name=result.actor.name)}
61
-      </p>
62
-      <p>
63
-        % if result.item.is_deleted:
64
-            <img style="vertical-align: middle; " src="${base_url+'/assets/icons/16x16/status/user-trash-full.png'}"/>
65
-            ${_('This item has been deleted.')}
66
-        % elif result.item.is_archived:
67
-            <img style="vertical-align: middle; " src="${base_url+'/assets/icons/16x16/mimetypes/package-x-generic.png'}"/>
68
-            ${_('This item has been archived.')}
69
-        % else:
70
-            <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;">
71
-              <img style="vertical-align: middle; " src="${base_url+'/assets/icons/16x16/actions/system-search.png'}"/>
72
-              ${_('Go to information')}
73
-            </a>
74
-        % endif
75
-        <div style="clear:both;"></div>
76
-      </p>
54
+    <p id="content-intro">${content_intro|n}</p>
55
+    <div id="content-body">
56
+        <div>${content_text|n}</div>
57
+        <div id="call-to-action-container">
58
+            <span style="">
59
+                <a href="${result.item.url}" id='call-to-action-button'>${call_to_action_text}</a>
60
+            </span>
61
+        </div>
62
+    </div>
63
+    
64
+    <div id="footer">
65
+        <p>
66
+            ${_('{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=user_role_label, workspace_url=result.item.workspace.url, workspace_label=workspace_label, website_title=h.CFG.WEBSITE_TITLE)|n}
67
+        </p>
68
+        <hr/>
69
+        <p>
70
+            ${_('This email was automatically sent by <i>Tracim</i>, a collaborative software developped by Algoo.')}<br/>
71
+            Algoo SAS &mdash; 9 rue du rocher de Lorzier, 38430 Moirans, France &mdash; <a style="text-decoration: none;" href="http://algoo.fr">www.algoo.fr</a>
72
+        </p>
77
     </div>
73
     </div>
78
-
79
-    <hr style="border: 0px solid #CCC; border-width: 1px 0 0 0;">
80
-
81
-    <p style="color: #999; margin-left: 0.5em;">
82
-      ${_('{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}
83
-    </p>
84
   </body>
74
   </body>
85
 </html>
75
 </html>

+ 30 - 24
tracim/tracim/templates/mail/content_update_body_text.mak Прегледај датотеку

1
 ## -*- coding: utf-8 -*-
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)}
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 /${h.CFG.WEBSITE_TITLE}/
19
+and you are /${user_role_label}/ in the workspace /${workspace_label}/
20
+${result.item.workspace.url}
21
+
22
+----
23
+
24
+This email was automatically sent by *Tracim*,
25
+a collaborative software developped by Algoo.
26
+
27
+**Algoo SAS**
28
+9 rue du rocher de Lorzier
29
+38430 Moirans
30
+France
31
+http://algoo.fr
26
 
32