浏览代码

Merge pull request #22 from lebouquetin/feature/full-history-for-files-and-pages

Tracim 9 年前
父节点
当前提交
5863967df9

+ 56 - 3
tracim/tracim/controllers/admin/user.py 查看文件

18
 from formencode.validators import FieldsMatch
18
 from formencode.validators import FieldsMatch
19
 
19
 
20
 from tracim.controllers import TIMRestController
20
 from tracim.controllers import TIMRestController
21
+from tracim.controllers.user import UserWorkspaceRestController
22
+
21
 from tracim.lib import CST
23
 from tracim.lib import CST
22
 from tracim.lib import helpers as h
24
 from tracim.lib import helpers as h
23
 from tracim.lib.base import logger
25
 from tracim.lib.base import logger
212
         tg.redirect(next_url)
214
         tg.redirect(next_url)
213
 
215
 
214
 
216
 
217
+class UserWorkspaceRestController(TIMRestController):
218
+
219
+    def _before(self, *args, **kw):
220
+        """
221
+        Instantiate the current workspace in tg.tmpl_context
222
+        :param args:
223
+        :param kw:
224
+        :return:
225
+        """
226
+        super(self.__class__, self)._before(args, kw)
227
+
228
+        api = UserApi(tg.tmpl_context.current_user)
229
+        user_id = tg.request.controller_state.routing_args.get('user_id')
230
+        user = api.get_one(user_id)
231
+        tg.tmpl_context.user_id = user_id
232
+        tg.tmpl_context.user = user
233
+
234
+    @tg.expose()
235
+    def enable_notifications(self, workspace_id, next_url=None):
236
+        workspace_id = int(workspace_id)
237
+        api = WorkspaceApi(tg.tmpl_context.current_user)
238
+
239
+        workspace = api.get_one(workspace_id)
240
+        api.enable_notifications(tg.tmpl_context.user, workspace)
241
+        tg.flash(_('User {}: notification enabled for workspace {}').format(
242
+            tg.tmpl_context.user.get_display_name(), workspace.label))
243
+
244
+        if next_url:
245
+            tg.redirect(tg.url(next_url))
246
+        tg.redirect(self.parent_controller.url(None, 'me'))
247
+
248
+    @tg.expose()
249
+    def disable_notifications(self, workspace_id, next_url=None):
250
+        workspace_id = int(workspace_id)
251
+        api = WorkspaceApi(tg.tmpl_context.current_user)
252
+
253
+        workspace = api.get_one(workspace_id)
254
+        api.disable_notifications(tg.tmpl_context.user, workspace)
255
+        tg.flash(_('User {}: notification disabled for workspace {}').format(
256
+            tg.tmpl_context.user.get_display_name(), workspace.label))
257
+
258
+        if next_url:
259
+            tg.redirect(tg.url(next_url))
260
+        tg.redirect(self.parent_controller.url(None, 'me'))
261
+
262
+
215
 class UserRestController(TIMRestController):
263
 class UserRestController(TIMRestController):
216
     """
264
     """
217
      CRUD Controller allowing to manage Users
265
      CRUD Controller allowing to manage Users
220
 
268
 
221
     password = UserPasswordAdminRestController()
269
     password = UserPasswordAdminRestController()
222
     profile = UserProfileAdminRestController()
270
     profile = UserProfileAdminRestController()
271
+    workspaces = UserWorkspaceRestController()
223
 
272
 
224
     @classmethod
273
     @classmethod
225
     def current_item_id_key_in_context(cls):
274
     def current_item_id_key_in_context(cls):
289
 
338
 
290
         user = api.get_one(user_id) # FIXME
339
         user = api.get_one(user_id) # FIXME
291
 
340
 
292
-        dictified_user = Context(CTX.USER).toDict(user, 'user')
341
+        role_api = RoleApi(tg.tmpl_context.current_user)
342
+        role_list = role_api.get_roles_for_select_field()
343
+
344
+        dictified_user = Context(CTX.ADMIN_USER).toDict(user, 'user')
293
         current_user_content = Context(CTX.CURRENT_USER).toDict(tmpl_context.current_user)
345
         current_user_content = Context(CTX.CURRENT_USER).toDict(tmpl_context.current_user)
294
-        fake_api_content = DictLikeClass(current_user=current_user_content)
295
-        fake_api = Context(CTX.WORKSPACE).toDict(fake_api_content)
346
+        fake_api_content = DictLikeClass(current_user=current_user_content,
347
+                                         role_types=role_list)
348
+        fake_api = Context(CTX.ADMIN_USER).toDict(fake_api_content)
296
 
349
 
297
         return DictLikeClass(result = dictified_user, fake_api=fake_api)
350
         return DictLikeClass(result = dictified_user, fake_api=fake_api)
298
 
351
 

+ 5 - 6
tracim/tracim/controllers/admin/workspace.py 查看文件

54
     def get_one(self, user_id):
54
     def get_one(self, user_id):
55
         pass
55
         pass
56
 
56
 
57
-
58
     def put(self, *args, **kw):
57
     def put(self, *args, **kw):
59
         pass
58
         pass
60
 
59
 
90
     def undelete(self, user_id, old_role):
89
     def undelete(self, user_id, old_role):
91
         user_id = int(user_id)
90
         user_id = int(user_id)
92
         role_id = int(old_role)
91
         role_id = int(old_role)
93
-        self._add_user_with_role(user_id, role_id, _('User {} restored in workspace {} as {}'))
92
+        self._add_user_with_role(user_id, role_id, None, _('User {} restored in workspace {} as {}'))
94
         tg.redirect(self.parent_controller.url(tg.tmpl_context.workspace_id))
93
         tg.redirect(self.parent_controller.url(tg.tmpl_context.workspace_id))
95
 
94
 
96
     @tg.expose()
95
     @tg.expose()
97
-    def post(self, user_id, role_id):
96
+    def post(self, user_id, role_id, with_notif=False):
98
         user_id = int(user_id)
97
         user_id = int(user_id)
99
         role_id = int(role_id)
98
         role_id = int(role_id)
100
-        self._add_user_with_role(user_id, role_id, _('User {} added to workspace {} as {}'))
99
+        self._add_user_with_role(user_id, role_id, with_notif, _('User {} added to workspace {} as {}'))
101
         tg.redirect(self.parent_controller.url(tg.tmpl_context.workspace_id))
100
         tg.redirect(self.parent_controller.url(tg.tmpl_context.workspace_id))
102
 
101
 
103
-    def _add_user_with_role(self, user_id: int, role_id: int, flash_msg_template)-> UserRoleInWorkspace:
102
+    def _add_user_with_role(self, user_id: int, role_id: int, with_notif: bool, flash_msg_template)-> UserRoleInWorkspace:
104
         user_api = UserApi(tg.tmpl_context.current_user)
103
         user_api = UserApi(tg.tmpl_context.current_user)
105
         user = user_api.get_one(user_id)
104
         user = user_api.get_one(user_id)
106
 
105
 
107
         role_api = RoleApi(tg.tmpl_context.current_user)
106
         role_api = RoleApi(tg.tmpl_context.current_user)
108
-        role = role_api.create_one(user, tg.tmpl_context.workspace, role_id)
107
+        role = role_api.create_one(user, tg.tmpl_context.workspace, role_id, with_notif)
109
 
108
 
