Switch to side-by-side view

--- a/Allura/allura/ext/admin/admin_main.py
+++ b/Allura/allura/ext/admin/admin_main.py
@@ -24,6 +24,8 @@
 from allura.lib import plugin
 from allura.controllers import BaseController
 
+from . import widgets as aw
+
 log = logging.getLogger(__name__)
 
 class W:
@@ -32,6 +34,10 @@
     mount_delete = ffw.Lightbox(name='mount_delete',trigger='a.mount_delete')
     admin_modal = ffw.Lightbox(name='admin_modal',trigger='a.admin_modal')
     install_modal = ffw.Lightbox(name='install_modal',trigger='a.install_trig')
+    group_card = aw.GroupCard()
+    permission_card = aw.PermissionCard()
+    group_settings = aw.GroupSettings()
+    new_group_settings = aw.NewGroupSettings()
 
 class AdminWidgets(WidgetController):
     widgets=['users', 'tool_status']
@@ -114,17 +120,17 @@
         admin_url = c.project.url()+'admin/'
         if len(links):
             links.append(SitemapEntry('Project'))
-        links = links + [SitemapEntry('Summary', admin_url+'overview', className='nav_child'),
-                         SitemapEntry('Homepage', admin_url+'homepage', className='nav_child'),
-                         SitemapEntry('Tools', admin_url+'tools', className='nav_child')]
+        links += [
+            SitemapEntry('Summary', admin_url+'overview', className='nav_child'),
+            SitemapEntry('Homepage', admin_url+'homepage', className='nav_child')
+            ]
+        if has_project_access('security')():
+            links.append(SitemapEntry('Permissions', admin_url+'permissions', className='nav_child'))
+        links.append(SitemapEntry('Tools', admin_url+'tools', className='nav_child'))
+        if has_project_access('security')():
+            links.append(SitemapEntry('Usergroups', admin_url+'groups', className='nav_child'))
         if len(c.project.neighborhood_invitations):
             links.append(SitemapEntry('Invitation(s)', admin_url+'invitations', className='nav_child'))
-        if has_project_access('security')():
-            links.append(SitemapEntry('Permissions', admin_url+'perms', className='nav_child'))
-        # if has_project_access('security')():
-        #     links.append(SitemapEntry('Permissions', admin_url+'permissions', className='nav_child'))
-        # if c.project.is_root and has_project_access('security')():
-        #     links.append(SitemapEntry('Roles', admin_url+'roles', className='nav_child'))
         return links
 
     def admin_menu(self):
@@ -141,6 +147,10 @@
     def _check_security(self):
         require(has_project_access('read'),
                 'Read access required')
+
+    def __init__(self):
+        self.permissions = PermissionsController()
+        self.groups = GroupsController()
 
     @with_trailing_slash
     @expose('jinja:project_admin.html')
@@ -190,22 +200,9 @@
             users=[M.User.query.get(_id=id) for id in c.project.acl.read ])
 
     @without_trailing_slash
-    @expose('jinja:project_perms.html')
-    def perms(self, **kw):
-        """Simplified permission management screen for the project"""
-        c.user_select = ffw.ProjectUserSelect()
+    @expose('jinja:project_permissions.html')
+    def groups(self, **kw):
         return dict()
-
-    @without_trailing_slash
-    @expose('jinja:project_permissions.html')
-    def permissions(self):
-        """Advanced permission management screen for the project. Currently not linked to anywhere."""
-        return dict()
-
-    @without_trailing_slash
-    @expose('jinja:project_roles.html')
-    def roles(self):
-        return dict(roles=M.ProjectRole.query.find(dict(project_id=c.project.root_project._id)).sort('_id').all())
 
     @expose()
     def _lookup(self, name, *remainder):
@@ -455,6 +452,140 @@
         g.publish('react', 'forge.project_updated')
         redirect('perms')
 
