diff --git a/rhodecode/apps/admin/__init__.py b/rhodecode/apps/admin/__init__.py
--- a/rhodecode/apps/admin/__init__.py
+++ b/rhodecode/apps/admin/__init__.py
@@ -162,6 +162,19 @@ def admin_routes(config):
name='edit_user_audit_logs',
pattern='/users/{user_id:\d+}/edit/audit')
+ # user groups admin
+ config.add_route(
+ name='user_groups',
+ pattern='/user_groups')
+
+ config.add_route(
+ name='user_groups_data',
+ pattern='/user_groups_data')
+
+ config.add_route(
+ name='user_group_members_data',
+ pattern='/user_groups/{user_group_id:\d+}/members')
+
def includeme(config):
settings = config.get_settings()
diff --git a/rhodecode/apps/admin/tests/test_admin_user_groups.py b/rhodecode/apps/admin/tests/test_admin_user_groups.py
new file mode 100644
--- /dev/null
+++ b/rhodecode/apps/admin/tests/test_admin_user_groups.py
@@ -0,0 +1,113 @@
+# -*- coding: utf-8 -*-
+
+# Copyright (C) 2010-2017 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/
+
+import pytest
+
+from rhodecode.model.db import UserGroup, User
+from rhodecode.model.meta import Session
+
+from rhodecode.tests import (
+ TestController, TEST_USER_REGULAR_LOGIN, assert_session_flash)
+from rhodecode.tests.fixture import Fixture
+
+fixture = Fixture()
+
+
+def route_path(name, params=None, **kwargs):
+ import urllib
+ from rhodecode.apps._base import ADMIN_PREFIX
+
+ base_url = {
+ 'user_groups': ADMIN_PREFIX + '/user_groups',
+ 'user_groups_data': ADMIN_PREFIX + '/user_groups_data',
+ 'user_group_members_data': ADMIN_PREFIX + '/user_groups/{user_group_id}/members',
+ }[name].format(**kwargs)
+
+ if params:
+ base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
+ return base_url
+
+
+class TestAdminUserGroupsView(TestController):
+
+ def test_show_users(self):
+ self.log_user()
+ self.app.get(route_path('user_groups'))
+
+ def test_show_user_groups_data(self, xhr_header):
+ self.log_user()
+ response = self.app.get(route_path(
+ 'user_groups_data'), extra_environ=xhr_header)
+
+ all_user_groups = UserGroup.query().count()
+ assert response.json['recordsTotal'] == all_user_groups
+
+ def test_show_user_groups_data_filtered(self, xhr_header):
+ self.log_user()
+ response = self.app.get(route_path(
+ 'user_groups_data', params={'search[value]': 'empty_search'}),
+ extra_environ=xhr_header)
+
+ all_user_groups = UserGroup.query().count()
+ assert response.json['recordsTotal'] == all_user_groups
+ assert response.json['recordsFiltered'] == 0
+
+ def test_usergroup_escape(self, user_util, xhr_header):
+ self.log_user()
+
+ xss_img = ''
+ user = user_util.create_user()
+ user.name = xss_img
+ user.lastname = xss_img
+ Session().add(user)
+ Session().commit()
+
+ user_group = user_util.create_user_group()
+
+ user_group.users_group_name = xss_img
+ user_group.user_group_description = 'DESC'
+
+ response = self.app.get(
+ route_path('user_groups_data'), extra_environ=xhr_header)
+
+ response.mustcontain(
+ '<strong onload="alert();">DESC</strong>')
+ response.mustcontain(
+ '<img src="/image1" onload="'
+ 'alert('Hello, World!');">')
+
+ def test_edit_user_group_autocomplete_empty_members(self, xhr_header, user_util):
+ self.log_user()
+ ug = user_util.create_user_group()
+ response = self.app.get(
+ route_path('user_group_members_data', user_group_id=ug.users_group_id),
+ extra_environ=xhr_header)
+
+ assert response.json == {'members': []}
+
+ def test_edit_user_group_autocomplete_members(self, xhr_header, user_util):
+ self.log_user()
+ members = [u.user_id for u in User.get_all()]
+ ug = user_util.create_user_group(members=members)
+ response = self.app.get(
+ route_path('user_group_members_data', user_group_id=ug.users_group_id),
+ extra_environ=xhr_header)
+
+ assert len(response.json['members']) == len(members)
diff --git a/rhodecode/apps/admin/views/user_groups.py b/rhodecode/apps/admin/views/user_groups.py
new file mode 100644
--- /dev/null
+++ b/rhodecode/apps/admin/views/user_groups.py
@@ -0,0 +1,195 @@
+# -*- coding: utf-8 -*-
+
+# Copyright (C) 2016-2017 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/
+
+import logging
+import datetime
+
+from pyramid.httpexceptions import HTTPFound
+from pyramid.view import view_config
+
+from rhodecode.lib.helpers import Page
+from rhodecode.model.scm import UserGroupList
+from rhodecode_tools.lib.ext_json import json
+
+from rhodecode.apps._base import BaseAppView, DataGridAppView
+from rhodecode.lib.auth import (
+ LoginRequired, HasPermissionAllDecorator, CSRFRequired, NotAnonymous,
+ HasUserGroupPermissionAnyDecorator)
+from rhodecode.lib import helpers as h
+from rhodecode.lib.utils import PartialRenderer
+from rhodecode.lib.utils2 import safe_int, safe_unicode
+from rhodecode.model.auth_token import AuthTokenModel
+from rhodecode.model.user import UserModel
+from rhodecode.model.user_group import UserGroupModel
+from rhodecode.model.db import User, UserGroup, UserGroupMember, or_, count
+from rhodecode.model.meta import Session
+
+log = logging.getLogger(__name__)
+
+
+class AdminUserGroupsView(BaseAppView, DataGridAppView):
+
+ def load_default_context(self):
+ c = self._get_local_tmpl_context()
+ self._register_global_c(c)
+ return c
+
+ # permission check in data loading of
+ # `user_groups_list_data` via UserGroupList
+ @NotAnonymous()
+ @view_config(
+ route_name='user_groups', request_method='GET',
+ renderer='rhodecode:templates/admin/user_groups/user_groups.mako')
+ def user_groups_list(self):
+ c = self.load_default_context()
+ return self._get_template_context(c)
+
+ # permission check inside
+ @NotAnonymous()
+ @view_config(
+ route_name='user_groups_data', request_method='GET',
+ renderer='json_ext', xhr=True)
+ def user_groups_list_data(self):
+ column_map = {
+ 'active': 'users_group_active',
+ 'description': 'user_group_description',
+ 'members': 'members_total',
+ 'owner': 'user_username',
+ 'sync': 'group_data'
+ }
+ draw, start, limit = self._extract_chunk(self.request)
+ search_q, order_by, order_dir = self._extract_ordering(
+ self.request, column_map=column_map)
+
+ _render = PartialRenderer('data_table/_dt_elements.mako')
+
+ def user_group_name(user_group_id, user_group_name):
+ return _render("user_group_name", user_group_id, user_group_name)
+
+ def user_group_actions(user_group_id, user_group_name):
+ return _render("user_group_actions", user_group_id, user_group_name)
+
+ def user_profile(username):
+ return _render('user_profile', username)
+
+ user_groups_data_total_count = UserGroup.query().count()
+
+ member_count = count(UserGroupMember.user_id)
+ base_q = Session.query(
+ UserGroup.users_group_name,
+ UserGroup.user_group_description,
+ UserGroup.users_group_active,
+ UserGroup.users_group_id,
+ UserGroup.group_data,
+ User,
+ member_count.label('member_count')
+ ) \
+ .outerjoin(UserGroupMember) \
+ .join(User, User.user_id == UserGroup.user_id) \
+ .group_by(UserGroup, User)
+
+ if search_q:
+ like_expression = u'%{}%'.format(safe_unicode(search_q))
+ base_q = base_q.filter(or_(
+ UserGroup.users_group_name.ilike(like_expression),
+ ))
+
+ user_groups_data_total_filtered_count = base_q.count()
+
+ if order_by == 'members_total':
+ sort_col = member_count
+ elif order_by == 'user_username':
+ sort_col = User.username
+ else:
+ sort_col = getattr(UserGroup, order_by, None)
+
+ if isinstance(sort_col, count) or sort_col:
+ if order_dir == 'asc':
+ sort_col = sort_col.asc()
+ else:
+ sort_col = sort_col.desc()
+
+ base_q = base_q.order_by(sort_col)
+ base_q = base_q.offset(start).limit(limit)
+
+ # authenticated access to user groups
+ user_group_list = base_q.all()
+ auth_user_group_list = UserGroupList(
+ user_group_list, perm_set=['usergroup.admin'])
+
+ user_groups_data = []
+ for user_gr in auth_user_group_list:
+ user_groups_data.append({
+ "users_group_name": user_group_name(
+ user_gr.users_group_id, h.escape(user_gr.users_group_name)),
+ "name_raw": h.escape(user_gr.users_group_name),
+ "description": h.escape(user_gr.user_group_description),
+ "members": user_gr.member_count,
+ # NOTE(marcink): because of advanced query we
+ # need to load it like that
+ "sync": UserGroup._load_group_data(
+ user_gr.group_data).get('extern_type'),
+ "active": h.bool2icon(user_gr.users_group_active),
+ "owner": user_profile(user_gr.User.username),
+ "action": user_group_actions(
+ user_gr.users_group_id, user_gr.users_group_name)
+ })
+
+ data = ({
+ 'draw': draw,
+ 'data': user_groups_data,
+ 'recordsTotal': user_groups_data_total_count,
+ 'recordsFiltered': user_groups_data_total_filtered_count,
+ })
+
+ return data
+
+ @LoginRequired()
+ @HasUserGroupPermissionAnyDecorator('usergroup.admin')
+ @view_config(
+ route_name='user_group_members_data', request_method='GET',
+ renderer='json_ext', xhr=True)
+ def user_group_members(self):
+ """
+ Return members of given user group
+ """
+ user_group_id = self.request.matchdict['user_group_id']
+ 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 = [
+ {
+ 'id': user.user_id,
+ 'first_name': user.first_name,
+ 'last_name': user.last_name,
+ '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
+ ]
+
+ return {
+ 'members': group_members
+ }
diff --git a/rhodecode/apps/admin/views/users.py b/rhodecode/apps/admin/views/users.py
--- a/rhodecode/apps/admin/views/users.py
+++ b/rhodecode/apps/admin/views/users.py
@@ -78,8 +78,13 @@ class AdminUsersView(BaseAppView, DataGr
route_name='users_data', request_method='GET',
renderer='json_ext', xhr=True)
def users_list_data(self):
+ column_map = {
+ 'first_name': 'name',
+ 'last_name': 'lastname',
+ }
draw, start, limit = self._extract_chunk(self.request)
- search_q, order_by, order_dir = self._extract_ordering(self.request)
+ search_q, order_by, order_dir = self._extract_ordering(
+ self.request, column_map=column_map)
_render = self.request.get_partial_renderer(
'data_table/_dt_elements.mako')
diff --git a/rhodecode/config/routing.py b/rhodecode/config/routing.py
--- a/rhodecode/config/routing.py
+++ b/rhodecode/config/routing.py
@@ -279,8 +279,6 @@ def make_map(config):
controller='admin/user_groups') as m:
m.connect('users_groups', '/user_groups',
action='create', conditions={'method': ['POST']})
- m.connect('users_groups', '/user_groups',
- action='index', conditions={'method': ['GET']})
m.connect('new_users_group', '/user_groups/new',
action='new', conditions={'method': ['GET']})
m.connect('update_users_group', '/user_groups/{user_group_id}',
@@ -317,10 +315,6 @@ def make_map(config):
'/user_groups/{user_group_id}/edit/advanced/sync',
action='edit_advanced_set_synchronization', conditions={'method': ['POST']})
- m.connect('edit_user_group_members',
- '/user_groups/{user_group_id}/edit/members', jsroute=True,
- action='user_group_members', conditions={'method': ['GET']})
-
# ADMIN DEFAULTS REST ROUTES
with rmap.submapper(path_prefix=ADMIN_PREFIX,
controller='admin/defaults') as m:
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
@@ -103,42 +103,6 @@ class UserGroupsController(BaseControlle
return True
return False
- # permission check inside
- @NotAnonymous()
- def index(self):
- # TODO(marcink): remove bind to self.request after pyramid migration
- self.request = c.pyramid_request
- _render = self.request.get_partial_renderer(
- 'data_table/_dt_elements.mako')
-
- def user_group_name(user_group_id, user_group_name):
- return _render("user_group_name", user_group_id, user_group_name)
-
- def user_group_actions(user_group_id, user_group_name):
- return _render("user_group_actions", user_group_id, user_group_name)
-
- # json generate
- group_iter = UserGroupList(UserGroup.query().all(),
- perm_set=['usergroup.admin'])
-
- user_groups_data = []
- for user_gr in group_iter:
- user_groups_data.append({
- "group_name": user_group_name(
- user_gr.users_group_id, h.escape(user_gr.users_group_name)),
- "group_name_raw": user_gr.users_group_name,
- "desc": h.escape(user_gr.user_group_description),
- "members": len(user_gr.members),
- "sync": user_gr.group_data.get('extern_type'),
- "active": h.bool2icon(user_gr.users_group_active),
- "owner": h.escape(h.link_to_user(user_gr.user.username)),
- "action": user_group_actions(
- user_gr.users_group_id, user_gr.users_group_name)
- })
-
- c.data = json.dumps(user_groups_data)
- return render('admin/user_groups/user_groups.mako')
-
@HasPermissionAnyDecorator('hg.admin', 'hg.usergroup.create.true')
@auth.CSRFRequired()
def create(self):
@@ -482,33 +446,3 @@ class UserGroupsController(BaseControlle
return redirect(
url('edit_user_group_advanced', user_group_id=user_group_id))
- @HasUserGroupPermissionAnyDecorator('usergroup.admin')
- @XHRRequired()
- @jsonify
- def user_group_members(self, user_group_id):
- """
- Return members of given user group
- """
- user_group_id = safe_int(user_group_id)
- 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 = [
- {
- 'id': user.user_id,
- 'first_name': user.first_name,
- 'last_name': user.last_name,
- '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
- ]
-
- return {
- 'members': group_members
- }
diff --git a/rhodecode/lib/utils.py b/rhodecode/lib/utils.py
--- a/rhodecode/lib/utils.py
+++ b/rhodecode/lib/utils.py
@@ -132,9 +132,9 @@ def get_user_group_slug(request):
if _group:
_group = _group.users_group_name
except Exception:
- log.debug(traceback.format_exc())
+ log.exception('Failed to get user group by id')
# catch all failures here
- pass
+ return None
return _group
diff --git a/rhodecode/model/db.py b/rhodecode/model/db.py
--- a/rhodecode/model/db.py
+++ b/rhodecode/model/db.py
@@ -41,6 +41,7 @@ from sqlalchemy.ext.hybrid import hybrid
from sqlalchemy.orm import (
relationship, joinedload, class_mapper, validates, aliased)
from sqlalchemy.sql.expression import true
+from sqlalchemy.sql.functions import coalesce, count # noqa
from beaker.cache import cache_region
from zope.cachedescriptors.property import Lazy as LazyProperty
@@ -1205,7 +1206,17 @@ class UserGroup(Base, BaseModel):
user_user_group_to_perm = relationship('UserUserGroupToPerm', cascade='all')
user_group_user_group_to_perm = relationship('UserGroupUserGroupToPerm ', primaryjoin="UserGroupUserGroupToPerm.target_user_group_id==UserGroup.users_group_id", cascade='all')
- user = relationship('User')
+ user = relationship('User', primaryjoin="User.user_id==UserGroup.user_id")
+
+ @classmethod
+ def _load_group_data(cls, column):
+ if not column:
+ return {}
+
+ try:
+ return json.loads(column) or {}
+ except TypeError:
+ return {}
@hybrid_property
def description_safe(self):
@@ -1214,13 +1225,11 @@ class UserGroup(Base, BaseModel):
@hybrid_property
def group_data(self):
- if not self._group_data:
- return {}
-
- try:
- return json.loads(self._group_data)
- except TypeError:
- return {}
+ return self._load_group_data(self._group_data)
+
+ @group_data.expression
+ def group_data(self, **kwargs):
+ return self._group_data
@group_data.setter
def group_data(self, val):
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
@@ -14,7 +14,6 @@ function registerRCRoutes() {
// routes registration
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('favicon', '/favicon.ico', []);
pyroutes.register('robots', '/robots.txt', []);
pyroutes.register('auth_home', '/_admin/auth*traverse', []);
@@ -72,6 +71,9 @@ function registerRCRoutes() {
pyroutes.register('edit_user_groups_management', '/_admin/users/%(user_id)s/edit/groups_management', ['user_id']);
pyroutes.register('edit_user_groups_management_updates', '/_admin/users/%(user_id)s/edit/edit_user_groups_management/updates', ['user_id']);
pyroutes.register('edit_user_audit_logs', '/_admin/users/%(user_id)s/edit/audit', ['user_id']);
+ pyroutes.register('user_groups', '/_admin/user_groups', []);
+ pyroutes.register('user_groups_data', '/_admin/user_groups_data', []);
+ pyroutes.register('user_group_members_data', '/_admin/user_groups/%(user_group_id)s/members', ['user_group_id']);
pyroutes.register('channelstream_connect', '/_admin/channelstream/connect', []);
pyroutes.register('channelstream_subscribe', '/_admin/channelstream/subscribe', []);
pyroutes.register('channelstream_proxy', '/_channelstream', []);
diff --git a/rhodecode/templates/admin/user_groups/user_group_edit_settings.mako b/rhodecode/templates/admin/user_groups/user_group_edit_settings.mako
--- a/rhodecode/templates/admin/user_groups/user_group_edit_settings.mako
+++ b/rhodecode/templates/admin/user_groups/user_group_edit_settings.mako
@@ -166,7 +166,7 @@
if (suggestion.value_type == 'user_group') {
$.getJSON(
- pyroutes.url('edit_user_group_members',
+ pyroutes.url('user_group_members_data',
{'user_group_id': suggestion.id}),
function(data) {
$.each(data.members, function(idx, user) {
diff --git a/rhodecode/templates/admin/user_groups/user_groups.mako b/rhodecode/templates/admin/user_groups/user_groups.mako
--- a/rhodecode/templates/admin/user_groups/user_groups.mako
+++ b/rhodecode/templates/admin/user_groups/user_groups.mako
@@ -38,26 +38,31 @@