Merge pull request #2260 from FinnStutzenstein/Feature435
New matrix interface for group editing (closes #435)
This commit is contained in:
commit
73095948ca
@ -27,6 +27,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.
|
||||
|
@ -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;
|
||||
|
@ -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) {
|
||||
|
@ -798,6 +798,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;
|
||||
@ -976,7 +1012,6 @@ tr.selected td {
|
||||
color: #9a9898;
|
||||
}
|
||||
|
||||
|
||||
/*////////////////////////////////////////
|
||||
=MEDIA QUERIES (RESPONSIVE DESIGN)
|
||||
////////////////////////////////////////*/
|
||||
@ -999,6 +1034,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; }
|
||||
}
|
||||
|
||||
|
||||
@ -1021,6 +1066,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 */
|
||||
@ -1035,4 +1083,7 @@ tr.selected td {
|
||||
.col2 .projector_full {
|
||||
margin-left: 0px;
|
||||
}
|
||||
#groups-table .perm-head {
|
||||
width: 150px;
|
||||
}
|
||||
}
|
||||
|
@ -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) {
|
||||
|
@ -123,6 +123,7 @@ angular.module('OpenSlidesApp.motions', [
|
||||
name: name,
|
||||
useClass: jsDataModel,
|
||||
verboseName: gettext('Motion'),
|
||||
verboseNamePlural: gettext('Motions'),
|
||||
methods: {
|
||||
getResourceName: function () {
|
||||
return name;
|
||||
|
@ -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):
|
||||
"""
|
||||
@ -30,12 +29,13 @@ class CustomizedModelBackend(ModelBackend):
|
||||
if not hasattr(user_obj, '_group_perm_cache'):
|
||||
if user_obj.is_superuser:
|
||||
perms = Permission.objects.all()
|
||||
else:
|
||||
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()
|
||||
# The next two lines are the customization.
|
||||
query = Q(**{user_groups_query: user_obj}) | Q(group__pk=2)
|
||||
perms = Permission.objects.filter(query)
|
||||
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
|
||||
|
@ -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'})
|
||||
|
@ -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.'))
|
||||
|
@ -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
|
||||
|
@ -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');
|
||||
}
|
||||
]);
|
||||
|
||||
|
@ -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');
|
||||
$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',
|
||||
|
@ -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>
|
27
openslides/users/static/templates/users/group-edit.html
Normal file
27
openslides/users/static/templates/users/group-edit.html
Normal 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>
|
@ -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>
|
@ -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>
|
||||
<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>
|
||||
|
||||
<!--delete-->
|
||||
<a os-perms="users.can_manage" class="btn btn-danger btn-sm"
|
||||
<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>
|
||||
<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>
|
||||
|
@ -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).
|
||||
|
@ -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',
|
||||
|
@ -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)
|
||||
|
@ -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):
|
||||
|
Loading…
Reference in New Issue
Block a user