New matrix interface for group editing (closes #435)

This commit is contained in:
Finn Stutzenstein 2016-08-08 09:37:46 +02:00
parent 08c734f1a3
commit bf3a1ce714
20 changed files with 440 additions and 239 deletions

View File

@ -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.

View File

@ -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;

View File

@ -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) {

View File

@ -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;
}
}

View File

@ -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) {

View File

@ -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;

View File

@ -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

View File

@ -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'})

View File

@ -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.'))

View File

@ -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

View File

@ -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');
}
]);

View File

@ -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',

View File

@ -1,19 +0,0 @@
<div class="header">
<div class="title">
<div class="submenu">
<a ui-sref="users.group.list" class="btn btn-sm btn-default">
<i class="fa fa-angle-double-left fa-lg"></i>
<translate>Back to overview</translate>
</a>
</div>
<h1>{{ group.name | translate }}</h1>
<h2 translate>Group</h2>
</div>
</div>
<div class="details">
<p class="lead" translate>Permissions:</p>
<ul ng-repeat="permission in groupPermissionNames">
<li>{{ permission | translate }}
</ul>
</div>

View File

@ -0,0 +1,27 @@
<h1 ng-if="group" translate>Edit name</h1>
<h1 ng-if="!group" translate>Create new group</h1>
<uib-alert ng-show="alert.show" type="danger" ng-click="alert={}" close="alert={}">
{{ alert.msg }}
</uib-alert>
<label for="name_1">
<span ng-if="group">
<translate>Please enter a new name for</translate>
{{ group.name }}
</span>
<span ng-if="!group" translate>
Please enter a name for the new group
</span>
</label>
<div>
<div>
<input class="form-control" id="name_1" type="text" ng-model="new_name">
</div>
<div class="spacer-top-lg">
<button ng-click="save()" ng-disabled="new_name == ''" class="btn btn-primary" translate>
Save
</button>
<button ng-click="closeThisDialog()" class="btn btn-default" translate>
Cancel
</button>
</div>
</div>

View File

@ -1,34 +0,0 @@
<div class="header">
<div class="title">
<div class="submenu">
<a ui-sref="users.group.list" class="btn btn-sm btn-default">
<i class="fa fa-angle-double-left fa-lg"></i>
<translate>Back to overview</translate>
</a>
</div>
<h1 ng-if="group.id" translate>Edit group</h1>
<h1 ng-if="!group.id" translate>New group</h1>
</div>
</div>
<div class="details">
<form name="groupForm">
<div class="form-group">
<label for="inputName" translate>Name</label>
<input type="text" ng-model="group.name" class="form-control" name="inputName" required>
</div>
<div class="form-group">
<label for="selectPermissions" translate>Permissions</label>
<select multiple size="15" ng-model="group.permissions" class="form-control" id="selectPermissions">
<option value="{{ permission.value }}" ng-repeat="permission in permissions">{{ permission.display_name | translate }}</option>
</select>
</div>
<button type="submit" ng-click="save(group)" class="btn btn-primary" translate>
Save
</button>
<button ui-sref="users.group.list" class="btn btn-default" translate>
Cancel
</button>
</form>
</div>

View File