110
         tg.flash(flash_msg_template.format(
109
         tg.flash(flash_msg_template.format(
111
             role.user.get_display_name(),
110
             role.user.get_display_name(),

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

79
 
79
 
80
         if not current_id:
80
         if not current_id:
81
             # Default case is to return list of workspaces
81
             # Default case is to return list of workspaces
82
-            print('ignore : ', ignored_ids)
83
             api = WorkspaceApi(tmpl_context.current_user)
82
             api = WorkspaceApi(tmpl_context.current_user)
84
             workspaces = api.get_all_for_user(tmpl_context.current_user,
83
             workspaces = api.get_all_for_user(tmpl_context.current_user,
85
                                               ignored_ids)
84
                                               ignored_ids)

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

373
         elif new_workspace:
373
         elif new_workspace:
374
             item.workspace = new_workspace
374
             item.workspace = new_workspace
375
 
375
 
376
-        item.revision_type = ActionDescription.EDITION
376
+        item.revision_type = ActionDescription.MOVE
377
 
377
 
378
     def move_recursively(self, item: Content,
378
     def move_recursively(self, item: Content,
379
                          new_parent: Content, new_workspace: Workspace):
379
                          new_parent: Content, new_workspace: Workspace):
380
         self.move(item, new_parent, False, new_workspace)
380
         self.move(item, new_parent, False, new_workspace)
381
         self.save(item, do_notify=False)
381
         self.save(item, do_notify=False)
382
-        print('saved item #', item.content_id, new_workspace)
382
+
383
         for child in item.children:
383
         for child in item.children:
384
             self.move_recursively(child, item, new_workspace)
384
             self.move_recursively(child, item, new_workspace)
385
         return
385
         return

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

177
     return ContentType._DELETE_LABEL[item.type]
177
     return ContentType._DELETE_LABEL[item.type]
178
 
178
 
179
 def is_item_still_editable(item):
179
 def is_item_still_editable(item):
180
+    if item.type.id != 'comment':
181
+        return False
182
+
180
     # HACK - D.A - 2014-12-24 - item contains a datetime object!!!
183
     # HACK - D.A - 2014-12-24 - item contains a datetime object!!!
181
     # 'item' is a variable which is created by serialization and it should be an instance of DictLikeClass.
184
     # 'item' is a variable which is created by serialization and it should be an instance of DictLikeClass.
182
     # therefore, it contains strins, integers and booleans (something json-ready or almost json-ready)
185
     # therefore, it contains strins, integers and booleans (something json-ready or almost json-ready)

+ 3 - 1
tracim/tracim/lib/userworkspace.py 查看文件

50
     def get_one(self, user_id, workspace_id):
50
     def get_one(self, user_id, workspace_id):
51
         return self._get_one_rsc(user_id, workspace_id).one()
51
         return self._get_one_rsc(user_id, workspace_id).one()
52
 
52
 
53
-    def create_one(self, user: User, workspace: Workspace, role_level: int, flush: bool=True) -> UserRoleInWorkspace:
53
+    def create_one(self, user: User, workspace: Workspace, role_level: int, with_notif: bool, flush: bool=True) -> UserRoleInWorkspace:
54
         role = self.create_role()
54
         role = self.create_role()
55
         role.user_id = user.user_id
55
         role.user_id = user.user_id
56
         role.workspace = workspace
56
         role.workspace = workspace
57
         role.role = role_level
57
         role.role = role_level
58
+        if with_notif is not None:
59
+            role.do_notify = with_notif
58
         if flush:
60
         if flush:
59
             DBSession.flush()
61
             DBSession.flush()
60
         return role
62
         return role

+ 4 - 1
tracim/tracim/lib/workspace.py 查看文件

45
         workspace.description = description
45
         workspace.description = description
46
 
46
 
47
         # By default, we force the current user to be the workspace manager
47
         # By default, we force the current user to be the workspace manager
48
-        role = RoleApi(self._user).create_one(self._user, workspace, UserRoleInWorkspace.WORKSPACE_MANAGER)
48
+        # And to receive email notifications
49
+        role = RoleApi(self._user).create_one(self._user, workspace,
50
+                                              UserRoleInWorkspace.WORKSPACE_MANAGER,
51
+                                              with_notif=True)
49
 
52
 
50
         DBSession.add(workspace)
53
         DBSession.add(workspace)
51
         DBSession.add(role)
54
         DBSession.add(role)

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

213
 
213
 
214
     def get_role(self, workspace: 'Workspace') -> int:
214
     def get_role(self, workspace: 'Workspace') -> int:
215
         for role in self.roles:
215
         for role in self.roles:
216
-            print('IS EQUALS ? ', role.workspace, workspace)
217
             if role.workspace == workspace:
216
             if role.workspace == workspace:
218
                 return role.role
217
                 return role.role
219
 
218
 

+ 99 - 17
tracim/tracim/model/data.py 查看文件

24
 from sqlalchemy.types import Text
24
 from sqlalchemy.types import Text
25
 from sqlalchemy.types import Unicode
25
 from sqlalchemy.types import Unicode
26
 
26
 
27
-from tg.i18n import lazy_ugettext as l_
27
+from tg.i18n import lazy_ugettext as l_, ugettext as _
28
 
28
 
29
 from tracim.model import DeclarativeBase
29
 from tracim.model import DeclarativeBase
30
 from tracim.model.auth import User
30
 from tracim.model.auth import User
169
     STATUS_UPDATE = 'status-update'
169
     STATUS_UPDATE = 'status-update'
170
     UNARCHIVING = 'unarchiving'
170
     UNARCHIVING = 'unarchiving'
171
     UNDELETION = 'undeletion'
171
     UNDELETION = 'undeletion'
172
+    MOVE = 'move'
172
 
173
 
173
     _ICONS = {
174
     _ICONS = {
174
         'archiving': 'fa fa-archive',
175
         'archiving': 'fa fa-archive',
175
         'content-comment': 'fa-comment-o',
176
         'content-comment': 'fa-comment-o',
176
         'creation': 'fa-magic',
177
         'creation': 'fa-magic',
177
-        'deletion': 'fa fa-trash',
178
-        'edition': 'fa fa-edit',
178
+        'deletion': 'fa-trash',
179
+        'edition': 'fa-edit',
179
         'revision': 'fa-history',
180
         'revision': 'fa-history',
180
         'status-update': 'fa-random',
181
         'status-update': 'fa-random',
181
-        'unarchiving': 'fa fa-file-archive-o',
182
-        'undeletion': 'fa-trash-o'
182
+        'unarchiving': 'fa-file-archive-o',
183
+        'undeletion': 'fa-trash-o',
184
+        'move': 'fa-arrows'
183
     }
185
     }
184
 
186
 
185
     _LABELS = {
187
     _LABELS = {
186
         'archiving': l_('archive'),
188
         'archiving': l_('archive'),
187
-        'content-comment': l_('commente'),
188
-        'creation': l_('creation'),
189
-        'deletion': l_('deletion'),
190
-        'edition': l_('modified'),
191
-        'revision': l_('revision'),
192
-        'status-update': l_('statut'),
193
-        'unarchiving': l_('un-archived'),
194
-        'undeletion': l_('un-deleted'),
189
+        'content-comment': l_('Item commented'),
190
+        'creation': l_('Item created'),
191
+        'deletion': l_('Item deleted'),
192
+        'edition': l_('item modified'),
193
+        'revision': l_('New revision'),
194
+        'status-update': l_('New status'),
195
+        'unarchiving': l_('Item unarchived'),
196
+        'undeletion': l_('Item undeleted'),
197
+        'move': l_('Item moved')
195
     }
198
     }
196
 
199
 
197
     def __init__(self, id):
200
     def __init__(self, id):
199
         self.id = id
202
         self.id = id
200
         self.label = ActionDescription._LABELS[id]
203
         self.label = ActionDescription._LABELS[id]
201
         self.icon = ActionDescription._ICONS[id]
204
         self.icon = ActionDescription._ICONS[id]
205
+        self.css = ''
202
 
206
 
203
     @classmethod
207
     @classmethod
204
     def allowed_values(cls):
208
     def allowed_values(cls):
210
                 cls.REVISION,
214
                 cls.REVISION,
211
                 cls.STATUS_UPDATE,
215
                 cls.STATUS_UPDATE,
212
                 cls.UNARCHIVING,
216
                 cls.UNARCHIVING,
213
-                cls.UNDELETION]
217
+                cls.UNDELETION,
218
+                cls.MOVE]
214
 
219
 
215
 
220
 
216
 class ContentStatus(object):
221
 class ContentStatus(object):
406
     def sorted(cls, types: ['ContentType']) -> ['ContentType']:
411
     def sorted(cls, types: ['ContentType']) -> ['ContentType']:
407
         return sorted(types, key=lambda content_type: content_type.priority)
412
         return sorted(types, key=lambda content_type: content_type.priority)
408
 
413
 
414
+    @property
415
+    def type(self):
416
+        return self.id
417
+
409
     def __init__(self, type):
418
     def __init__(self, type):
410
-        self.type = type
419
+        self.id = type
411
         self.icon = ContentType._CSS_ICONS[type]
420
         self.icon = ContentType._CSS_ICONS[type]
412
-        self.color = ContentType._CSS_COLORS[type]
421
+        self.color = ContentType._CSS_COLORS[type]  # deprecated
422
+        self.css = ContentType._CSS_COLORS[type]
413
         self.label = ContentType._LABEL[type]
423
         self.label = ContentType._LABEL[type]
414
         self.priority = ContentType._ORDER_WEIGHT[type]
424
         self.priority = ContentType._ORDER_WEIGHT[type]
415
 
425
 
594
         except Exception as e:
604
         except Exception as e:
595
             print(e.__str__())
605
             print(e.__str__())
596
             print('----- /*\ *****')
606
             print('----- /*\ *****')
597
-            raise ValueError('No allowed content property')
607
+            raise ValueError('Not allowed content property')
598
 
608
 
599
         return ContentType.sorted(types)
609
         return ContentType.sorted(types)
600
 
610
 
611
+    def get_history(self) -> '[VirtualEvent]':
612
+        events = []
613
+        for comment in self.get_comments():
614
+            events.append(VirtualEvent.create_from_content(comment))
615
+        for revision in self.revisions:
616
+            events.append(VirtualEvent.create_from_content_revision(revision))
617
+
618
+        sorted_events = sorted(events,
619
+                               key=lambda event: event.created, reverse=True)
620
+        return sorted_events
621
+
601
 
622
 
602
 class ContentChecker(object):
623
 class ContentChecker(object):
603
 
624
 
649
     file_mimetype = Column(Unicode(255),  unique=False, nullable=False, default='')
670
     file_mimetype = Column(Unicode(255),  unique=False, nullable=False, default='')
650
     file_content = deferred(Column(LargeBinary(), unique=False, nullable=False, default=None))
671
     file_content = deferred(Column(LargeBinary(), unique=False, nullable=False, default=None))
651
 
672
 
673
+    type = Column(Unicode(32), unique=False, nullable=False)
652
     status = Column(Unicode(32), unique=False, nullable=False)
674
     status = Column(Unicode(32), unique=False, nullable=False)
653
     created = Column(DateTime, unique=False, nullable=False)
675
     created = Column(DateTime, unique=False, nullable=False)
654
     updated = Column(DateTime, unique=False, nullable=False)
676
     updated = Column(DateTime, unique=False, nullable=False)
665
     def get_status(self):
687
     def get_status(self):
666
         return ContentStatus(self.status)
688
         return ContentStatus(self.status)
667
 
689
 
690
+    def get_label(self):
691
+        return self.label if self.label else self.file_name if self.file_name else ''
692
+
668
     def get_last_action(self) -> ActionDescription:
693
     def get_last_action(self) -> ActionDescription:
669
         return ActionDescription(self.revision_type)
694
         return ActionDescription(self.revision_type)
670
 
695
 
678
         self.node = node
703
         self.node = node
679
         self.children = children
704
         self.children = children
680
         self.is_selected = is_selected
705
         self.is_selected = is_selected
706
+
707
+class VirtualEvent(object):
708
+    @classmethod
709
+    def create_from(cls, object):
710
+        if Content == object.__class__:
711
+            return cls.create_from_content(object)
712
+        elif ContentRevisionRO == object.__class__:
713
+            return cls.create_from_content_revision(object)
714
+
715
+    @classmethod
716
+    def create_from_content(cls, content: Content):
717
+        content_type = ContentType(content.type)
718
+
719
+        label = content.get_label()
720
+        if content.type==ContentType.Comment:
721
+            label = _('<strong>{}</strong> wrote:').format(content.owner.get_display_name())
722
+
723
+        return VirtualEvent(id=content.content_id,
724
+                            created=content.created,
725
+                            owner=content.owner,
726
+                            type=content_type,
727
+                            label=label,
728
+                            content=content.description,
729
+                            ref_object=content)
730
+
731
+    @classmethod
732
+    def create_from_content_revision(cls, revision: ContentRevisionRO):
733
+        action_description = ActionDescription(revision.revision_type)
734
+
735
+        return VirtualEvent(id=revision.revision_id,
736
+                            created=revision.created,
737
+                            owner=revision.owner,
738
+                            type=action_description,
739
+                            label=action_description.label,
740
+                            content='',
741
+                            ref_object=revision)
742
+
743
+    def __init__(self, id, created, owner, type, label, content, ref_object):
744
+        self.id = id
745
+        self.created = created
746
+        self.owner = owner
747
+        self.type = type
748
+        self.label = label
749
+        self.content = content
750
+        self.ref_object = ref_object
751
+
752
+        print(type)
753
+        assert hasattr(type, 'id')
754
+        assert hasattr(type, 'css')
755
+        assert hasattr(type, 'icon')
756
+        assert hasattr(type, 'label')
757
+
758
+    def created_as_delta(self, delta_from_datetime:datetime=None):
759
+        if not delta_from_datetime:
760
+            delta_from_datetime = datetime.now()
761
+        return format_timedelta(delta_from_datetime - self.created,
762
+                                locale=tg.i18n.get_lang()[0])

+ 53 - 20
tracim/tracim/model/serializers.py 查看文件

22
 from tracim.model.data import ContentType
22
 from tracim.model.data import ContentType
23
 from tracim.model.data import RoleType
23
 from tracim.model.data import RoleType
24
 from tracim.model.data import UserRoleInWorkspace
24
 from tracim.model.data import UserRoleInWorkspace
25
+from tracim.model.data import VirtualEvent
25
 from tracim.model.data import Workspace
26
 from tracim.model.data import Workspace
26
 
27
 
27
 from tracim.model import data as pmd
28
 from tracim.model import data as pmd
61
 
62
 
62
 class CTX(object):
63
 class CTX(object):
63
     """ constants that are used for serialization / dictification of models"""
64
     """ constants that are used for serialization / dictification of models"""
65
+    ADMIN_USER = 'ADMIN_USER'
64
     ADMIN_WORKSPACE = 'ADMIN_WORKSPACE'
66
     ADMIN_WORKSPACE = 'ADMIN_WORKSPACE'
65
     ADMIN_WORKSPACES = 'ADMIN_WORKSPACES'
67
     ADMIN_WORKSPACES = 'ADMIN_WORKSPACES'
66
     CONTENT_LIST = 'CONTENT_LIST'
68
     CONTENT_LIST = 'CONTENT_LIST'
69
+    CONTENT_HISTORY = 'CONTENT_HISTORY'
67
     CURRENT_USER = 'CURRENT_USER'
70
     CURRENT_USER = 'CURRENT_USER'
68
     DEFAULT = 'DEFAULT' # default context. This will allow to define a serialization method to be used by default
71
     DEFAULT = 'DEFAULT' # default context. This will allow to define a serialization method to be used by default
69
     EMAIL_NOTIFICATION = 'EMAIL_NOTIFICATION'
72
     EMAIL_NOTIFICATION = 'EMAIL_NOTIFICATION'
351
     if content.type in (ContentType.Page, ContentType.File) :
354
     if content.type in (ContentType.Page, ContentType.File) :
352
         data_container = content
355
         data_container = content
353
 
356
 
354
-
355
-
356
         # The following properties are overriden by revision values
357
         # The following properties are overriden by revision values
357
         if content.revision_to_serialize>0:
358
         if content.revision_to_serialize>0:
358
             for revision in content.revisions:
359
             for revision in content.revisions:
361
                     break
362
                     break
362
 
363
 
363
         result = DictLikeClass(
364
         result = DictLikeClass(
364
-            id = content.content_id,
365
-            parent = context.toDict(content.parent),
366
-            workspace = context.toDict(content.workspace),
367
-            type = content.type,
368
-
369
-            content = data_container.description,
370
-            created = data_container.created,
371
-            label = data_container.label,
372
-            icon = ContentType.get_icon(content.type),
373
-            owner = context.toDict(data_container.owner),
374
-            status = context.toDict(data_container.get_status()),
375
-            links = context.toDict(content.extract_links_from_content(data_container.description)),
376
-            revisions = context.toDict(sorted(content.revisions, key=lambda v: v.created, reverse=True)),
377
-            selected_revision = 'latest' if content.revision_to_serialize<=0 else content.revision_to_serialize
365
+            id=content.content_id,
366
+            parent=context.toDict(content.parent),
367
+            workspace=context.toDict(content.workspace),
368
+            type=content.type,
369
+
370
+            content=data_container.description,
371
+            created=data_container.created,
372
+            label=data_container.label,
373
+            icon=ContentType.get_icon(content.type),
374
+            owner=context.toDict(data_container.owner),
375
+            status=context.toDict(data_container.get_status()),
376
+            links=context.toDict(content.extract_links_from_content(data_container.description)),
377
+            revisions=context.toDict(sorted(content.revisions, key=lambda v: v.created, reverse=True)),
378
+            selected_revision='latest' if content.revision_to_serialize<=0 else content.revision_to_serialize,
379
+            history=Context(CTX.CONTENT_HISTORY).toDict(content.get_history())
378
         )
380
         )
379
 
381
 
380
         if content.type==ContentType.File:
382
         if content.type==ContentType.File:
395
     raise NotImplementedError
397
     raise NotImplementedError
396
 
398
 
397
 
399
 
400
+@pod_serializer(VirtualEvent, CTX.CONTENT_HISTORY)
401
+def serialize_content_for_history(event: VirtualEvent, context: Context):
402
+    urls = DictLikeClass({'delete': None})
403
+    if ContentType.Comment == event.type.id:
404
+        urls = context.toDict({
405
+          'delete': context.url('/workspaces/{wid}/folders/{fid}/{ctype}/{cid}/comments/{commentid}/put_delete'.format(
406
+              wid = event.ref_object.workspace_id,
407
+              fid=event.ref_object.parent.parent_id,
408
+              ctype=event.ref_object.parent.type+'s',
409
+              cid=event.ref_object.parent.content_id,
410
+              commentid=event.ref_object.content_id))
411
+        })
412
+
413
+    return DictLikeClass(
414
+        owner=context.toDict(event.owner),
415
+        id=event.id,
416
+        label=event.label,
417
+        type=context.toDict(event.type),
418
+        created=event.created,
419
+        created_as_delta=event.created_as_delta(),
420
+        content=event.content,
421
+        urls = urls
422
+    )
423
+
424
+
398
 @pod_serializer(Content, CTX.THREAD)
425
 @pod_serializer(Content, CTX.THREAD)
399
 def serialize_node_for_page(item: Content, context: Context):
426
 def serialize_node_for_page(item: Content, context: Context):
400
     if item.type==ContentType.Thread:
427
     if item.type==ContentType.Thread:
411
             status = context.toDict(item.get_status()),
438
             status = context.toDict(item.get_status()),
412
             type = item.type,
439
             type = item.type,
413
             workspace = context.toDict(item.workspace),
440
             workspace = context.toDict(item.workspace),
414
-            comments = reversed(context.toDict(item.get_comments()))
441
+            comments = reversed(context.toDict(item.get_comments())),
442
+            history = Context(CTX.CONTENT_HISTORY).toDict(item.get_history())
415
         )
443
         )
