Browse Source

add support for dynamic view of groups and rights

Damien Accorsi 11 years ago
parent
commit
a0bf661568

+ 14 - 0
pboard/pboard/controllers/api.py View File

49
         loNewAccount.email_address = email
49
         loNewAccount.email_address = email
50
         loNewAccount.display_name  = email
50
         loNewAccount.display_name  = email
51
         loNewAccount.password      = password
51
         loNewAccount.password      = password
52
+
52
         loUserGroup = pld.PODStaticController.getGroup('user')
53
         loUserGroup = pld.PODStaticController.getGroup('user')
53
         loUserGroup.users.append(loNewAccount)
54
         loUserGroup.users.append(loNewAccount)
55
+
56
+        pm.DBSession.add(loNewAccount)
54
         pm.DBSession.flush()
57
         pm.DBSession.flush()
58
+        pm.DBSession.refresh(loNewAccount)
59
+
60
+        loUserSpecificGroup = pld.PODStaticController.createGroup()
61
+
62
+        loUserSpecificGroup.group_id = 0-loNewAccount.user_id # group id of a given user is the opposite of the user id
63
+        loUserSpecificGroup.group_name = ''
64
+        loUserSpecificGroup.personnal_group = True
65
+        loUserSpecificGroup.users.append(loNewAccount)
66
+
67
+        pm.DBSession.flush()
68
+
55
         flash(_('Account successfully created: %s') % (email), 'info')
69
         flash(_('Account successfully created: %s') % (email), 'info')
56
         redirect(lurl('/'))
70
         redirect(lurl('/'))
57
 
71
 

+ 3 - 1
pboard/pboard/controllers/root.py View File

147
             root_node_list=loRootNodeList,
147
             root_node_list=loRootNodeList,
148
             current_node=loCurrentNode,
148
             current_node=loCurrentNode,
149
             node_status_list = loNodeStatusList,
149
             node_status_list = loNodeStatusList,
150
-            keywords = highlight
150
+            keywords = highlight,
151
+            user_specific_groups = pld.PODStaticController.getUserSpecificGroups(),
152
+            real_groups = pld.PODStaticController.getRealGroups()
151
         )
153
         )
152
 
154
 
153
     @expose('pboard.templates.search')
155
     @expose('pboard.templates.search')

+ 13 - 11
pboard/pboard/lib/auth.py View File

11
         pass
11
         pass
12
 
12
 
13
     def evaluate(self, environ, credentials):
13
     def evaluate(self, environ, credentials):
14
-        node_id = environ['webob.adhoc_attrs']['validation']['values']['node']
15
-        has_right = session.execute("""
16
-                select *
17
-                from pod_group_node pgn
18
-                join pod_user_group pug on pug.group_id = pgn.group_id
19
-                join pod_user pu on pug.user_id = pu.user_id
20
-                where rights > 0
21
-                and email_address = :mail
22
-                and node_id = :node""", {"mail":credentials["repoze.who.userid"], "node":node_id})
23
-        if has_right.rowcount == 0 :
24
-            self.unmet()
14
+        if 'node' in environ['webob.adhoc_attrs']['validation']['values']:
15
+            node_id = environ['webob.adhoc_attrs']['validation']['values']['node']
16
+            if node_id!=0:
17
+                has_right = session.execute("""
18
+                        select *
19
+                        from pod_group_node pgn
20
+                        join pod_user_group pug on pug.group_id = pgn.group_id
21
+                        join pod_user pu on pug.user_id = pu.user_id
22
+                        where rights > 0
23
+                        and email_address = :mail
24
+                        and node_id = :node""", {"mail":credentials["repoze.who.userid"], "node":node_id})
25
+                if has_right.rowcount == 0 :
26
+                    self.unmet()

+ 17 - 0
pboard/pboard/lib/dbapi.py View File

41
     loGroup = pbma.Group.by_group_name(psGroupName)
41
     loGroup = pbma.Group.by_group_name(psGroupName)
42
     return loGroup
42
     return loGroup
43
 
43
 
44
+  @classmethod
45
+  def createGroup(cls):
46
+    loGroup = pbma.Group()
47
+    return loGroup
48
+
49
+  @classmethod
50
+  def getGroups(cls):
51
+    loGroups = pbma.Group.real_groups_first()
52
+    return loGroups
53
+
54
+  @classmethod
55
+  def getUserSpecificGroups(cls):
56
+    return DBSession.query(pbma.Group).filter(pbma.Group.personnal_group==True).all()
57
+
58
+  @classmethod
59
+  def getRealGroups(cls):
60
+    return DBSession.query(pbma.Group).filter(pbma.Group.personnal_group==False).all()
44
 
61
 
45
 class PODUserFilteredApiController(object):
62
 class PODUserFilteredApiController(object):
46
   
63
   

+ 33 - 13
pboard/pboard/model/auth.py View File

11
 import os
11
 import os
12
 from datetime import datetime
12
 from datetime import datetime
13
 from hashlib import sha256
13
 from hashlib import sha256