@ -1,7 +1,8 @@
<div class="header">
<div class="title">
<div class="submenu">
<a ui-sref="users.group.create" os-perms="users.can_manage" class="btn btn-primary btn-sm">
<a os-perms="users.can_manage" class="btn btn-primary btn-sm"
ng-click="openDialog()" title="add new group">
<i class="fa fa-plus fa-lg"></i>
<translate>New</translate>
</a>
@ -15,48 +16,58 @@
</div>
<div class="details">
<div class="row form-group">
<div class="col-sm-4 pull-right">
<div class="form-group">
<div class="input-group">
<div class="input-group-addon"><i class="fa fa-search"></i></div>
<input type="text" ng-model="filter.search" class="form-control"
placeholder="{{ 'Search' | translate}}">
</div>
</div>
</div>
</div>
<table class="table table-striped table-bordered table-hover">
<p translate>
All your changes are saved immediately. Changes you make are only effective once you (or the users concerned) reload the page.
</p>
<table id="groups-table" class="table table-bordered">
<thead>
<tr>
<th ng-click="sortby='id';reverse=!reverse">
<translate>ID</translate>
<i class="fa" ng-show="sortby == 'id'"
ng-class="reverse ? 'fa-caret-down' : 'fa-caret-up'"></i>
<th ng-click="sortby='name';reverse=!reverse">
<translate>Name</translate>
<i class="fa" ng-show="sortby == 'name'"
ng-class="reverse ? 'fa-caret-down' : 'fa-caret-up'"></i>
<th os-perms="users.can_manage core.can_manage_projector" class="mini_width" translate>Actions</th>
<tbody>
<tr ng-repeat="group in groups | filter: filter.search | orderBy:sortby:reverse">
<td>{{ group.id }}
<td><a ui-sref="users.group.detail({id: group.id})">{{ group.name | translate }}</a>
<td os-perms="users.can_manage" class="nobr">
<!-- edit -->
<a ui-sref="users.group.detail.update({id: group.id})" os-perms="users.can_manage"
class="btn btn-default btn-sm"
title="{{ 'Edit' | translate }}">
<i class="fa fa-pencil"></i>
</a>
<!-- delete -->
<a os-perms="users.can_manage" class="btn btn-danger btn-sm"
<th class="perm-head">
<h4 translate>Permissions</h4>
<th ng-repeat="group in groups" ng-mouseover="group.hover=true" ng-mouseleave="group.hover=false">
<span class="optional">
{{ group.name | translate }}
</span>
<span class="optional-show" uib-tooltip="{{ group.name }}">
{{ group.name | translate | limitTo: 1 }}...
</span>
<i class="fa fa-info-circle" ng-if="group.id == 1"
uib-tooltip="{{ 'Users without any assigned group gain the permissions from this group.' | translate }}"></i>
<div os-perms="users.can_manage" ng-hide="group.id == 1" class="hoverActions"
ng-class="{'hiddenDiv': !group.hover}">
<!--edit name-->
<a href="" ng-click="openDialog(group)">
<i class="fa fa-pencil fa-lg"></i></a>
&nbsp;
<!--delete-->
<a href="" class="text-danger"
ng-bootbox-confirm="{{ 'Are you sure you want to delete this entry?' | translate }}<br>
<b>{{ group.name }}</b>"
ng-bootbox-confirm-action="delete(group)"
title="{{ 'Delete' | translate }}">
<i class="fa fa-trash-o"></i>
</a>
<b>{{ group.name | translate }}</b>"
ng-bootbox-confirm-action="delete(group)" translate>
<i class="fa fa-trash fa-lg"></i>
</a>
</div>
<tbody ng-repeat="app in apps" os-perms="users.can_manage">
<tr class="pointer" ng-click="app.app_visible=!app.app_visible">
<td>
<b>{{ app.app_name | translate}}</b>
<i class="fa" ng-class="app.app_visible ? 'fa-minus-square' : 'fa-plus-square'">
<td ng-repeat="group in groups">
<!-- empty, just filling the table -->
<tr ng-repeat="permission in app.permissions" ng-class="{'collapse': !app.app_visible}">
<td>
{{ permission.display_name | translate }}
<td class="pointer" ng-repeat="group in groups"
ng-click="changePermission(group, permission)">
<!-- Simulating a checkbox with FontAwesome icons. -->
<i class="fa"
ng-if="!danger(group, permission)"
ng-class="hasPerm(group, permission) ? 'fa-check-square-o' : 'fa-square-o'"></i>
<span class="fa fa-stack"
ng-if="danger(group, permission)"
uib-tooltip="{{ 'You are not allowed to lock yourself out of the configuration!' | translate }}">
<i class="fa fa-check-square-o fa-stack-1x"></i>
<i class="fa fa-ban fa-stack-2x text-danger"></i>
</span>
</table>
</div>

View File

@ -70,9 +70,9 @@
<li><translate>Required comma or semicolon separated values with these column header names in the first row</translate>:<br>
<code>title, first_name, last_name, structure_level, number, groups, comment, is_active, is_committee</code>
<li><translate>Default groups</translate>:
<translate>Delegates</translate> <code>3</code>,
<translate>Staff</translate> <code>4</code>
<translate>Committees</translate> <code>5</code>
<translate>Delegates</translate> <code>2</code>,
<translate>Staff</translate> <code>3</code>
<translate>Committees</translate> <code>4</code>
<li translate>At least first name or last name have to be filled in. All
other fields are optional and may be empty.
<li translate>Only double quotes are accepted as text delimiter (no single quotes).

View File

