瀏覽代碼

- fixes search result filtering by allowed content

Damien ACCORSI 10 年之前
父節點
當前提交
b67ad8598a

+ 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))

二進制
tracim/tracim/i18n/fr/LC_MESSAGES/tracim.mo 查看文件


+ 156 - 101
tracim/tracim/i18n/fr/LC_MESSAGES/tracim.po 查看文件

7
 msgstr ""
7
 msgstr ""
8
 "Project-Id-Version: pod 0.1\n"
8
 "Project-Id-Version: pod 0.1\n"
9
 "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
9
 "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
10
-"POT-Creation-Date: 2015-03-10 18:35+0100\n"
11
-"PO-Revision-Date: 2015-03-10 18:32+0100\n"
10
+"POT-Creation-Date: 2015-03-20 00:44+0100\n"
11
+"PO-Revision-Date: 2015-03-20 00:37+0100\n"
12
 "Last-Translator: Damien Accorsi <damien.accorsi@free.fr>\n"
12
 "Last-Translator: Damien Accorsi <damien.accorsi@free.fr>\n"
13
 "Language-Team: fr_FR <LL@li.org>\n"
13
 "Language-Team: fr_FR <LL@li.org>\n"
14
 "Plural-Forms: nplurals=2; plural=(n > 1)\n"
14
 "Plural-Forms: nplurals=2; plural=(n > 1)\n"
17
 "Content-Transfer-Encoding: 8bit\n"
17
 "Content-Transfer-Encoding: 8bit\n"
18
 "Generated-By: Babel 1.3\n"
18
 "Generated-By: Babel 1.3\n"
19
 
19
 
20
-#: tracim/config/app_cfg.py:135
20
+#: tracim/config/app_cfg.py:137
21
 msgid "Password reset request"
21
 msgid "Password reset request"
22
 msgstr "Réinitialisation du mot de passe"
22
 msgstr "Réinitialisation du mot de passe"
23
 
23
 
24
-#: tracim/config/app_cfg.py:136 tracim/config/app_cfg.py:165
24
+#: tracim/config/app_cfg.py:138 tracim/config/app_cfg.py:167
25
 #, python-format
25
 #, python-format
26
 msgid ""
26
 msgid ""
27
 "\n"
27
 "\n"
44
 " à l'originie de cette requête, merci d'ignorer et/ou supprimer cet "
44
 " à l'originie de cette requête, merci d'ignorer et/ou supprimer cet "
45
 "e-mail.\n"
45
 "e-mail.\n"
46
 
46
 
47
-#: tracim/config/app_cfg.py:152 tracim/templates/user_workspace_forms.mak:251
47
+#: tracim/config/app_cfg.py:154 tracim/templates/user_workspace_forms.mak:251
48
 #: tracim/templates/user_workspace_forms.mak:252
48
 #: tracim/templates/user_workspace_forms.mak:252
49
 msgid "New password"
49
 msgid "New password"
50
 msgstr "Nouveau mot de passe"
50
 msgstr "Nouveau mot de passe"
51
 
51
 
52
-#: tracim/config/app_cfg.py:153
52
+#: tracim/config/app_cfg.py:155
53
 msgid "Confirm new password"
53
 msgid "Confirm new password"
54
 msgstr "Confirmer le nouveau mot de passe"
54
 msgstr "Confirmer le nouveau mot de passe"
55
 
55
 
56
-#: tracim/config/app_cfg.py:154
56
+#: tracim/config/app_cfg.py:156
57
 msgid "Save new password"
57
 msgid "Save new password"
58
 msgstr "Enregistrer le nouveau mot de passe"
58
 msgstr "Enregistrer le nouveau mot de passe"
59
 
59
 
60
-#: tracim/config/app_cfg.py:155 tracim/templates/user_get_all.mak:34
60
+#: tracim/config/app_cfg.py:157 tracim/templates/user_get_all.mak:34
61
 msgid "Email address"
61
 msgid "Email address"
62
 msgstr "Adresse email"
62
 msgstr "Adresse email"
63
 
63
 
64
-#: tracim/config/app_cfg.py:156
64
+#: tracim/config/app_cfg.py:158
65
 msgid "Send Request"