14
+
14
 __all__ = ['User', 'Group', 'Permission']
15
 __all__ = ['User', 'Group', 'Permission']
15
 
16
 
16
 from sqlalchemy import Table, ForeignKey, Column
17
 from sqlalchemy import Table, ForeignKey, Column
17
 from sqlalchemy.types import Unicode, Integer, DateTime, Boolean
18
 from sqlalchemy.types import Unicode, Integer, DateTime, Boolean
18
-from sqlalchemy.orm import relation, synonym
19
+from sqlalchemy.orm import relation, relationship, synonym
19
 
20
 
20
 from pboard.model import DeclarativeBase, metadata, DBSession
21
 from pboard.model import DeclarativeBase, metadata, DBSession
21
 
22
 
38
 )
39
 )
39
 
40
 
40
 
41
 
41
-class Group(DeclarativeBase):
42
-    """
43
-    Group definition
44
 
42
 
45
-    Only the ``group_name`` column is required.
46
 
43
 
47
-    """
44
+class Rights(DeclarativeBase):
45
+    READ_ACCESS = 1
46
+    WRITE_ACCESS = 2
47
+
48
+    __tablename__ = 'pod_group_node'
49
+
50
+    group_id = Column(Integer, ForeignKey('pod_group.group_id'), autoincrement=True, primary_key=True)
51
+    node_id = Column(Integer, ForeignKey('pod_nodes.node_id'), autoincrement=True, primary_key=True)
52
+    rights = Column(Integer)
53
+
54
+    def hasReadAccess(self):
55
+        return self.rights & Rights.READ_ACCESS
56
+
57
+    def hasWriteAccess(self):
58
+        return self.rights & Rights.WRITE_ACCESS
59
+
60
+class Group(DeclarativeBase):
48
 
61
 
49
     __tablename__ = 'pod_group'
62
     __tablename__ = 'pod_group'
50
 
63
 
55
     personnal_group = Column(Boolean)
68
     personnal_group = Column(Boolean)
56
     users = relation('User', secondary=user_group_table, backref='groups')
69
     users = relation('User', secondary=user_group_table, backref='groups')
57
 
70
 
71
+    users = relation('User', secondary=user_group_table, backref='groups')
72
+    _lRights = relationship('Rights', remote_side=[Rights.group_id], backref='_oGroup')
73
+
58
     def __repr__(self):
74
     def __repr__(self):
59
         return '<Group: name=%s>' % repr(self.group_name)
75
         return '<Group: name=%s>' % repr(self.group_name)
60
 
76
 
66
         """Return the user object whose email address is ``email``."""
82
         """Return the user object whose email address is ``email``."""
67
         return DBSession.query(cls).filter_by(group_name=group_name).first()
83
         return DBSession.query(cls).filter_by(group_name=group_name).first()
68
 
84
 
85
+    def getDisplayName(self) -> str:
86
+        if self.group_id<0:
87
+            return self.users[0].display_name
88
+
89
+        return self.display_name
90
+
91
+    @property
92
+    def rights(self):
93
+        return self._lRights
94
+
95
+
69
 class User(DeclarativeBase):
96
 class User(DeclarativeBase):
70
     """
97
     """
71
     User definition.
98
     User definition.
154
         hash.update((password + self.password[:64]).encode('utf-8'))
181
         hash.update((password + self.password[:64]).encode('utf-8'))
155
         return self.password[64:] == hash.hexdigest()
182
         return self.password[64:] == hash.hexdigest()
156
 
183
 
157
-class Rights(DeclarativeBase):
158
-    __tablename__ = 'pod_group_node'
159
-
160
-    group_id = Column(Integer, ForeignKey('pod_group.group_id'), autoincrement=True, primary_key=True)
161
-    node_id = Column(Integer, ForeignKey('pod_nodes.node_id'), autoincrement=True, primary_key=True)
162
-    rights = Column(Integer)
163
 
184
 
164
-    groups = relation('PBNode')
165
 
185
 
166
 class Permission(DeclarativeBase):
186
 class Permission(DeclarativeBase):
167
     """
187
     """

+ 1 - 34
pboard/pboard/model/data.py View File

21
 from pboard.model import DeclarativeBase, metadata, DBSession
21
 from pboard.model import DeclarativeBase, metadata, DBSession
22
 from pboard.model import auth as pma
22
 from pboard.model import auth as pma
23
 
23
 