416
 
444
 
417
     if item.type==ContentType.Comment:
445
     if item.type==ContentType.Comment:
660
 def serialize_breadcrumb_item(content_type: ContentType, context: Context):
688
 def serialize_breadcrumb_item(content_type: ContentType, context: Context):
661
     return DictLikeClass(content_type.toDict())
689
     return DictLikeClass(content_type.toDict())
662
 
690
 
691
+
663
 @pod_serializer(Content, CTX.SEARCH)
692
 @pod_serializer(Content, CTX.SEARCH)
664
 def serialize_content_for_search_result(content: Content, context: Context):
693
 def serialize_content_for_search_result(content: Content, context: Context):
665
 
694
 
757
 
786
 
758
 
787
 
759
 @pod_serializer(RoleType, CTX.ADMIN_WORKSPACE)
788
 @pod_serializer(RoleType, CTX.ADMIN_WORKSPACE)
789
+@pod_serializer(RoleType, CTX.ADMIN_USER)
760
 def serialize_role_list_for_select_field_in_workspace(role_type: RoleType, context: Context):
790
 def serialize_role_list_for_select_field_in_workspace(role_type: RoleType, context: Context):
761
     """
791
     """
762
     Actually, roles are serialized as users (with minimal information)
792
     Actually, roles are serialized as users (with minimal information)