@ -150,7 +150,7 @@ class RetrieveMotion(TestCase):
self.motion.create_poll()
def test_number_of_queries(self):
with self.assertNumQueries(16):
with self.assertNumQueries(17):
self.client.get(reverse('motion-detail', args=[self.motion.pk]))
@ -312,7 +312,7 @@ class SupportMotion(TestCase):
"""
def setUp(self):
self.admin = get_user_model().objects.get(username='admin')
self.admin.groups.add(3)
self.admin.groups.add(2)
self.client.login(username='admin', password='admin')
self.motion = Motion(
title='test_title_chee7ahCha6bingaew4e',

View File

@ -44,7 +44,7 @@ class UserCreate(TestCase):
def test_creation_with_group(self):
self.client.login(username='admin', password='admin')
# These are the builtin groups 'Delegates' and 'Staff'. The pks are valid.
group_pks = (3, 4,)
group_pks = (2, 3,)
self.client.post(
reverse('user-list'),
@ -55,19 +55,19 @@ class UserCreate(TestCase):
self.assertTrue(user.groups.filter(pk=group_pks[0]).exists())
self.assertTrue(user.groups.filter(pk=group_pks[1]).exists())
def test_creation_with_anonymous_or_registered_group(self):
def test_creation_with_default_group(self):
self.client.login(username='admin', password='admin')
# These are the builtin groups 'Anonymous' and 'Registered'.
# The pks are valid. But these groups can not be added to users.
group_pks = (1, 2,)
# This is the builtin groups 'default'.
# The pk is valid. But this group can not be added to users.
group_pk = (1,)
response = self.client.post(
reverse('user-list'),
{'last_name': 'Test name aedah1iequoof0Ashed4',
'groups_id': group_pks})
'groups_id': group_pk})
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertEqual(response.data, {'groups_id': ["Invalid pk \"%d\" - object does not exist." % group_pks[0]]})
self.assertEqual(response.data, {'groups_id': ["Invalid pk \"%d\" - object does not exist." % group_pk]})
class UserUpdate(TestCase):
@ -211,8 +211,8 @@ class GroupReceive(TestCase):
user = User(username='test')
user.set_password('test')
user.save()
registered_group = Group.objects.get(pk=2)
registered_group.permissions.all().delete()
default_group = Group.objects.get(pk=1)
default_group.permissions.all().delete()
self.client.login(username='test', password='test')
response = self.client.get('/rest/users/group/')
@ -277,7 +277,7 @@ class GroupUpdate(TestCase):
admin_client = APIClient()
admin_client.login(username='admin', password='admin')
# This is the builtin group 'Delegates'. The pk is valid.
group_pk = 3
group_pk = 2
# This contains one valid permission of the users app.
permissions = ('users.can_see_name',)
@ -295,7 +295,7 @@ class GroupUpdate(TestCase):
admin_client = APIClient()
admin_client.login(username='admin', password='admin')
# This is the builtin group 'Delegates'. The pk is valid.
group_pk = 3
group_pk = 2
# This contains one valid permission of the users app.
permissions = ('users.can_see_name',)
@ -324,9 +324,8 @@ class GroupDelete(TestCase):
def test_delete_builtin_groups(self):
admin_client = APIClient()
admin_client.login(username='admin', password='admin')
# The pks of builtin groups 'Anonymous' and 'Registered'
group_pks = (1, 2,)
# The pk of builtin group 'Default'
group_pk = 1
for group_pk in group_pks:
response = admin_client.delete(reverse('group-detail', args=[group_pk]))
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
response = admin_client.delete(reverse('group-detail', args=[group_pk]))
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)

View File

@ -290,12 +290,10 @@ class UserManagerGeneratePassword(TestCase):
class UserManagerCreateOrResetAdminUser(TestCase):
def test_get_admin_group(self, mock_group):
"""
Tests that the Group with pk=4 is added to the admin.
Tests that the Group with pk=3 is added to the admin.
"""
def mock_side_effect(pk):
if pk == 2:
result = 'mock_registered'
elif pk == 4:
if pk == 3:
result = 'mock_staff'
else:
result = ''
@ -308,7 +306,7 @@ class UserManagerCreateOrResetAdminUser(TestCase):
manager.create_or_reset_admin_user()
mock_group.objects.get.assert_called_once_with(pk=4)
mock_group.objects.get.assert_called_once_with(pk=3)
admin_user.groups.add.assert_called_once_with('mock_staff')
def test_password_set_to_admin(self, mock_group):