65
 msgid "Send Request"
66
 msgstr "Valider"
66
 msgstr "Valider"
67
 
67
 
68
-#: tracim/config/app_cfg.py:159
68
+#: tracim/config/app_cfg.py:161
69
 msgid "Password reset request sent"
69
 msgid "Password reset request sent"
70
 msgstr "Requête de réinitialisation du mot de passe envoyée"
70
 msgstr "Requête de réinitialisation du mot de passe envoyée"
71
 
71
 
72
-#: tracim/config/app_cfg.py:160 tracim/config/app_cfg.py:162
72
+#: tracim/config/app_cfg.py:162 tracim/config/app_cfg.py:164
73
 msgid "Invalid password reset request"
73
 msgid "Invalid password reset request"
74
 msgstr "Requête de réinitialisation du mot de passe invalide"
74
 msgstr "Requête de réinitialisation du mot de passe invalide"
75
 
75
 
76
-#: tracim/config/app_cfg.py:161
76
+#: tracim/config/app_cfg.py:163
77
 msgid "Password reset request timed out"
77
 msgid "Password reset request timed out"
78
 msgstr "Echec de la requête de réinitialisation du mot de passe"
78
 msgstr "Echec de la requête de réinitialisation du mot de passe"
79
 
79
 
80
-#: tracim/config/app_cfg.py:163
80
+#: tracim/config/app_cfg.py:165
81
 msgid "Password changed successfully"
81
 msgid "Password changed successfully"
82
 msgstr "Mot de passe changé"
82
 msgstr "Mot de passe changé"
83
 
83
 
84
-#: tracim/controllers/__init__.py:247 tracim/controllers/content.py:283
84
+#: tracim/controllers/__init__.py:247 tracim/controllers/content.py:310
85
+#: tracim/controllers/content.py:425
85
 msgid "{} updated"
86
 msgid "{} updated"
86
 msgstr "{} mis(e) à jour"
87
 msgstr "{} mis(e) à jour"
87
 
88
 
88
-#: tracim/controllers/__init__.py:252 tracim/controllers/content.py:288
89
+#: tracim/controllers/__init__.py:252 tracim/controllers/content.py:315
90
+#: tracim/controllers/content.py:430
89
 msgid "{} not updated - error: {}"
91
 msgid "{} not updated - error: {}"
90
 msgstr "{} pas mis(e) à jour - erreur : {}"
92
 msgstr "{} pas mis(e) à jour - erreur : {}"
91
 
93
 
97
 msgid "{} status not updated: {}"
99
 msgid "{} status not updated: {}"
98
 msgstr "Statut de {} non mis(e) à jour : {}"
100
 msgstr "Statut de {} non mis(e) à jour : {}"
99
 
101
 
100
-#: tracim/controllers/__init__.py:302 tracim/controllers/content.py:748
102
+#: tracim/controllers/__init__.py:302 tracim/controllers/content.py:798
101
 msgid "{} archived. <a class=\"alert-link\" href=\"{}\">Cancel action</a>"
103
 msgid "{} archived. <a class=\"alert-link\" href=\"{}\">Cancel action</a>"
102
 msgstr "{} archivé(e). <a class=\"alert-link\" href=\"{}\">Annuler l'opération</a>"
104
 msgstr "{} archivé(e). <a class=\"alert-link\" href=\"{}\">Annuler l'opération</a>"
103
 
105
 
104
-#: tracim/controllers/__init__.py:311 tracim/controllers/content.py:757
106
+#: tracim/controllers/__init__.py:311 tracim/controllers/content.py:807
105
 msgid "{} not archived: {}"
107
 msgid "{} not archived: {}"
106
 msgstr "{} non archivé(e) : {}"
108
 msgstr "{} non archivé(e) : {}"
107
 
109
 
108
-#: tracim/controllers/__init__.py:325 tracim/controllers/content.py:771
110
+#: tracim/controllers/__init__.py:325 tracim/controllers/content.py:821
109
 msgid "{} unarchived."
111
 msgid "{} unarchived."
110
 msgstr "{} désarchivé(e)"
112
 msgstr "{} désarchivé(e)"
111
 
113
 