24
-# This is the association table for the many-to-many relationship between
25
-# groups and permissions.
26
-"""pod_node_table = Table('pod_nodes', metadata,
27
-    Column('node_id', Integer, Sequence('pod_nodes__node_id__sequence'), primary_key=True),
28
-    Column('parent_id', Integer, ForeignKey('pod_nodes.node_id'), nullable=True, default=None),
29
-    Column('node_order', Integer, nullable=True, default=1),
30
-    Column('node_type',   Unicode(16), unique=False, nullable=False, default='data'),
31
-    Column('node_status', Unicode(16), unique=False, nullable=False, default='new'),
32
-
33
-    Column('created_at', DateTime, unique=False, nullable=False),
34
-    Column('updated_at', DateTime, unique=False, nullable=False),
35
-
36
-    Column('data_label',   Unicode(1024), unique=False, nullable=False, default=''),
37
-    Column('data_content', Text(), unique=False, nullable=False, default=''),
38
-    Column('data_datetime', DateTime, unique=False, nullable=False),
39
-    Column('data_reminder_datetime', DateTime, unique=False, nullable=True),
40
-    
41
-    Column('data_file_name', Unicode(255), unique=False, nullable=False, default=''),
42
-    Column('data_file_mime_type', Unicode(255), unique=False, nullable=False, default=''),
43
-    Column('data_file_content', LargeBinary(), unique=False, nullable=False, default=None),
44
-)
45
-"""
46
-"""
47
-- node_type
48
-
49
-- node_created_at
50
-- node_updated_at
51
-
52
-- data_label
53
-- data_content
54
-- data_source_url
55
-- data_status_id
56
-"""
57
-
58
 
24
 
59
 class PBNodeStatusItem(object):
25
 class PBNodeStatusItem(object):
60
   def __init__(self, psStatusId, psStatusLabel, psStatusFamily, psIconId, psCssClass): #, psBackgroundColor):
26
   def __init__(self, psStatusId, psStatusLabel, psStatusFamily, psIconId, psCssClass): #, psBackgroundColor):
228
   _oParent = relationship('PBNode', remote_side=[node_id], backref='_lAllChildren')
194
   _oParent = relationship('PBNode', remote_side=[node_id], backref='_lAllChildren')
229
   _oOwner = relationship('User', remote_side=[pma.User.user_id], backref='_lAllNodes')
195
   _oOwner = relationship('User', remote_side=[pma.User.user_id], backref='_lAllNodes')
230
 
196
 
197
+
231
   def getChildrenOfType(self, plNodeTypeList, poKeySortingMethod=None, pbDoReverseSorting=False):
198
   def getChildrenOfType(self, plNodeTypeList, poKeySortingMethod=None, pbDoReverseSorting=False):
232
     """return all children nodes of type 'data' or 'node' or 'folder'"""
199
     """return all children nodes of type 'data' or 'node' or 'folder'"""
233
     llChildren = []
200
     llChildren = []

+ 42 - 27
pboard/pboard/templates/document-widgets-tabs.mak View File

45
           <th></th>
45
           <th></th>
46
         </tr>
46
         </tr>
47
       </thead>
47
       </thead>
48
-      <tr>
49
-        <td>Recherche et Développement</td>
50
-        <td>
51
-          <span class="label label-success" title="${_('Read access')}">R</span>
52
-          <span class="label label-warning" title="${_('Write access')}">W</span>
53
-        </td>
54
-      </tr>
48
+      % for loCurrentGroup in real_groups:
49
+        <tr>
50
+          <td>${loCurrentGroup.getDisplayName()}</td>
51
+          <td>
52
+            % for loRight in loCurrentGroup.rights:
53
+              % if loRight.node_id==poNode.node_id:
54
+                % if loRight.hasReadAccess():
55
+                  <span class="label label-success">R</span>
56
+                % endif
57
+                % if loRight.hasWriteAccess():
58
+                  <span class="label label-warning">W</span>
59
+                % endif
60
+              % endif
61
+            % endfor
62
+          </td>
63
+        </tr>
64
+      % endfor
55
       <thead>
65
       <thead>
56
         <tr>
66
         <tr>
57
           <th><i class="fa fa-user"></i> ${_('Users')}</th>
67
           <th><i class="fa fa-user"></i> ${_('Users')}</th>
58
           <th></th>
68
           <th></th>
59
         </tr>
69
         </tr>
60
       </thead>
70
       </thead>
61
-      <tr>
62
-        <td>Damien Accorsi</td>
63
-        <td>
64
-          <span class="label label-success">R</span>
65
-        </td>
66
-      </tr>
67
-      <tr>
68
-        <td>Sylvain Ferot</td>
69
-        <td>
70
-          <span class="label label-success">R</span>
71
-          <span class="label label-warning">W</span>
72
-        </td>
73
-      </tr>
71
+      % for loCurrentGroup in user_specific_groups:
72
+        <tr>
73
+          <td>${loCurrentGroup.getDisplayName()}</td>
74
+          <td>
75
+            % for loRight in loCurrentGroup.rights:
76
+              % if loRight.node_id==poNode.node_id:
77
+                % if loRight.hasReadAccess():
78
+                  <span class="label label-success">R</span>
79
+                % endif
80
+                % if loRight.hasWriteAccess():
81
+                  <span class="label label-warning">W</span>
82
+                % endif
83
+              % endif
84
+            % endfor
85
+          </td>
86
+        </tr>
87
+      % endfor
74
     </table>
88
     </table>
75
     
89
     
76
     % endif
90
     % endif
160
     ##
174
     ##
161
     ## FIXME - SET A DYNAMIC SELECT LIST HERE
175
     ## FIXME - SET A DYNAMIC SELECT LIST HERE