810
 
840
 
811
 
841
 
812
 @pod_serializer(User, CTX.USER)
842
 @pod_serializer(User, CTX.USER)
843
+@pod_serializer(User, CTX.ADMIN_USER)
813
 @pod_serializer(User, CTX.CURRENT_USER)
844
 @pod_serializer(User, CTX.CURRENT_USER)
814
 def serialize_user_for_user(user: User, context: Context):
845
 def serialize_user_for_user(user: User, context: Context):
815
     """
846
     """
848
     result['style'] = role.style
879
     result['style'] = role.style
849
     result['role_description'] = role.role_as_label()
880
     result['role_description'] = role.role_as_label()
850
     result['email'] = role.user.email
881
     result['email'] = role.user.email
851
-    result['user'] = role.user
882
+    result['user'] = context.toDict(role.user)
852
     result['notifications_subscribed'] = role.do_notify
883
     result['notifications_subscribed'] = role.do_notify
853
     return result
884
     return result
854
 
885
 
855
 
886
 
856
 @pod_serializer(UserRoleInWorkspace, CTX.USER)
887
 @pod_serializer(UserRoleInWorkspace, CTX.USER)
857
 @pod_serializer(UserRoleInWorkspace, CTX.CURRENT_USER)
888
 @pod_serializer(UserRoleInWorkspace, CTX.CURRENT_USER)