112
-#: tracim/controllers/__init__.py:333 tracim/controllers/content.py:779
114
+#: tracim/controllers/__init__.py:333 tracim/controllers/content.py:829
113
 msgid "{} not un-archived: {}"
115
 msgid "{} not un-archived: {}"
114
 msgstr "{} non désarchivé(e) : {}"
116
 msgstr "{} non désarchivé(e) : {}"
115
 
117
 
116
 #: tracim/controllers/__init__.py:351 tracim/controllers/content.py:88
118
 #: tracim/controllers/__init__.py:351 tracim/controllers/content.py:88
117
-#: tracim/controllers/content.py:796
119
+#: tracim/controllers/content.py:846
118
 msgid "{} deleted. <a class=\"alert-link\" href=\"{}\">Cancel action</a>"
120
 msgid "{} deleted. <a class=\"alert-link\" href=\"{}\">Cancel action</a>"
119
 msgstr ""
121
 msgstr ""
120
 "{} supprimé(e). <a class=\"alert-link\" href=\"{}\">Annuler "
122
 "{} supprimé(e). <a class=\"alert-link\" href=\"{}\">Annuler "
121
 "l'opération</a>"
123
 "l'opération</a>"
122
 
124
 
123
 #: tracim/controllers/__init__.py:360 tracim/controllers/content.py:99
125
 #: tracim/controllers/__init__.py:360 tracim/controllers/content.py:99
124
-#: tracim/controllers/content.py:805
126
+#: tracim/controllers/content.py:855
125
 msgid "{} not deleted: {}"
127
 msgid "{} not deleted: {}"
126
 msgstr "{} non supprimé(e) : {}"
128
 msgstr "{} non supprimé(e) : {}"
127
 
129
 
128
 #: tracim/controllers/__init__.py:375 tracim/controllers/content.py:116
130
 #: tracim/controllers/__init__.py:375 tracim/controllers/content.py:116
129
-#: tracim/controllers/content.py:820
131
+#: tracim/controllers/content.py:870
130
 msgid "{} undeleted."
132
 msgid "{} undeleted."
131
 msgstr "{} restauré(e)."
133
 msgstr "{} restauré(e)."
132
 
134
 
133
 #: tracim/controllers/__init__.py:385 tracim/controllers/content.py:128
135
 #: tracim/controllers/__init__.py:385 tracim/controllers/content.py:128
134
-#: tracim/controllers/content.py:830
136
+#: tracim/controllers/content.py:880
135
 msgid "{} not un-deleted: {}"
137
 msgid "{} not un-deleted: {}"
136
 msgstr "{} non restauré(e) : {}"
138
 msgstr "{} non restauré(e) : {}"
137
 
139
 
152
 msgid "File created"
154
 msgid "File created"
153
 msgstr "Fichier créé"
155
 msgstr "Fichier créé"
154
 
156
 
155
-#: tracim/controllers/content.py:313
157
+#: tracim/controllers/content.py:340
156
 msgid "Page"
158
 msgid "Page"
157
 msgstr "Page"
159
 msgstr "Page"
158
 
160
 
159
-#: tracim/controllers/content.py:381
161
+#: tracim/controllers/content.py:408
160
 msgid "Page created"
162
 msgid "Page created"
161
 msgstr "Page créée"
163
 msgstr "Page créée"
162
 
164
 
163
-#: tracim/controllers/content.py:421
165
+#: tracim/controllers/content.py:470
164
 msgid "Thread"
166
 msgid "Thread"
165
 msgstr "Discussion"
167
 msgstr "Discussion"
166
 
168
 
167
-#: tracim/controllers/content.py:462
169
+#: tracim/controllers/content.py:512
168
 msgid "Thread created"
170
 msgid "Thread created"
169
 msgstr "Discussion créée"
171
 msgstr "Discussion créée"
170
 
172
 
171
-#: tracim/controllers/content.py:547
173
+#: tracim/controllers/content.py:597
172
 msgid "Item moved to {}"
174
 msgid "Item moved to {}"
173
 msgstr "Element déplacé vers {}"
175
 msgstr "Element déplacé vers {}"
174
 
176
 
