Damien ACCORSI 10 年前
父节点
当前提交
c1255380a8

+ 17 - 0
tracim/tracim/config/app_cfg.py 查看文件

@@ -25,6 +25,8 @@ from tg.i18n import lazy_ugettext as l_
25 25
 import tracim
26 26
 from tracim import model
27 27
 from tracim.lib.base import logger
28
+from tracim.model.data import ActionDescription
29
+from tracim.model.data import ContentType
28 30
 
29 31
 base_config = AppConfig()
30 32
 base_config.renderers = []
@@ -235,6 +237,21 @@ class CFG(object):
235 237
 
236 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 256
     def get_tracker_js_content(self, js_tracker_file_path = None):
240 257
         js_tracker_file_path = tg.config.get('js_tracker_path', None)

+ 61 - 11
tracim/tracim/controllers/content.py 查看文件

@@ -267,18 +267,45 @@ class UserWorkspaceFolderFileRestController(TIMWorkspaceContentRestController):
267 267
         workspace = tmpl_context.workspace
268 268
 
269 269
         try:
270
+            item_saved = False
270 271
             api = ContentApi(tmpl_context.current_user)
271 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 310
             msg = _('{} updated').format(self._item_type_label)
284 311
             tg.flash(msg, CST.STATUS_OK)
@@ -376,12 +403,34 @@ class UserWorkspaceFolderPageRestController(TIMWorkspaceContentRestController):
376 403
 
377 404
         page = api.create(ContentType.Page, workspace, tmpl_context.folder, label)
378 405
         page.description = content
379
-        api.save(page, ActionDescription.CREATION)
406
+        api.save(page, ActionDescription.CREATION, do_notify=True)
380 407
 
381 408
         tg.flash(_('Page created'), CST.STATUS_OK)
382 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 435
 class UserWorkspaceFolderThreadRestController(TIMWorkspaceContentRestController):