889
+@pod_serializer(UserRoleInWorkspace, CTX.ADMIN_USER)
858
 def serialize_role_in_list_for_user(role: UserRoleInWorkspace, context: Context):
890
 def serialize_role_in_list_for_user(role: UserRoleInWorkspace, context: Context):
859
     """
891
     """
860
     Actually, roles are serialized as users (with minimal information)
892
     Actually, roles are serialized as users (with minimal information)
868
     result['label'] = role.role_as_label()
900
     result['label'] = role.role_as_label()
869
     result['style'] = RoleType(role.role).css_style
901
     result['style'] = RoleType(role.role).css_style
870
     result['workspace'] =  context.toDict(role.workspace)
902
     result['workspace'] =  context.toDict(role.workspace)
871
-    result['user'] = role.user
903
+    result['user'] = Context(CTX.DEFAULT).toDict(role.user)
872
     result['notifications_subscribed'] = role.do_notify
904
     result['notifications_subscribed'] = role.do_notify
873
 
905
 
874
     # result['workspace_name'] = role.workspace.label
906
     # result['workspace_name'] = role.workspace.label
882
 def serialize_workspace_default(workspace: Workspace, context: Context):
914
 def serialize_workspace_default(workspace: Workspace, context: Context):
883
     result = DictLikeClass(
915
     result = DictLikeClass(
884
         id = workspace.workspace_id,
916
         id = workspace.workspace_id,
885
-        label = workspace.label,
917
+        label = workspace.label,  # FIXME - 2015-08-20 - remove this property
918
+        name = workspace.label,  # use name instead of label
886
         url = context.url('/workspaces/{}'.format(workspace.workspace_id))
919
         url = context.url('/workspaces/{}'.format(workspace.workspace_id))
887
     )
920
     )
888
     return result
921
     return result

+ 5 - 1
tracim/tracim/public/assets/css/dashboard.css 查看文件

349
     padding: 0;
349
     padding: 0;
350
     position: absolute;
350
     position: absolute;
351
     top: 0;
351
     top: 0;
352
-}
352
+}
353
+
354
+#t-full-app-alert-message-id > div.alert {
355
+    box-shadow: 0px 0px 5px 5px rgba(0, 0, 0, 0.3);
356
+}

+ 6 - 2
tracim/tracim/templates/admin/user_getone.mak 查看文件

61
                             ${ICON.FA('fa-bar-chart t-less-visible')}
61
                             ${ICON.FA('fa-bar-chart t-less-visible')}
62
                             ${_('Global profile')}
62
                             ${_('Global profile')}
63
                         </h3>
63
                         </h3>
64
-                        ${P.USER_PROFILE(result.user)}
64
+                        ${P.USER_PROFILE(fake_api.current_user, result.user)}
65
                     </div>
65
                     </div>
66
                     <div style="margin-top: 4em;">
66
                     <div style="margin-top: 4em;">
67
                         <h3>
67
                         <h3>
81
                                     </tr>
81
                                     </tr>
82
                                 </thead>
82
                                 </thead>
83
                                 % for role in result.user.roles:
83
                                 % for role in result.user.roles:
84
-                                    ${TABLE_ROW.USER_ROLE_IN_WORKSPACE(role)}
84
+<%
85
+    enable_link = '/admin/users/{user}/workspaces/{workspace}/enable_notifications?next_url=/admin/users/{user}'
86
+    disable_link = '/admin/users/{user}/workspaces/{workspace}/disable_notifications?next_url=/admin/users/{user}'
87
+%>
88
+                                    ${TABLE_ROW.USER_ROLE_IN_WORKSPACE(fake_api.current_user, role, show_id=True, enable_link=enable_link, disable_link=disable_link)}
85
                                 % endfor
89
                                 % endfor
86
                             </table>
90
                             </table>
87
                         % endif
91
                         % endif

+ 6 - 0
tracim/tracim/templates/admin/workspace_getone.mak 查看文件

83
                                             % endfor
83
                                             % endfor
84
                                         </div>
84
                                         </div>
85
 
85
 
86
+                                        <div class="checkbox">
87
+                                            <label>
88
+                                                <input type="checkbox" id="with_notif" name="with_notif" checked="checked"/> ${_('Subscribe to mail notifications')}
89
+                                            </label>
90
+                                        </div>
91
+
86
                                         <span class="pull-right" style="margin-top: 0.5em;">
92
                                         <span class="pull-right" style="margin-top: 0.5em;">
87
                                             <button id="current-document-add-comment-save-button" type="submit" class="btn btn-small btn-success" title="Add first comment"><i class=" fa fa-check"></i> ${_('Validate')}</button>
93
                                             <button id="current-document-add-comment-save-button" type="submit" class="btn btn-small btn-success" title="Add first comment"><i class=" fa fa-check"></i> ${_('Validate')}</button>
88
                                         </span>
94
                                         </span>

+ 5 - 21
tracim/tracim/templates/file/getone.mak 查看文件

154
     <div class="col-sm-7 col-sm-offset-3">
154
     <div class="col-sm-7 col-sm-offset-3">
155
         <div class="t-spacer-above">
155
         <div class="t-spacer-above">
156
             <span id="associated-revisions" ></span>
156
             <span id="associated-revisions" ></span>
157
-            <h4 class="anchored-title">${_('File revisions')}</h4>
157
+            <h4 class="anchored-title">${_('File history')}</h4>
158
             <div>
158
             <div>
159
                 <table class="table table-striped table-hover">
159
                 <table class="table table-striped table-hover">
160
-                    % for revid, revision in reversed(list(enumerate(reversed(result.file.revisions)))):
161
-                        <% warning_or_not = ('', 'warning')[result.file.selected_revision==revision.id] %>
162
-                        <tr class="${warning_or_not}">
163
-## FIXME - 2015-07-22 - D.A. - Do we really need to show a rev. id ?!
164
-## <td><span class="label label-default">v${revid}</span></td>
165
-                            <td class="t-less-visible">
166
-                                <span class="label label-default">${ICON.FA_FW(revision.action.icon)}
167
-                                ${revision.action.label}</span>
168
-                            </td>
169
-                            <td>${h.date(revision.created)}</td>
170
-                            <td>${h.time(revision.created)}</td>
171
-                            <td>${revision.owner.name}</td>
172
-                            <td><a href="${tg.url('/workspaces/{}/folders/{}/files/{}?revision_id={}').format(result.file.workspace.id, result.file.parent.id, result.file.id, revision.id)}">${revision.label}</a></td>
173
-                            <td class="t-less-visible" title="${_('Currently shown')}">
174
-                                % if warning_or_not:
175
-                                    ${ICON.FA_FW('fa fa-caret-left')} ${_('shown').format(result.file.selected_revision)}
176
-                                % endif
177
-                            </td>
178
-                        </tr>
160
+                    % for event in result.file.history:
161
+                        ${WIDGETS.SECURED_HISTORY_VIRTUAL_EVENT_AS_TABLE_ROW(fake_api.current_user, event, result.file.selected_revision)}
179
                     % endfor