162
     ##
176
     ##
163
-              % for loCurrentUser in ((3, 'Research and Development'), (4, 'Sylvain Ferot'), (5, 'Damien Accorsi')):
164
-              <tr id='user-${loCurrentUser[0]}-rights-row'>
177
+              % for loCurrentGroup in user_specific_groups:
178
+              <tr id='user-${loCurrentGroup.group_id}-rights-row'>
165
                 <td>
179
                 <td>
166
                   <a
180
                   <a
167
                     class="btn btn-mini"
181
                     class="btn btn-mini"
168
-                    onclick="updateRights(${loCurrentUser[0]})"
182
+                    onclick="updateRights(${loCurrentGroup.group_id})"
169
                   >
183
                   >
170
                     <i class="fa fa-key"></i>
184
                     <i class="fa fa-key"></i>
171
                   </a>
185
                   </a>
172
                 </td>
186
                 </td>
173
                 <td class='pod-highlightable-access-management-cell'>
187
                 <td class='pod-highlightable-access-management-cell'>
174
-                  ${loCurrentUser[1]}
188
+                  ${loCurrentGroup.getDisplayName()}
175
                   <input
189
                   <input
176
                     type="hidden"
190
                     type="hidden"
177
-                    id="user-${loCurrentUser[0]}-value"
178
-                    name="user[${loCurrentUser[0]}]"
191
+                    id="user-${loCurrentGroup.group_id}-value"
192
+                    name="user[${loCurrentGroup.group_id}]"
179
                     value=""
193
                     value=""
180
                   />
194
                   />
181
                 </td>
195
                 </td>
182
-                <td id="user-${loCurrentUser[0]}-rights" class="pod-right-cell"></td>
196
+                <td id="user-${loCurrentGroup.group_id}-rights" class="pod-right-cell"></td>
183
               </tr>
197
               </tr>
184
               % endfor
198
               % endfor
185
             </table>
199
             </table>
325
           >
339
           >
326
             ${loFile.getTruncatedLabel(50)}
340
             ${loFile.getTruncatedLabel(50)}
327
           </a>
341
           </a>
342
+          ## FIXME SHOW IMAGE THUMBNAIL <img src="${tg.url('/api/get_file_content_thumbnail/%i'%loFile.node_id)}"/>
328
           <a
343
           <a
329
             class="pull-right"
344
             class="pull-right"
330
             href="${tg.url('/api/get_file_content/%s'%(loFile.node_id))}"
345
             href="${tg.url('/api/get_file_content/%s'%(loFile.node_id))}"

+ 514 - 0
pboard/pboard/templates/document-widgets-tabs.mak~ View File

