浏览代码

dirty misc commit including:

Damien ACCORSI 10 年前
父节点
当前提交
0636050fa1

+ 2 - 1
doc/database/tracim-init-database.new.sql 查看文件

152
 CREATE TABLE user_workspace (
152
 CREATE TABLE user_workspace (
153
     user_id integer NOT NULL,
153
     user_id integer NOT NULL,
154
     workspace_id integer NOT NULL,
154
     workspace_id integer NOT NULL,
155
-    role integer
155
+    role integer,
156
+    do_notify boolean DEFAULT FALSE NOT NULL
156
 );
157
 );
157
 
158
 
158
 CREATE TABLE workspaces (
159
 CREATE TABLE workspaces (

+ 2 - 0
doc/database/tracim-migrate-to-first-stable-release.sql 查看文件

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

+ 67 - 3
tracim/tracim/config/app_cfg.py 查看文件

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

+ 1 - 2
tracim/tracim/controllers/admin/__init__.py 查看文件

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

+ 0 - 1
tracim/tracim/controllers/content.py 查看文件

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

+ 39 - 0
tracim/tracim/controllers/user.py 查看文件

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

+ 4 - 8
tracim/tracim/controllers/workspace.py 查看文件

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

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


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

+ 6 - 3
tracim/tracim/lib/content.py 查看文件

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

+ 49 - 0
tracim/tracim/lib/email.py 查看文件

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

+ 3 - 15
tracim/tracim/lib/helpers.py 查看文件

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

+ 199 - 0
tracim/tracim/lib/notifications.py 查看文件

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

+ 18 - 0
tracim/tracim/lib/workspace.py 查看文件

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

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

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

+ 41 - 25
tracim/tracim/model/serializers.py 查看文件

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

二进制
tracim/tracim/public/assets/icons/16x16/actions/mail-notification-none.png 查看文件


+ 3 - 3
tracim/tracim/templates/index.mak 查看文件

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

+ 0 - 0
tracim/tracim/templates/mail/__init__.py 查看文件


+ 84 - 0
tracim/tracim/templates/mail/content_update_body_html.mak 查看文件

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

+ 26 - 0
tracim/tracim/templates/mail/content_update_body_text.mak 查看文件

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

+ 1 - 1
tracim/tracim/templates/master_anonymous.mak 查看文件

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

+ 16 - 14
tracim/tracim/templates/master_authenticated.mak 查看文件

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

+ 2 - 2
tracim/tracim/templates/reset_password_change_password.mak 查看文件

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

+ 2 - 2
tracim/tracim/templates/reset_password_index.mak 查看文件

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

+ 21 - 4
tracim/tracim/templates/user_get_me.mak 查看文件

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