162
                     % endfor
180
                 </table>
163
                 </table>
181
             </div>
164
             </div>
182
         </div>
165
         </div>
183
     </div>
166
     </div>
184
-<div/>
167
+<div/>
168
+

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

157
                                                 </tr>
157
                                                 </tr>
158
                                             </thead>
158
                                             </thead>
159
                                             % for role in fake_api.current_user.roles:
159
                                             % for role in fake_api.current_user.roles:
160
-                                                ${TABLE_ROW.USER_ROLE_IN_WORKSPACE(role, show_id=False, enable_link='/user/me/workspaces/{workspace}/enable_notifications?next_url=/home', disable_link='/user/me/workspaces/{workspace}/disable_notifications?next_url=/home')}
160
+                                                ${TABLE_ROW.USER_ROLE_IN_WORKSPACE(fake_api.current_user, role, show_id=False, enable_link='/user/me/workspaces/{workspace}/enable_notifications?next_url=/home', disable_link='/user/me/workspaces/{workspace}/disable_notifications?next_url=/home')}
161
                                             % endfor
161
                                             % endfor
162
                                         </table>
162
                                         </table>
163
                                     % endif
163
                                     % endif

+ 4 - 20
tracim/tracim/templates/page/getone.mak 查看文件

98
     <div class="col-sm-7 col-sm-offset-3">
98
     <div class="col-sm-7 col-sm-offset-3">
99
         <div class="t-spacer-above">
99
         <div class="t-spacer-above">
100
             <span id="associated-revisions" ></span>
100
             <span id="associated-revisions" ></span>
101
-            <h4 class="anchored-title">${_('Page revisions')}</h4>
101
+            <h4 class="anchored-title">${_('Page history')}</h4>
102
             <div>
102
             <div>
103
                 <table class="table table-striped table-hover">
103
                 <table class="table table-striped table-hover">
104
-                    % for revid, revision in reversed(list(enumerate(reversed(result.page.revisions)))):
105
-                        <% warning_or_not = ('', 'warning')[result.page.selected_revision==revision.id] %>
106
-                        <tr class="${warning_or_not}">
107
-## FIXME - 2015-07-22 - D.A. - Do we really need to show a rev. id ?!
108
-## <td><span class="label label-default">v${revid}</span></td>
109
-                            <td class="t-less-visible">
110
-                                <span class="label label-default">${ICON.FA_FW(revision.action.icon)}
111
-                                ${revision.action.label}</span>
112
-                            </td>
113
-                            <td>${h.date(revision.created)}</td>
114
-                            <td>${h.time(revision.created)}</td>
115
-                            <td>${revision.owner.name}</td>
116
-                            <td><a href="${tg.url('/workspaces/{}/folders/{}/pages/{}?revision_id={}').format(result.page.workspace.id, result.page.parent.id, result.page.id, revision.id)}">${revision.label}</a></td>
117
-                            <td class="t-less-visible" title="${_('Currently shown')}">
118
-                                % if warning_or_not:
119
-                                    ${ICON.FA_FW('fa fa-caret-left')} ${_('shown').format(result.page.selected_revision)}
120
-                                % endif
121
-                            </td>
122
-                        </tr>
104
+                    % for event in result.page.history:
105
+                        ${WIDGETS.SECURED_HISTORY_VIRTUAL_EVENT_AS_TABLE_ROW(fake_api.current_user, event, result.page.selected_revision)}
123
                     % endfor
106
                     % endfor
124
                 </table>
107
                 </table>
125
             </div>
108
             </div>
126
         </div>
109
         </div>
127
     </div>
110
     </div>
128
 <div/>
111
 <div/>
112
+

+ 10 - 22
tracim/tracim/templates/thread/getone.mak 查看文件

100
     </div>
100
     </div>
101
 </div>
101
 </div>
102
 
102
 
103
-% for comment in result.thread.comments:
104
-    ${WIDGETS.SECURED_TIMELINE_ITEM(fake_api.current_user, comment)}
103
+% for event in result.thread.history:
104
+    ## TODO - D.A. - 2015-08-20
105
+    ## Allow to show full history (with status change and archive/unarchive)
106
+    % if event.type.id in ('comment', 'creation'):
107
+        ${WIDGETS.SECURED_HISTORY_VIRTUAL_EVENT(fake_api.current_user, event)}
108
+    % endif
105
 % endfor
109
 % endfor
106
 
110
 
107
-## <hr class="tracim-panel-separator"/>
108
-## <div>
109
-##     <h4 id="associated-links" class="anchored-title" >${_('Links extracted from the thread')}</h4>
110
-##     <div>
111
-##         % if len(result.thread.links)<=0:
112
-##             <p class="pod-empty">${_('No link found.')}</p>
113
-##         % else:
114
-##             <ul>
115
-##                 % for link in result.thread.links:
116
-##                     <li><a href="${link.href}">${link.label if link.label else link.href}</a></li>
117
-##                 % endfor
118
-##             </ul>
119
-##         % endif
120
-##     </div>
121
-##     <hr/>
122
-## 
123
-##     % for comment in result.thread.comments:
124
-##         ${comment}
125
-##     % endfor
126
-## </div>
111
+## % for comment in result.thread.comments:
112
+##     ${WIDGETS.SECURED_TIMELINE_ITEM(fake_api.current_user, comment)}
113
+## % endfor
114
+##

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

62
                             </table>
62
                             </table>
63
                         % endif
63
                         % endif
64
                     </div>
64
                     </div>
65
-                    % if len(result.user.roles)>0:
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>
66
                         <p class="alert alert-info">${_('You can configure your email notifications by clicking on the email icons above')}</p>
67
                     % endif
67
                     % endif
68
                 </div>
68
                 </div>

+ 76 - 10
tracim/tracim/templates/user_workspace_widgets.mak 查看文件

303
 </%def>
303
 </%def>
304
 
304
 
305
 <%def name="SECURED_TIMELINE_ITEM(user, item)">
305
 <%def name="SECURED_TIMELINE_ITEM(user, item)">
306
+##     <div class="row t-odd-or-even t-hacky-thread-comment-border-top">
307
+##         <div class="col-sm-7 col-sm-offset-3">
308
+##             <div class="t-timeline-item">
309
+## ##                <i class="fa fa-fw fa-3x fa-comment-o t-less-visible" style="margin-left: -1.5em; float:left;"></i>
310
+##                 ${ICON.FA_FW('fa fa-3x fa-comment-o t-less-visible t-timeline-item-icon')}
311
+##
312
+##                 <h5 style="margin: 0;">
313
+##                     <span class="tracim-less-visible">${_('<strong>{}</strong> wrote:').format(item.owner.name)|n}</span>
314
+##
315
+##                     <div class="pull-right text-right t-timeline-item-moment" title="${h.date_time(item.created)|n}">
316
+##                         ${_('{delta} ago').format(delta=item.created_as_delta)}
317
+##
318
+##                         % if h.is_item_still_editable(item) and item.owner.id==user.id:
319
+##                             <br/>
320
+## ##                            <div class="btn-group">
321
+##                                 <a class="t-timeline-comment-delete-button" href="${item.urls.delete}">
322
+##                                     ${_('delete')} ${ICON.FA('fa fa-trash-o')}
323
+## ##                                    ${TIM.ICO_TOOLTIP(16, 'status/user-trash-full', h.delete_label_for_item(item))}
324
+##                                 </a>
325
+## ##                            </div>
326
+##                         % endif
327
+##                     </div>
328
+##                 </h5>
329
+##                 <div class="t-timeline-item-content">
330
+##                     <div>${item.content|n}</div>
331
+##                     <br/>
332
+##                 </div>
333
+##             </div>
334
+##         </div>
335
+##     </div>
336
+</%def>
337
+
338
+<%def name="SECURED_HISTORY_VIRTUAL_EVENT(user, event)">
306
     <div class="row t-odd-or-even t-hacky-thread-comment-border-top">
