diff --git a/CHANGELOG b/CHANGELOG index 118e79ecf..f03480f19 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -26,6 +26,7 @@ Motions: Users: - Added field is_committee and new default group Committees. - Added field number. +- Added new matrix-interface for managing groups and their permissions. Other: - Removed config cache to support multiple threads or processes. diff --git a/openslides/agenda/static/js/agenda/base.js b/openslides/agenda/static/js/agenda/base.js index e597b41d5..3d4739338 100644 --- a/openslides/agenda/static/js/agenda/base.js +++ b/openslides/agenda/static/js/agenda/base.js @@ -28,11 +28,13 @@ angular.module('OpenSlidesApp.agenda', ['OpenSlidesApp.users']) 'jsDataModel', 'Projector', 'gettextCatalog', - function($http, DS, Speaker, jsDataModel, Projector, gettextCatalog) { + 'gettext', + function($http, DS, Speaker, jsDataModel, Projector, gettextCatalog, gettext) { var name = 'agenda/item'; return DS.defineResource({ name: name, useClass: jsDataModel, + verboseName: gettext('Agenda'), methods: { getResourceName: function () { return name; diff --git a/openslides/assignments/static/js/assignments/base.js b/openslides/assignments/static/js/assignments/base.js index 5c8b95b6a..cd1a35c5c 100644 --- a/openslides/assignments/static/js/assignments/base.js +++ b/openslides/assignments/static/js/assignments/base.js @@ -167,6 +167,7 @@ angular.module('OpenSlidesApp.assignments', []) name: name, useClass: jsDataModel, verboseName: gettext('Election'), + verboseNamePlural: gettext('Elections'), phases: phases, getPhases: function () { if (!this.phases) { diff --git a/openslides/core/static/css/app.css b/openslides/core/static/css/app.css index f6d2974d4..37ae61dbc 100644 --- a/openslides/core/static/css/app.css +++ b/openslides/core/static/css/app.css @@ -671,6 +671,42 @@ img { content: ":"; } +/* group list */ +#groups-table { + table-layout: fixed; + text-align: center; +} + +#groups-table > thead > tr > th { + vertical-align: top; + text-align: center; + min-width: 40px; +} + +#groups-table .perm-head { + width: 300px; +} + +#groups-table > thead > tr > th > div { + text-align: center; +} + +#groups-table > tbody > tr:hover { + background-color: #f5f5f5 !important; +} + +#groups-table > tbody > tr:first-child { + background-color: #f9f9f9; +} + +#groups-table > tbody > tr > td:first-child { + text-align: left; +} + +#groups-table .optional-show { /* hide optional-show column */ + display: none; +} + /* search results */ .searchresults li { margin-bottom: 12px; @@ -849,7 +885,6 @@ tr.selected td { color: #9a9898; } - /*//////////////////////////////////////// =MEDIA QUERIES (RESPONSIVE DESIGN) ////////////////////////////////////////*/ @@ -872,6 +907,16 @@ tr.selected td { @media only screen and (max-width: 900px) { #nav .navbar li a { padding: 24px 5px; } + + #groups-table .perm-head { + width: 200px; + } + + /* hide text in groups-table earlier */ + #groups-table .optional { display: none; } + + /* show replacement elements, if any */ + #groups-table .optional-show { display: block !important; } } @@ -894,6 +939,9 @@ tr.selected td { /* hide marked element / column */ .optional, .hide-sm { display: none; } + + /* show replacement elements, if any */ + .optional-show { display: block !important; } } /* display for resolutions smaller that 560px */ @@ -908,4 +956,7 @@ tr.selected td { .col2 .projector_full { margin-left: 0px; } + #groups-table .perm-head { + width: 150px; + } } diff --git a/openslides/mediafiles/static/js/mediafiles/base.js b/openslides/mediafiles/static/js/mediafiles/base.js index af1e7749c..6611b8a77 100644 --- a/openslides/mediafiles/static/js/mediafiles/base.js +++ b/openslides/mediafiles/static/js/mediafiles/base.js @@ -7,11 +7,14 @@ angular.module('OpenSlidesApp.mediafiles', []) .factory('Mediafile', [ 'DS', 'jsDataModel', - function(DS, jsDataModel) { + 'gettext', + function(DS, jsDataModel, gettext) { var name = 'mediafiles/mediafile'; return DS.defineResource({ name: name, useClass: jsDataModel, + verboseName: gettext('Files'), + verboseNamePlural: gettext('Files'), getAllImages: function () { var images = []; angular.forEach(this.getAll(), function(file) { diff --git a/openslides/motions/static/js/motions/base.js b/openslides/motions/static/js/motions/base.js index 69d7b8c1d..3fe3d7d98 100644 --- a/openslides/motions/static/js/motions/base.js +++ b/openslides/motions/static/js/motions/base.js @@ -118,6 +118,7 @@ angular.module('OpenSlidesApp.motions', ['OpenSlidesApp.users']) name: name, useClass: jsDataModel, verboseName: gettext('Motion'), + verboseNamePlural: gettext('Motions'), methods: { getResourceName: function () { return name; diff --git a/openslides/users/auth.py b/openslides/users/auth.py index 27bd0bfea..d826a6707 100644 --- a/openslides/users/auth.py +++ b/openslides/users/auth.py @@ -3,7 +3,6 @@ from django.contrib.auth import get_user_model from django.contrib.auth.backends import ModelBackend from django.contrib.auth.models import AnonymousUser as DjangoAnonymousUser from django.contrib.auth.models import Permission -from django.db.models import Q from django.utils.functional import SimpleLazyObject from rest_framework.authentication import BaseAuthentication @@ -14,9 +13,9 @@ from ..core.config import config class CustomizedModelBackend(ModelBackend): """ - Customized backend for authentication. Ensures that registered users - have all permissions of the group 'Registered' (pk=2). See - AUTHENTICATION_BACKENDS settings. + Customized backend for authentication. Ensures that all users + without a group have the permissions of the group 'Default' (pk=1). + See AUTHENTICATION_BACKENDS settings. """ def get_group_permissions(self, user_obj, obj=None): """ @@ -31,11 +30,12 @@ class CustomizedModelBackend(ModelBackend): if user_obj.is_superuser: perms = Permission.objects.all() else: - user_groups_field = get_user_model()._meta.get_field('groups') - user_groups_query = 'group__%s' % user_groups_field.related_query_name() - # The next two lines are the customization. - query = Q(**{user_groups_query: user_obj}) | Q(group__pk=2) - perms = Permission.objects.filter(query) + if user_obj.groups.all().count() == 0: # user is in no group + perms = Permission.objects.filter(group__pk=1) # group 'default' (pk=1) + else: + user_groups_field = get_user_model()._meta.get_field('groups') + user_groups_query = 'group__%s' % user_groups_field.related_query_name() + perms = Permission.objects.filter(**{user_groups_query: user_obj}) perms = perms.values_list('content_type__app_label', 'codename').order_by() user_obj._group_perm_cache = set("%s.%s" % (ct, name) for ct, name in perms) return user_obj._group_perm_cache diff --git a/openslides/users/models.py b/openslides/users/models.py index dbabd6747..f645dd7b4 100644 --- a/openslides/users/models.py +++ b/openslides/users/models.py @@ -35,13 +35,13 @@ class UserManager(BaseUserManager): """ Creates an user with the username 'admin'. If such a user already exists, resets it. The password is (re)set to 'admin'. The user - becomes member of the group 'Staff' (pk=4). + becomes member of the group 'Staff' (pk=3). """ try: - staff = Group.objects.get(pk=4) + staff = Group.objects.get(pk=3) except Group.DoesNotExist: raise UsersError("Admin user can not be created or reset because " - "the group 'Staff' (pk=4) is not available.") + "the group 'Staff' (pk=3) is not available.") admin, created = self.get_or_create( username='admin', defaults={'last_name': 'Administrator'}) diff --git a/openslides/users/serializers.py b/openslides/users/serializers.py index f1b40737d..7ab7ad71c 100644 --- a/openslides/users/serializers.py +++ b/openslides/users/serializers.py @@ -44,7 +44,7 @@ class UserFullSerializer(ModelSerializer): """ groups = IdPrimaryKeyRelatedField( many=True, - queryset=Group.objects.exclude(pk__in=(1, 2)), + queryset=Group.objects.exclude(pk=1), help_text=ugettext_lazy('The groups this user belongs to. A user will ' 'get all permissions granted to each of ' 'his/her groups.')) diff --git a/openslides/users/signals.py b/openslides/users/signals.py index 52486fbe3..95a008701 100644 --- a/openslides/users/signals.py +++ b/openslides/users/signals.py @@ -6,13 +6,12 @@ from .models import Group, User def create_builtin_groups_and_admin(**kwargs): """ - Creates the builtin groups: Anonymous, Registered, Delegates, Staff and - Committees. + Creates the builtin groups: Default, Delegates, Staff and Committees. Creates the builtin user: admin. """ - # Check whether the group pk's 1 to 5 are free. - if Group.objects.filter(pk__in=range(1, 6)).exists(): + # Check whether the group pk's 1 to 4 are free. + if Group.objects.filter(pk__in=range(1, 5)).exists(): # Do completely nothing if there are already some of our groups in the database. return @@ -53,7 +52,7 @@ def create_builtin_groups_and_admin(**kwargs): permission_string = '.'.join((permission.content_type.app_label, permission.codename)) permission_dict[permission_string] = permission - # Anonymous (pk 1) and Registered (pk 2) + # Default (pk 1) base_permissions = ( permission_dict['agenda.can_see'], permission_dict['agenda.can_see_hidden_items'], @@ -63,40 +62,54 @@ def create_builtin_groups_and_admin(**kwargs): permission_dict['mediafiles.can_see'], permission_dict['motions.can_see'], permission_dict['users.can_see_name'], ) - group_anonymous = Group.objects.create(name='Guests', pk=1) - group_anonymous.permissions.add(*base_permissions) - group_registered = Group.objects.create(name='Registered users', pk=2) - group_registered.permissions.add( - permission_dict['agenda.can_be_speaker'], - *base_permissions) + group_default = Group.objects.create(name='Default', pk=1) + group_default.permissions.add(*base_permissions) - # Delegates (pk 3) + # Delegates (pk 2) delegates_permissions = ( + permission_dict['agenda.can_see'], + permission_dict['agenda.can_see_hidden_items'], + permission_dict['agenda.can_be_speaker'], + permission_dict['assignments.can_see'], permission_dict['assignments.can_nominate_other'], permission_dict['assignments.can_nominate_self'], + permission_dict['core.can_see_frontpage'], + permission_dict['core.can_see_projector'], + permission_dict['mediafiles.can_see'], permission_dict['mediafiles.can_upload'], + permission_dict['motions.can_see'], permission_dict['motions.can_create'], - permission_dict['motions.can_support'], ) - group_delegates = Group.objects.create(name='Delegates', pk=3) + permission_dict['motions.can_support'], + permission_dict['users.can_see_name'], ) + group_delegates = Group.objects.create(name='Delegates', pk=2) group_delegates.permissions.add(*delegates_permissions) - # Staff (pk 4) + # Staff (pk 3) staff_permissions = ( + permission_dict['agenda.can_see'], + permission_dict['agenda.can_see_hidden_items'], + permission_dict['agenda.can_be_speaker'], permission_dict['agenda.can_manage'], + permission_dict['assignments.can_see'], permission_dict['assignments.can_manage'], permission_dict['assignments.can_nominate_other'], permission_dict['assignments.can_nominate_self'], + permission_dict['core.can_see_frontpage'], + permission_dict['core.can_see_projector'], permission_dict['core.can_manage_config'], permission_dict['core.can_manage_projector'], permission_dict['core.can_manage_tags'], permission_dict['core.can_use_chat'], + permission_dict['mediafiles.can_see'], permission_dict['mediafiles.can_manage'], permission_dict['mediafiles.can_upload'], + permission_dict['motions.can_see'], permission_dict['motions.can_create'], permission_dict['motions.can_manage'], + permission_dict['users.can_see_name'], permission_dict['users.can_manage'], permission_dict['users.can_see_extra_data'],) - group_staff = Group.objects.create(name='Staff', pk=4) + group_staff = Group.objects.create(name='Staff', pk=3) group_staff.permissions.add(*staff_permissions) # Add users.can_see_name permission to staff @@ -105,12 +118,21 @@ def create_builtin_groups_and_admin(**kwargs): group_staff.permissions.add( permission_dict['users.can_see_name']) - # Committees (pk 5) + # Committees (pk 4) committees_permissions = ( + permission_dict['agenda.can_see'], + permission_dict['agenda.can_see_hidden_items'], + permission_dict['agenda.can_be_speaker'], + permission_dict['assignments.can_see'], + permission_dict['core.can_see_frontpage'], + permission_dict['core.can_see_projector'], + permission_dict['mediafiles.can_see'], permission_dict['mediafiles.can_upload'], + permission_dict['motions.can_see'], permission_dict['motions.can_create'], - permission_dict['motions.can_support'], ) - group_committee = Group.objects.create(name='Committees', pk=5) + permission_dict['motions.can_support'], + permission_dict['users.can_see_name'], ) + group_committee = Group.objects.create(name='Committees', pk=4) group_committee.permissions.add(*committees_permissions) # Create or reset admin user diff --git a/openslides/users/static/js/users/base.js b/openslides/users/static/js/users/base.js index 6e9c8c0f2..d4aa56b50 100644 --- a/openslides/users/static/js/users/base.js +++ b/openslides/users/static/js/users/base.js @@ -55,6 +55,10 @@ angular.module('OpenSlidesApp.users', []) } return _.intersection(perms, operator.perms).length > 0; }, + // Returns true if the operator is a member of group. + isInGroup: function(group) { + return _.indexOf(operator.user.groups_id, group.id) > -1; + }, }; return operator; } @@ -64,11 +68,14 @@ angular.module('OpenSlidesApp.users', []) 'DS', 'Group', 'jsDataModel', - function(DS, Group, jsDataModel) { + 'gettext', + function(DS, Group, jsDataModel, gettext) { var name = 'users/user'; return DS.defineResource({ name: name, useClass: jsDataModel, + verboseName: gettext('Participants'), + verboseNamePlural: gettext('Participants'), computed: { full_name: function () { return this.get_full_name(); @@ -125,8 +132,9 @@ angular.module('OpenSlidesApp.users', []) if (this.groups_id) { allGroups = this.groups_id.slice(0); } - // Add registered group - allGroups.push(2); + if (allGroups.length === 0) { + allGroups.push(1); // add default group + } _.forEach(allGroups, function(groupId) { var group = Group.get(groupId); if (group) { @@ -191,10 +199,10 @@ angular.module('OpenSlidesApp.users', []) 'gettext', function (gettext) { // default group names (from users/signals.py) - gettext('Guests'); - gettext('Registered users'); + gettext('Default'); gettext('Delegates'); gettext('Staff'); + gettext('Committees'); } ]); diff --git a/openslides/users/static/js/users/site.js b/openslides/users/static/js/users/site.js index a386302ce..980b406d7 100644 --- a/openslides/users/static/js/users/site.js +++ b/openslides/users/static/js/users/site.js @@ -111,36 +111,12 @@ angular.module('OpenSlidesApp.users.site', ['OpenSlidesApp.users']) resolve: { groups: function(Group) { return Group.findAll(); - } - } - }) - .state('users.group.create', { - resolve: { - permissions: function(Group) { - return Group.getPermissions(); - } - } - }) - .state('users.group.detail', { - resolve: { - group: function(Group, $stateParams) { - return Group.find($stateParams.id); }, permissions: function(Group) { return Group.getPermissions(); } } }) - .state('users.group.detail.update', { - views: { - '@users.group': {} - }, - resolve: { - permissions: function(Group) { - return Group.getPermissions(); - } - } - }) .state('login', { template: null, url: '/login', @@ -331,7 +307,7 @@ angular.module('OpenSlidesApp.users.site', ['OpenSlidesApp.users']) label: gettextCatalog.getString('Groups'), options: Group.getAll(), ngOptions: 'option.id as option.name | translate for option in to.options | ' + - 'filter: {id: "!1"} | filter: {id: "!2"}', + 'filter: {id: "!1"}', placeholder: gettextCatalog.getString('Select or search a group ...') } }, @@ -850,79 +826,233 @@ angular.module('OpenSlidesApp.users.site', ['OpenSlidesApp.users']) .controller('GroupListCtrl', [ '$scope', + '$http', + 'operator', 'Group', - function($scope, Group) { - Group.bindAll({}, $scope, 'groups'); + 'permissions', + 'gettext', + 'Agenda', + 'Assignment', + 'Mediafile', + 'Motion', + 'User', + 'ngDialog', + function($scope, $http, operator, Group, permissions, gettext, Agenda, Assignment, Mediafile, Motion, User, ngDialog) { + //Group.bindAll({}, $scope, 'groups'); + $scope.permissions = permissions; + + $scope.$watch(function() { + return Group.lastModified(); + }, function() { + $scope.groups = Group.getAll(); + + // find all groups with the 2 dangerous permissions + var groups_danger = []; + $scope.groups.forEach(function (group) { + if ((_.indexOf(group.permissions, 'users.can_see_name') > -1) && + (_.indexOf(group.permissions, 'users.can_manage') > -1)){ + if (operator.isInGroup(group)){ + groups_danger.push(group); + } + } + }); + // if there is only one dangerous group, block it. + $scope.group_danger = groups_danger.length == 1 ? groups_danger[0] : null; + }); + + $scope.apps = []; + // Create the main clustering with appname->permissions + angular.forEach(permissions, function(perm) { + var permissionApp = perm.value.split('.')[0]; // get appname + + // To insert perm in the right spot in $scope.apps + var insert = function (id, perm, verboseName) { + if (!$scope.apps[id]) { + $scope.apps[id] = { + app_name: verboseName, + app_visible: true, + permissions: [] + }; + } + $scope.apps[id].permissions.push(perm); + }; + + switch(permissionApp) { + case 'core': // id 0 (projector) and id 6 (general) + if (perm.value.indexOf('projector') > -1) { + insert(0, perm, gettext('Projector')); + } else { + insert(6, perm, gettext('General')); + } + break; + case 'agenda': // id 1 + insert(1, perm, Agenda.verboseName); + break; + case 'motions': // id 2 + insert(2, perm, Motion.verboseNamePlural); + break; + case 'assignments': // id 3 + insert(3, perm, Assignment.verboseNamePlural); + break; + case 'mediafiles': // id 4 + insert(4, perm, Mediafile.verboseNamePlural); + break; + case 'users': // id 5 + insert(5, perm, User.verboseNamePlural); + break; + default: // plugins: id>5 + var display_name = permissionApp.charAt(0).toUpperCase() + permissionApp.slice(1); + // does the app exists? + var result = -1; + angular.forEach($scope.apps, function (app, index) { + if (app.app_name === display_name) + result = index; + }); + var id = result == -1 ? $scope.apps.length : result; + insert(id, perm, display_name); + break; + } + }); + + // sort each app: first all permission with 'see', then 'manage', then the rest + // save the permissions in different lists an concat them in the right order together + // Special Users: the two "see"-permissions are normally swapped. To create the right + // order, we could simply reverse the whole permissions. + angular.forEach($scope.apps, function (app, index) { + if(index == 5) { // users + app.permissions.reverse(); + } else { // rest + var see = []; + var manage = []; + var others = []; + angular.forEach(app.permissions, function (perm) { + if (perm.value.indexOf('see') > -1) { + see.push(perm); + } else if (perm.value.indexOf('manage') > -1) { + manage.push(perm); + } else { + others.push(perm); + } + }); + app.permissions = see.concat(manage.concat(others)); + } + }); + + // check if the given group has the given permission + $scope.hasPerm = function (group, permission) { + return _.indexOf(group.permissions, permission.value) > -1; + }; + + // The current user is not allowed to lock himself out of the configuration: + // - if the permission is 'users.can_manage' or 'users.can_see' + // - if the user is in only one group with these permissions (group_danger is set) + $scope.danger = function (group, permission){ + if ($scope.group_danger){ + if (permission.value == 'users.can_see_name' || + permission.value == 'users.can_manage'){ + return $scope.group_danger == group; + } + } + return false; + }; // delete selected group $scope.delete = function (group) { Group.destroy(group.id); }; + + // save changed permission + $scope.changePermission = function (group, perm) { + if (!$scope.danger(group, perm)) { + if (!$scope.hasPerm(group, perm)) { // activate perm + group.permissions.push(perm.value); + } else { + // delete perm in group.permissions + group.permissions = _.filter(group.permissions, function(value) { + return value != perm.value; // remove perm + }); + } + Group.save(group); + } + }; + + $scope.openDialog = function (group) { + var resolve; + if (group) { + resolve = { + group: function() {return Group.find(group.id);} + }; + } + ngDialog.open({ + template: 'static/templates/users/group-edit.html', + controller: group ? 'GroupRenameCtrl' : 'GroupCreateCtrl', + className: 'ngdialog-theme-default wide-form', + closeByEscape: false, + closeByDocument: false, + resolve: (resolve) ? resolve : null + }); + }; + } +]) + +.controller('GroupRenameCtrl', [ + '$scope', + 'Group', + 'group', + function($scope, Group, group) { + $scope.group = group; + $scope.new_name = group.name; + + $scope.alert = {}; + $scope.save = function() { + var old_name = $scope.group.name; + $scope.group.name = $scope.new_name; + Group.save($scope.group).then( + function (success) { + $scope.closeThisDialog(); + }, + function (error) { + var message = ''; + for (var e in error.data) { + message += e + ': ' + error.data[e] + ' '; + } + $scope.alert = { msg: message, show: true }; + $scope.group.name = old_name; + } + ); + }; } ]) .controller('GroupCreateCtrl', [ '$scope', - '$state', 'Group', - 'permissions', - function($scope, $state, Group, permissions) { - // get all permissions - $scope.permissions = permissions; - $scope.group = {}; - $scope.save = function (group) { - if (!group.permissions) { - group.permissions = []; - } + function($scope, Group) { + $scope.new_name = ''; + $scope.alert = {}; + + $scope.save = function() { + var group = { + name: $scope.new_name, + permissions: [] + }; + Group.create(group).then( - function(success) { - $state.go('users.group.list'); + function (success) { + $scope.closeThisDialog(); + }, + function (error) { + var message = ''; + for (var e in error.data) { + message += e + ': ' + error.data[e] + ' '; + } + $scope.alert = { msg: message, show: true }; } ); }; } ]) -.controller('GroupUpdateCtrl', [ - '$scope', - '$state', - 'Group', - 'permissions', - 'group', - function($scope, $state, Group, permissions, group) { - // get all permissions - $scope.permissions = permissions; - $scope.group = group; // autoupdate is not activated - $scope.save = function (group) { - Group.save(group).then( - function(success) { - $state.go('users.group.list'); - } - ); - }; - } -]) - -.controller('GroupDetailCtrl', [ - '$scope', - 'Group', - 'group', - 'permissions', - function($scope, Group, group, permissions) { - Group.bindOne(group.id, $scope, 'group'); - $scope.groupPermissionNames = []; - // get display names of group permissions - // from an object array with all available permissions [{display_name, value}] - angular.forEach(group.permissions, function(permValue) { - angular.forEach(permissions, function(p) { - if (p.value == permValue) { - $scope.groupPermissionNames.push(p.display_name); - } - }); - }); - } -]) - .controller('userMenu', [ '$scope', '$http', diff --git a/openslides/users/static/templates/users/group-detail.html b/openslides/users/static/templates/users/group-detail.html deleted file mode 100644 index 578170220..000000000 --- a/openslides/users/static/templates/users/group-detail.html +++ /dev/null @@ -1,19 +0,0 @@ -
Permissions:
-
- |
- | Actions | -|
---|---|---|---|
{{ group.id }} - | {{ group.name | translate }} - |
-
-
-
-
-
-
+ Permissions+ | + + {{ group.name | translate }} + + + {{ group.name | translate | limitTo: 1 }}... + + + + |
+ {{ app.app_name | translate}} + + | + + | ||
+ {{ permission.display_name | translate }} + | + + + + + + |
title, first_name, last_name, structure_level, number, groups, comment, is_active, is_committee
3
,
- 4
- 5
+ 2
,
+ 3
+ 4