387 436
     """
@@ -452,12 +501,13 @@ class UserWorkspaceFolderThreadRestController(TIMWorkspaceContentRestController)
452 501
 
453 502
         thread = api.create(ContentType.Thread, workspace, tmpl_context.folder, label)
454 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 506
         comment = api.create(ContentType.Comment, workspace, thread, label)
458 507
         comment.label = ''
459 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 512
         tg.flash(_('Thread created'), CST.STATUS_OK)
463 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,6 +10,7 @@ msgstr ""
10 10
 <<<<<<< HEAD
11 11
 <<<<<<< HEAD
12 12
 <<<<<<< HEAD
13
+<<<<<<< HEAD
13 14
 "POT-Creation-Date: 2015-03-02 22:28+0100\n"
14 15
 "PO-Revision-Date: 2015-03-02 22:32+0100\n"
15 16
 =======
@@ -24,6 +25,10 @@ msgstr ""
24 25
 "POT-Creation-Date: 2015-03-10 18:35+0100\n"
25 26
 "PO-Revision-Date: 2015-03-10 18:32+0100\n"
26 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 32
 "Last-Translator: Damien Accorsi <damien.accorsi@free.fr>\n"
28 33
 "Language-Team: fr_FR <LL@li.org>\n"
29 34
 "Plural-Forms: nplurals=2; plural=(n > 1)\n"
@@ -32,11 +37,11 @@ msgstr ""
32 37
 "Content-Transfer-Encoding: 8bit\n"
33 38
 "Generated-By: Babel 1.3\n"
34 39
 
35
-#: tracim/config/app_cfg.py:135
40
+#: tracim/config/app_cfg.py:137
36 41
 msgid "Password reset request"
37 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 45
 #, python-format
41 46
 msgid ""
42 47
 "\n"
@@ -59,46 +64,47 @@ msgstr ""
59 64
 " à l'originie de cette requête, merci d'ignorer et/ou supprimer cet "
60 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 68
 #: tracim/templates/user_workspace_forms.mak:252
64 69
 msgid "New password"
65 70
 msgstr "Nouveau mot de passe"
66 71
 
67
-#: tracim/config/app_cfg.py:153
72
+#: tracim/config/app_cfg.py:155
68 73
 msgid "Confirm new password"
69 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 77
 msgid "Save new password"
73 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 81
 msgid "Email address"
77 82
 msgstr "Adresse email"
78 83
 
79
-#: tracim/config/app_cfg.py:156
84
+#: tracim/config/app_cfg.py:158
80 85
 msgid "Send Request"
81 86
 msgstr "Valider"
82 87
 
83
-#: tracim/config/app_cfg.py:159
88
+#: tracim/config/app_cfg.py:161
84 89
 msgid "Password reset request sent"
85 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 93
 msgid "Invalid password reset request"
89 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 97
 msgid "Password reset request timed out"
93 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 101
 msgid "Password changed successfully"
97 102
 msgstr "Mot de passe changé"
98 103
 
99 104
 <<<<<<< HEAD
100 105
 <<<<<<< HEAD
101 106
 <<<<<<< HEAD
107
+<<<<<<< HEAD
102 108
 #: tracim/controllers/__init__.py:139
103 109
 #: tracim/templates/dashboard.mak:36
104 110
 #: tracim/templates/master_authenticated.mak:94
@@ -143,6 +149,15 @@ msgstr "{} mis(e) à jour"
143 149
 
144 150
 #: tracim/controllers/__init__.py:252 tracim/controllers/content.py:288
145 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 161
 msgid "{} not updated - error: {}"
147 162
 msgstr "{} pas mis(e) à jour - erreur : {}"
148 163
 
@@ -167,6 +182,7 @@ msgstr "Statut de {} non mis(e) à jour : {}"
167 182
 
168 183
 <<<<<<< HEAD
169 184
 <<<<<<< HEAD
185
+<<<<<<< HEAD
170 186
 #: tracim/controllers/__init__.py:302
171 187
 >>>>>>> b2e4664ae2ad0cc8fbc3344f6fa37845cf7e344d
172 188
 #: tracim/controllers/content.py:748
@@ -198,22 +214,26 @@ msgstr "{} non désarchivé(e) : {}"
198 214
 #: tracim/controllers/content.py:797
199 215
 =======
200 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 220
 msgid "{} archived. <a class=\"alert-link\" href=\"{}\">Cancel action</a>"
202 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 224
 msgid "{} not archived: {}"
206 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 228
 msgid "{} unarchived."
210 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 232
 msgid "{} not un-archived: {}"
214 233
 msgstr "{} non désarchivé(e) : {}"
215 234
 
216 235
 #: tracim/controllers/__init__.py:351 tracim/controllers/content.py:88
236
+<<<<<<< HEAD
217 237
 #: tracim/controllers/content.py:796
218 238
 >>>>>>> 139e854c54cbb438fb69dda1149d450228cf2e8a
219 239
 msgid "{} deleted. <a class=\"alert-link\" href=\"{}\">Cancel action</a>"
@@ -260,18 +280,21 @@ msgstr "{} non désarchivé(e) : {}"
260 280
 
261 281
 #: tracim/controllers/__init__.py:351 tracim/controllers/content.py:88
262 282
 #: tracim/controllers/content.py:796
283
+=======
284
+#: tracim/controllers/content.py:846
285
+>>>>>>> b67ad8598a3cfae087b99a8b7bf905ae8b8ec991
263 286
 msgid "{} deleted. <a class=\"alert-link\" href=\"{}\">Cancel action</a>"
264 287
 msgstr ""
265 288
 "{} supprimé(e). <a class=\"alert-link\" href=\"{}\">Annuler "
266 289
 "l'opération</a>"
267 290
 
268 291
 #: tracim/controllers/__init__.py:360 tracim/controllers/content.py:99
269
-#: tracim/controllers/content.py:805
292
+#: tracim/controllers/content.py:855
270 293
 msgid "{} not deleted: {}"
271 294
 msgstr "{} non supprimé(e) : {}"
272 295
 
273 296
 #: tracim/controllers/__init__.py:375 tracim/controllers/content.py:116
274
-#: tracim/controllers/content.py:820
297
+#: tracim/controllers/content.py:870
275 298
 msgid "{} undeleted."
276 299
 msgstr "{} restauré(e)."
277 300
 
@@ -288,8 +311,12 @@ msgstr "{} restauré(e)."
288 311
 
289 312
 >>>>>>> 139e854c54cbb438fb69dda1149d450228cf2e8a
290 313
 #: tracim/controllers/__init__.py:385 tracim/controllers/content.py:128
314
+<<<<<<< HEAD
291 315
 #: tracim/controllers/content.py:830
292 316
 >>>>>>> b2e4664ae2ad0cc8fbc3344f6fa37845cf7e344d
317
+=======
318
+#: tracim/controllers/content.py:880
319
+>>>>>>> b67ad8598a3cfae087b99a8b7bf905ae8b8ec991
293 320
 msgid "{} not un-deleted: {}"
294 321
 msgstr "{} non restauré(e) : {}"
295 322
 
@@ -310,47 +337,47 @@ msgstr "Fichier"
310 337
 msgid "File created"
311 338
 msgstr "Fichier créé"
312 339
 
313
-#: tracim/controllers/content.py:313
340
+#: tracim/controllers/content.py:340
314 341
 msgid "Page"
315 342
 msgstr "Page"
316 343
 
317
-#: tracim/controllers/content.py:381
344
+#: tracim/controllers/content.py:408
318 345
 msgid "Page created"
319 346
 msgstr "Page créée"
320 347
 
321
-#: tracim/controllers/content.py:421
348
+#: tracim/controllers/content.py:470
322 349
 msgid "Thread"
323 350
 msgstr "Discussion"
324 351
 
325
-#: tracim/controllers/content.py:462
352
+#: tracim/controllers/content.py:512
326 353
 msgid "Thread created"
327 354
 msgstr "Discussion créée"
328 355
 
329
-#: tracim/controllers/content.py:547
356
+#: tracim/controllers/content.py:597
330 357
 msgid "Item moved to {}"
331 358
 msgstr "Element déplacé vers {}"
332 359
 
333
-#: tracim/controllers/content.py:549
360
+#: tracim/controllers/content.py:599
334 361
 msgid "Item moved to workspace root"
335 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 365
 msgid "Folder created"
339 366
 msgstr "Dossier créé"
340 367
 
341
-#: tracim/controllers/content.py:675
368
+#: tracim/controllers/content.py:725
342 369
 msgid "Folder not created: {}"
343 370
 msgstr "Dossier non créé : {}"
344 371
 
345
-#: tracim/controllers/content.py:710
372
+#: tracim/controllers/content.py:760
346 373
 msgid "Folder updated"
347 374
 msgstr "Dossier mis à jour"
348 375
 
349
-#: tracim/controllers/content.py:715
376
+#: tracim/controllers/content.py:765
350 377
 msgid "Folder not updated: {}"
351 378
 msgstr "Dossier non mis à jour : {}"
352 379
 
353
-#: tracim/controllers/content.py:730
380
+#: tracim/controllers/content.py:780
354 381
 msgid "Folder"
355 382
 <<<<<<< HEAD
356 383
 msgstr "Dossier"
@@ -505,6 +532,7 @@ msgstr "le %d %B à %H:%M"
505 532
 <<<<<<< HEAD
506 533
 <<<<<<< HEAD
507 534
 <<<<<<< HEAD
535
+<<<<<<< HEAD
508 536
 =======
509 537
 #: tracim/lib/content.py:98
510 538
 #: tracim/templates/dashboard.mak:36
@@ -514,6 +542,9 @@ msgstr "le %d %B à %H:%M"
514 542
 =======
515 543
 #: tracim/lib/content.py:98 tracim/templates/dashboard.mak:36
516 544
 >>>>>>> 139e854c54cbb438fb69dda1149d450228cf2e8a
545
+=======
546
+#: tracim/lib/content.py:100 tracim/templates/dashboard.mak:36
547
+>>>>>>> b67ad8598a3cfae087b99a8b7bf905ae8b8ec991
517 548
 #: tracim/templates/master_authenticated.mak:94
518 549
 #: tracim/templates/master_no_toolbar_no_login.mak:112
519 550
 #: tracim/templates/search.mak:11 tracim/templates/user_get_one.mak:39
@@ -528,7 +559,71 @@ msgstr "le %d %B à %H:%M"
528 559
 msgid "Workspaces"
529 560
 msgstr "Espaces de travail"
530 561
 
562
+<<<<<<< HEAD
531 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 627
 #: tracim/lib/predicates.py:17
533 628
 msgid "You are not authorized to access this resource"
534 629
 msgstr "Vous n'êtes pas autorisé à accéder à cette ressource"
@@ -616,35 +711,35 @@ msgstr "Elément désarchivé"
616 711
 msgid "Item undeleted"
617 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 715
 msgid "work in progress"
621 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 719
 msgid "closed — validated"
625 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 723
 msgid "closed — cancelled"
629 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 727
 msgid "deprecated"
633 728
 msgstr "obsolète"
634 729
 
635
-#: tracim/model/data.py:207
730
+#: tracim/model/data.py:206
636 731
 msgid "subject in progress"
637 732
 msgstr "discussion en cours"
638 733
 
639
-#: tracim/model/data.py:208
734
+#: tracim/model/data.py:207
640 735
 msgid "subject closed — resolved"
641 736
 msgstr "discussion close — résolue"
642 737
 
643
-#: tracim/model/data.py:209
738
+#: tracim/model/data.py:208
644 739
 msgid "subject closed — cancelled"
645 740
 msgstr "discussion close — annulée"
646 741
 
647
-#: tracim/model/data.py:292
742
+#: tracim/model/data.py:291
648 743
 msgid "Delete this workspace"
649 744
 <<<<<<< HEAD
650 745
 msgstr "Supprimer cet espace de travail"
@@ -652,11 +747,11 @@ msgstr "Supprimer cet espace de travail"
652 747
 msgstr "Supprimer l'espace de travail"
653 748
 >>>>>>> b2e4664ae2ad0cc8fbc3344f6fa37845cf7e344d
654 749
 
655
-#: tracim/model/data.py:293
750
+#: tracim/model/data.py:292
656 751
 msgid "Delete this folder"
657 752
 msgstr "Supprimer ce dossier"
658 753
 
659
-#: tracim/model/data.py:294
754
+#: tracim/model/data.py:293
660 755
 msgid "Delete this file"
661 756
 <<<<<<< HEAD
662 757
 msgstr "Supprimer ce fichier"
@@ -664,15 +759,15 @@ msgstr "Supprimer ce fichier"
664 759
 msgstr "Supprimer le fichier"
665 760
 >>>>>>> b2e4664ae2ad0cc8fbc3344f6fa37845cf7e344d
666 761
 
667
-#: tracim/model/data.py:295
762
+#: tracim/model/data.py:294
668 763
 msgid "Delete this page"
669 764
 msgstr "Supprimer cette page"
670 765
 
671
-#: tracim/model/data.py:296
766
+#: tracim/model/data.py:295
672 767
 msgid "Delete this thread"
673 768
 msgstr "Supprimer cette discussion"
674 769
 
675
-#: tracim/model/data.py:297
770
+#: tracim/model/data.py:296
676 771
 msgid "Delete this comment"
677 772
 msgstr "Supprimer ce commentaire"
678 773
 
@@ -1699,58 +1794,24 @@ msgstr ""
1699 1794
 "Comme <span style=\"color: #ea983d;\">gestionnaire de contenu</span> + "
1700 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 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 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 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 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 1816
 #~ msgid "You have no document yet."
1756 1817
 #~ msgstr "Vous n'avez pas de document pour le moment."
@@ -2310,12 +2371,38 @@ msgstr ""
2310 2371
 #~ msgid "Create a topic"
2311 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 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 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,8 +25,10 @@ from tracim.model.data import Content
25 25
 from tracim.model.data import ContentType
26 26
 from tracim.model.data import NodeTreeItem
27 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 33
     :param content1:
32 34
     :param content2:
@@ -131,6 +133,12 @@ class ContentApi(object):
131 133
         if workspace:
132 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 142
         return result
135 143
 
136 144
     def _base_query(self, workspace: Workspace=None):
@@ -178,7 +186,8 @@ class ContentApi(object):
178 186
         :param workspace:
179 187
         :param filter_by_allowed_content_types:
180 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 191
         :return:
183 192
         """