339
     <div class="row t-odd-or-even t-hacky-thread-comment-border-top">
307
         <div class="col-sm-7 col-sm-offset-3">
340
         <div class="col-sm-7 col-sm-offset-3">
308
             <div class="t-timeline-item">
341
             <div class="t-timeline-item">
309
 ##                <i class="fa fa-fw fa-3x fa-comment-o t-less-visible" style="margin-left: -1.5em; float:left;"></i>
342
 ##                <i class="fa fa-fw fa-3x fa-comment-o t-less-visible" style="margin-left: -1.5em; float:left;"></i>
310
-                ${ICON.FA_FW('fa fa-3x fa-comment-o t-less-visible t-timeline-item-icon')}
343
+
344
+                ${ICON.FA_FW('fa fa-3x t-less-visible t-timeline-item-icon '+event.type.icon)}
311
 
345
 
312
                 <h5 style="margin: 0;">
346
                 <h5 style="margin: 0;">
313
-                    <span class="tracim-less-visible">${_('<strong>{}</strong> wrote:').format(item.owner.name)|n}</span>
314
 
347
 
315
-                    <div class="pull-right text-right t-timeline-item-moment" title="${h.date_time(item.created)|n}">
316
-                        ${_('{delta} ago').format(delta=item.created_as_delta)}
348
+                    % if 'comment' == event.type.id:
349
+                        <span class="tracim-less-visible">${_('<strong>{}</strong> wrote:').format(event.owner.name)|n}</span>
350
+                    %else:
351
+                        <span class="tracim-less-visible">${_('{} by <strong>{}</strong>').format(event.label, event.owner.name)|n}</span>
352
+                    % endif
353
+
354
+                    <div class="pull-right text-right t-timeline-item-moment" title="${h.date_time(event.created)|n}">
355
+                        ${_('{delta} ago').format(delta=event.created_as_delta)}
317
 
356
 
318
-                        % if h.is_item_still_editable(item) and item.owner.id==user.id:
357
+                        % if h.is_item_still_editable(event) and event.owner.id==user.id:
319
                             <br/>
358
                             <br/>
320
-##                            <div class="btn-group">
321
-                                <a class="t-timeline-comment-delete-button" href="${item.urls.delete}">
359
+                                <a class="t-timeline-comment-delete-button" href="${event.urls.delete}">
322
                                     ${_('delete')} ${ICON.FA('fa fa-trash-o')}
360
                                     ${_('delete')} ${ICON.FA('fa fa-trash-o')}
323
-##                                    ${TIM.ICO_TOOLTIP(16, 'status/user-trash-full', h.delete_label_for_item(item))}
324
                                 </a>
361
                                 </a>
325
-##                            </div>
326
                         % endif
362
                         % endif
327
                     </div>
363
                     </div>
328
                 </h5>
364
                 </h5>
329
                 <div class="t-timeline-item-content">
365
                 <div class="t-timeline-item-content">
330
-                    <div>${item.content|n}</div>
366
+                    <div>${event.content|n}</div>
331
                     <br/>
367
                     <br/>
332
                 </div>
368
                 </div>
333
             </div>
369
             </div>
334
         </div>
370
         </div>
335
     </div>
371
     </div>
336
 </%def>
372
 </%def>
373
+
374
+<%def name="SECURED_HISTORY_VIRTUAL_EVENT_AS_TABLE_ROW(user, event, current_revision_id)">
375
+    <% warning_or_not = ('', 'warning')[current_revision_id==event.id] %>
376
+    <tr class="${warning_or_not}">
377
+        <td class="t-less-visible">
378
+            <span class="label label-default">${ICON.FA_FW(event.type.icon)} ${event.type.label}</span>
379
+        </td>
380
+        <td title="${h.date_time(event.created)|n}">${_('{delta} ago').format(delta=event.created_as_delta)}</td>
381
+        <td>${event.owner.name}</td>
382
+## FIXME - REMOVE                            <td>${event}</td>
383
+
384
+        % if 'comment' == event.type.id:
385
+            <td colspan="2">
386
+                ${event.content|n}
387
+            </td>
388
+        % else:
389
+
390
+            <td>
391
+                % if event.type.id in ('creation', 'edition', 'revision'):
392
+                    <a href="${'?revision_id={}'.format(event.id)}">${_('View revision')}</a>
393
+                % endif
394
+            </td>
395
+            <td class="t-less-visible" title="${_('Currently shown')}">
396
+                % if warning_or_not:
397
+                    ${ICON.FA_FW('fa fa-caret-left')}&nbsp;${_('shown')}
398
+                % endif
399
+            </td>
400
+        % endif
401
+    </tr>
402
+</%def>

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

1
 <%namespace name="ICON" file="tracim.templates.widgets.icon"/>
1
 <%namespace name="ICON" file="tracim.templates.widgets.icon"/>
2
 <%namespace name="BUTTON" file="tracim.templates.widgets.button"/>
2
 <%namespace name="BUTTON" file="tracim.templates.widgets.button"/>
3
 
3
 
4
-<%def name="USER_PROFILE(user)">
4
+<%def name="USER_PROFILE(current_user, user)">
5
     % if user.profile.id >= 1:
5
     % if user.profile.id >= 1:
6
         <p>${ICON.FA('fa-male t-green fa-lg fa-fw')}<span> ${_('This user a standard user.')}</span></p>
6
         <p>${ICON.FA('fa-male t-green fa-lg fa-fw')}<span> ${_('This user a standard user.')}</span></p>
7
     %else:
7
     %else:

+ 30 - 4
tracim/tracim/templates/widgets/table_row.mak 查看文件

2
 <%namespace name="ICON" file="tracim.templates.widgets.icon"/>
2
 <%namespace name="ICON" file="tracim.templates.widgets.icon"/>
3
 <%namespace name="SPAN" file="tracim.templates.widgets.span"/>
3
 <%namespace name="SPAN" file="tracim.templates.widgets.span"/>
4
 
4
 
5
-<%def name="USER_ROLE_IN_WORKSPACE(role, show_id=True, enable_link=None, disable_link=None)">
5
+<%def name="USER_ROLE_IN_WORKSPACE(current_user, role, show_id=True, enable_link=None, disable_link=None, role_types=None)">
6
     <tr>
6
     <tr>
7
         % if show_id:
7
         % if show_id:
8
             <td class="text-right">${role.workspace.id}</td>
8
             <td class="text-right">${role.workspace.id}</td>
9
         % endif
9
         % endif
10
         <td><a href="${tg.url('/admin/workspaces/{}').format(role.workspace.id)}">${role.workspace.name}</a></td>