+class PermissionsController(BaseController):
+
+    def _check_security(self):
+        require(has_project_access('security'),
+                'Security access required')
+
+    @with_trailing_slash
+    @expose('jinja:project_permissions.html')
+    def index(self, **kw):
+        c.admin_modal = W.admin_modal
+        c.card = W.permission_card
+        return dict(permissions=c.project.acl)
+
+    @without_trailing_slash
+    @expose()
+    @h.vardec
+    def update(self, card=None, **kw):
+        for args in card:
+            perm = args['id']
+            new_group_ids = args.get('new', [])
+            group_ids = args.get('value', [])
+            if isinstance(new_group_ids, basestring):
+                new_group_ids = [ new_group_ids ]
+            if isinstance(group_ids, basestring):
+                group_ids = [ group_ids ]
+            role_ids = map(ObjectId, group_ids + new_group_ids)
+            roles = M.ProjectRole.query.find(dict(
+                _id={'$in':role_ids},
+                project_id=c.project._id))
+            c.project.acl[perm] = [ r._id for r in roles ]
+        redirect('.')
+
+class GroupsController(BaseController):
+
+    def _check_security(self):
+        require(has_project_access('security'),
+                'Security access required')
+
+    @with_trailing_slash
+    @expose('jinja:project_groups.html')
+    def index(self, **kw):
+        c.admin_modal = W.admin_modal
+        c.card = W.group_card
+        roles = list(sorted(
+            (role for role in c.project.roles if role.name and role.name[0] != '*'),
+            key=lambda r:r.name))
+        roles.append(None)
+        return dict(roles=roles)
+
+    @without_trailing_slash
+    @expose()
+    @h.vardec
+    def update(self, card=None, **kw):
+        for pr in card:
+            group = M.ProjectRole.query.get(_id=ObjectId(pr['id']))
+            assert group.project == c.project, 'Security violation'
+            user_ids = pr.get('value', [])
+            new_users = pr.get('new', [])
+            if isinstance(user_ids, basestring):
+                user_ids = [ user_ids ]
+            if isinstance(new_users, basestring):
+                new_users = [ new_users ]
+            # Handle new users in groups
+            for username in new_users:
+                user = M.User.by_username(username)
+                if not user:
+                    flash('User %s not found' % pr['new'], 'error')
+                    redirect('.')
+                user.project_role().roles.append(group._id)
+            # Handle users removed from groups
+            user_ids = set(map(ObjectId, user_ids))
+            for role in M.ProjectRole.query.find(
+                dict(user_id={'$ne':None}, roles=group._id)):
+                if role.user_id not in user_ids:
+                    role.roles.remove(group._id)
+        redirect('.')
+
+    @without_trailing_slash
+    @expose('jinja:project_group.html')
+    def new(self):
+        c.form = W.new_group_settings
+        return dict(
+            group=None,
+            show_settings=True,
+            action="create")
+
+    @expose()
+    @validate(W.new_group_settings)
+    @h.vardec
+    def create(self, name=None, **kw):
+        if M.ProjectRole.by_name(name):
+            flash('%s already exists', 'error')
+        else:
+            M.ProjectRole(project_id=c.project._id, name=name)
+        redirect('.')
+
+    @expose()
+    def _lookup(self, name, *remainder):
+        return GroupController(name), remainder
+
+class GroupController(BaseController):
+
+    def __init__(self, name):
+        self._group = M.ProjectRole.query.get(_id=ObjectId(name))
+
+    @with_trailing_slash
+    @expose('jinja:project_group.html')
+    def index(self):
+        if self._group.name in ('Admin', 'Developer', 'Member'):
+            show_settings = False
+        else:
+            show_settings = True
+        c.form = W.group_settings
+        return dict(
+            group=self._group,
+            show_settings=show_settings,
+            action=self._group.settings_href + 'update')
+
+    @expose()
+    @h.vardec
+    @validate(W.group_settings)
+    def update(self, _id=None, delete=None, name=None, **kw):
+        pr = M.ProjectRole.by_name(name)
+        if pr and pr._id != _id._id:
+            flash('%s already exists' % name, 'error')
+            redirect('..')
+        if delete:
+            _id.delete()
+            flash('%s deleted' % name)
+            redirect('..')
+        _id.name = name
+        flash('%s updated' % name)
+        redirect('..')
+
 class AdminAppAdminController(DefaultAdminController):
     '''Administer the admin app'''
     pass