# HG changeset patch # User Marcin Kuzminski # Date 2016-11-06 15:01:21 # Node ID 4d236b89acbcba4ea4291d58112e90a7caf0c368 # Parent 7d451b5a6238df01db0f1dbdc4608cc31013c7b5 user-groups: new selector for user group memebers. - removed old two column selector in replacement of autocomplete list - scales much better for alrge number of users - still allows to select single users and user groups - fixes #4291 (user group shows only username in members) diff --git a/grunt_config.json b/grunt_config.json --- a/grunt_config.json +++ b/grunt_config.json @@ -67,7 +67,6 @@ "<%= dirs.js.src %>/rhodecode/utils/ie.js", "<%= dirs.js.src %>/rhodecode/utils/os.js", "<%= dirs.js.src %>/rhodecode/utils/topics.js", - "<%= dirs.js.src %>/rhodecode/widgets/multiselect.js", "<%= dirs.js.src %>/rhodecode/init.js", "<%= dirs.js.src %>/rhodecode/codemirror.js", "<%= dirs.js.src %>/rhodecode/comments.js", diff --git a/rhodecode/config/routing.py b/rhodecode/config/routing.py --- a/rhodecode/config/routing.py +++ b/rhodecode/config/routing.py @@ -305,7 +305,7 @@ def make_map(config): m.connect('delete_user', '/users/{user_id}', action='delete', conditions={'method': ['DELETE']}) m.connect('edit_user', '/users/{user_id}/edit', - action='edit', conditions={'method': ['GET']}) + action='edit', conditions={'method': ['GET']}, jsroute=True) m.connect('user', '/users/{user_id}', action='show', conditions={'method': ['GET']}) m.connect('force_password_reset_user', '/users/{user_id}/password_reset', @@ -389,7 +389,7 @@ def make_map(config): m.connect('edit_user_group_members', '/user_groups/{user_group_id}/edit/members', jsroute=True, - action='edit_members', conditions={'method': ['GET']}) + action='user_group_members', conditions={'method': ['GET']}) # ADMIN PERMISSIONS ROUTES with rmap.submapper(path_prefix=ADMIN_PREFIX, diff --git a/rhodecode/controllers/admin/user_groups.py b/rhodecode/controllers/admin/user_groups.py --- a/rhodecode/controllers/admin/user_groups.py +++ b/rhodecode/controllers/admin/user_groups.py @@ -25,6 +25,7 @@ User Groups crud controller for pylons import logging import formencode +import peppercorn from formencode import htmlfill from pylons import request, tmpl_context as c, url, config from pylons.controllers.util import redirect @@ -40,7 +41,7 @@ from rhodecode.lib.utils import jsonify, from rhodecode.lib.utils2 import safe_unicode, str2bool, safe_int from rhodecode.lib.auth import ( LoginRequired, NotAnonymous, HasUserGroupPermissionAnyDecorator, - HasPermissionAnyDecorator) + HasPermissionAnyDecorator, XHRRequired) from rhodecode.lib.base import BaseController, render from rhodecode.model.permission import PermissionModel from rhodecode.model.scm import UserGroupList @@ -69,13 +70,8 @@ class UserGroupsController(BaseControlle def __load_data(self, user_group_id): c.group_members_obj = [x.user for x in c.user_group.members] c.group_members_obj.sort(key=lambda u: u.username.lower()) - c.group_members = [(x.user_id, x.username) for x in c.group_members_obj] - c.available_members = [(x.user_id, x.username) - for x in User.query().all()] - c.available_members.sort(key=lambda u: u[1].lower()) - def __load_defaults(self, user_group_id): """ Load defaults settings for edit, and update @@ -207,20 +203,21 @@ class UserGroupsController(BaseControlle c.active = 'settings' self.__load_data(user_group_id) - available_members = [safe_unicode(x[0]) for x in c.available_members] - users_group_form = UserGroupForm( - edit=True, old_data=c.user_group.get_dict(), - available_members=available_members, allow_disabled=True)() + edit=True, old_data=c.user_group.get_dict(), allow_disabled=True)() try: form_result = users_group_form.to_python(request.POST) + pstruct = peppercorn.parse(request.POST.items()) + form_result['users_group_members'] = pstruct['user_group_members'] + UserGroupModel().update(c.user_group, form_result) - gr = form_result['users_group_name'] + updated_user_group = form_result['users_group_name'] action_logger(c.rhodecode_user, - 'admin_updated_users_group:%s' % gr, + 'admin_updated_users_group:%s' % updated_user_group, None, self.ip_addr, self.sa) - h.flash(_('Updated user group %s') % gr, category='success') + h.flash(_('Updated user group %s') % updated_user_group, + category='success') Session().commit() except formencode.Invalid as errors: defaults = errors.value @@ -462,19 +459,29 @@ class UserGroupsController(BaseControlle return render('admin/user_groups/user_group_edit.html') @HasUserGroupPermissionAnyDecorator('usergroup.admin') - def edit_members(self, user_group_id): + @XHRRequired() + @jsonify + def user_group_members(self, user_group_id): user_group_id = safe_int(user_group_id) - c.user_group = UserGroup.get_or_404(user_group_id) - c.active = 'members' - c.group_members_obj = sorted((x.user for x in c.user_group.members), - key=lambda u: u.username.lower()) + user_group = UserGroup.get_or_404(user_group_id) + group_members_obj = sorted((x.user for x in user_group.members), + key=lambda u: u.username.lower()) - group_members = [(x.user_id, x.username) for x in c.group_members_obj] + group_members = [ + { + 'id': user.user_id, + 'first_name': user.name, + 'last_name': user.lastname, + 'username': user.username, + 'icon_link': h.gravatar_url(user.email, 30), + 'value_display': h.person(user.email), + 'value': user.username, + 'value_type': 'user', + 'active': user.active, + } + for user in group_members_obj + ] - if request.is_xhr: - return jsonify(lambda *a, **k: { - 'members': group_members - }) - - c.group_members = group_members - return render('admin/user_groups/user_group_edit.html') + return { + 'members': group_members + } diff --git a/rhodecode/model/forms.py b/rhodecode/model/forms.py --- a/rhodecode/model/forms.py +++ b/rhodecode/model/forms.py @@ -143,10 +143,8 @@ def UserForm(edit=False, available_langu return _UserForm -def UserGroupForm(edit=False, old_data=None, available_members=None, - allow_disabled=False): +def UserGroupForm(edit=False, old_data=None, allow_disabled=False): old_data = old_data or {} - available_members = available_members or [] class _UserGroupForm(formencode.Schema): allow_extra_fields = True @@ -162,10 +160,6 @@ def UserGroupForm(edit=False, old_data=N users_group_active = v.StringBoolean(if_missing=False) if edit: - users_group_members = v.OneOf( - available_members, hideList=False, testValueList=True, - if_missing=None, not_empty=False - ) # this is user group owner user = All( v.UnicodeString(not_empty=True), diff --git a/rhodecode/model/user_group.py b/rhodecode/model/user_group.py --- a/rhodecode/model/user_group.py +++ b/rhodecode/model/user_group.py @@ -189,18 +189,15 @@ class UserGroupModel(BaseModel): self._log_user_changes('removed from', user_group, removed) def _clean_members_data(self, members_data): - # TODO: anderson: this should be in the form validation but I couldn't - # make it work there as it conflicts with the other validator if not members_data: members_data = [] - if isinstance(members_data, basestring): - new_members = [members_data] - else: - new_members = members_data - - new_members = [int(uid) for uid in new_members] - return new_members + members = [] + for user in members_data: + uid = int(user['member_user_id']) + if uid not in members and user['type'] in ['new', 'existing']: + members.append(uid) + return members def update(self, user_group, form_data): user_group = self._get_user_group(user_group) diff --git a/rhodecode/public/css/main.less b/rhodecode/public/css/main.less --- a/rhodecode/public/css/main.less +++ b/rhodecode/public/css/main.less @@ -1370,7 +1370,27 @@ table.integrations { margin-right: .5em; margin-left: 3px; } + + .to-delete { + .user { + text-decoration: line-through; + } + } } + +// new entry in group_members +.td-author-new-entry { + background-color: rgba(red(@alert1), green(@alert1), blue(@alert1), 0.3); +} + +.usergroup_member_remove { + width: 16px; + margin-bottom: 10px; + padding: 0; + color: black !important; + cursor: pointer; +} + .reviewer_ac .ac-input { width: 92%; margin-bottom: 1em; diff --git a/rhodecode/public/js/rhodecode/routes.js b/rhodecode/public/js/rhodecode/routes.js --- a/rhodecode/public/js/rhodecode/routes.js +++ b/rhodecode/public/js/rhodecode/routes.js @@ -16,6 +16,7 @@ function registerRCRoutes() { pyroutes.register('user_autocomplete_data', '/_users', []); pyroutes.register('user_group_autocomplete_data', '/_user_groups', []); pyroutes.register('new_repo', '/_admin/create_repository', []); + pyroutes.register('edit_user', '/_admin/users/%(user_id)s/edit', ['user_id']); pyroutes.register('edit_user_group_members', '/_admin/user_groups/%(user_group_id)s/edit/members', ['user_group_id']); pyroutes.register('gists', '/_admin/gists', []); pyroutes.register('new_gist', '/_admin/gists/new', []); diff --git a/rhodecode/public/js/src/rhodecode/widgets/multiselect.js b/rhodecode/public/js/src/rhodecode/widgets/multiselect.js deleted file mode 100644 --- a/rhodecode/public/js/src/rhodecode/widgets/multiselect.js +++ /dev/null @@ -1,132 +0,0 @@ -// # Copyright (C) 2010-2016 RhodeCode GmbH -// # -// # This program is free software: you can redistribute it and/or modify -// # it under the terms of the GNU Affero General Public License, version 3 -// # (only), as published by the Free Software Foundation. -// # -// # This program is distributed in the hope that it will be useful, -// # but WITHOUT ANY WARRANTY; without even the implied warranty of -// # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// # GNU General Public License for more details. -// # -// # You should have received a copy of the GNU Affero General Public License -// # along with this program. If not, see . -// # -// # This program is dual-licensed. If you wish to learn more about the -// # RhodeCode Enterprise Edition, including its added features, Support services, -// # and proprietary license terms, please see https://rhodecode.com/licenses/ - -/** - * Multi select widget - */ -var MultiSelectWidget = function(selected_id, available_id, form_id){ - // definition of containers ID's - var selected_container = selected_id; - var available_container = available_id; - // temp container for selected storage. - var cache = []; - var av_cache = []; - var c = $('#'+selected_container).get(0); - var ac = $('#'+available_container).get(0); - // get only selected options for further fullfilment - for (var i = 0; node = c.options[i]; i++){ - if (node.selected){ - // push selected to my temp storage left overs :) - cache.push(node); - } - } - // get all available options to cache - for (i = 0; node = ac.options[i]; i++){ - // push selected to my temp storage left overs :) - av_cache.push(node); - } - // fill available only with those not in chosen - ac.options.length = 0; - tmp_cache = []; - - for (i = 0; node = av_cache[i]; i++){ - var add = true; - for (var i2 = 0; node_2 = cache[i2]; i2++){ - if (node.value === node_2.value){ - add=false; - break; - } - } - if(add){ - tmp_cache.push(new Option(node.text, node.value, false, false)); - } - } - for (i = 0; node = tmp_cache[i]; i++){ - ac.options[i] = node; - } - function prompts_action_callback(e){ - var chosen = $('#'+selected_container).get(0); - var available = $('#'+available_container).get(0); - // get checked and unchecked options from field - function get_checked(from_field){ - // temp container for storage. - var sel_cache = []; - var oth_cache = []; - - for (i = 0; node = from_field.options[i]; i++){ - if(node.selected){ - // push selected fields :) - sel_cache.push(node); - } - else { - oth_cache.push(node); - } - } - return [sel_cache,oth_cache]; - } - // fill the field with given options - function fill_with(field,options){ - // clear firtst - field.options.length=0; - for (var i = 0; node = options[i]; i++){ - field.options[i] = new Option(node.text, node.value, false, false); - } - } - // adds to current field - function add_to(field,options){ - for (i = 0; node = options[i]; i++){ - field.appendChild(new Option(node.text, node.value, false, false)); - } - } - // add action - if (this.id ==='add_element'){ - var c = get_checked(available); - add_to(chosen,c[0]); - fill_with(available,c[1]); - } - // remove action - if (this.id ==='remove_element'){ - c = get_checked(chosen); - add_to(available,c[0]); - fill_with(chosen,c[1]); - } - // add all elements - if(this.id === 'add_all_elements'){ - for (i=0; node = available.options[i]; i++){ - chosen.appendChild(new Option(node.text, node.value, false, false)); - } - available.options.length = 0; - } - // remove all elements - if (this.id === 'remove_all_elements'){ - for (i=0; node = chosen.options[i]; i++){ - available.appendChild(new Option(node.text, node.value, false, false)); - } - chosen.options.length = 0; - } - } - $('#add_element, #remove_element, #add_all_elements, #remove_all_elements').click(prompts_action_callback); - if (form_id !== undefined) { - $('#'+form_id).submit(function(){ - var chosen = $('#'+selected_container).get(0); - for (i = 0; i < chosen.options.length; i++) { - chosen.options[i].selected = 'selected'; - } - }); - } -}; diff --git a/rhodecode/templates/admin/user_groups/user_group_edit.html b/rhodecode/templates/admin/user_groups/user_group_edit.html --- a/rhodecode/templates/admin/user_groups/user_group_edit.html +++ b/rhodecode/templates/admin/user_groups/user_group_edit.html @@ -35,7 +35,6 @@
  • ${_('Advanced')}
  • ${_('Global permissions')}
  • ${_('Permissions summary')}
  • -
  • ${_('Members')}
  • diff --git a/rhodecode/templates/admin/user_groups/user_group_edit_members.html b/rhodecode/templates/admin/user_groups/user_group_edit_members.html deleted file mode 100644 --- a/rhodecode/templates/admin/user_groups/user_group_edit_members.html +++ /dev/null @@ -1,30 +0,0 @@ -<%namespace name="base" file="/base/base.html"/> - -
    -
    -

    ${_('Members of User Group: %s') % c.user_group.users_group_name}

    -
    -
    - % if c.group_members_obj: - - - - - - %for user in c.group_members_obj: - - - - - %endfor -
    UsernameName
    -
    - ${base.gravatar(user.email, 16)} - ${h.link_to(user.username, h.url( 'edit_user',user_id=user.user_id))} -
    -
    ${user.full_name}
    - %else: -

    ${_('No members yet')}

    - %endif -
    -
    diff --git a/rhodecode/templates/admin/user_groups/user_group_edit_settings.html b/rhodecode/templates/admin/user_groups/user_group_edit_settings.html --- a/rhodecode/templates/admin/user_groups/user_group_edit_settings.html +++ b/rhodecode/templates/admin/user_groups/user_group_edit_settings.html @@ -54,40 +54,53 @@ ${h.checkbox('users_group_active',value=True)} -
    -
    - - ${h.text('from_user_group', - placeholder="user/usergroup", - class_="medium")} + +
    +
    + +
    +
    + ${h.text('user_group_add_members', placeholder="user/usergroup", class_="medium")}
    -
    -
    - - ${h.select('users_group_members',[x[0] for x in c.group_members],c.group_members,multiple=True,size=8,)} -
    - ${_('Remove all elements')} - -
    -
    -
    - -
    - -
    -
    - - ${h.select('available_members',[],c.available_members,multiple=True,size=8,)} -
    - ${_('Add all elements')} -
    -
    -
    -
    -
    - ${h.submit('Save',_('Save'),class_="btn")} -
    +
    + + + + + + + + + % if c.group_members_obj: + % for user in c.group_members_obj: + + + + + % endfor + + % else: + + % endif +
    ${_('Username')}${_('Action')}
    +
    + ${base.gravatar(user.email, 16)} + ${h.link_to(h.person(user), h.url( 'edit_user',user_id=user.user_id))} + + + + +
    +
    +
    + +
    +
    ${_('No members yet')}
    + + +
    + ${h.submit('Save',_('Save'),class_="btn")} +
    ${h.end_form()} @@ -95,15 +108,18 @@ diff --git a/rhodecode/templates/debug_style/form-elements.html b/rhodecode/templates/debug_style/form-elements.html --- a/rhodecode/templates/debug_style/form-elements.html +++ b/rhodecode/templates/debug_style/form-elements.html @@ -545,12 +545,6 @@ - -
    @@ -595,12 +589,6 @@
    - -
    diff --git a/rhodecode/tests/functional/test_admin_user_groups.py b/rhodecode/tests/functional/test_admin_user_groups.py --- a/rhodecode/tests/functional/test_admin_user_groups.py +++ b/rhodecode/tests/functional/test_admin_user_groups.py @@ -36,7 +36,7 @@ class TestAdminUsersGroupsController(Tes def test_index(self): self.log_user() response = self.app.get(url('users_groups')) - response.status_int == 200 + assert response.status_int == 200 def test_create(self): self.log_user() @@ -148,19 +148,21 @@ class TestAdminUsersGroupsController(Tes fixture.destroy_user_group(users_group_name) - def test_edit(self): + def test_edit_autocomplete(self): self.log_user() ug = fixture.create_user_group(TEST_USER_GROUP, skip_if_exists=True) response = self.app.get( url('edit_users_group', user_group_id=ug.users_group_id)) fixture.destroy_user_group(TEST_USER_GROUP) - def test_edit_user_group_members(self): + def test_edit_user_group_autocomplete_members(self, xhr_header): self.log_user() ug = fixture.create_user_group(TEST_USER_GROUP, skip_if_exists=True) response = self.app.get( - url('edit_user_group_members', user_group_id=ug.users_group_id)) - response.mustcontain('No members yet') + url('edit_user_group_members', user_group_id=ug.users_group_id), + extra_environ=xhr_header) + + assert response.body == '{"members": []}' fixture.destroy_user_group(TEST_USER_GROUP) def test_usergroup_escape(self): @@ -181,7 +183,7 @@ class TestAdminUsersGroupsController(Tes 'csrf_token': self.csrf_token } - response = self.app.post(url('users_groups'), data) + self.app.post(url('users_groups'), data) response = self.app.get(url('users_groups')) response.mustcontain( @@ -190,3 +192,42 @@ class TestAdminUsersGroupsController(Tes response.mustcontain( '<img src="/image2" onload="' 'alert('Hello, World!');">') + + def test_update_members_from_user_ids(self, user_regular): + uid = user_regular.user_id + username = user_regular.username + self.log_user() + + user_group = fixture.create_user_group('test_gr_ids') + assert user_group.members == [] + assert user_group.user != user_regular + expected_active_state = not user_group.users_group_active + + form_data = [ + ('csrf_token', self.csrf_token), + ('_method', 'put'), + ('user', username), + ('users_group_name', 'changed_name'), + ('users_group_active', expected_active_state), + ('user_group_description', 'changed_description'), + + ('__start__', 'user_group_members:sequence'), + ('__start__', 'member:mapping'), + ('member_user_id', uid), + ('type', 'existing'), + ('__end__', 'member:mapping'), + ('__end__', 'user_group_members:sequence'), + ] + ugid = user_group.users_group_id + self.app.post(url('update_users_group', user_group_id=ugid), form_data) + + user_group = UserGroup.get(ugid) + assert user_group + + assert user_group.members[0].user_id == uid + assert user_group.user_id == uid + assert 'changed_name' in user_group.users_group_name + assert 'changed_description' in user_group.user_group_description + assert user_group.users_group_active == expected_active_state + + fixture.destroy_user_group(user_group) diff --git a/rhodecode/tests/models/test_user_groups.py b/rhodecode/tests/models/test_user_groups.py --- a/rhodecode/tests/models/test_user_groups.py +++ b/rhodecode/tests/models/test_user_groups.py @@ -113,40 +113,20 @@ def test_add_and_remove_user_from_group( assert user_group.members == [] -@pytest.mark.parametrize( - 'data, expected', [ - ("1", [1]), (["1", "2"], [1, 2]) - ] -) +@pytest.mark.parametrize('data, expected', [ + ([], []), + ([{"member_user_id": 1, "type": "new"}], [1]), + ([{"member_user_id": 1, "type": "new"}, + {"member_user_id": 1, "type": "existing"}], [1]), + ([{"member_user_id": 1, "type": "new"}, + {"member_user_id": 2, "type": "new"}, + {"member_user_id": 3, "type": "remove"}], [1, 2]) +]) def test_clean_members_data(data, expected): cleaned = UserGroupModel()._clean_members_data(data) assert cleaned == expected -def test_update_members_from_user_ids(user_regular, user_util): - user_group = user_util.create_user_group() - assert user_group.members == [] - assert user_group.user != user_regular - expected_active_state = not user_group.users_group_active - - form_data = { - 'users_group_members': str(user_regular.user_id), - 'user': str(user_regular.username), - 'users_group_name': 'changed_name', - 'users_group_active': expected_active_state, - 'user_group_description': 'changed_description' - } - - UserGroupModel().update(user_group, form_data) - assert user_group.members[0].user_id == user_regular.user_id - assert user_group.user_id == user_regular.user_id - assert 'changed_name' in user_group.users_group_name - assert 'changed_description' in user_group.user_group_description - assert user_group.users_group_active == expected_active_state - # Ignore changes on the test - Session().rollback() - - def _create_test_members(): members = [] for member_number in range(3): diff --git a/rhodecode/tests/plugin.py b/rhodecode/tests/plugin.py --- a/rhodecode/tests/plugin.py +++ b/rhodecode/tests/plugin.py @@ -38,6 +38,7 @@ import requests from webtest.app import TestApp import rhodecode +from rhodecode.lib.utils2 import AttributeDict from rhodecode.model.changeset_status import ChangesetStatusModel from rhodecode.model.comment import ChangesetCommentsModel from rhodecode.model.db import (