##// END OF EJS Templates
user-groups: new selector for user group memebers....
marcink -
r1089:4d236b89 default
parent child Browse files
Show More
@@ -67,7 +67,6 b''
67 67 "<%= dirs.js.src %>/rhodecode/utils/ie.js",
68 68 "<%= dirs.js.src %>/rhodecode/utils/os.js",
69 69 "<%= dirs.js.src %>/rhodecode/utils/topics.js",
70 "<%= dirs.js.src %>/rhodecode/widgets/multiselect.js",
71 70 "<%= dirs.js.src %>/rhodecode/init.js",
72 71 "<%= dirs.js.src %>/rhodecode/codemirror.js",
73 72 "<%= dirs.js.src %>/rhodecode/comments.js",
@@ -305,7 +305,7 b' def make_map(config):'
305 305 m.connect('delete_user', '/users/{user_id}',
306 306 action='delete', conditions={'method': ['DELETE']})
307 307 m.connect('edit_user', '/users/{user_id}/edit',
308 action='edit', conditions={'method': ['GET']})
308 action='edit', conditions={'method': ['GET']}, jsroute=True)
309 309 m.connect('user', '/users/{user_id}',
310 310 action='show', conditions={'method': ['GET']})
311 311 m.connect('force_password_reset_user', '/users/{user_id}/password_reset',
@@ -389,7 +389,7 b' def make_map(config):'
389 389
390 390 m.connect('edit_user_group_members',
391 391 '/user_groups/{user_group_id}/edit/members', jsroute=True,
392 action='edit_members', conditions={'method': ['GET']})
392 action='user_group_members', conditions={'method': ['GET']})
393 393
394 394 # ADMIN PERMISSIONS ROUTES
395 395 with rmap.submapper(path_prefix=ADMIN_PREFIX,
@@ -25,6 +25,7 b' User Groups crud controller for pylons'
25 25 import logging
26 26 import formencode
27 27
28 import peppercorn
28 29 from formencode import htmlfill
29 30 from pylons import request, tmpl_context as c, url, config
30 31 from pylons.controllers.util import redirect
@@ -40,7 +41,7 b' from rhodecode.lib.utils import jsonify,'
40 41 from rhodecode.lib.utils2 import safe_unicode, str2bool, safe_int
41 42 from rhodecode.lib.auth import (
42 43 LoginRequired, NotAnonymous, HasUserGroupPermissionAnyDecorator,
43 HasPermissionAnyDecorator)
44 HasPermissionAnyDecorator, XHRRequired)
44 45 from rhodecode.lib.base import BaseController, render
45 46 from rhodecode.model.permission import PermissionModel
46 47 from rhodecode.model.scm import UserGroupList
@@ -69,13 +70,8 b' class UserGroupsController(BaseControlle'
69 70 def __load_data(self, user_group_id):
70 71 c.group_members_obj = [x.user for x in c.user_group.members]
71 72 c.group_members_obj.sort(key=lambda u: u.username.lower())
72
73 73 c.group_members = [(x.user_id, x.username) for x in c.group_members_obj]
74 74
75 c.available_members = [(x.user_id, x.username)
76 for x in User.query().all()]
77 c.available_members.sort(key=lambda u: u[1].lower())
78
79 75 def __load_defaults(self, user_group_id):
80 76 """
81 77 Load defaults settings for edit, and update
@@ -207,20 +203,21 b' class UserGroupsController(BaseControlle'
207 203 c.active = 'settings'
208 204 self.__load_data(user_group_id)
209 205
210 available_members = [safe_unicode(x[0]) for x in c.available_members]
211
212 206 users_group_form = UserGroupForm(
213 edit=True, old_data=c.user_group.get_dict(),
214 available_members=available_members, allow_disabled=True)()
207 edit=True, old_data=c.user_group.get_dict(), allow_disabled=True)()
215 208
216 209 try:
217 210 form_result = users_group_form.to_python(request.POST)
211 pstruct = peppercorn.parse(request.POST.items())
212 form_result['users_group_members'] = pstruct['user_group_members']
213
218 214 UserGroupModel().update(c.user_group, form_result)
219 gr = form_result['users_group_name']
215 updated_user_group = form_result['users_group_name']
220 216 action_logger(c.rhodecode_user,
221 'admin_updated_users_group:%s' % gr,
217 'admin_updated_users_group:%s' % updated_user_group,
222 218 None, self.ip_addr, self.sa)
223 h.flash(_('Updated user group %s') % gr, category='success')
219 h.flash(_('Updated user group %s') % updated_user_group,
220 category='success')
224 221 Session().commit()
225 222 except formencode.Invalid as errors:
226 223 defaults = errors.value
@@ -462,19 +459,29 b' class UserGroupsController(BaseControlle'
462 459 return render('admin/user_groups/user_group_edit.html')
463 460
464 461 @HasUserGroupPermissionAnyDecorator('usergroup.admin')
465 def edit_members(self, user_group_id):
462 @XHRRequired()
463 @jsonify
464 def user_group_members(self, user_group_id):
466 465 user_group_id = safe_int(user_group_id)
467 c.user_group = UserGroup.get_or_404(user_group_id)
468 c.active = 'members'
469 c.group_members_obj = sorted((x.user for x in c.user_group.members),
466 user_group = UserGroup.get_or_404(user_group_id)
467 group_members_obj = sorted((x.user for x in user_group.members),
470 468 key=lambda u: u.username.lower())
471 469
472 group_members = [(x.user_id, x.username) for x in c.group_members_obj]
470 group_members = [
471 {
472 'id': user.user_id,
473 'first_name': user.name,
474 'last_name': user.lastname,
475 'username': user.username,
476 'icon_link': h.gravatar_url(user.email, 30),
477 'value_display': h.person(user.email),
478 'value': user.username,
479 'value_type': 'user',
480 'active': user.active,
481 }
482 for user in group_members_obj
483 ]
473 484
474 if request.is_xhr:
475 return jsonify(lambda *a, **k: {
485 return {
476 486 'members': group_members
477 })
478
479 c.group_members = group_members
480 return render('admin/user_groups/user_group_edit.html')
487 }
@@ -143,10 +143,8 b' def UserForm(edit=False, available_langu'
143 143 return _UserForm
144 144
145 145
146 def UserGroupForm(edit=False, old_data=None, available_members=None,
147 allow_disabled=False):
146 def UserGroupForm(edit=False, old_data=None, allow_disabled=False):
148 147 old_data = old_data or {}
149 available_members = available_members or []
150 148
151 149 class _UserGroupForm(formencode.Schema):
152 150 allow_extra_fields = True
@@ -162,10 +160,6 b' def UserGroupForm(edit=False, old_data=N'
162 160 users_group_active = v.StringBoolean(if_missing=False)
163 161
164 162 if edit:
165 users_group_members = v.OneOf(
166 available_members, hideList=False, testValueList=True,
167 if_missing=None, not_empty=False
168 )
169 163 # this is user group owner
170 164 user = All(
171 165 v.UnicodeString(not_empty=True),
@@ -189,18 +189,15 b' class UserGroupModel(BaseModel):'
189 189 self._log_user_changes('removed from', user_group, removed)
190 190
191 191 def _clean_members_data(self, members_data):
192 # TODO: anderson: this should be in the form validation but I couldn't
193 # make it work there as it conflicts with the other validator
194 192 if not members_data:
195 193 members_data = []
196 194
197 if isinstance(members_data, basestring):
198 new_members = [members_data]
199 else:
200 new_members = members_data
201
202 new_members = [int(uid) for uid in new_members]
203 return new_members
195 members = []
196 for user in members_data:
197 uid = int(user['member_user_id'])
198 if uid not in members and user['type'] in ['new', 'existing']:
199 members.append(uid)
200 return members
204 201
205 202 def update(self, user_group, form_data):
206 203 user_group = self._get_user_group(user_group)
@@ -1370,7 +1370,27 b' table.integrations {'
1370 1370 margin-right: .5em;
1371 1371 margin-left: 3px;
1372 1372 }
1373
1374 .to-delete {
1375 .user {
1376 text-decoration: line-through;
1373 1377 }
1378 }
1379 }
1380
1381 // new entry in group_members
1382 .td-author-new-entry {
1383 background-color: rgba(red(@alert1), green(@alert1), blue(@alert1), 0.3);
1384 }
1385
1386 .usergroup_member_remove {
1387 width: 16px;
1388 margin-bottom: 10px;
1389 padding: 0;
1390 color: black !important;
1391 cursor: pointer;
1392 }
1393
1374 1394 .reviewer_ac .ac-input {
1375 1395 width: 92%;
1376 1396 margin-bottom: 1em;
@@ -16,6 +16,7 b' function registerRCRoutes() {'
16 16 pyroutes.register('user_autocomplete_data', '/_users', []);
17 17 pyroutes.register('user_group_autocomplete_data', '/_user_groups', []);
18 18 pyroutes.register('new_repo', '/_admin/create_repository', []);
19 pyroutes.register('edit_user', '/_admin/users/%(user_id)s/edit', ['user_id']);
19 20 pyroutes.register('edit_user_group_members', '/_admin/user_groups/%(user_group_id)s/edit/members', ['user_group_id']);
20 21 pyroutes.register('gists', '/_admin/gists', []);
21 22 pyroutes.register('new_gist', '/_admin/gists/new', []);
@@ -35,7 +35,6 b''
35 35 <li class="${'active' if c.active=='advanced' else ''}"><a href="${h.url('edit_user_group_advanced', user_group_id=c.user_group.users_group_id)}">${_('Advanced')}</a></li>
36 36 <li class="${'active' if c.active=='global_perms' else ''}"><a href="${h.url('edit_user_group_global_perms', user_group_id=c.user_group.users_group_id)}">${_('Global permissions')}</a></li>
37 37 <li class="${'active' if c.active=='perms_summary' else ''}"><a href="${h.url('edit_user_group_perms_summary', user_group_id=c.user_group.users_group_id)}">${_('Permissions summary')}</a></li>
38 <li class="${'active' if c.active=='members' else ''}"><a href="${h.url('edit_user_group_members', user_group_id=c.user_group.users_group_id)}">${_('Members')}</a></li>
39 38 </ul>
40 39 </div>
41 40
@@ -54,37 +54,50 b''
54 54 ${h.checkbox('users_group_active',value=True)}
55 55 </div>
56 56 </div>
57
57 58 <div class="field">
58 <div class="label">
59 <label for="users_group_active">${_('Search')}:</label>
60 ${h.text('from_user_group',
61 placeholder="user/usergroup",
62 class_="medium")}
59 <div class="label label-checkbox">
60 <label for="users_group_active">${_('Add members')}:</label>
63 61 </div>
64 <div class="select side-by-side-selector">
65 <div class="left-group">
66 <label class="text"><strong>${_('Chosen group members')}</strong></label>
67 ${h.select('users_group_members',[x[0] for x in c.group_members],c.group_members,multiple=True,size=8,)}
68 <div class="btn" id="remove_all_elements" >
69 ${_('Remove all elements')}
70 <i class="icon-chevron-right"></i>
62 <div class="input">
63 ${h.text('user_group_add_members', placeholder="user/usergroup", class_="medium")}
71 64 </div>
72 65 </div>
73 <div class="middle-group">
74 <i id="add_element" class="icon-chevron-left"></i>
75 <br />
76 <i id="remove_element" class="icon-chevron-right"></i>
66
67 <input type="hidden" name="__start__" value="user_group_members:sequence"/>
68 <table id="group_members_placeholder" class="rctable group_members">
69 <tr>
70 <th>${_('Username')}</th>
71 <th>${_('Action')}</th>
72 </tr>
73
74 % if c.group_members_obj:
75 % for user in c.group_members_obj:
76 <tr>
77 <td id="member_user_${user.user_id}" class="td-author">
78 <div class="group_member">
79 ${base.gravatar(user.email, 16)}
80 <span class="username user">${h.link_to(h.person(user), h.url( 'edit_user',user_id=user.user_id))}</span>
81 <input type="hidden" name="__start__" value="member:mapping">
82 <input type="hidden" name="member_user_id" value="${user.user_id}">
83 <input type="hidden" name="type" value="existing" id="member_${user.user_id}">
84 <input type="hidden" name="__end__" value="member:mapping">
77 85 </div>
78 <div class="right-group">
79 <label class="text" >${_('Available users')}
80 </label>
81 ${h.select('available_members',[],c.available_members,multiple=True,size=8,)}
82 <div class="btn" id="add_all_elements" >
83 <i class="icon-chevron-left"></i>${_('Add all elements')}
86 </td>
87 <td class="">
88 <div class="usergroup_member_remove action_button" onclick="removeUserGroupMember(${user.user_id}, true)" style="visibility: visible;">
89 <i class="icon-remove-sign"></i>
84 90 </div>
85 </div>
86 </div>
87 </div>
91 </td>
92 </tr>
93 % endfor
94
95 % else:
96 <tr><td colspan="2">${_('No members yet')}</td></tr>
97 % endif
98 </table>
99 <input type="hidden" name="__end__" value="user_group_members:sequence"/>
100
88 101 <div class="buttons">
89 102 ${h.submit('Save',_('Save'),class_="btn")}
90 103 </div>
@@ -95,15 +108,18 b''
95 108 </div>
96 109 <script>
97 110 $(document).ready(function(){
98 MultiSelectWidget('users_group_members','available_members','edit_users_group');
99
100 111 $("#group_parent_id").select2({
101 112 'containerCssClass': "drop-menu",
102 113 'dropdownCssClass': "drop-menu-dropdown",
103 114 'dropdownAutoWidth': true
104 115 });
105 116
106 $('#from_user_group').autocomplete({
117 removeUserGroupMember = function(userId){
118 $('#member_'+userId).val('remove');
119 $('#member_user_'+userId).addClass('to-delete');
120 };
121
122 $('#user_group_add_members').autocomplete({
107 123 serviceUrl: pyroutes.url('user_autocomplete_data'),
108 124 minChars:2,
109 125 maxHeight:400,
@@ -115,9 +131,37 b''
115 131 lookupFilter: autocompleteFilterResult,
116 132 onSelect: function(element, suggestion){
117 133
118 function preSelectUserIds(uids) {
119 $('#available_members').val(uids);
120 $('#users_group_members').val(uids);
134 function addMember(user, fromUserGroup) {
135 var gravatar = user.icon_link;
136 var username = user.value_display;
137 var userLink = pyroutes.url('edit_user', {"user_id": user.id});
138 var uid = user.id;
139
140 if (fromUserGroup) {
141 username = username +" "+ _gettext('(from usergroup {0})'.format(fromUserGroup))
142 }
143
144 var elem = $(
145 ('<tr>'+
146 '<td id="member_user_{6}" class="td-author td-author-new-entry">'+
147 '<div class="group_member">'+
148 '<img class="gravatar" src="{0}" height="16" width="16">'+
149 '<span class="username user"><a href="{1}">{2}</a></span>'+
150 '<input type="hidden" name="__start__" value="member:mapping">'+
151 '<input type="hidden" name="member_user_id" value="{3}">'+
152 '<input type="hidden" name="type" value="new" id="member_{4}">'+
153 '<input type="hidden" name="__end__" value="member:mapping">'+
154 '</div>'+
155 '</td>'+
156 '<td class="td-author-new-entry">'+
157 '<div class="usergroup_member_remove action_button" onclick="removeUserGroupMember({5}, true)" style="visibility: visible;">'+
158 '<i class="icon-remove-sign"></i>'+
159 '</div>'+
160 '</td>'+
161 '</tr>').format(gravatar, userLink, username,
162 uid, uid, uid, uid)
163 );
164 $('#group_members_placeholder').append(elem)
121 165 }
122 166
123 167 if (suggestion.value_type == 'user_group') {
@@ -125,20 +169,18 b''
125 169 pyroutes.url('edit_user_group_members',
126 170 {'user_group_id': suggestion.id}),
127 171 function(data) {
128 var uids = [];
129 172 $.each(data.members, function(idx, user) {
130 var userid = user[0],
131 username = user[1];
132 uids.push(userid.toString());
173 addMember(user, suggestion.value)
133 174 });
134 preSelectUserIds(uids)
135 175 }
136 176 );
137 177 } else if (suggestion.value_type == 'user') {
138 preSelectUserIds([suggestion.id.toString()]);
178 addMember(suggestion, null);
139 179 }
140 180 }
141 181 });
182
183
142 184 UsersAutoComplete('user', '${c.rhodecode_user.user_id}');
143 185 })
144 186 </script>
@@ -545,12 +545,6 b''
545 545 </div>
546 546 </div>
547 547
548 <script>
549 $(document).ready(function(){
550 MultiSelectWidget('users_group_members','available_members','edit_users_group');
551 })
552 </script>
553
554 548 </div>
555 549
556 550 <div class='field'>
@@ -595,12 +589,6 b''
595 589 </div>
596 590 </div>
597 591
598 <script>
599 $(document).ready(function(){
600 MultiSelectWidget('users_group_members2','available_members','edit_users_group');
601 })
602 </script>
603
604 592 </div>
605 593
606 594 <div class='field'>
@@ -36,7 +36,7 b' class TestAdminUsersGroupsController(Tes'
36 36 def test_index(self):
37 37 self.log_user()
38 38 response = self.app.get(url('users_groups'))
39 response.status_int == 200
39 assert response.status_int == 200
40 40
41 41 def test_create(self):
42 42 self.log_user()
@@ -148,19 +148,21 b' class TestAdminUsersGroupsController(Tes'
148 148
149 149 fixture.destroy_user_group(users_group_name)
150 150
151 def test_edit(self):
151 def test_edit_autocomplete(self):
152 152 self.log_user()
153 153 ug = fixture.create_user_group(TEST_USER_GROUP, skip_if_exists=True)
154 154 response = self.app.get(
155 155 url('edit_users_group', user_group_id=ug.users_group_id))
156 156 fixture.destroy_user_group(TEST_USER_GROUP)
157 157
158 def test_edit_user_group_members(self):
158 def test_edit_user_group_autocomplete_members(self, xhr_header):
159 159 self.log_user()
160 160 ug = fixture.create_user_group(TEST_USER_GROUP, skip_if_exists=True)
161 161 response = self.app.get(
162 url('edit_user_group_members', user_group_id=ug.users_group_id))
163 response.mustcontain('No members yet')
162 url('edit_user_group_members', user_group_id=ug.users_group_id),
163 extra_environ=xhr_header)
164
165 assert response.body == '{"members": []}'
164 166 fixture.destroy_user_group(TEST_USER_GROUP)
165 167
166 168 def test_usergroup_escape(self):
@@ -181,7 +183,7 b' class TestAdminUsersGroupsController(Tes'
181 183 'csrf_token': self.csrf_token
182 184 }
183 185
184 response = self.app.post(url('users_groups'), data)
186 self.app.post(url('users_groups'), data)
185 187 response = self.app.get(url('users_groups'))
186 188
187 189 response.mustcontain(
@@ -190,3 +192,42 b' class TestAdminUsersGroupsController(Tes'
190 192 response.mustcontain(
191 193 '&lt;img src=&#34;/image2&#34; onload=&#34;'
192 194 'alert(&#39;Hello, World!&#39;);&#34;&gt;')
195
196 def test_update_members_from_user_ids(self, user_regular):
197 uid = user_regular.user_id
198 username = user_regular.username
199 self.log_user()
200
201 user_group = fixture.create_user_group('test_gr_ids')
202 assert user_group.members == []
203 assert user_group.user != user_regular
204 expected_active_state = not user_group.users_group_active
205
206 form_data = [
207 ('csrf_token', self.csrf_token),
208 ('_method', 'put'),
209 ('user', username),
210 ('users_group_name', 'changed_name'),
211 ('users_group_active', expected_active_state),
212 ('user_group_description', 'changed_description'),
213
214 ('__start__', 'user_group_members:sequence'),
215 ('__start__', 'member:mapping'),
216 ('member_user_id', uid),
217 ('type', 'existing'),
218 ('__end__', 'member:mapping'),
219 ('__end__', 'user_group_members:sequence'),
220 ]
221 ugid = user_group.users_group_id
222 self.app.post(url('update_users_group', user_group_id=ugid), form_data)
223
224 user_group = UserGroup.get(ugid)
225 assert user_group
226
227 assert user_group.members[0].user_id == uid
228 assert user_group.user_id == uid
229 assert 'changed_name' in user_group.users_group_name
230 assert 'changed_description' in user_group.user_group_description
231 assert user_group.users_group_active == expected_active_state
232
233 fixture.destroy_user_group(user_group)
@@ -113,40 +113,20 b' def test_add_and_remove_user_from_group('
113 113 assert user_group.members == []
114 114
115 115
116 @pytest.mark.parametrize(
117 'data, expected', [
118 ("1", [1]), (["1", "2"], [1, 2])
119 ]
120 )
116 @pytest.mark.parametrize('data, expected', [
117 ([], []),
118 ([{"member_user_id": 1, "type": "new"}], [1]),
119 ([{"member_user_id": 1, "type": "new"},
120 {"member_user_id": 1, "type": "existing"}], [1]),
121 ([{"member_user_id": 1, "type": "new"},
122 {"member_user_id": 2, "type": "new"},
123 {"member_user_id": 3, "type": "remove"}], [1, 2])
124 ])
121 125 def test_clean_members_data(data, expected):
122 126 cleaned = UserGroupModel()._clean_members_data(data)
123 127 assert cleaned == expected
124 128
125 129
126 def test_update_members_from_user_ids(user_regular, user_util):
127 user_group = user_util.create_user_group()
128 assert user_group.members == []
129 assert user_group.user != user_regular
130 expected_active_state = not user_group.users_group_active
131
132 form_data = {
133 'users_group_members': str(user_regular.user_id),
134 'user': str(user_regular.username),
135 'users_group_name': 'changed_name',
136 'users_group_active': expected_active_state,
137 'user_group_description': 'changed_description'
138 }
139
140 UserGroupModel().update(user_group, form_data)
141 assert user_group.members[0].user_id == user_regular.user_id
142 assert user_group.user_id == user_regular.user_id
143 assert 'changed_name' in user_group.users_group_name
144 assert 'changed_description' in user_group.user_group_description
145 assert user_group.users_group_active == expected_active_state
146 # Ignore changes on the test
147 Session().rollback()
148
149
150 130 def _create_test_members():
151 131 members = []
152 132 for member_number in range(3):
@@ -38,6 +38,7 b' import requests'
38 38 from webtest.app import TestApp
39 39
40 40 import rhodecode
41 from rhodecode.lib.utils2 import AttributeDict
41 42 from rhodecode.model.changeset_status import ChangesetStatusModel
42 43 from rhodecode.model.comment import ChangesetCommentsModel
43 44 from rhodecode.model.db import (
1 NO CONTENT: file was removed
1 NO CONTENT: file was removed
General Comments 0
You need to be logged in to leave comments. Login now