175
-#: tracim/controllers/content.py:549
177
+#: tracim/controllers/content.py:599
176
 msgid "Item moved to workspace root"
178
 msgid "Item moved to workspace root"
177
 msgstr "Element déplacé à la racine de l'espace de travail"
179
 msgstr "Element déplacé à la racine de l'espace de travail"
178
 
180
 
179
-#: tracim/controllers/content.py:669
181
+#: tracim/controllers/content.py:719
180
 msgid "Folder created"
182
 msgid "Folder created"
181
 msgstr "Dossier créé"
183
 msgstr "Dossier créé"
182
 
184
 
183
-#: tracim/controllers/content.py:675
185
+#: tracim/controllers/content.py:725
184
 msgid "Folder not created: {}"
186
 msgid "Folder not created: {}"
185
 msgstr "Dossier non créé : {}"
187
 msgstr "Dossier non créé : {}"
186
 
188
 
187
-#: tracim/controllers/content.py:710
189
+#: tracim/controllers/content.py:760
188
 msgid "Folder updated"
190
 msgid "Folder updated"
189
 msgstr "Dossier mis à jour"
191
 msgstr "Dossier mis à jour"
190
 
192
 
191
-#: tracim/controllers/content.py:715
193
+#: tracim/controllers/content.py:765
192
 msgid "Folder not updated: {}"
194
 msgid "Folder not updated: {}"
193
 msgstr "Dossier non mis à jour : {}"
195
 msgstr "Dossier non mis à jour : {}"
194
 
196
 
195
-#: tracim/controllers/content.py:730
197
+#: tracim/controllers/content.py:780
196
 msgid "Folder"
198
 msgid "Folder"
197
 msgstr "Dossiers"
199
 msgstr "Dossiers"
198
 
200
 
340
 msgid "%B %d at %I:%M%p"
342
 msgid "%B %d at %I:%M%p"
341
 msgstr "le %d %B à %H:%M"
343
 msgstr "le %d %B à %H:%M"
342
 
344
 
343
-#: tracim/lib/content.py:98 tracim/templates/dashboard.mak:36
345
+#: tracim/lib/content.py:100 tracim/templates/dashboard.mak:36
344
 #: tracim/templates/master_authenticated.mak:94
346
 #: tracim/templates/master_authenticated.mak:94
345
 #: tracim/templates/master_no_toolbar_no_login.mak:112
347
 #: tracim/templates/master_no_toolbar_no_login.mak:112
346
 #: tracim/templates/search.mak:11 tracim/templates/user_get_one.mak:39
348
 #: tracim/templates/search.mak:11 tracim/templates/user_get_one.mak:39
355
 msgid "Workspaces"
357
 msgid "Workspaces"
356
 msgstr "Espaces de travail"
358
 msgstr "Espaces de travail"
357
 
359
 