1
+<%inherit file="local:templates.master"/>
2
+<%namespace name="POD" file="pboard.templates.pod"/>
3
+<%namespace name="DOC" file="pboard.templates.document-widgets"/>
4
+
5
+<%def name="HistoryTabContent(poNode)">
6
+  <h4>History</h4>
7
+  <ul>
8
+  % for version in poNode.getHistory():
9
+  	<li><a href="${tg.url('/document/%i/%i'%(version.node_id, version.version_id))}">${version.created_at.strftime("%a %x %X")}</a></li>
10
+  % endfor
11
+  </ul>
12
+</%def>
13
+
14
+<%def name="AccessManagementTab(poNode)">
15
+  ######
16
+  ##
17
+  ## THIS WIDGET IS INTENDED TO BE USED ONE TIME ONLY IN A PAGE
18
+  ##
19
+  <h4>${_('Share options')}</h4> 
20
+  <p>
21
+    This document is
22
+    % if poNode.is_shared==False:
23
+      <span class="label label-info">
24
+        <i class="fa fa-user"></i>
25
+        ${_('private')}
26
+      </span>
27
+    % else:
28
+      <span class="label label-info">
29
+        <i class="fa fa-group"></i>
30
+        ${_('collaborative')}
31
+      </span>
32
+    % endif
33
+  </p>
34
+  <p>
35
+    % if poNode.is_shared==True or poNode.is_shared==False:
36
+      ${_('People working on it are:')}
37
+######
38
+##
39
+## FIXME - SHOW LIST OF GROUPS ALLOWED TO WORK ON THE DOCUMENT
40
+##
41
+    <table class="table table-striped table-hover table-condensed">
42
+      <thead>
43
+        <tr>
44
+          <th><i class="fa fa-group"></i> ${_('Groups')}</th>
45
+          <th></th>
46
+        </tr>
47
+      </thead>
48
+      % for loCurrentGroup in real_groups:
49
+        <tr>
50
+          <td>${loCurrentGroup.getDisplayName()}</td>
51
+          <td>
52
+            % for loRight in loCurrentGroup.rights:
53
+              % if loRight.node_id==poNode.node_id:
54
+                % if loRight.hasReadAccess():
55
+                  <span class="label label-success">R</span>
56
+                % endif
57
+                % if loRight.hasWriteAccess():
58
+                  <span class="label label-warning">W</span>
59
+                % endif
60
+              % endif
61
+            % endfor
62
+          </td>
63
+        </tr>
64
+      % endfor
65
+      <tr>
66
+        <td>Recherche et Développement</td>
67
+        <td>
68
+          <span class="label label-success" title="${_('Read access')}">R</span>
69
+          <span class="label label-warning" title="${_('Write access')}">W</span>
70
+        </td>
71
+      </tr>
72
+      <thead>
73
+        <tr>
74
+          <th><i class="fa fa-user"></i> ${_('Users')}</th>
75
+          <th></th>
76
+        </tr>
77
+      </thead>
78
+      % for loCurrentGroup in user_specific_groups:
79
+        <tr>
80
+          <td>${loCurrentGroup.getDisplayName()}</td>
81
+          <td>
82
+            % for loRight in loCurrentGroup.rights:
83
+              % if loRight.node_id==poNode.node_id:
84
+                % if loRight.hasReadAccess():
85
+                  <span class="label label-success">R</span>
86
+                % endif
87
+                % if loRight.hasWriteAccess():
88
+                  <span class="label label-warning">W</span>
89
+                % endif
90
+              % endif
91
+            % endfor
92
+          </td>
93
+        </tr>
94
+      % endfor
95
+    </table>
96
+    
97
+    % endif
98
+  <p>
99
+
100
+######
101
+##
102
+## 2014-05-06 - D.A. We do not share documents on internet yet.
103
+##
104
+##  <p>
105
+##    % if poNode.is_public==False:
106
+##      ${_('This document is not shared on internet')|n}
107
+##    % else:
108
+##      ${_('This document is <span class="label label-warning"><i class="fa fa-globe"></i><span>shared</span></span> on internet')|n}.
109
+##      ${_('The associated url is:')} <a href="FIXME">${poNode.public_url_key}</a>
110
+##    % endif
111
+##  </p>
112
+  <!-- Button to trigger modal -->
113
+  <a href="#edit-document-share-properties" role="button" class="btn btn-success" data-toggle="modal">
114
+    <i class="fa fa-edit"></i>
115
+    ${_('Edit share options')}
116
+  </a>
117
+     
118
+  <!-- Modal -->
119
+  <div
120
+    id="edit-document-share-properties"
121
+    class="modal hide"
122
+    tabindex="-1"
123
+    role="dialog"
124
+    aria-labelledby="myModalLabel"
125
+    aria-hidden="true">
126
+    
127
+    <div class="modal-header">
128
+      <button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button>
129
+      <h3 id="myModalLabel">Document sharing options</h3>
130
+    </div>
131
+    <div class="modal-body">
132
+
133
+      <form id='document-share-form' method="GET" action="${tg.url('/api/set_access_management?node_id=%d'%poNode.node_id)}">
134
+        <fieldset>
135
+          <label class="checkbox">
136
+            <input name="is_shared" type="checkbox" id="document-share-selector" ${('', 'checked')[poNode.is_shared]}/>
137
+            ${_('Share document with collaborators.')} <i class="fa fa-group"></i>
138
+          </label>
139
+          <div id="document-share-people-selector">
140
+            <p>
141
+              ${_('Select read and write access for each group or people...')}</p>
142
+            <script>
143
+            function updateRights(psUserId) {
144
+              var ACCESS_NONE = '';
145
+              var ACCESS_READ = 'R';
146
+              var ACCESS_WRITE = 'RW';
147
+              
148
+              var nodeIdForSelectedUser = 'user-'+psUserId+'-value';
149
+              var widget = $('#'+nodeIdForSelectedUser);
150
+              var oldValue = widget.val();
151
+              var newValue = '';
152
+              if(oldValue==ACCESS_NONE) {
153
+                newValue = ACCESS_READ;
154
+                newHtml = '<span class="label label-success">R</span>';
155
+              } else if(oldValue==ACCESS_READ) {
156
+                newValue = ACCESS_WRITE;
157
+                newHtml = '<span class="label label-success">R</span> <span class="label label-warning">W</span>';
158
+              } else if (oldValue==ACCESS_WRITE) {
159
+                newValue = ACCESS_NONE;
160
+                newHtml = '';
161
+              } else {
162
+                newValue = ACCESS_READ;
163
+                newHtml = '<span class="label label-success">R</span>';
164
+              }
165
+              
166
+              widget.val(newValue);
167
+              visibleid = 'user-'+psUserId+'-rights';
168
+              $("#"+visibleid).html(newHtml);
169
+            }
170
+            </script>
171
+            
172
+            <table class="table table-striped table-hover table-condensed">
173
+              <thead>
174
+                <tr>
175
+                  <th></th>
176
+                  <th>${_('Group')}</th>
177
+                  <th>${_('Access')}</th>
178
+                </tr>
179
+              </thead>
180
+    ######
181
+    ##
182
+    ## FIXME - SET A DYNAMIC SELECT LIST HERE
183
+    ##
184
+              % for loCurrentGroup in user_specific_groups:
185
+              <tr id='user-${loCurrentGroup.group_id}-rights-row'>
186
+                <td>
187
+                  <a
188
+                    class="btn btn-mini"
189
+                    onclick="updateRights(${loCurrentGroup.group_id})"
190
+                  >
191
+                    <i class="fa fa-key"></i>
192
+                  </a>
193
+                </td>
194
+                <td class='pod-highlightable-access-management-cell'>
195
+                  ${loCurrentGroup.getDisplayName()}
196
+                  <input
197
+                    type="hidden"
198
+                    id="user-${loCurrentGroup.group_id}-value"
199
+                    name="user[${loCurrentGroup.group_id}]"
200
+                    value=""
201
+                  />
202
+                </td>
203
+                <td id="user-${loCurrentGroup.group_id}-rights" class="pod-right-cell"></td>
204
+              </tr>
205
+              % endfor
206
+            </table>
207
+          </div>
208
+        </fieldset>
209
+######
210
+##
211
+## 2014-05-06 - D.A. The documents are not yet sharable through internet
212
+##
213
+##        <fieldset>
214
+##          <label class="checkbox">
215
+##            <input name="is_public" type="checkbox" id="document-public-selector" ${('', 'checked')[poNode.is_public]}/>
216
+##            ${_('Internet shared document')}
217
+##            <i class="fa fa-globe"></i>
218
+##          </label>
219
+##          <label id="document-public-key-selector">
220
+##            ${_('Key')}
221
+##            <div class="input-append">
222
+##              <input name="url_public_key" id="document-public-key" type="text">
223
+##              <span id="document-public-key-refresh-button" class="add-on btn" title="${_('Regenerate key')}">
224
+##                <i class="fa fa-refresh"></i>
225
+##              </span>
226
+##            </div>
227
+##            <p><a id='document-public-key-url' href="">http://share.pod.com/document/azefnzeioguneriugnreiugnre</a></p>
228
+##          </label>
229
+
230
+        </fieldset>
231
+####
232
+## Button replaced by modal dialog button
233
+##        <button type="submit" class="btn btn-success">
234
+##          <i class="fa fa-check"></i>
235
+##          ${_('Save')}
236
+##        </button>
237
+      </form>
238
+    </div>
239
+    <div class="modal-footer">
240
+    <button class="btn" data-dismiss="modal" aria-hidden="true">
241
+      <i class="fa fa-ban"></i> ${_('Cancel')}
242
+    </button>
243
+    <button class="btn btn-success" id="document-share-form-submit-button">
244
+      <i class="fa fa-check"></i> ${_('Save changes')}
245
+    </button>
246
+    </div>
247
+    <script>
248
+##
249
+## 2014-05-06 - D.A. - Documents are not yet sharable through internet
250
+##
251
+##        function refreshDocumentPublicKey(psNewPublicKey) {
252
+##          var lsNewUrl = 'http://share.pod.com/document/'+psNewPublicKey;
253
+##          $('#document-public-key').val(psNewPublicKey);
254
+##          $('#document-public-key-url').attr('href', lsNewUrl);
255
+##          $('#document-public-key-url').text(lsNewUrl);
256
+##        }
257
+      
258
+      function toggleDocumentSharePeopleSelector(pbShowIt) {
259
+        if (pbShowIt) {
260
+          $('#document-share-people-selector').show();
261
+          // $('#document-share-people-selector input').removeAttr("disabled");
262
+        } else {
263
+          $('#document-share-people-selector').hide();
264
+          // $('#document-share-people-selector input').prop('disabled', 'disabled');
265
+        }
266
+      }
267
+
268
+##
269
+## 2014-05-06 - D.A. - Documents are not yet sharable through internet
270
+##
271
+##        function toggleDocumentPublicKeyGenerator(pbShowIt) {
272
+##          if (pbShowIt) {
273
+##            $('#document-public-key-selector input').removeAttr("disabled");
274
+##            $('#document-public-key-refresh-button').removeProp('disabled');
275
+##            $('#document-public-key-refresh-button').removeClass('btn-disabled');
276
+##            $('#document-public-key-selector a').show();
277
+##            $('#document-public-key-refresh-button').on("click").click(function () {
278
+##              refreshDocumentPublicKey(generateStringId()); // New random 32-char id
279
+##            });
280
+##            if($('#document-public-key-selector input').val()=='') {
281
+##              refreshDocumentPublicKey(generateStringId());
282
+##            }
283
+##          } else {
284
+##            $('#document-public-key-refresh-button').prop('disabled', true);
285
+##            $('#document-public-key-refresh-button').addClass('btn-disabled');
286
+##            $('#document-public-key-selector input').prop('disabled', 'disabled');
287
+##            $('#document-public-key-refresh-button').off("click");
288
+##            $('#document-public-key-selector a').hide();
289
+##          }
290
+##        }
291
+##
292
+##
293
+##
294
+
295
+      // Callbacks setup
296
+      $('#document-share-selector').change(function () {
297
+        var checkedValue = $('#document-share-selector').prop("checked");
298
+        toggleDocumentSharePeopleSelector(checkedValue);
299
+      });
300
+
301
+##        $('#document-public-selector').change(function () {
302
+##          var checkedValue = $('#document-public-selector').prop("checked");
303
+##          toggleDocumentPublicKeyGenerator(checkedValue);
304
+##        });
305
+
306
+      // Submit access-management modal dialog form
307
+      $('#document-share-form-submit-button').click(function(){
308
+        $('#document-share-form')[0].submit();
309
+      });
310
+
311
+      // Initial setup
312
+      // Activate or disactivate users selector according
313
+      // to current state of the is_shared property
314
+      //
315
+      // FIXME - 2014-05-06 - This is not working (should be done at document.ready time)
316
+      // note: putting this in a document.ready callback does not work.
317
+      //
318
+      $('#document-share-form')[0].reset();
319
+      toggleDocumentSharePeopleSelector($('#document-share-selector').prop("checked"));
320
+##        toggleDocumentPublicKeyGenerator($('#document-public-selector').prop("checked"));  
321
+##        
322
+##        refreshDocumentPublicKey($('#document-public-key').val()); // First init
323
+
324
+    </script>
325
+  </div>
326
+</%def>
327
+
328
+<%def name="FileTabContent(poNode)">
329
+  <h4>${_('Attachments')}</h4>
330
+  
331
+  % if len(poNode.getFiles())<=0:
332
+    <p class="pod-grey">${_("There is currently no attachment.")}<br/></p>
333
+    <p>${POD.OpenModalButton(h.ID.AddFileModalForm(poNode), _(' Attach first file'))}</p>
334
+  % else:
335
+    <p>${POD.OpenModalButton(h.ID.AddFileModalForm(poNode), _(' Attach a file'))}</p>
336
+  % endif
337
+
338
+  <div>
339
+    % if len(poNode.getFiles())>0:
340
+      % for loFile in poNode.getFiles():
341
+        <p style="list-style-type:none; margin-bottom: 0.5em;">
342
+          <i class="fa fa-paperclip"></i>
343
+          <a
344
+            href="${tg.url('/document/%i'%loFile.node_id)}"
345
+            title="${_('View the attachment')}: ${loFile.getTruncatedLabel(-1)}"
346
+          >
347
+            ${loFile.getTruncatedLabel(50)}
348
+          </a>
349
+          ## FIXME SHOW IMAGE THUMBNAIL <img src="${tg.url('/api/get_file_content_thumbnail/%i'%loFile.node_id)}"/>
350
+          <a
351
+            class="pull-right"
352
+            href="${tg.url('/api/get_file_content/%s'%(loFile.node_id))}"
353
+            title="${_('View the attachment')}"
354
+          >
355
+            <i class="fa fa-download"></i>
356
+          </a>
357
+        </p>
358
+      % endfor
359
+    % endif
360
+  </div>
361
+</%def>
362
+
363
+<%def name="SubdocumentContent(poNode)">
364
+  <h4>${_('Sub-documents')}</h4>
365
+  
366
+  % if len(poNode.getChildren())<=0:
367
+    <p class="pod-grey">${_("There is currently no child documents.")}</p>
368
+  % endif
369
+  <p>${POD.OpenModalButton(h.ID.AddDocumentModalForm(poNode), _('Add a document'))}</p>
370
+
371
+  % if len(poNode.getChildren())>0:
372
+    <div>
373
+      % for subnode in poNode.getChildren():
374
+        <p style="list-style-type:none;">
375
+          <i class="fa-fw ${subnode.getIconClass()}"></i>
376
+            <a href="${tg.url('/document/%i'%subnode.node_id)}">
377
+              ${subnode.data_label}
378
+            </a>
379
+        </p>
380
+      % endfor
381
+    </div>
382
+  % endif
383
+</%def>
384
+
385
+<%def name="EventTabContent(poNode)">
386
+  <h4>${_('Calendar')}</h4>
387
+  
388
+  % if len(poNode.getEvents())<=0:
389
+    <p class="pod-grey">${_("The calendar is empty.")}<br/></p>
390
+    <p>${POD.OpenModalButton(h.ID.AddEventModalForm(poNode), _(' Add first event'))}</p>
391
+  % else:
392
+    <p>${POD.OpenModalButton(h.ID.AddEventModalForm(poNode), _(' Add an event'))}</p>
393
+  % endif
394
+
395
+  % if len(poNode.getEvents())>0:
396
+    <table class="table table-striped table-hover table-condensed">
397
+      <thead>
398
+        <tr>
399
+          <th>Date</th>
400
+          <th>Time</th>
401
+          <th>
402
+            Event
403
+          </th>
404
+          <th>
405
+            <a href="" title="Add an event"><i class="icon-g-plus"></i></a>
406
+          </th>
407
+        </tr>
408
+      </thead>
409
+      % for event in poNode.getEvents():
410
+        <tr class="item-with-data-popoverable" data-content="${event.data_content}" rel="popover" data-placement="left" data-trigger="hover">
411
+          <td>${event.getFormattedDate(event.data_datetime)}</td>
412
+          <td>${event.getFormattedTime(event.data_datetime)}</td>
413
+          <td>${event.data_label}</td>
414
+        </tr>
415
+  ## FIXME                    <script>
416
+  ##                      $('.item-with-data-popoverable').popover({ html: true});
417
+  ##                    </script>
418
+
419
+      % endfor
420
+    </table>
421
+  % endif
422
+</%def>
423
+
424
+<%def name="ContactTabContent(poNode)">
425
+  <h4>${_('Address book')}</h4> 
426
+  % if len(poNode.getContacts())<=0:
427
+    <p class="pod-grey">${_("The address book is empty.")}<br/></p>
428
+    <p>${POD.OpenModalButton(h.ID.AddContactModalForm(poNode), _('Add first contact'))}</p>
429
+  % else:
430
+    <p>${POD.OpenModalButton(h.ID.AddContactModalForm(poNode), _('Add a contact'))}</p>
431
+  % endif
432
+
433
+  <!-- LIST OF CONTACT NODES -->
434
+  % for contact in poNode.getContacts():
435
+    <div class="well">
436
+      <legend class="text-info">
437
+        ${contact.data_label}
438
+        ## TODO - 2013-11-20 - Use the right form in order to update meta-data
439
+        <a class="pull-right" href="${tg.url('/document/%i'%contact.node_id)}"><i class="fa fa-edit"></i></a>
440
+      </legend>
441
+      
442
+      <div>
443
+        ## FIXME - D.A. - 2013-11-15 - Implement localisation stuff <a style='float: right;' href="" title='${_('Search on google maps')}'><i class='icon-g-google-maps'></i></a>
444
+        ${contact.data_content|n}
445
+      </div>
446
+    </div>
447
+  % endfor
448
+</%def>
449
+
450
+<%def name="CommentTabContent(poNode)">
451
+  <h4>${_('Comment thread')}</h4>
452
+  
453
+  % if len(poNode.getComments())<=0:
454
+    <p class="pod-grey">${_("The comment thread is empty.")}<br/></p>
455
+  % endif
456
+
457
+  % if len(poNode.getComments())>0:
458
+    % if len(poNode.getComments())>5:
459
+      ##
460
+      ## We show a "direct down" button in case the page is too long
461
+      ##
462
+      <p>${POD.OpenLinkButton(h.ID.AddCommentInlineForm(), _('Add a comment'))}</p>
463
+    % endif
464
+    <div>
465
+      % for comment in poNode.getComments():
466
+        <p>
467
+          <a href="${tg.url('/api/toggle_share_status', dict(node_id=comment.node_id))}">
468
+            % if comment.is_shared:
469
+              <span class="label label-warning" title="${_('Shared comment. Click to make private.')}">${h.ICON.Shared|n}</span>
470
+            % else:
471
+              <span class="label label-info" title="${_('Private comment. Click to share.')}">${h.ICON.Private|n}</span>
472
+            % endif
473
+          </a>
474
+          <strong>${comment._oOwner.display_name}</strong>
475
+          <i class="pull-right">
476
+            The ${comment.getFormattedDate(comment.updated_at)} 
477
+            at ${comment.getFormattedTime(comment.updated_at)}
478
+          </i>
479
+          <br/>
480
+          ${comment.data_content|n}
481
+          <hr style="border-top: 1px dotted #ccc; margin: 0;"/>
482
+        </p>
483
+      % endfor
484
+    </div>
485
+  % endif
486
+
487
+  <form class="form" id="${h.ID.AddCommentInlineForm()}" action="${tg.url('/api/create_comment')}" method="POST">
488
+    <input type="hidden" name='parent_id' value='${poNode.node_id}'/>
489
+    <input type="hidden" name='data_label' value=""/>
490
+    <input type="hidden" id="add_comment_data_content_textarea" name='data_content' />
491
+    <label>
492
+      ${_('Write your comment below:')}
493
+      ${POD.RichTextEditor('add_comment_data_content_textarea_wysiwyg', '', 'boldanditalic')}
494
+    </label>
495
+    <label>
496
+      <input type="checkbox" name='is_shared'/> ${_('Share this comment')}
497
+    </label>
498
+    <span class="pull-right">
499
+      % if len(poNode.getComments())<=0:
500
+        ${POD.SaveButton('current-document-add-comment-save-button', True, _('Add first comment'))}
501
+      % else:
502
+        ${POD.SaveButton('current-document-add-comment-save-button', True, _('Comment'))}
503
+      % endif
504
+    </span>
505
+  </form>
506
+  <script>
507
+      $('#current-document-add-comment-save-button').on('click', function(e){
508
+      e.preventDefault(); // We don't want this to act as a link so cancel the link action
509
+      $('#add_comment_data_content_textarea_wysiwyg').cleanHtml();
510
+      $('#add_comment_data_content_textarea').val($('#add_comment_data_content_textarea_wysiwyg').html());
511
+      $('#current-document-add-comment-form').submit();
512
+    });
513
+  </script>
514
+</%def>