diff --git a/rhodecode/controllers/admin/repo_groups.py b/rhodecode/controllers/admin/repo_groups.py
new file mode 100644
--- /dev/null
+++ b/rhodecode/controllers/admin/repo_groups.py
@@ -0,0 +1,471 @@
+# -*- coding: utf-8 -*-
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# 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 General Public License
+# along with this program. If not, see .
+"""
+rhodecode.controllers.admin.repo_groups
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Repository groups controller for RhodeCode
+
+:created_on: Mar 23, 2010
+:author: marcink
+:copyright: (c) 2013 RhodeCode GmbH.
+:license: GPLv3, see LICENSE for more details.
+"""
+
+import logging
+import traceback
+import formencode
+import itertools
+
+from formencode import htmlfill
+
+from pylons import request, tmpl_context as c, url
+from pylons.controllers.util import abort, redirect
+from pylons.i18n.translation import _, ungettext
+
+from sqlalchemy.exc import IntegrityError
+
+import rhodecode
+from rhodecode.lib import helpers as h
+from rhodecode.lib.compat import json
+from rhodecode.lib.auth import LoginRequired, HasPermissionAnyDecorator,\
+ HasRepoGroupPermissionAnyDecorator, HasRepoGroupPermissionAll,\
+ HasPermissionAll
+from rhodecode.lib.base import BaseController, render
+from rhodecode.model.db import RepoGroup, Repository
+from rhodecode.model.scm import RepoGroupList
+from rhodecode.model.repo_group import RepoGroupModel
+from rhodecode.model.forms import RepoGroupForm, RepoGroupPermsForm
+from rhodecode.model.meta import Session
+from rhodecode.model.repo import RepoModel
+from webob.exc import HTTPInternalServerError, HTTPNotFound
+from rhodecode.lib.utils2 import str2bool, safe_int
+from sqlalchemy.sql.expression import func
+
+
+log = logging.getLogger(__name__)
+
+
+class RepoGroupsController(BaseController):
+ """REST Controller styled on the Atom Publishing Protocol"""
+
+ @LoginRequired()
+ def __before__(self):
+ super(RepoGroupsController, self).__before__()
+
+ def __load_defaults(self, allow_empty_group=False, exclude_group_ids=[]):
+ if HasPermissionAll('hg.admin')('group edit'):
+ #we're global admin, we're ok and we can create TOP level groups
+ allow_empty_group = True
+
+ #override the choices for this form, we need to filter choices
+ #and display only those we have ADMIN right
+ groups_with_admin_rights = RepoGroupList(RepoGroup.query().all(),
+ perm_set=['group.admin'])
+ c.repo_groups = RepoGroup.groups_choices(groups=groups_with_admin_rights,
+ show_empty_group=allow_empty_group)
+ # exclude filtered ids
+ c.repo_groups = filter(lambda x: x[0] not in exclude_group_ids,
+ c.repo_groups)
+ c.repo_groups_choices = map(lambda k: unicode(k[0]), c.repo_groups)
+ repo_model = RepoModel()
+ c.users_array = repo_model.get_users_js()
+ c.user_groups_array = repo_model.get_user_groups_js()
+
+ def __load_data(self, group_id):
+ """
+ Load defaults settings for edit, and update
+
+ :param group_id:
+ """
+ repo_group = RepoGroup.get_or_404(group_id)
+ data = repo_group.get_dict()
+ data['group_name'] = repo_group.name
+
+ # fill repository group users
+ for p in repo_group.repo_group_to_perm:
+ data.update({'u_perm_%s' % p.user.username:
+ p.permission.permission_name})
+
+ # fill repository group groups
+ for p in repo_group.users_group_to_perm:
+ data.update({'g_perm_%s' % p.users_group.users_group_name:
+ p.permission.permission_name})
+
+ return data
+
+ def _revoke_perms_on_yourself(self, form_result):
+ _up = filter(lambda u: c.rhodecode_user.username == u[0],
+ form_result['perms_updates'])
+ _new = filter(lambda u: c.rhodecode_user.username == u[0],
+ form_result['perms_new'])
+ if _new and _new[0][1] != 'group.admin' or _up and _up[0][1] != 'group.admin':
+ return True
+ return False
+
+ def index(self, format='html'):
+ """GET /repo_groups: All items in the collection"""
+ # url('repos_groups')
+ _list = RepoGroup.query()\
+ .order_by(func.lower(RepoGroup.group_name))\
+ .all()
+ group_iter = RepoGroupList(_list, perm_set=['group.admin'])
+ repo_groups_data = []
+ total_records = len(group_iter)
+ _tmpl_lookup = rhodecode.CONFIG['pylons.app_globals'].mako_lookup
+ template = _tmpl_lookup.get_template('data_table/_dt_elements.html')
+
+ repo_group_name = lambda repo_group_name, children_groups: (
+ template.get_def("repo_group_name")
+ .render(repo_group_name, children_groups, _=_, h=h, c=c)
+ )
+ repo_group_actions = lambda repo_group_id, repo_group_name, gr_count: (
+ template.get_def("repo_group_actions")
+ .render(repo_group_id, repo_group_name, gr_count, _=_, h=h, c=c,
+ ungettext=ungettext)
+ )
+
+ for repo_gr in group_iter:
+ children_groups = map(h.safe_unicode,
+ itertools.chain((g.name for g in repo_gr.parents),
+ (x.name for x in [repo_gr])))
+ repo_count = repo_gr.repositories.count()
+ repo_groups_data.append({
+ "raw_name": repo_gr.group_name,
+ "group_name": repo_group_name(repo_gr.group_name, children_groups),
+ "desc": repo_gr.group_description,
+ "repos": repo_count,
+ "owner": h.person(repo_gr.user.username),
+ "action": repo_group_actions(repo_gr.group_id, repo_gr.group_name,
+ repo_count)
+ })
+
+ c.data = json.dumps({
+ "totalRecords": total_records,
+ "startIndex": 0,
+ "sort": None,
+ "dir": "asc",
+ "records": repo_groups_data
+ })
+
+ return render('admin/repo_groups/repo_groups.html')
+
+ def create(self):
+ """POST /repo_groups: Create a new item"""
+ # url('repos_groups')
+
+ self.__load_defaults()
+
+ # permissions for can create group based on parent_id are checked
+ # here in the Form
+ repo_group_form = RepoGroupForm(available_groups=
+ map(lambda k: unicode(k[0]), c.repo_groups))()
+ try:
+ form_result = repo_group_form.to_python(dict(request.POST))
+ RepoGroupModel().create(
+ group_name=form_result['group_name'],
+ group_description=form_result['group_description'],
+ parent=form_result['group_parent_id'],
+ owner=self.rhodecode_user.user_id,
+ copy_permissions=form_result['group_copy_permissions']
+ )
+ Session().commit()
+ h.flash(_('Created repository group %s') \
+ % form_result['group_name'], category='success')
+ #TODO: in futureaction_logger(, '', '', '', self.sa)
+ except formencode.Invalid, errors:
+ return htmlfill.render(
+ render('admin/repo_groups/repo_group_add.html'),
+ defaults=errors.value,
+ errors=errors.error_dict or {},
+ prefix_error=False,
+ encoding="UTF-8")
+ except Exception:
+ log.error(traceback.format_exc())
+ h.flash(_('Error occurred during creation of repository group %s') \
+ % request.POST.get('group_name'), category='error')
+ parent_group_id = form_result['group_parent_id']
+ #TODO: maybe we should get back to the main view, not the admin one
+ return redirect(url('repos_groups', parent_group=parent_group_id))
+
+ def new(self):
+ """GET /repo_groups/new: Form to create a new item"""
+ # url('new_repos_group')
+ if HasPermissionAll('hg.admin')('group create'):
+ #we're global admin, we're ok and we can create TOP level groups
+ pass
+ else:
+ # we pass in parent group into creation form, thus we know
+ # what would be the group, we can check perms here !
+ group_id = safe_int(request.GET.get('parent_group'))
+ group = RepoGroup.get(group_id) if group_id else None
+ group_name = group.group_name if group else None
+ if HasRepoGroupPermissionAll('group.admin')(group_name, 'group create'):
+ pass
+ else:
+ return abort(403)
+
+ self.__load_defaults()
+ return render('admin/repo_groups/repo_group_add.html')
+
+ @HasRepoGroupPermissionAnyDecorator('group.admin')
+ def update(self, group_name):
+ """PUT /repo_groups/group_name: Update an existing item"""
+ # Forms posted to this method should contain a hidden field:
+ #
+ # Or using helpers:
+ # h.form(url('repos_group', group_name=GROUP_NAME),
+ # method='put')
+ # url('repos_group', group_name=GROUP_NAME)
+
+ c.repo_group = RepoGroupModel()._get_repo_group(group_name)
+ if HasPermissionAll('hg.admin')('group edit'):
+ #we're global admin, we're ok and we can create TOP level groups
+ allow_empty_group = True
+ elif not c.repo_group.parent_group:
+ allow_empty_group = True
+ else:
+ allow_empty_group = False
+ self.__load_defaults(allow_empty_group=allow_empty_group,
+ exclude_group_ids=[c.repo_group.group_id])
+
+ repo_group_form = RepoGroupForm(
+ edit=True,
+ old_data=c.repo_group.get_dict(),
+ available_groups=c.repo_groups_choices,
+ can_create_in_root=allow_empty_group,
+ )()
+ try:
+ form_result = repo_group_form.to_python(dict(request.POST))
+
+ new_gr = RepoGroupModel().update(group_name, form_result)
+ Session().commit()
+ h.flash(_('Updated repository group %s') \
+ % form_result['group_name'], category='success')
+ # we now have new name !
+ group_name = new_gr.group_name
+ #TODO: in future action_logger(, '', '', '', self.sa)
+ except formencode.Invalid, errors:
+
+ return htmlfill.render(
+ render('admin/repo_groups/repo_group_edit.html'),
+ defaults=errors.value,
+ errors=errors.error_dict or {},
+ prefix_error=False,
+ encoding="UTF-8")
+ except Exception:
+ log.error(traceback.format_exc())
+ h.flash(_('Error occurred during update of repository group %s') \
+ % request.POST.get('group_name'), category='error')
+
+ return redirect(url('edit_repo_group', group_name=group_name))
+
+ @HasRepoGroupPermissionAnyDecorator('group.admin')
+ def delete(self, group_name):
+ """DELETE /repo_groups/group_name: Delete an existing item"""
+ # Forms posted to this method should contain a hidden field:
+ #
+ # Or using helpers:
+ # h.form(url('repos_group', group_name=GROUP_NAME),
+ # method='delete')
+ # url('repos_group', group_name=GROUP_NAME)
+
+ gr = c.repo_group = RepoGroupModel()._get_repo_group(group_name)
+ repos = gr.repositories.all()
+ if repos:
+ h.flash(_('This group contains %s repositores and cannot be '
+ 'deleted') % len(repos), category='warning')
+ return redirect(url('repos_groups'))
+
+ children = gr.children.all()
+ if children:
+ h.flash(_('This group contains %s subgroups and cannot be deleted'
+ % (len(children))), category='warning')
+ return redirect(url('repos_groups'))
+
+ try:
+ RepoGroupModel().delete(group_name)
+ Session().commit()
+ h.flash(_('Removed repository group %s') % group_name,
+ category='success')
+ #TODO: in future action_logger(, '', '', '', self.sa)
+ except Exception:
+ log.error(traceback.format_exc())
+ h.flash(_('Error occurred during deletion of repository group %s')
+ % group_name, category='error')
+
+ return redirect(url('repos_groups'))
+
+ def show_by_name(self, group_name):
+ """
+ This is a proxy that does a lookup group_name -> id, and shows
+ the group by id view instead
+ """
+ group_name = group_name.rstrip('/')
+ id_ = RepoGroup.get_by_group_name(group_name)
+ if id_:
+ return self.show(group_name)
+ raise HTTPNotFound
+
+ @HasRepoGroupPermissionAnyDecorator('group.read', 'group.write',
+ 'group.admin')
+ def show(self, group_name):
+ """GET /repo_groups/group_name: Show a specific item"""
+ # url('repos_group', group_name=GROUP_NAME)
+ c.active = 'settings'
+
+ c.group = c.repo_group = RepoGroupModel()._get_repo_group(group_name)
+ c.group_repos = c.group.repositories.all()
+
+ #overwrite our cached list with current filter
+ gr_filter = c.group_repos
+ c.repo_cnt = 0
+
+ groups = RepoGroup.query().order_by(RepoGroup.group_name)\
+ .filter(RepoGroup.group_parent_id == c.group.group_id).all()
+ c.groups = self.scm_model.get_repo_groups(groups)
+
+ c.repos_list = Repository.query()\
+ .filter(Repository.group_id == c.group.group_id)\
+ .order_by(func.lower(Repository.repo_name))\
+ .all()
+
+ repos_data = RepoModel().get_repos_as_dict(repos_list=c.repos_list,
+ admin=False)
+ #json used to render the grid
+ c.data = json.dumps(repos_data)
+
+ return render('admin/repo_groups/repo_group_show.html')
+
+ @HasRepoGroupPermissionAnyDecorator('group.admin')
+ def edit(self, group_name):
+ """GET /repo_groups/group_name/edit: Form to edit an existing item"""
+ # url('edit_repo_group', group_name=GROUP_NAME)
+ c.active = 'settings'
+
+ c.repo_group = RepoGroupModel()._get_repo_group(group_name)
+ #we can only allow moving empty group if it's already a top-level
+ #group, ie has no parents, or we're admin
+ if HasPermissionAll('hg.admin')('group edit'):
+ #we're global admin, we're ok and we can create TOP level groups
+ allow_empty_group = True
+ elif not c.repo_group.parent_group:
+ allow_empty_group = True
+ else:
+ allow_empty_group = False
+
+ self.__load_defaults(allow_empty_group=allow_empty_group,
+ exclude_group_ids=[c.repo_group.group_id])
+ defaults = self.__load_data(c.repo_group.group_id)
+
+ return htmlfill.render(
+ render('admin/repo_groups/repo_group_edit.html'),
+ defaults=defaults,
+ encoding="UTF-8",
+ force_defaults=False
+ )
+
+ @HasRepoGroupPermissionAnyDecorator('group.admin')
+ def edit_repo_group_advanced(self, group_name):
+ """GET /repo_groups/group_name/edit: Form to edit an existing item"""
+ # url('edit_repo_group', group_name=GROUP_NAME)
+ c.active = 'advanced'
+ c.repo_group = RepoGroupModel()._get_repo_group(group_name)
+
+ return render('admin/repo_groups/repo_group_edit.html')
+
+ @HasRepoGroupPermissionAnyDecorator('group.admin')
+ def edit_repo_group_perms(self, group_name):
+ """GET /repo_groups/group_name/edit: Form to edit an existing item"""
+ # url('edit_repo_group', group_name=GROUP_NAME)
+ c.active = 'perms'
+ c.repo_group = RepoGroupModel()._get_repo_group(group_name)
+ self.__load_defaults()
+ defaults = self.__load_data(c.repo_group.group_id)
+
+ return htmlfill.render(
+ render('admin/repo_groups/repo_group_edit.html'),
+ defaults=defaults,
+ encoding="UTF-8",
+ force_defaults=False
+ )
+
+ @HasRepoGroupPermissionAnyDecorator('group.admin')
+ def update_perms(self, group_name):
+ """
+ Update permissions for given repository group
+
+ :param group_name:
+ """
+
+ c.repo_group = RepoGroupModel()._get_repo_group(group_name)
+ valid_recursive_choices = ['none', 'repos', 'groups', 'all']
+ form_result = RepoGroupPermsForm(valid_recursive_choices)().to_python(request.POST)
+ if not c.rhodecode_user.is_admin:
+ if self._revoke_perms_on_yourself(form_result):
+ msg = _('Cannot revoke permission for yourself as admin')
+ h.flash(msg, category='warning')
+ return redirect(url('edit_repo_group_perms', group_name=group_name))
+ recursive = form_result['recursive']
+ # iterate over all members(if in recursive mode) of this groups and
+ # set the permissions !
+ # this can be potentially heavy operation
+ RepoGroupModel()._update_permissions(c.repo_group,
+ form_result['perms_new'],
+ form_result['perms_updates'],
+ recursive)
+ #TODO: implement this
+ #action_logger(self.rhodecode_user, 'admin_changed_repo_permissions',
+ # repo_name, self.ip_addr, self.sa)
+ Session().commit()
+ h.flash(_('Repository Group permissions updated'), category='success')
+ return redirect(url('edit_repo_group_perms', group_name=group_name))
+
+ @HasRepoGroupPermissionAnyDecorator('group.admin')
+ def delete_perms(self, group_name):
+ """
+ DELETE an existing repository group permission user
+
+ :param group_name:
+ """
+ try:
+ obj_type = request.POST.get('obj_type')
+ obj_id = None
+ if obj_type == 'user':
+ obj_id = safe_int(request.POST.get('user_id'))
+ elif obj_type == 'user_group':
+ obj_id = safe_int(request.POST.get('user_group_id'))
+
+ if not c.rhodecode_user.is_admin:
+ if obj_type == 'user' and c.rhodecode_user.user_id == obj_id:
+ msg = _('Cannot revoke permission for yourself as admin')
+ h.flash(msg, category='warning')
+ raise Exception('revoke admin permission on self')
+ recursive = request.POST.get('recursive', 'none')
+ if obj_type == 'user':
+ RepoGroupModel().delete_permission(repo_group=group_name,
+ obj=obj_id, obj_type='user',
+ recursive=recursive)
+ elif obj_type == 'user_group':
+ RepoGroupModel().delete_permission(repo_group=group_name,
+ obj=obj_id,
+ obj_type='user_group',
+ recursive=recursive)
+
+ Session().commit()
+ except Exception:
+ log.error(traceback.format_exc())
+ h.flash(_('An error occurred during revoking of permission'),
+ category='error')
+ raise HTTPInternalServerError()