360
+#: tracim/lib/notifications.py:289
361
+msgid "<span id=\"content-intro-username\">{}</span> added a comment:"
362
+msgstr "<span id=\"content-intro-username\">{}</span> a ajouté un commentaire :"
363
+
364
+#: tracim/lib/notifications.py:291 tracim/lib/notifications.py:300
365
+msgid "Answer"
366
+msgstr "Répondre"
367
+
368
+#: tracim/lib/notifications.py:297 tracim/lib/notifications.py:321
369
+#: tracim/lib/notifications.py:347
370
+msgid "View online"
371
+msgstr "Voir en ligne"
372
+
373
+#: tracim/lib/notifications.py:301
374
+msgid "<span id=\"content-intro-username\">{}</span> started a thread entitled:"
375
+msgstr ""
376
+"<span id=\"content-intro-username\">{}</span> a lancé une discussion "
377
+"intitulée :"
378
+
379
+#: tracim/lib/notifications.py:306
380
+msgid "<span id=\"content-intro-username\">{}</span> added a file entitled:"
381
+msgstr ""
382
+"<span id=\"content-intro-username\">{}</span> a ajouté un fichier "
383
+"intitulé :"
384
+
385
+#: tracim/lib/notifications.py:316
386
+msgid "<span id=\"content-intro-username\">{}</span> added a page entitled:"
387
+msgstr ""
388
+"<span id=\"content-intro-username\">{}</span> a ajouté une page intitulée"
389
+" :"
390
+
391
+#: tracim/lib/notifications.py:324
392
+msgid "<span id=\"content-intro-username\">{}</span> uploaded a new revision."
393
+msgstr ""
394
+"<span id=\"content-intro-username\">{}</span> a téléchargé une nouvelle "
395
+"version."
396
+
397
+#: tracim/lib/notifications.py:328
398
+msgid "<span id=\"content-intro-username\">{}</span> updated this page."
399
+msgstr "<span id=\"content-intro-username\">{}</span> a mis à jour cette page."
400
+
401
+#: tracim/lib/notifications.py:333
402
+msgid "<p id=\"content-body-intro\">Here is an overview of the changes:</p>"
403
+msgstr "<p id=\"content-body-intro\">Voici un aperçu des modifications :</p>"
404
+
405
+#: tracim/lib/notifications.py:338
406
+msgid ""
407
+"<span id=\"content-intro-username\">{}</span> updated the thread "
408
+"description."
409
+msgstr ""
410
+"<span id=\"content-intro-username\">{}</span> a mis à jour la description"
411
+" de la discussion."
412
+
413
+#: tracim/lib/notifications.py:350
414
+msgid ""
415
+"<span id=\"content-intro-username\">{}</span> updated the file "
416
+"description."
417
+msgstr ""
418
+"<span id=\"content-intro-username\">{}</span> a mis à jour la description"
419
+" du fichier."
420
+
358
 #: tracim/lib/predicates.py:17
421
 #: tracim/lib/predicates.py:17
359
 msgid "You are not authorized to access this resource"
422
 msgid "You are not authorized to access this resource"
360
 msgstr "Vous n'êtes pas autorisé à accéder à cette ressource"
423
 msgstr "Vous n'êtes pas autorisé à accéder à cette ressource"
442
 msgid "Item undeleted"
505
 msgid "Item undeleted"
443
 msgstr "Elément restauré"
506
 msgstr "Elément restauré"
444
 
507
 
445
-#: tracim/model/data.py:202 tracim/model/data.py:212
508
+#: tracim/model/data.py:201 tracim/model/data.py:211
446
 msgid "work in progress"
509
 msgid "work in progress"
447
 msgstr "travail en cours"
510
 msgstr "travail en cours"
448
 
511
 
449
-#: tracim/model/data.py:203 tracim/model/data.py:213
512
+#: tracim/model/data.py:202 tracim/model/data.py:212
450
 msgid "closed — validated"
513
 msgid "closed — validated"
451
 msgstr "clos(e) — validé(e)"
514
 msgstr "clos(e) — validé(e)"
452
 
515
 
453
-#: tracim/model/data.py:204 tracim/model/data.py:214
516
+#: tracim/model/data.py:203 tracim/model/data.py:213
454
 msgid "closed — cancelled"
517
 msgid "closed — cancelled"
455
 msgstr "clos(e) — annulé(e)"
518
 msgstr "clos(e) — annulé(e)"
456
 
519
 
457
-#: tracim/model/data.py:205 tracim/model/data.py:210 tracim/model/data.py:215
520
+#: tracim/model/data.py:204 tracim/model/data.py:209 tracim/model/data.py:214
458
 msgid "deprecated"
521
 msgid "deprecated"
459
 msgstr "obsolète"
522
 msgstr "obsolète"
460
 
523
 
461
-#: tracim/model/data.py:207
524
+#: tracim/model/data.py:206
462
 msgid "subject in progress"
525
 msgid "subject in progress"
463
 msgstr "discussion en cours"
526
 msgstr "discussion en cours"
464
 
527
 
465
-#: tracim/model/data.py:208
528
+#: tracim/model/data.py:207
466
 msgid "subject closed — resolved"
529
 msgid "subject closed — resolved"
467
 msgstr "discussion close — résolue"
530
 msgstr "discussion close — résolue"
468
 
531
 
469
-#: tracim/model/data.py:209
532
+#: tracim/model/data.py:208
470
 msgid "subject closed — cancelled"
533
 msgid "subject closed — cancelled"
471
 msgstr "discussion close — annulée"
534
 msgstr "discussion close — annulée"
