Merge pull request #2260 from FinnStutzenstein/Feature435

New matrix interface for group editing (closes #435)
This commit is contained in:
Emanuel Schütze 2016-08-22 15:26:10 +02:00 committed by GitHub
commit 73095948ca
20 changed files with 440 additions and 239 deletions

View File

@ -27,6 +27,7 @@ Motions:
Users: Users:
- Added field is_committee and new default group Committees. - Added field is_committee and new default group Committees.
- Added field number. - Added field number.
- Added new matrix-interface for managing groups and their permissions.
Other: Other:
- Removed config cache to support multiple threads or processes. - Removed config cache to support multiple threads or processes.

View File

@ -28,11 +28,13 @@ angular.module('OpenSlidesApp.agenda', ['OpenSlidesApp.users'])
'jsDataModel', 'jsDataModel',
'Projector', 'Projector',
'gettextCatalog', 'gettextCatalog',
function($http, DS, Speaker, jsDataModel, Projector, gettextCatalog) { 'gettext',
function($http, DS, Speaker, jsDataModel, Projector, gettextCatalog, gettext) {
var name = 'agenda/item'; var name = 'agenda/item';
return DS.defineResource({ return DS.defineResource({
name: name, name: name,
useClass: jsDataModel, useClass: jsDataModel,
verboseName: gettext('Agenda'),
methods: { methods: {
getResourceName: function () { getResourceName: function () {
return name; return name;

View File

@ -167,6 +167,7 @@ angular.module('OpenSlidesApp.assignments', [])
name: name, name: name,
useClass: jsDataModel, useClass: jsDataModel,
verboseName: gettext('Election'), verboseName: gettext('Election'),
verboseNamePlural: gettext('Elections'),
phases: phases, phases: phases,
getPhases: function () { getPhases: function () {
if (!this.phases) { if (!this.phases) {

View File

@ -798,6 +798,42 @@ img {
content: ":"; 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 */ /* search results */
.searchresults li { .searchresults li {
margin-bottom: 12px; margin-bottom: 12px;
@ -976,7 +1012,6 @@ tr.selected td {
color: #9a9898; color: #9a9898;
} }
/*//////////////////////////////////////// /*////////////////////////////////////////
=MEDIA QUERIES (RESPONSIVE DESIGN) =MEDIA QUERIES (RESPONSIVE DESIGN)
////////////////////////////////////////*/ ////////////////////////////////////////*/
@ -999,6 +1034,16 @@ tr.selected td {
@media only screen and (max-width: 900px) { @media only screen and (max-width: 900px) {
#nav .navbar li a { padding: 24px 5px; } #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; }
} }
@ -1021,6 +1066,9 @@ tr.selected td {
/* hide marked element / column */ /* hide marked element / column */
.optional, .hide-sm { display: none; } .optional, .hide-sm { display: none; }
/* show replacement elements, if any */
.optional-show { display: block !important; }
} }
/* display for resolutions smaller that 560px */ /* display for resolutions smaller that 560px */
@ -1035,4 +1083,7 @@ tr.selected td {
.col2 .projector_full { .col2 .projector_full {
margin-left: 0px; margin-left: 0px;
} }
#groups-table .perm-head {
width: 150px;
}
} }

View File

@ -7,11 +7,14 @@ angular.module('OpenSlidesApp.mediafiles', [])
.factory('Mediafile', [ .factory('Mediafile', [
'DS', 'DS',
'jsDataModel', 'jsDataModel',
function(DS, jsDataModel) { 'gettext',
function(DS, jsDataModel, gettext) {
var name = 'mediafiles/mediafile'; var name = 'mediafiles/mediafile';
return DS.defineResource({ return DS.defineResource({
name: name, name: name,
useClass: jsDataModel, useClass: jsDataModel,
verboseName: gettext('Files'),
verboseNamePlural: gettext('Files'),
getAllImages: function () { getAllImages: function () {
var images = []; var images = [];
angular.forEach(this.getAll(), function(file) { angular.forEach(this.getAll(), function(file) {

View File

@ -123,6 +123,7 @@ angular.module('OpenSlidesApp.motions', [
name: name, name: name,
useClass: jsDataModel, useClass: jsDataModel,
verboseName: gettext('Motion'), verboseName: gettext('Motion'),
verboseNamePlural: gettext('Motions'),
methods: { methods: {
getResourceName: function () { getResourceName: function () {
return name; 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.backends import ModelBackend
from django.contrib.auth.models import AnonymousUser as DjangoAnonymousUser from django.contrib.auth.models import AnonymousUser as DjangoAnonymousUser
from django.contrib.auth.models import Permission from django.contrib.auth.models import Permission
from django.db.models import Q
from django.utils.functional import SimpleLazyObject from django.utils.functional import SimpleLazyObject
from rest_framework.authentication import BaseAuthentication from rest_framework.authentication import BaseAuthentication
@ -14,9 +13,9 @@ from ..core.config import config
class CustomizedModelBackend(ModelBackend): class CustomizedModelBackend(ModelBackend):
""" """
Customized backend for authentication. Ensures that registered users Customized backend for authentication. Ensures that all users
have all permissions of the group 'Registered' (pk=2). See without a group have the permissions of the group 'Default' (pk=1).
AUTHENTICATION_BACKENDS settings. See AUTHENTICATION_BACKENDS settings.
""" """
def get_group_permissions(self, user_obj, obj=None): def get_group_permissions(self, user_obj, obj=None):
""" """
@ -31,11 +30,12 @@ class CustomizedModelBackend(ModelBackend):
if user_obj.is_superuser: if user_obj.is_superuser:
perms = Permission.objects.all() perms = Permission.objects.all()
else: else:
user_groups_field = get_user_model()._meta.get_field('groups') if user_obj.groups.all().count() == 0: # user is in no group
user_groups_query = 'group__%s' % user_groups_field.related_query_name() perms = Permission.objects.filter(group__pk=1) # group 'default' (pk=1)
# The next two lines are the customization. else:
query = Q(**{user_groups_query: user_obj}) | Q(group__pk=2) user_groups_field = get_user_model()._meta.get_field('groups')
perms = Permission.objects.filter(query) 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() 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) user_obj._group_perm_cache = set("%s.%s" % (ct, name) for ct, name in perms)
return user_obj._group_perm_cache 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 Creates an user with the username 'admin'. If such a user already
exists, resets it. The password is (re)set to 'admin'. The user 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: try:
staff = Group.objects.get(pk=4) staff = Group.objects.get(pk=3)
except Group.DoesNotExist: except Group.DoesNotExist:
raise UsersError("Admin user can not be created or reset because " 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( admin, created = self.get_or_create(
username='admin', username='admin',
defaults={'last_name': 'Administrator'}) defaults={'last_name': 'Administrator'})

View File

@ -44,7 +44,7 @@ class UserFullSerializer(ModelSerializer):
""" """
groups = IdPrimaryKeyRelatedField( groups = IdPrimaryKeyRelatedField(
many=True, 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 ' help_text=ugettext_lazy('The groups this user belongs to. A user will '
'get all permissions granted to each of ' 'get all permissions granted to each of '
'his/her groups.')) 'his/her groups.'))

View File

@ -6,13 +6,12 @@ from .models import Group, User
def create_builtin_groups_and_admin(**kwargs): def create_builtin_groups_and_admin(**kwargs):
""" """
Creates the builtin groups: Anonymous, Registered, Delegates, Staff and Creates the builtin groups: Default, Delegates, Staff and Committees.
Committees.
Creates the builtin user: admin. Creates the builtin user: admin.
""" """
# Check whether the group pk's 1 to 5 are free. # Check whether the group pk's 1 to 4 are free.
if Group.objects.filter(pk__in=range(1, 6)).exists(): if Group.objects.filter(pk__in=range(1, 5)).exists():
# Do completely nothing if there are already some of our groups in the database. # Do completely nothing if there are already some of our groups in the database.
return return
@ -53,7 +52,7 @@ def create_builtin_groups_and_admin(**kwargs):
permission_string = '.'.join((permission.content_type.app_label, permission.codename)) permission_string = '.'.join((permission.content_type.app_label, permission.codename))
permission_dict[permission_string] = permission permission_dict[permission_string] = permission
# Anonymous (pk 1) and Registered (pk 2) # Default (pk 1)
base_permissions = ( base_permissions = (
permission_dict['agenda.can_see'], permission_dict['agenda.can_see'],
permission_dict['agenda.can_see_hidden_items'], 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['mediafiles.can_see'],
permission_dict['motions.can_see'], permission_dict['motions.can_see'],
permission_dict['users.can_see_name'], ) permission_dict['users.can_see_name'], )
group_anonymous = Group.objects.create(name='Guests', pk=1) group_default = Group.objects.create(name='Default', pk=1)
group_anonymous.permissions.add(*base_permissions) group_default.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)
# Delegates (pk 3) # Delegates (pk 2)
delegates_permissions = ( 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_other'],
permission_dict['assignments.can_nominate_self'], 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['mediafiles.can_upload'],
permission_dict['motions.can_see'],
permission_dict['motions.can_create'], permission_dict['motions.can_create'],
permission_dict['motions.can_support'], ) permission_dict['motions.can_support'],
group_delegates = Group.objects.create(name='Delegates', pk=3) permission_dict['users.can_see_name'], )
group_delegates = Group.objects.create(name='Delegates', pk=2)
group_delegates.permissions.add(*delegates_permissions) group_delegates.permissions.add(*delegates_permissions)
# Staff (pk 4) # Staff (pk 3)
staff_permissions = ( 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['agenda.can_manage'],
permission_dict['assignments.can_see'],
permission_dict['assignments.can_manage'], permission_dict['assignments.can_manage'],
permission_dict['assignments.can_nominate_other'], permission_dict['assignments.can_nominate_other'],
permission_dict['assignments.can_nominate_self'], 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_config'],
permission_dict['core.can_manage_projector'], permission_dict['core.can_manage_projector'],
permission_dict['core.can_manage_tags'], permission_dict['core.can_manage_tags'],
permission_dict['core.can_use_chat'], permission_dict['core.can_use_chat'],
permission_dict['mediafiles.can_see'],
permission_dict['mediafiles.can_manage'], permission_dict['mediafiles.can_manage'],
permission_dict['mediafiles.can_upload'], permission_dict['mediafiles.can_upload'],
permission_dict['motions.can_see'],
permission_dict['motions.can_create'], permission_dict['motions.can_create'],
permission_dict['motions.can_manage'], permission_dict['motions.can_manage'],
permission_dict['users.can_see_name'],
permission_dict['users.can_manage'], permission_dict['users.can_manage'],
permission_dict['users.can_see_extra_data'],) 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) group_staff.permissions.add(*staff_permissions)
# Add users.can_see_name permission to staff # Add users.can_see_name permission to staff
@ -105,12 +118,21 @@ def create_builtin_groups_and_admin(**kwargs):
group_staff.permissions.add( group_staff.permissions.add(
permission_dict['users.can_see_name']) permission_dict['users.can_see_name'])
# Committees (pk 5) # Committees (pk 4)
committees_permissions = ( 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['mediafiles.can_upload'],
permission_dict['motions.can_see'],
permission_dict['motions.can_create'], permission_dict['motions.can_create'],
permission_dict['motions.can_support'], ) permission_dict['motions.can_support'],
group_committee = Group.objects.create(name='Committees', pk=5) permission_dict['users.can_see_name'], )
group_committee = Group.objects.create(name='Committees', pk=4)
group_committee.permissions.add(*committees_permissions) group_committee.permissions.add(*committees_permissions)
# Create or reset admin user # Create or reset admin user

View File

@ -55,6 +55,10 @@ angular.module('OpenSlidesApp.users', [])
} }
return _.intersection(perms, operator.perms).length > 0; 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; return operator;
} }
@ -64,11 +68,14 @@ angular.module('OpenSlidesApp.users', [])
'DS', 'DS',
'Group', 'Group',
'jsDataModel', 'jsDataModel',
function(DS, Group, jsDataModel) { 'gettext',
function(DS, Group, jsDataModel, gettext) {
var name = 'users/user'; var name = 'users/user';
return DS.defineResource({ return DS.defineResource({
name: name, name: name,
useClass: jsDataModel, useClass: jsDataModel,
verboseName: gettext('Participants'),
verboseNamePlural: gettext('Participants'),
computed: { computed: {
full_name: function () { full_name: function () {
return this.get_full_name(); return this.get_full_name();
@ -125,8 +132,9 @@ angular.module('OpenSlidesApp.users', [])
if (this.groups_id) { if (this.groups_id) {
allGroups = this.groups_id.slice(0); allGroups = this.groups_id.slice(0);
} }
// Add registered group if (allGroups.length === 0) {
allGroups.push(2); allGroups.push(1); // add default group
}
_.forEach(allGroups, function(groupId) { _.forEach(allGroups, function(groupId) {
var group = Group.get(groupId); var group = Group.get(groupId);
if (group) { if (group) {
@ -191,10 +199,10 @@ angular.module('OpenSlidesApp.users', [])
'gettext', 'gettext',
function (gettext) { function (gettext) {
// default group names (from users/signals.py) // default group names (from users/signals.py)
gettext('Guests'); gettext('Default');
gettext('Registered users');
gettext('Delegates'); gettext('Delegates');
gettext('Staff'); gettext('Staff');
gettext('Committees');
} }
]); ]);

View File

@ -111,36 +111,12 @@ angular.module('OpenSlidesApp.users.site', ['OpenSlidesApp.users'])
resolve: { resolve: {
groups: function(Group) { groups: function(Group) {
return Group.findAll(); 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) { permissions: function(Group) {
return Group.getPermissions(); return Group.getPermissions();
} }
} }
}) })
.state('users.group.detail.update', {
views: {
'@users.group': {}
},
resolve: {
permissions: function(Group) {
return Group.getPermissions();
}
}
})
.state('login', { .state('login', {
template: null, template: null,
url: '/login', url: '/login',
@ -331,7 +307,7 @@ angular.module('OpenSlidesApp.users.site', ['OpenSlidesApp.users'])
label: gettextCatalog.getString('Groups'), label: gettextCatalog.getString('Groups'),
options: Group.getAll(), options: Group.getAll(),
ngOptions: 'option.id as option.name | translate for option in to.options | ' + 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 ...') placeholder: gettextCatalog.getString('Select or search a group ...')
} }
}, },
@ -850,79 +826,233 @@ angular.module('OpenSlidesApp.users.site', ['OpenSlidesApp.users'])
.controller('GroupListCtrl', [ .controller('GroupListCtrl', [
'$scope', '$scope',
'$http',
'operator',
'Group', 'Group',
function($scope, Group) { 'permissions',
Group.bindAll({}, $scope, 'groups'); '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 // delete selected group
$scope.delete = function (group) { $scope.delete = function (group) {
Group.destroy(group.id); 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', [ .controller('GroupCreateCtrl', [
'$scope', '$scope',
'$state',
'Group', 'Group',
'permissions', function($scope, Group) {
function($scope, $state, Group, permissions) { $scope.new_name = '';
// get all permissions $scope.alert = {};
$scope.permissions = permissions;
$scope.group = {}; $scope.save = function() {
$scope.save = function (group) { var group = {
if (!group.permissions) { name: $scope.new_name,
group.permissions = []; permissions: []
} };
Group.create(group).then( Group.create(group).then(
function(success) { function (success) {
$state.go('users.group.list'); $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', [ .controller('userMenu', [
'$scope', '$scope',
'$http', '$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="header">
<div class="title"> <div class="title">
<div class="submenu"> <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> <i class="fa fa-plus fa-lg"></i>
<translate>New</translate> <translate>New</translate>
</a> </a>
@ -15,48 +16,58 @@
</div> </div>
<div class="details"> <div class="details">
<div class="row form-group"> <p translate>
<div class="col-sm-4 pull-right"> All your changes are saved immediately. Changes you make are only effective once you (or the users concerned) reload the page.
<div class="form-group"> </p>
<div class="input-group"> <table id="groups-table" class="table table-bordered">
<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">
<thead> <thead>
<tr> <tr>
<th ng-click="sortby='id';reverse=!reverse"> <th class="perm-head">
<translate>ID</translate> <h4 translate>Permissions</h4>
<i class="fa" ng-show="sortby == 'id'" <th ng-repeat="group in groups" ng-mouseover="group.hover=true" ng-mouseleave="group.hover=false">
ng-class="reverse ? 'fa-caret-down' : 'fa-caret-up'"></i> <span class="optional">
<th ng-click="sortby='name';reverse=!reverse"> {{ group.name | translate }}
<translate>Name</translate> </span>
<i class="fa" ng-show="sortby == 'name'" <span class="optional-show" uib-tooltip="{{ group.name }}">
ng-class="reverse ? 'fa-caret-down' : 'fa-caret-up'"></i> {{ group.name | translate | limitTo: 1 }}...
<th os-perms="users.can_manage core.can_manage_projector" class="mini_width" translate>Actions</th> </span>
<tbody> <i class="fa fa-info-circle" ng-if="group.id == 1"
<tr ng-repeat="group in groups | filter: filter.search | orderBy:sortby:reverse"> uib-tooltip="{{ 'Users without any assigned group gain the permissions from this group.' | translate }}"></i>
<td>{{ group.id }} <div os-perms="users.can_manage" ng-hide="group.id == 1" class="hoverActions"
<td><a ui-sref="users.group.detail({id: group.id})">{{ group.name | translate }}</a> ng-class="{'hiddenDiv': !group.hover}">
<td os-perms="users.can_manage" class="nobr"> <!--edit name-->
<!-- edit --> <a href="" ng-click="openDialog(group)">
<a ui-sref="users.group.detail.update({id: group.id})" os-perms="users.can_manage" <i class="fa fa-pencil fa-lg"></i></a>
class="btn btn-default btn-sm" &nbsp;
title="{{ 'Edit' | translate }}"> <!--delete-->
<i class="fa fa-pencil"></i> <a href="" class="text-danger"
</a>
<!-- delete -->
<a os-perms="users.can_manage" class="btn btn-danger btn-sm"
ng-bootbox-confirm="{{ 'Are you sure you want to delete this entry?' | translate }}<br> ng-bootbox-confirm="{{ 'Are you sure you want to delete this entry?' | translate }}<br>
<b>{{ group.name }}</b>" <b>{{ group.name | translate }}</b>"
ng-bootbox-confirm-action="delete(group)" ng-bootbox-confirm-action="delete(group)" translate>
title="{{ 'Delete' | translate }}"> <i class="fa fa-trash fa-lg"></i>
<i class="fa fa-trash-o"></i> </a>
</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> </table>
</div> </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> <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> <code>title, first_name, last_name, structure_level, number, groups, comment, is_active, is_committee</code>
<li><translate>Default groups</translate>: <li><translate>Default groups</translate>:
<translate>Delegates</translate> <code>3</code>, <translate>Delegates</translate> <code>2</code>,
<translate>Staff</translate> <code>4</code> <translate>Staff</translate> <code>3</code>
<translate>Committees</translate> <code>5</code> <translate>Committees</translate> <code>4</code>
<li translate>At least first name or last name have to be filled in. All <li translate>At least first name or last name have to be filled in. All
other fields are optional and may be empty. other fields are optional and may be empty.
<li translate>Only double quotes are accepted as text delimiter (no single quotes). <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() self.motion.create_poll()
def test_number_of_queries(self): def test_number_of_queries(self):
with self.assertNumQueries(16): with self.assertNumQueries(17):
self.client.get(reverse('motion-detail', args=[self.motion.pk])) self.client.get(reverse('motion-detail', args=[self.motion.pk]))
@ -312,7 +312,7 @@ class SupportMotion(TestCase):
""" """
def setUp(self): def setUp(self):
self.admin = get_user_model().objects.get(username='admin') 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.client.login(username='admin', password='admin')
self.motion = Motion( self.motion = Motion(
title='test_title_chee7ahCha6bingaew4e', title='test_title_chee7ahCha6bingaew4e',

View File

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

View File

@ -290,12 +290,10 @@ class UserManagerGeneratePassword(TestCase):
class UserManagerCreateOrResetAdminUser(TestCase): class UserManagerCreateOrResetAdminUser(TestCase):
def test_get_admin_group(self, mock_group): 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): def mock_side_effect(pk):
if pk == 2: if pk == 3:
result = 'mock_registered'
elif pk == 4:
result = 'mock_staff' result = 'mock_staff'
else: else:
result = '' result = ''
@ -308,7 +306,7 @@ class UserManagerCreateOrResetAdminUser(TestCase):
manager.create_or_reset_admin_user() 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') admin_user.groups.add.assert_called_once_with('mock_staff')
def test_password_set_to_admin(self, mock_group): def test_password_set_to_admin(self, mock_group):