184 193
         if not allowed_node_types:
@@ -193,10 +202,17 @@ class ContentApi(object):
193 202
             filter(Content.content_id.notin_(removed_item_ids)).\
194 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 216
         result = []
201 217
         for folder in folders:
202 218
             for allowed_content_type in filter_by_allowed_content_types:
@@ -379,8 +395,16 @@ class ContentApi(object):
379 395
             DBSession.flush()
380 396
 
381 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 409
     def get_keywords(self, search_string, search_string_separators=None) -> [str]:
386 410
         """

+ 151 - 9
tracim/tracim/lib/notifications.py 查看文件

@@ -3,6 +3,8 @@
3 3
 from email.mime.multipart import MIMEMultipart
4 4
 from email.mime.text import MIMEText
5 5
 
6
+import lxml
7
+from lxml.html.diff import htmldiff
6 8
 
7 9
 from mako.template import Template
8 10
 
@@ -21,7 +23,8 @@ from tracim.model.serializers import Context
21 23
 from tracim.model.serializers import CTX
22 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 28
 from tracim.model.auth import User
26 29
 
27 30
 
@@ -75,14 +78,59 @@ class RealNotifier(object):
75 78
                                        cfg.EMAIL_NOTIFICATION_SMTP_PASSWORD)
76 79
 
77 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 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 135
         try:
88 136
             if global_config.EMAIL_NOTIFICATION_PROCESSING_MODE.lower()==global_config.CST.ASYNC.lower():
@@ -146,7 +194,7 @@ class EmailNotifier(object):
146 194
         logger.debug(self, 'Content: {}'.format(event_content_id))
147 195
 
148 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 198
         notifiable_roles = WorkspaceApi(user).get_notifiable_roles(content.workspace)
151 199
 
152 200
         if len(notifiable_roles)<=0:
@@ -175,9 +223,9 @@ class EmailNotifier(object):
175 223
             #
176 224
             subject = self._global_config.EMAIL_NOTIFICATION_CONTENT_UPDATE_SUBJECT
177 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 230
             message = MIMEMultipart('alternative')
183 231
             message['Subject'] = subject
@@ -185,7 +233,11 @@ class EmailNotifier(object):
185 233
             message['To'] = to_addr
186 234
 
187 235
             body_text = self._build_email_body(self._global_config.EMAIL_NOTIFICATION_CONTENT_UPDATE_TEMPLATE_TEXT, role, content, user)
236
+
237
+
238
+
188 239
             body_html = self._build_email_body(self._global_config.EMAIL_NOTIFICATION_CONTENT_UPDATE_TEMPLATE_HTML, role, content, user)
240
+
189 241
             part1 = MIMEText(body_text, 'plain', 'utf-8')
190 242
             part2 = MIMEText(body_html, 'html', 'utf-8')
191 243
             # Attach parts into message container.
@@ -223,12 +275,102 @@ class EmailNotifier(object):
223 275
         dictified_item = Context(CTX.EMAIL_NOTIFICATION, self._global_config.WEBSITE_BASE_URL).toDict(content)
224 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 364
         body_content = template.render(base_url=self._global_config.WEBSITE_BASE_URL,
227 365
                                _=_,
228 366
                                h=helpers,
229 367
                                user_display_name=role.user.display_name,
230 368
                                user_role_label=role.role_as_label(),
231 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 374
                                result = DictLikeClass(item=dictified_item, actor=dictified_actor))
233 375
 
234 376
         return body_content

+ 27 - 1
tracim/tracim/model/data.py 查看文件

@@ -165,7 +165,6 @@ class ActionDescription(object):
165 165
         'undeletion': l_('Item undeleted'),
166 166
     }
167 167
 
168
-
169 168
     def __init__(self, id):
170 169
         assert id in ActionDescription.allowed_values()
171 170
         self.id = id
@@ -448,6 +447,33 @@ class Content(DeclarativeBase):
448 447
                 children.append(child)
449 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 479
 class ContentChecker(object):

+ 8 - 2
tracim/tracim/model/serializers.py 查看文件

@@ -259,7 +259,10 @@ def serialize_breadcrumb_item(content: Content, context: Context):
259 259
 
260 260
 @pod_serializer(Content, CTX.EMAIL_NOTIFICATION)
261 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 266
         id = content.content_id,
264 267
         label = content.label if content.label else content.file_name,
265 268
         icon = ContentType.icon(content.type),
@@ -268,9 +271,12 @@ def serialize_item(content: Content, context: Context):
268 271
         workspace = context.toDict(content.workspace),
269 272
         is_deleted = content.is_deleted,
270 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 281
 @pod_serializer(Content, CTX.MENU_API)
276 282
 def serialize_content_for_menu_api(content: Content, context: Context):

+ 45 - 55
tracim/tracim/templates/mail/content_update_body_html.mak 查看文件

@@ -14,72 +14,62 @@
14 14
       }
15 15
       a.call-to-action img { vertical-align: middle;}
16 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 34
     </style>
18 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 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 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 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 49
             </span>
43
-          </span>
44 50
         </td>
45 51
       </tr>
46 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 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 74
   </body>
85 75
 </html>

+ 30 - 24
tracim/tracim/templates/mail/content_update_body_text.mak 查看文件

@@ -1,26 +1,32 @@
1 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