472
 
535
 
473
-#: tracim/model/data.py:292
536
+#: tracim/model/data.py:291
474
 msgid "Delete this workspace"
537
 msgid "Delete this workspace"
475
 msgstr "Supprimer l'espace de travail"
538
 msgstr "Supprimer l'espace de travail"
476
 
539
 
477
-#: tracim/model/data.py:293
540
+#: tracim/model/data.py:292
478
 msgid "Delete this folder"
541
 msgid "Delete this folder"
479
 msgstr "Supprimer ce dossier"
542
 msgstr "Supprimer ce dossier"
480
 
543
 
481
-#: tracim/model/data.py:294
544
+#: tracim/model/data.py:293
482
 msgid "Delete this file"
545
 msgid "Delete this file"
483
 msgstr "Supprimer le fichier"
546
 msgstr "Supprimer le fichier"
484
 
547
 
485
-#: tracim/model/data.py:295
548
+#: tracim/model/data.py:294
486
 msgid "Delete this page"
549
 msgid "Delete this page"
487
 msgstr "Supprimer cette page"
550
 msgstr "Supprimer cette page"
488
 
551
 
489
-#: tracim/model/data.py:296
552
+#: tracim/model/data.py:295
490
 msgid "Delete this thread"
553
 msgid "Delete this thread"
491
 msgstr "Supprimer cette discussion"
554
 msgstr "Supprimer cette discussion"
492
 
555
 
493
-#: tracim/model/data.py:297
556
+#: tracim/model/data.py:296
494
 msgid "Delete this comment"
557
 msgid "Delete this comment"
495
 msgstr "Supprimer ce commentaire"
558
 msgstr "Supprimer ce commentaire"
496
 
559
 
1483
 "Comme <span style=\"color: #ea983d;\">gestionnaire de contenu</span> + "
1546
 "Comme <span style=\"color: #ea983d;\">gestionnaire de contenu</span> + "
1484
 "modification de l'espace de travail."
1547
 "modification de l'espace de travail."
1485
 
1548
 
1486
-#: tracim/templates/mail/content_update_body_html.mak:52
1487
-msgid "Some activity has been detected"
1488
-msgstr "Il se passe des choses"
1489
-
1490
-#: tracim/templates/mail/content_update_body_html.mak:60
1491
-msgid "<span style=\"{style}\">&mdash; by {actor_name}</span>"
1492
-msgstr "<span style=\"{style}\">&mdash; par {actor_name}</span>"
1493
-
1494
-#: tracim/templates/mail/content_update_body_html.mak:65
1495
-#: tracim/templates/mail/content_update_body_text.mak:16
1496
-msgid "This item has been deleted."
1497
-msgstr "Cet élément a été supprimé"
1498
-
1499
-#: tracim/templates/mail/content_update_body_html.mak:68
1500
-#: tracim/templates/mail/content_update_body_text.mak:18
1501
-msgid "This item has been archived."
1502
-msgstr "Cet élément a été archivé."
1503
-
1504
-#: tracim/templates/mail/content_update_body_html.mak:72
1505
-msgid "Go to information"
1506
-msgstr "Voir en ligne"
1507
-
1508
-#: tracim/templates/mail/content_update_body_html.mak:82
1549
+#: tracim/templates/mail/content_update_body_html.mak:66
1509
 msgid ""
1550
 msgid ""
1510
-"{user_display_name}, you receive this email because you are "
1511
-"<b>{user_role_label}</b> in the workspace <a "
1512
-"href=\"{workspace_url}\">{workspace_label}</a>"
1551
+"{user_display_name}, you receive this email because you are registered on"
1552
+" <i>{website_title}</i> and you are <i>{user_role_label}</i> in the "
1553
+"workspace <a href=\"{workspace_url}\">{workspace_label}</a>."
1513
 msgstr ""
1554
 msgstr ""
1514
-"{user_display_name}, vous recevez cet email car vous êtes "
1515
-"<b>{user_role_label}</b> dans l'espace de travail <a "
1516
-"href=\"{workspace_url}\">{workspace_label}</a>"
1555
+"{user_display_name}, vous recevez cet email car vous êtes enregistré sur "
1556
+"le site web <i>{website_title}</i> et vous êtes <i>{user_role_label}</i> "
1557
+"dans l'espace de travail <a "
1558
+"href=\"{workspace_url}\">{workspace_label}</a>."
1517
 