10
         <td><a href="${tg.url('/admin/workspaces/{}').format(role.workspace.id)}">${role.workspace.name}</a></td>
11
-        <td><span style="${role.style}"><i class="fa ${role.icon}"></i> ${role.label}</span></td>
12
-        % if enable_link or disable_link:
11
+
12
+        % if role_types:
13
+            ## <td>${BUTTON.SECURED_ROLE_SELECTOR(fake_api.current_user, result.workspace, member, fake_api.role_types)}</td>
14
+            <td><span style="${role.style}"><i class="fa ${role.icon}"></i> ${role.label}</span></td>
15
+        % else:
16
+            <td><span style="${role.style}"><i class="fa ${role.icon}"></i> ${role.label}</span></td>
17
+        % endif
18
+
19
+        <%
20
+            user_is_himself = current_user.id == role.user.id
21
+            user_is_manager = h.user_role(current_user, role.workspace) >= 8
22
+            ## allow user to change notification status only if current user is manager on the given workspace
23
+        %>
24
+
25
+        % if (enable_link or disable_link) and (user_is_himself or user_is_manager) :
13
             <td>${SPAN.NOTIFICATION_SUBSCRIBED(role.user, role.workspace, role.notifications_subscribed, enable_link, disable_link)}</td>
26
             <td>${SPAN.NOTIFICATION_SUBSCRIBED(role.user, role.workspace, role.notifications_subscribed, enable_link, disable_link)}</td>
14
         % else:
27
         % else:
15
             <td>${SPAN.NOTIFICATION_SUBSCRIBED(role.user, role.workspace, role.notifications_subscribed)}</td>
28
             <td>${SPAN.NOTIFICATION_SUBSCRIBED(role.user, role.workspace, role.notifications_subscribed)}</td>
23
         <td class="text-right">${member.id}</td>
36
         <td class="text-right">${member.id}</td>
24
         <td ><a href="${tg.url('/admin/users/{}'.format(member.id))}">${member.name}</a></td>
37
         <td ><a href="${tg.url('/admin/users/{}'.format(member.id))}">${member.name}</a></td>
25
         <td>${BUTTON.SECURED_ROLE_SELECTOR(fake_api.current_user, result.workspace, member, fake_api.role_types)}</td>
38
         <td>${BUTTON.SECURED_ROLE_SELECTOR(fake_api.current_user, result.workspace, member, fake_api.role_types)}</td>
26
-        <td>${SPAN.NOTIFICATION_SUBSCRIBED(member, workspace, member.notifications_subscribed)}</td>
39
+        <%
40
+            user_is_himself = current_user.id == member.id
41
+            user_is_manager = h.user_role(current_user, workspace) >= 8
42
+            ## allow user to change notification status only if current user is manager on the given workspace
43
+
44
+            enable_link = '/admin/users/{user}/workspaces/{workspace}/enable_notifications?next_url=/admin/workspaces/{workspace}'
45
+            disable_link = '/admin/users/{user}/workspaces/{workspace}/disable_notifications?next_url=/admin/workspaces/{workspace}'
46
+        %>
47
+        % if (enable_link or disable_link) and (user_is_himself or user_is_manager) :
48
+            <td>${SPAN.NOTIFICATION_SUBSCRIBED(member, workspace, member.notifications_subscribed, enable_link, disable_link)}</td>
49
+        % else:
50
+            <td>${SPAN.NOTIFICATION_SUBSCRIBED(member, workspace, member.notifications_subscribed)}</td>
51
+        % endif
52
+
27
         <td><a title="${_('Remove this user from the current workspace')}" class="t-less-visible t-red-on-hover t-red btn btn-default btn-xs" href="${tg.url('/admin/workspaces/{}/roles/{}/delete'.format(result.workspace.id, member.id))}">${ICON.FA('fa-remove fa-fw')}</a></td>
53
         <td><a title="${_('Remove this user from the current workspace')}" class="t-less-visible t-red-on-hover t-red btn btn-default btn-xs" href="${tg.url('/admin/workspaces/{}/roles/{}/delete'.format(result.workspace.id, member.id))}">${ICON.FA('fa-remove fa-fw')}</a></td>
28
     </tr>
54
     </tr>
29
 </%def>
55
 </%def>

+ 4 - 0
tracim/tracim/tests/library/test_content_api.py 查看文件

342
 
342
 
343
         RoleApi(user1).create_one(user2, workspace,
343
         RoleApi(user1).create_one(user2, workspace,
344
                                   UserRoleInWorkspace.CONTENT_MANAGER,
344
                                   UserRoleInWorkspace.CONTENT_MANAGER,
345
+                                  with_notif=False,
345
                                   flush=True)
346
                                   flush=True)
346
 
347
 
347
         # Test starts here
348
         # Test starts here
405
 
406
 
406
         RoleApi(user1).create_one(user2, workspace,
407
         RoleApi(user1).create_one(user2, workspace,
407
                                   UserRoleInWorkspace.CONTENT_MANAGER,
408
                                   UserRoleInWorkspace.CONTENT_MANAGER,
409
+                                  with_notif=True,
408
                                   flush=True)
410
                                   flush=True)
409
 
411
 
410
         # Test starts here
412
         # Test starts here
471
 
473
 
472
         RoleApi(user1).create_one(user2, workspace,
474
         RoleApi(user1).create_one(user2, workspace,
473
                                   UserRoleInWorkspace.CONTENT_MANAGER,
475
                                   UserRoleInWorkspace.CONTENT_MANAGER,
476
+                                  with_notif=True,
474
                                   flush=True)
477
                                   flush=True)
475
 
478
 
476
         # show archived is used at the top end of the test
479
         # show archived is used at the top end of the test
545
 
548
 
546
         RoleApi(user1).create_one(user2, workspace,
549
         RoleApi(user1).create_one(user2, workspace,
547
                                   UserRoleInWorkspace.CONTENT_MANAGER,
550
                                   UserRoleInWorkspace.CONTENT_MANAGER,
551
+                                  with_notif=True,
548
                                   flush=True)
552
                                   flush=True)
549
 
553
 
550
         # show archived is used at the top end of the test
554
         # show archived is used at the top end of the test

+ 6 - 0
tracim/tracim/tests/library/test_helpers.py 查看文件

25
 
25
 
26
         h.CFG.DATA_UPDATE_ALLOWED_DURATION = 0
26
         h.CFG.DATA_UPDATE_ALLOWED_DURATION = 0
27
         item.created = datetime.datetime.now() - datetime.timedelta(0, 10)
27
         item.created = datetime.datetime.now() - datetime.timedelta(0, 10)
28
+
29
+        item.type = DictLikeClass({'id': 5})
30
+        eq_(False, h.is_item_still_editable(item))
31
+
32
+        item.type.id = 'comment'
28
         eq_(False, h.is_item_still_editable(item))
33
         eq_(False, h.is_item_still_editable(item))
29
 
34
 
30
         h.CFG.DATA_UPDATE_ALLOWED_DURATION = -1
35
         h.CFG.DATA_UPDATE_ALLOWED_DURATION = -1
36
+        item.type.id = 'comment'
31
         item.created = datetime.datetime.now() - datetime.timedelta(0, 10)
37
         item.created = datetime.datetime.now() - datetime.timedelta(0, 10)
32
         eq_(True, h.is_item_still_editable(item))
38
         eq_(True, h.is_item_still_editable(item))
33
 
39