1559
 
1518
-#: tracim/templates/mail/content_update_body_text.mak:8
1519
-msgid "Some activity has been detected on the item above"
1520
-msgstr "Il se passe des choses"
1521
-
1522
-#: tracim/templates/mail/content_update_body_text.mak:8
1523
-msgid "-- by {actor_name}"
1524
-msgstr "-- par {actor_name}"
1525
-
1526
-#: tracim/templates/mail/content_update_body_text.mak:20
1527
-msgid "Go to information:"
1528
-msgstr "Voir en ligne :"
1529
-
1530
-#: tracim/templates/mail/content_update_body_text.mak:25
1560
+#: tracim/templates/mail/content_update_body_html.mak:70
1531
 msgid ""
1561
 msgid ""
1532
-"*{user_display_name}*, you receive this email because you are "
1533
-"*{user_role_label}* in the workspace {workspace_label} - {workspace_url}"
1562
+"This email was automatically sent by <i>Tracim</i>, a collaborative "
1563
+"software developped by Algoo."
1534
 msgstr ""
1564
 msgstr ""
1535
-"*{user_display_name}*, vous recevez cet email car vous êtes "
1536
-"*{user_role_label}* dans l'espace de travail {workspace_label} - "
1537
-"{workspace_url}"
1565
+"Cet email a été envoyé automatiquement par <i>Tracim</i>, un logiciel "
1566
+"collaboratif développé par Algoo."
1538
 
1567
 
1539
 #~ msgid "You have no document yet."
1568
 #~ msgid "You have no document yet."
1540
 #~ msgstr "Vous n'avez pas de document pour le moment."
1569
 #~ msgstr "Vous n'avez pas de document pour le moment."
2094
 #~ msgid "Create a topic"
2123
 #~ msgid "Create a topic"
2095
 #~ msgstr "Créer un sujet"
2124
 #~ msgstr "Créer un sujet"
2096
 
2125
 
2126
+#~ msgid "Some activity has been detected"
2127
+#~ msgstr "Il se passe des choses"
2128
+
2129
+#~ msgid "<span style=\"{style}\">&mdash; by {actor_name}</span>"
2130
+#~ msgstr "<span style=\"{style}\">&mdash; par {actor_name}</span>"
2131
+
2132
+#~ msgid "Go to information"
2133
+#~ msgstr "Voir en ligne"
2134
+
2135
+#~ msgid "Some activity has been detected on the item above"
2136
+#~ msgstr "Il se passe des choses"
2137
+
2138
+#~ msgid "-- by {actor_name}"
2139
+#~ msgstr "-- par {actor_name}"
2140
+
2141
+#~ msgid "This item has been deleted."
2142
+#~ msgstr "Cet élément a été supprimé"
2143
+
2144
+#~ msgid "This item has been archived."
2145
+#~ msgstr "Cet élément a été archivé."
2146
+
2147
+#~ msgid "Go to information:"
2148
+#~ msgstr "Voir en ligne :"
2149
+
2097
 #~ msgid ""
2150
 #~ msgid ""
2098
-#~ "This user has no password. Visit "
2099
-#~ "<a href=\"{}\">Reset password</a> page for "
2100
-#~ "init."
2151
+#~ "*{user_display_name}*, you receive this email"
2152
+#~ " because you are *{user_role_label}* in "
2153
+#~ "the workspace {workspace_label} - "
2154
+#~ "{workspace_url}"
2101
 #~ msgstr ""
2155
 #~ msgstr ""
2102
-#~ "Cet utilisateur n'a pas de mot de"
2103
-#~ " passe. Visitez <a href=\"{}\">cette "
2104
-#~ "page</a> pour l'initialiser."
2156
+#~ "*{user_display_name}*, vous recevez cet email"
2157
+#~ " car vous êtes *{user_role_label}* dans "
2158
+#~ "l'espace de travail {workspace_label} - "
2159
+#~ "{workspace_url}"
2105
 
2160
 

+ 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