Implemented auth via AngularJS

Also added the derective osPerms to check if the current user has permissions.
Removed old Django views and urls for user.
Created utils.views.APIView which should be used instead of the AjaxView.

Fixes: #1470
Fixes: #1454
This commit is contained in:
Oskar Hahn 2015-02-12 22:42:54 +01:00 committed by Norman Jäckel
parent a6d0c88730
commit 1969416e64
28 changed files with 470 additions and 1230 deletions

View File

@ -3,8 +3,6 @@ angular.module('OpenSlidesApp.core', [])
.config(function($stateProvider) { .config(function($stateProvider) {
// Use stateProvider.decorator to give default values to our states // Use stateProvider.decorator to give default values to our states
$stateProvider.decorator('views', function(state, parent) { $stateProvider.decorator('views', function(state, parent) {
var result = {}, var result = {},
views = parent(state); views = parent(state);
@ -69,12 +67,9 @@ angular.module('OpenSlidesApp.core', [])
}); });
}) })
.config(function($stateProvider, $urlRouterProvider, $locationProvider) { .config(function($stateProvider, $locationProvider) {
// Core urls // Core urls
$urlRouterProvider.otherwise('/'); $stateProvider.state('dashboard', {
$stateProvider
.state('dashboard', {
url: '/', url: '/',
templateUrl: 'static/templates/dashboard.html' templateUrl: 'static/templates/dashboard.html'
}); });
@ -119,16 +114,6 @@ angular.module('OpenSlidesApp.core', [])
}); });
}) })
.run(function($rootScope, i18n) {
// Puts the gettext methods into each scope.
// Uses the methods that are known by xgettext by default.
methods = ['gettext', 'dgettext', 'dcgettext', 'ngettext', 'dngettext',
'pgettext', 'dpgettext'];
_.forEach(methods, function(method) {
$rootScope[method] = _.bind(i18n[method], i18n);
});
})
.run(function($rootScope, Config) { .run(function($rootScope, Config) {
// Puts the config object into each scope. // Puts the config object into each scope.
// TODO: maybe rootscope.config has to set before findAll() is finished // TODO: maybe rootscope.config has to set before findAll() is finished

View File

@ -22,14 +22,13 @@
<a href="/" class="logo"><img src="/static/img/logo.png" alt="OpenSlides" /></a> <a href="/" class="logo"><img src="/static/img/logo.png" alt="OpenSlides" /></a>
<span class="title optional">{{ config('event_name') }}</span> <span class="title optional">{{ config('event_name') }}</span>
</div> </div>
{% block loginbutton %} <div class="navbar-right" ng-controller="userMenu">
<div class="navbar-right">
<!-- login/logout button --> <!-- login/logout button -->
<div class="btn-group"> <div class="btn-group">
{% if user.is_authenticated %} <div ng-if="operator.isAuthenticated()">
<a href="#" data-toggle="dropdown" class="btn btn-default dropdown-toggle"> <a href="#" data-toggle="dropdown" class="btn btn-default dropdown-toggle">
<span class="glyphicon glyphicon-user" aria-hidden="true"></span> <span class="glyphicon glyphicon-user" aria-hidden="true"></span>
<span class="optional-small">{{ user.username }}</span> <span class="optional-small">{{ operator.user.get_short_name() }}</span>
<span class="caret"></span> <span class="caret"></span>
</a> </a>
<ul class="dropdown-menu pull-right"> <ul class="dropdown-menu pull-right">
@ -40,19 +39,29 @@
<span class="glyphicon glyphicon-lock" aria-hidden="true"></span> <span class="glyphicon glyphicon-lock" aria-hidden="true"></span>
{% trans "Change password" %}</a></li> {% trans "Change password" %}</a></li>
<li class="divider"></li> <li class="divider"></li>
<li><a href="{% url 'user_logout' %}"> <li>
<a ng-click="logout()">
<span class="glyphicon glyphicon-log-out" aria-hidden="true"></span> <span class="glyphicon glyphicon-log-out" aria-hidden="true"></span>
{% trans "Logout" %}</a></li> Logout
</a>
</li>
</ul> </ul>
{% else %} </div>
<a href="{% url 'user_login' %}" class="btn btn-default"> <div ng-if="!operator.isAuthenticated()">
<a href="#" ng-click="showLoginForm = true" class="btn btn-default">
<span class="glyphicon glyphicon-log-in" aria-hidden="true"></span> <span class="glyphicon glyphicon-log-in" aria-hidden="true"></span>
{{ gettext("Login") }} {{ gettext("Login") }}
</a> </a>
{% endif %} <div ng-if="showLoginForm">
<form>
username: <input type="text" ng-model="username"><br>
password: <input type="password" ng-model="password"><br>
<input type="submit" ng-click="login(username, password)" value="Save" />
</form>
</div>
</div>
</div> </div>
</div> </div>
{% endblock %}
</div> </div>
</nav> </nav>
<!-- Container --> <!-- Container -->

View File

@ -61,7 +61,7 @@
{% trans "Logout" %}</a></li> {% trans "Logout" %}</a></li>
</ul> </ul>
{% else %} {% else %}
<a href="{% url 'user_login' %}" class="btn btn-default"> <a href="" class="btn btn-default">
<span class="glyphicon glyphicon-log-in" aria-hidden="true"></span> <span class="glyphicon glyphicon-log-in" aria-hidden="true"></span>
{% trans "Login" %}</a> {% trans "Login" %}</a>
{% endif %} {% endif %}

View File

@ -10,6 +10,7 @@ from django.utils.importlib import import_module
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
from haystack.views import SearchView as _SearchView from haystack.views import SearchView as _SearchView
from django.http import HttpResponse from django.http import HttpResponse
from django.views.decorators.csrf import ensure_csrf_cookie
from openslides import get_version as get_openslides_version from openslides import get_version as get_openslides_version
from openslides import get_git_commit_id, RELEASE from openslides import get_git_commit_id, RELEASE
@ -35,6 +36,14 @@ class IndexView(utils_views.View):
to the custom staticfiles directory. See STATICFILES_DIRS in settings.py. to the custom staticfiles directory. See STATICFILES_DIRS in settings.py.
""" """
@classmethod
def as_view(cls, *args, **kwargs):
"""
Makes sure that the csrf cookie is send.
"""
view = super().as_view(*args, **kwargs)
return ensure_csrf_cookie(view)
def get(self, *args, **kwargs): def get(self, *args, **kwargs):
with open(finders.find('templates/index.html')) as f: with open(finders.find('templates/index.html')) as f:
content = f.read() content = f.read()
@ -341,6 +350,6 @@ class TagViewSet(ModelViewSet):
Calls self.permission_denied() if the requesting user has not the Calls self.permission_denied() if the requesting user has not the
permission to manage tags and it is a create, update or detroy request. permission to manage tags and it is a create, update or detroy request.
""" """
if (self.action in ('create', 'update', 'destroy') if (self.action in ('create', 'update', 'destroy') and
and not request.user.has_perm('core.can_manage_tags')): not request.user.has_perm('core.can_manage_tags')):
self.permission_denied(request) self.permission_denied(request)

View File

@ -11,7 +11,7 @@ AUTH_USER_MODEL = 'users.User'
AUTHENTICATION_BACKENDS = ('openslides.users.auth.CustomizedModelBackend',) AUTHENTICATION_BACKENDS = ('openslides.users.auth.CustomizedModelBackend',)
LOGIN_URL = '/login/' LOGIN_URL = '/users/'
LOGIN_REDIRECT_URL = '/' LOGIN_REDIRECT_URL = '/'
SESSION_COOKIE_NAME = 'OpenSlidesSessionID' SESSION_COOKIE_NAME = 'OpenSlidesSessionID'

View File

@ -11,14 +11,15 @@ handler500 = ErrorView.as_view(status_code=500)
urlpatterns = patterns( urlpatterns = patterns(
'', '',
url(r'^rest/', include(router.urls)), url(r'^rest/', include(router.urls)),
# TODO: add "special" urls, for example pdf views etc. (r'^users/', include('openslides.users.urls')),
url(r'^user.*', IndexView.as_view()), url(r'^users.*', IndexView.as_view()),
# activate next line go get more angular views # activate next line go get more angular views
# url(r'^$', IndexView.as_view()), # url(r'^$', IndexView.as_view()),
# url(r'^assignment.*', IndexView.as_view()), # url(r'^assignment.*', IndexView.as_view()),
# url(r'^agenda.*', IndexView.as_view()), # url(r'^agenda.*', IndexView.as_view()),
) )
@ -31,7 +32,6 @@ urlpatterns += patterns(
(r'^agenda/', include('openslides.agenda.urls')), (r'^agenda/', include('openslides.agenda.urls')),
(r'^motion/', include('openslides.motion.urls')), (r'^motion/', include('openslides.motion.urls')),
(r'^assignment/', include('openslides.assignment.urls')), (r'^assignment/', include('openslides.assignment.urls')),
(r'^user/', include('openslides.users.urls')),
(r'^mediafile/', include('openslides.mediafile.urls')), (r'^mediafile/', include('openslides.mediafile.urls')),
(r'^config/', include('openslides.config.urls')), (r'^config/', include('openslides.config.urls')),
(r'^projector/', include('openslides.projector.urls')), (r'^projector/', include('openslides.projector.urls')),
@ -44,14 +44,6 @@ urlpatterns += patterns(
'', '',
(r'^jsi18n/$', 'django.views.i18n.javascript_catalog', js_info_dict), (r'^jsi18n/$', 'django.views.i18n.javascript_catalog', js_info_dict),
url(r'^login/$',
'openslides.users.views.login',
name='user_login'),
url(r'^logout/$',
'django.contrib.auth.views.logout_then_login',
name='user_logout'),
url(r'^myusersettings/$', url(r'^myusersettings/$',
UserSettingsView.as_view(), UserSettingsView.as_view(),
name='user_settings'), name='user_settings'),

View File

@ -1,147 +1,10 @@
from django import forms from django import forms
from django.conf import settings from django.conf import settings
from django.utils.translation import ugettext as _, ugettext_lazy from django.utils.translation import ugettext_lazy
from openslides.config.api import config from openslides.utils.forms import CssClassMixin
from openslides.utils.forms import (CssClassMixin,
LocalizedModelMultipleChoiceField)
from .models import Group, Permission, User from .models import User
from .api import get_protected_perm
class UserCreateForm(CssClassMixin, forms.ModelForm):
groups = LocalizedModelMultipleChoiceField(
# Hide the built-in groups 'Anonymous' (pk=1) and 'Registered' (pk=2)
queryset=Group.objects.exclude(pk__in=[1, 2]),
label=ugettext_lazy('Groups'), required=False)
class Meta:
model = User
fields = ('title', 'first_name', 'last_name', 'groups',
'structure_level', 'about_me', 'comment', 'is_active',
'default_password')
def clean(self, *args, **kwargs):
"""
Ensures that a user has either a first name or a last name.
"""
cleaned_data = super(UserCreateForm, self).clean(*args, **kwargs)
if not (cleaned_data['first_name'] or cleaned_data['last_name']):
error_msg = _('First name and last name can not both be empty.')
raise forms.ValidationError(error_msg)
return cleaned_data
class UserMultipleCreateForm(forms.Form):
users_block = forms.CharField(
widget=forms.Textarea,
label=ugettext_lazy('Users'),
help_text=ugettext_lazy('Use one line per user for its name '
'(first name and last name).'))
class UserUpdateForm(UserCreateForm):
"""
Form to update an user. It raises a validation error, if a non-superuser
user edits himself and removes the last group containing the permission
to manage users.
"""
class Meta:
model = User
fields = ('username', 'title', 'first_name', 'last_name',
'groups', 'structure_level', 'about_me', 'comment',
'is_active', 'default_password')
def __init__(self, *args, **kwargs):
self.request = kwargs.pop('request')
return super().__init__(*args, **kwargs)
def clean(self, *args, **kwargs):
"""
Raises a validation error if a non-superuser user edits himself
and removes the last group containing the permission to manage users.
"""
# TODO: Check this in clean_groups
if (self.request.user == self.instance and
not self.instance.is_superuser and
not self.cleaned_data['groups'].filter(permissions__in=[get_protected_perm()]).exists()):
error_msg = _('You can not remove the last group containing the permission to manage users.')
raise forms.ValidationError(error_msg)
return super().clean(*args, **kwargs)
class GroupForm(CssClassMixin, forms.ModelForm):
permissions = LocalizedModelMultipleChoiceField(
queryset=Permission.objects.all(), label=ugettext_lazy('Permissions'), required=False,
widget=forms.SelectMultiple(attrs={'class': 'dont_use_bsmselect'}))
users = forms.ModelMultipleChoiceField(
queryset=User.objects.all(), label=ugettext_lazy('Participants'), required=False,
widget=forms.SelectMultiple(attrs={'class': 'dont_use_bsmselect'}))
class Meta:
model = Group
fields = '__all__'
def __init__(self, *args, **kwargs):
# Take request argument
self.request = kwargs.pop('request', None)
# Initial users
if kwargs.get('instance', None) is not None:
initial = kwargs.setdefault('initial', {})
initial['users'] = kwargs['instance'].user_set.all()
super().__init__(*args, **kwargs)
if config['users_sort_users_by_first_name']:
self.fields['users'].queryset = self.fields['users'].queryset.order_by('first_name')
def save(self, commit=True):
instance = forms.ModelForm.save(self, False)
old_save_m2m = self.save_m2m
def save_m2m():
old_save_m2m()
instance.user_set.clear()
for user in self.cleaned_data['users']:
instance.user_set.add(user)
self.save_m2m = save_m2m
if commit:
instance.save()
self.save_m2m()
return instance
def clean(self, *args, **kwargs):
"""
Raises a validation error if a non-superuser user removes himself
from the last group containing the permission to manage users.
Raises also a validation error if a non-superuser removes his last
permission to manage users from the (last) group.
"""
# TODO: Check this in clean_users or clean_permissions
if (self.request and
not self.request.user.is_superuser and
self.request.user not in self.cleaned_data['users'] and
not Group.objects.exclude(pk=self.instance.pk).filter(
permissions__in=[get_protected_perm()],
user__pk=self.request.user.pk).exists()):
error_msg = _('You can not remove yourself from the last group containing the permission to manage users.')
raise forms.ValidationError(error_msg)
if (self.request and
not self.request.user.is_superuser and
not get_protected_perm() in self.cleaned_data['permissions'] and
not Group.objects.exclude(pk=self.instance.pk).filter(
permissions__in=[get_protected_perm()],
user__pk=self.request.user.pk).exists()):
error_msg = _('You can not remove the permission to manage users from the last group you are in.')
raise forms.ValidationError(error_msg)
return super(GroupForm, self).clean(*args, **kwargs)
class UsersettingsForm(CssClassMixin, forms.ModelForm): class UsersettingsForm(CssClassMixin, forms.ModelForm):

View File

@ -10,5 +10,5 @@ class UserMainMenuEntry(MainMenuEntry):
verbose_name = ugettext_lazy('Users') verbose_name = ugettext_lazy('Users')
required_permission = 'users.can_see_extra_data' required_permission = 'users.can_see_extra_data'
default_weight = 50 default_weight = 50
pattern_name = 'user_list' pattern_name = 'core_dashboard'
icon_css_class = 'glyphicon-user' icon_css_class = 'glyphicon-user'

View File

@ -3,12 +3,13 @@
from random import choice from random import choice
from django.contrib.auth.hashers import make_password from django.contrib.auth.hashers import make_password
from django.contrib.auth.models import ( from django.contrib.auth.models import ( # noqa
AbstractBaseUser, AbstractBaseUser,
BaseUserManager, BaseUserManager,
PermissionsMixin) Group,
from django.contrib.auth.models import Group, Permission # noqa Permission,
from django.core.urlresolvers import reverse PermissionsMixin
)
from django.db import models from django.db import models
from django.utils.translation import ugettext_lazy, ugettext_noop from django.utils.translation import ugettext_lazy, ugettext_noop
@ -158,11 +159,9 @@ class User(RESTModelMixin, SlideMixin, AbsoluteUrlMixin, PermissionsMixin, Abstr
Returns the URL to the user. Returns the URL to the user.
""" """
if link == 'detail': if link == 'detail':
url = reverse('user_detail', args=[str(self.pk)]) url = "/users/%s/" % self.pk
elif link == 'update': elif link == 'update':
url = reverse('user_update', args=[str(self.pk)]) url = "/users/%s/edit/" % self.pk
elif link == 'delete':
url = reverse('user_delete', args=[str(self.pk)])
else: else:
url = super().get_absolute_url(link) url = super().get_absolute_url(link)
return url return url

View File

@ -3,7 +3,7 @@ angular.module('OpenSlidesApp.users', [])
.config(function($stateProvider) { .config(function($stateProvider) {
$stateProvider $stateProvider
.state('users', { .state('users', {
url: '/user', url: '/users',
abstract: true, abstract: true,
template: "<ui-view/>", template: "<ui-view/>",
}) })
@ -33,7 +33,48 @@ angular.module('OpenSlidesApp.users', [])
}); });
}) })
.factory('User', function(DS) { .factory('operator', function(User, Group) {
var operator = {
user: null,
perms: [],
isAuthenticated: function() {
return !!this.user;
},
setUser: function(user_id) {
if (user_id) {
User.find(user_id).then(function(user) {
operator.user = user;
// TODO: load only the needed groups
Group.findAll().then(function() {
operator.perms = user.getPerms();
});
});
} else {
operator.user = null;
Group.find(1).then(function(group) {
operator.perms = group.permissions;
});
}
},
hasPerms: function(perms) {
if (typeof perms == 'string') {
perms = perms.split(' ');
}
return _.intersection(perms, operator.perms).length > 0;
},
}
return operator;
})
.run(function(operator, $rootScope, $http) {
// Put the operator into the root scope
$http.get('/users/whoami/').success(function(data) {
operator.setUser(data.user_id);
});
$rootScope.operator = operator;
})
.factory('User', function(DS, Group) {
return DS.defineResource({ return DS.defineResource({
name: 'users/user', name: 'users/user',
endpoint: '/rest/users/user/', endpoint: '/rest/users/user/',
@ -52,8 +93,24 @@ angular.module('OpenSlidesApp.users', [])
} }
return name; return name;
}, },
getPerms: function() {
var allPerms = [];
_.forEach(this.groups, function(groupId) {
// Get group from server
Group.find(groupId);
// But do not work with the returned promise, because in
// this case this method can not be called in $watch
group = Group.get(groupId);
if (group) {
_.forEach(group.permissions, function(perm) {
allPerms.push(perm);
});
} }
}); });
return _.uniq(allPerms);
},
},
});
}) })
.factory('Group', function(DS) { .factory('Group', function(DS) {
@ -63,6 +120,73 @@ angular.module('OpenSlidesApp.users', [])
}); });
}) })
/*
* Directive to check for permissions
*
* This is the Code from angular.js ngIf.
*
* TODO: find a way not to copy the code.
*/
.directive('osPerms', ['$animate', function($animate) {
return {
multiElement: true,
transclude: 'element',
priority: 600,
terminal: true,
restrict: 'A',
$$tlb: true,
link: function($scope, $element, $attr, ctrl, $transclude) {
var block, childScope, previousElements, perms;
if ($attr.osPerms[0] === '!') {
perms = _.trimLeft($attr.osPerms, '!')
} else {
perms = $attr.osPerms;
}
$scope.$watch(
function (scope) {
return scope.operator.hasPerms(perms);
},
function (value) {
if ($attr.osPerms[0] === '!') {
value = !value;
}
if (value) {
if (!childScope) {
$transclude(function(clone, newScope) {
childScope = newScope;
clone[clone.length++] = document.createComment(' end ngIf: ' + $attr.ngIf + ' ');
// Note: We only need the first/last node of the cloned nodes.
// However, we need to keep the reference to the jqlite wrapper as it might be changed later
// by a directive with templateUrl when its template arrives.
block = {
clone: clone
};
$animate.enter(clone, $element.parent(), $element);
});
}
} else {
if (previousElements) {
previousElements.remove();
previousElements = null;
}
if (childScope) {
childScope.$destroy();
childScope = null;
}
if (block) {
previousElements = getBlockNodes(block.clone);
$animate.leave(previousElements).then(function() {
previousElements = null;
});
block = null;
}
}
}
);
}
};
}])
.controller('UserListCtrl', function($scope, User, i18n) { .controller('UserListCtrl', function($scope, User, i18n) {
User.bindAll({}, $scope, 'users'); User.bindAll({}, $scope, 'users');
}) })
@ -85,4 +209,42 @@ angular.module('OpenSlidesApp.users', [])
User.save(user); User.save(user);
// TODO: redirect to list-view // TODO: redirect to list-view
}; };
})
.controller('userMenu', function($scope, $http, DS, User, operator) {
$scope.logout = function() {
$http.post('/users/logout/').success(function(data) {
operator.setUser(null);
// TODO: remove all data from cache and reload page
// DS.flush();
}); });
};
$scope.login = function(username, password) {
$http.post(
'/users/login/',
{'username': username, 'password': password}
).success(function(data) {
operator.setUser(data.user_id);
$scope.showLoginForm = false;
});
};
});
// this is code from angular.js. Find a way to call this function from this file
function getBlockNodes(nodes) {
// TODO(perf): just check if all items in `nodes` are siblings and if they are return the original
// collection, otherwise update the original collection.
var node = nodes[0];
var endNode = nodes[nodes.length - 1];
var blockNodes = [node];
do {
node = node.nextSibling;
if (!node) break;
blockNodes.push(node);
} while (node !== endNode);
return $(blockNodes);
}

View File

@ -4,4 +4,7 @@
<a ui-sref="users.user.detail.update({id: user.id })">{{ gettext('Edit') }}</a> <a ui-sref="users.user.detail.update({id: user.id })">{{ gettext('Edit') }}</a>
</li> </li>
</ul> </ul>
<a ui-sref="users.user.create">{{ gettext('New') }}</a>
<a os-perms="users.can_manage" ui-sref="users.user.create">{{ gettext('New') }}</a>
<span os-perms="!users.can_manage">No Permission to create</span>
<a href="/users/print/" target="_self">PDF</a>

View File

@ -1,55 +0,0 @@
{% extends "base.html" %}
{% load i18n %}
{% load tags %}
{% block title %}{% trans group.name %} {{ block.super }}{% endblock %}
{% block content %}
<h1>{% trans group.name %}
<small class="pull-right">
<a href="{% url 'group_list' %}" class="btn btn-mini">
<i class="icon-chevron-left"></i>
<span class="optional-small">{% trans "Back to overview" %}</span>
</a>
{% if perms.users.can_manage and group.pk != 1 and group.pk != 2 %}
<div class="btn-group">
<a data-toggle="dropdown" class="btn btn-mini dropdown-toggle">
<span class="optional-small">{% trans 'More actions' %} </span>
<span class="caret"></span>
</a>
<ul class="dropdown-menu pull-right">
<!-- edit -->
<li>
<a href="{% url 'group_update' group.id %}">
<i class="icon-pencil"></i>
{% trans 'Edit group' %}
</a>
</li>
<!-- delete -->
<li>
<a href="{% url 'group_delete' group.id %}">
<i class="icon-remove"></i>
{% trans 'Delete group' %}
</a>
</li>
</ul>
</div>
{% endif %}
</small>
</h1>
<p>{{ group.description }}</p>
<h4>{% trans "Members" %}</h4>
<ol>
{% for member in group_members %}
<li><a href="{{ member|absolute_url }}">{{ member }}</a></li>
{% empty %}
<p>{% trans "No members available." %}</p>
{% endfor %}
</ol>
{% endblock %}

View File

@ -1,39 +0,0 @@
{% extends "base.html" %}
{% load i18n %}
{% block title %}
{% if group %}
{% trans "Edit group" %}
{% else %}
{% trans "New group" %}
{% endif %}
{{ block.super }}
{% endblock %}
{% block content %}
<h1>
{% if group %}
{% trans "Edit group" %}
{% else %}
{% trans "New group" %}
{% endif %}
<small class="pull-right">
<a href="{% url 'group_list' %}" class="btn btn-mini">
<i class="icon-chevron-left"></i>
{% trans "Back to overview" %}
</a>
</small>
</h1>
<form action="" method="post">{% csrf_token %}
{% include "form.html" %}
<p>
{% include "formbuttons_saveapply.html" %}
<a href="{% url 'group_list' %}" class="btn">
{% trans 'Cancel' %}
</a>
</p>
<small>* {% trans "required" %}</small>
</form>
{% endblock %}

View File

@ -1,72 +0,0 @@
{% extends "base.html" %}
{% load i18n %}
{% load staticfiles %}
{% load tags %}
{% block title %}{% trans "Groups" %} {{ block.super }}{% endblock %}
{% block header %}
<link href="{% static 'css/dataTables/dataTables.bootstrap.css' %}" type="text/css" rel="stylesheet">
{% endblock %}
{% block javascript %}
<script src="{% static 'js/jquery/jquery.dataTables.min.js' %}" type="text/javascript"></script>
<script src="{% static 'js/jquery/dataTables.bootstrap.js' %}" type="text/javascript"></script>
{% endblock %}
{% block content %}
<h1>
{% trans "Groups" %}
<small class="pull-right">
<a href="{% url 'group_create' %}" class="btn btn-mini btn-primary" rel="tooltip" data-original-title="{% trans 'New group' %}">
<i class="icon-plus icon-white"></i>
{% trans "New" %}
</a>
<a href="{% url 'user_list' %}" class="btn btn-mini">
<i class="icon-chevron-left"></i>
<span class="optional-small">
{% trans "Back to users overview" %}
</span>
</a>
</small>
</h1>
<table id="dataTable" class="table table-striped table-bordered">
<thead>
<tr>
<th class="mini_width">{% trans "ID" %}</th>
<th>{% trans "Group" %}</th>
<th>{% trans "Members" %}</th>
<th class="mini_width">{% trans "Actions" %}</th>
</tr>
</thead>
{% for group in groups %}
<tr class="{% if group.is_active_slide %}activeline{% endif %}">
<td class="nobr">{{ group.pk }}
{% if group.pk == 1 or group.pk == 2 %}
<a title="{% blocktrans %}The groups 1 ('Anonymous') and 2 ('Registered') are fixed default groups which can not be deleted. Each created or imported user is a member of group 2. Use custom groups to set additional permissions for a subset of users.{% endblocktrans %}"><i class="icon-info-sign"></i></a>
{% endif %}
</td>
<td>
<a href="{% url 'group_detail' group.pk %}">{% trans group.name %}</a>
</td>
<td>
<span class="badge badge-info">{{ group.user_set.all.count }}</span>
</td>
<td>
<span style="width: 1px; white-space: nowrap;">
<a href="{% url 'group_update' group.id %}" title="{% trans 'Edit' %}" class="btn btn-mini">
<i class="icon-pencil"></i>
</a>
{% if group.pk != 1 and group.pk != 2 %}
<a href="{% url 'group_delete' group.id %}" title="{% trans 'Delete' %}" class="btn btn-mini">
<i class="icon-remove"></i>
</a>
{% endif %}
</span>
</td>
</tr>
{% endfor %}
</table>
{% endblock %}

View File

@ -1,59 +0,0 @@
{% extends "base.html" %}
{% load i18n %}
{% load staticfiles %}
{% block title %}{{ block.super }} {% trans "Login" %} {% endblock %}
{% block loginbutton %}
{% endblock %}
{% block body %}
<div id="login-page" class="container">
<h2><img src="{% static 'img/logo-login.png' %}"></h2>
{% if form.errors %}
<div class="alert alert-danger" role="alert">
{% for msg in form.non_field_errors %}
<em>{{ msg }}</em>
{% if not forloop.last %}<br />{% endif %}
{% empty %}
<em>{% trans "Your username and password were not accepted. Please try again." %}</em>
{% endfor %}
</div>
{% endif %}
{% if first_time_message %}
<div class="alert alert-info">
<em>{{ first_time_message|safe }}</em>
</div>
{% endif %}
<form method="post" action="{% url 'user_login' %}{% if next %}?next={{ next }}{% endif %}" class="well">
{% csrf_token %}
{# username #}
<div class="form-group input-group">
<div class="input-group-addon">
<span class="glyphicon glyphicon-user" aria-hidden="true"></span>
</div>
<input type="text" class="form-control input-lg" name="username" id="id_username" placeholder="{% trans 'Username' %}">
</div>
{# password #}
<div class="form-group input-group">
<div class="input-group-addon">
<span class="glyphicon glyphicon-lock" aria-hidden="true"></span>
</div>
<input type="password" class="form-control input-lg" name="password" id="id_password" placeholder="{% trans 'Password' %}">
</div>
<div class="form-group">
<button type="submit" class="btn btn-primary">
{% trans 'Login' %}
</button>
{% if os_enable_anonymous_login %}
<a id="anonymous_login" class="btn btn-default" href="{% url 'core_dashboard' %}">
{% trans 'Continue as guest' %}
</a>
{% endif %}
<input type="hidden" name="next" value="{{ next }}" />
</div>
</form>
{% endblock %}

View File

@ -1,92 +0,0 @@
{% extends "base.html" %}
{% load i18n %}
{% load tags %}
{% block title %}{{ shown_user }} {{ block.super }}{% endblock %}
{% block content %}
<h1>
{{ shown_user.clean_name }}
<small class="pull-right">
<a href="{% url 'user_list' %}" class="btn btn-mini">
<i class="icon-chevron-left"></i>
<span class="optional-small">
{% trans "Back to overview" %}
</span>
</a>
<!-- activate projector -->
{% if perms.core.can_manage_projector %}
<a href="{% url 'projector_activate_slide' shown_user.sid %}" class="activate_link btn {% if shown_user.active %}btn-primary{% endif %} btn-mini" rel="tooltip" data-original-title="{% trans 'Show users' %}">
<i class="icon-facetime-video {% if shown_user.active %}icon-white{% endif %}"></i>
</a>
{% endif %}
{% if perms.users.can_manage %}
<div class="btn-group">
<a data-toggle="dropdown" class="btn btn-mini dropdown-toggle">
<span class="optional-small">{% trans 'More actions' %} </span><span class="caret"></span>
</a>
<ul class="dropdown-menu pull-right">
<!-- edit -->
<li>
<a href="{{ user|absolute_url:'update' }}">
<i class="icon-pencil"></i>
{% trans 'Edit user' %}
</a>
</li>
<!-- delete -->
<li><a href="{% url 'user_delete' shown_user.id %}"><i class="icon-remove"></i> {% trans 'Delete user' %}</a></li>
</ul>
</div>
{% endif %}
</small>
</h1>
<div class="user_details">
<fieldset>
<legend>{% trans "Personal data" %}</legend>
<label>{% trans "Gender" %}</label>
{{ shown_user.get_gender_display }}
<label>{% trans "Email" %}</label>
{{ shown_user.email }}
<label>{% trans "About me" %}</label>
{{ shown_user.about_me|linebreaks }}
</fieldset>
<fieldset>
<legend>{% trans "Event data" %}</legend>
<label>{% trans "Structure level" %}</label>
{{ shown_user.structure_level }}
<label>{% trans "Committee" %}</label>
{{ shown_user.committee }}
<label>{% trans "Groups" %}</label>
{% if shown_user.groups.all %}
{% for group in shown_user.groups.all %}
{% if group.pk != 2 %}
{% trans group.name %}
{% if not forloop.last %}, {% endif %}
{% endif %}
{% endfor %}
{% else %}
{% trans "The user is not member of any group." %}
{% endif %}
</fieldset>
{% if perms.users.can_manage %}
<fieldset>
<legend>{% trans "Administrative data" %}</legend>
<label>{% trans "Username" %}</label>
{{ shown_user.username }}
<label>{% trans "Comment" %}</label>
{{ shown_user.comment|linebreaks }}
<label>{% trans "Last Login" %}</label>
{% if shown_user.last_login > shown_user.date_joined %}
{{ shown_user.last_login }}
{% else %}
{% trans "The user has not logged in yet." %}
{% endif %}
{% endif %}
</div>
{% endblock %}

View File

@ -1,49 +0,0 @@
{% extends "base.html" %}
{% load i18n %}
{% load staticfiles %}
{% block title %}
{% if edit_user %}
{% trans "Edit user" %}
{% else %}
{% trans "New user" %}
{% endif %}
{{ block.super }}
{% endblock %}
{% block content %}
<h1>
{% if edit_user %}
{% trans "Edit user" %}
{% else %}
{% trans "New user" %}
{% endif %}
<small class="pull-right">
<a href="{% url 'user_list' %}" class="btn btn-mini">
<i class="icon-chevron-left"></i>
{% trans "Back to overview" %}
</a>
</small>
</h1>
<form action="" method="post">{% csrf_token %}
{% include "form.html" %}
{% if edit_user %}
<p style="margin: -15px 0 25px 0;">
<a class="btn btn-mini" href="{% url 'user_reset_password' edit_user.id %}">
<i class="icon-exclamation-sign"></i>
{% trans 'Reset to First Password' %}
</a>
</p>
{% endif %}
<p>
{% include "formbuttons_saveapply.html" %}
<a href="{% url 'user_list' %}" class="btn">
{% trans 'Cancel' %}
</a>
</p>
<small>* {% trans "required" %}</small>
</form>
{% endblock %}

View File

@ -1,28 +0,0 @@
{% extends "base.html" %}
{% load i18n %}
{% block title %}
{% trans 'New multiple users' %} {{ block.super }}
{% endblock %}
{% block content %}
<h1>
{% trans 'New multiple users' %}
<small class="pull-right">
<a href="{% url 'user_list' %}" class="btn btn-mini">
<i class="icon-chevron-left"></i> {% trans 'Back to overview' %}
</a>
</small>
</h1>
<form action="" method="post">{% csrf_token %}
{% include 'form.html' %}
<p>
{% include 'formbuttons_save.html' %}
<a href="{% url 'user_list' %}" class="btn">
{% trans 'Cancel' %}
</a>
</p>
<small>* {% trans 'required' %}</small>
</form>
{% endblock %}

View File

@ -1,158 +0,0 @@
{% extends "base.html" %}
{% load i18n %}
{% load staticfiles %}
{% load tags %}
{% block title %}{% trans "Users" %} {{ block.super }}{% endblock %}
{% block header %}
<link href="{% static 'css/dataTables/dataTables.bootstrap.css' %}" type="text/css" rel="stylesheet">
{% endblock %}
{% block javascript %}
<script src="{% static 'js/jquery/jquery.dataTables.min.js' %}" type="text/javascript"></script>
<script src="{% static 'js/jquery/dataTables.bootstrap.js' %}" type="text/javascript"></script>
{% endblock %}
{% block content %}
<h1>
{% trans "Users" %}
<small class="pull-right">
{% if perms.users.can_manage %}
<a href="{% url 'user_create' %}" class="btn btn-mini btn-primary" rel="tooltip" data-original-title="{% trans 'New user' %}">
<i class="icon-plus icon-white"></i>
{% trans "New" %}
</a>
<a href="{% url 'user_create_multiple' %}" class="btn btn-mini" rel="tooltip" data-original-title="{% trans 'New multiple users' %}">
<i class="icon-plus"></i>
{% trans 'New multiple' %}
</a>
<a href="{% url 'group_list' %}" class="btn btn-mini" rel="tooltip" data-original-title="{% trans 'All groups' %}">
<i class="icon-group"></i>
<span class="optional-small">{% trans "Groups" %}</span>
</a>
<a href="{% url 'user_csv_import' %}" class="btn btn-mini" rel="tooltip" data-original-title="{% trans 'Import users' %}">
<i class="icon-import"></i>
<span class="optional-small"> {% trans 'Import' %}</span>
</a>
{% endif %}
{% if perms.users.can_see and perms.users.can_manage %}
<div class="btn-group">
<a data-toggle="dropdown" class="btn btn-mini dropdown-toggle">
<i class="icon-print"></i>
<span class="optional-small"> PDF</span>
<span class="caret"></span>
</a>
<ul class="dropdown-menu pull-right">
{% url 'user_settings' as url_usersettings %}
<li>
<a href="{% url 'user_print' %}" target="_blank">
<i class="icon-list"></i>
{% trans 'List of users' %}
</a>
</li>
<li>
<a href="{% url 'print_passwords' %}" target="_blank">
<i class="icon-th-large"></i>
{% trans 'List of access data' %}
</a>
</li>
</ul>
</div>
{% else %}
<a href="{% url 'user_print' %}" class="btn btn-mini" rel="tooltip" data-original-title="{% trans 'Print list of users as PDF' %}" target="_blank"><i class="icon-print"></i> PDF</a>
{% endif %}
</small>
</h1>
<table id="dataTable" class="table table-striped table-bordered" cellpadding="0" cellspacing="0" border="0">
<thead>
<tr>
<th>{% trans "Present" %}</th>
<th class="optional">{% trans "Title" %}</th>
<th>{% trans "Name" %}</th>
<th class="optional">{% trans "Structure level" %}</th>
<th class="optional">{% trans "Group" %}</th>
<th class="optional">{% trans "Committee" %}</th>
{% if perms.users.can_manage %}
<th class="optional">{% trans "Comment" %}</th>
<th class="optional">{% trans "Last Login" %}</th>
<th class="mini_width">{% trans "Actions" %}</th>
{% endif %}
</tr>
</thead>
<tbody>
{% for user in users %}
<tr class="{% if user.is_active_slide %}activeline{% endif %}">
<td>{% if perms.users.can_manage %}
{% if user != request_user %}
<a href="{% url 'user_status_toggle' user.id %}"
class="status_link btn btn-mini {% if user.is_active %}btn-success{% endif %}"
rel="tooltip" data-original-title="{% if user.is_active %}{% trans 'present' %}{% else %}{% trans 'absent' %}{% endif %}">
<i class="{% if user.is_active %}icon-on{% else %}icon-off{% endif %}"></i>
</a>
{% endif %}
{% else %}
<span class="status_link">
<i class="status_link {% if user.is_active %}icon-on{% else %}icon-off{% endif %} tooltip-bottom"
data-original-title="{% if user.is_active %}{% trans 'present' %}{% else %}{% trans 'absent' %}{% endif %}"></i>
{% endif %}
</td>
<td class="optional">{{ user.title }}</td>
<td>
{% if 'users_sort_users_by_first_name'|get_config %}
<a href="{{ user|absolute_url }}">{{ user.first_name }} {{ user.last_name }}</a>
{% else %}
<a href="{{ user|absolute_url }}">{{ user.last_name }}{% if user.last_name and user.first_name %},{% endif %} {{ user.first_name }}</a>
{% endif %}
</td>
<td class="optional">{{ user.structure_level }}</td>
<td class="optional">
{% for group in user.groups.all %}
{% if group.pk != 2 %}
{% trans group.name %}
{% if not forloop.last %}<br>{% endif %}
{% endif %}
{% endfor %}
</td>
<td class="optional">{{ user.committee }}</td>
{% if perms.users.can_manage %}
<td class="optional">
{% if user.comment %}
<a class="btn btn-mini" rel="popover" data-content="{{ user.comment|linebreaks }}">
<i class="icon icon-search"></i>
</a>
{% endif %}
</td>
<td class="optional">
{% if user.last_login > user.date_joined %}
{{ user.last_login }}
{% endif %}
</td>
<td>
<span style="width: 1px; white-space: nowrap;">
{% if perms.core.can_manage_projector %}
<a href="{{ user|absolute_url:'projector' }}" class="activate_link btn {% if user.is_active_slide %}btn-primary{% endif %} btn-mini"
rel="tooltip" data-original-title="{% trans 'Show user' %}">
<i class="icon-facetime-video {% if user.is_active_slide %}icon-white{% endif %}"></i>
</a>
{% endif %}
<a href="{{ user|absolute_url:'update' }}" rel="tooltip" data-original-title="{% trans 'Edit' %}"
class="btn btn-mini">
<i class="icon-pencil"></i>
</a>
{% if user != request_user %}
<a href="{% url 'user_delete' user.id %}" rel="tooltip" data-original-title="{% trans 'Delete' %}"
class="btn btn-mini">
<i class="icon-remove"></i>
</a>
{% endif %}
</span>
</td>
{% endif %}
</tr>
{% endfor %}
</tbody>
</table>
{% endblock %}

View File

@ -4,75 +4,10 @@ from . import views
urlpatterns = patterns( urlpatterns = patterns(
'', '',
# User
url(r'^$',
views.UserListView.as_view(),
name='user_list'),
url(r'^new/$',
views.UserCreateView.as_view(),
name='user_create'),
url(r'^new_multiple/$',
views.UserMultipleCreateView.as_view(),
name='user_create_multiple'),
url(r'^(?P<pk>\d+)/$',
views.UserDetailView.as_view(),
name='user_detail'),
url(r'^(?P<pk>\d+)/edit/$',
views.UserUpdateView.as_view(),
name='user_update'),
url(r'^(?P<pk>\d+)/del/$',
views.UserDeleteView.as_view(),
name='user_delete'),
url(r'^(?P<pk>\d+)/reset_password/$',
views.ResetPasswordView.as_view(),
name='user_reset_password'),
url(r'^(?P<pk>\d+)/status/activate/$',
views.SetUserStatusView.as_view(),
{'action': 'activate'},
name='user_status_activate'),
url(r'^(?P<pk>\d+)/status/deactivate/$',
views.SetUserStatusView.as_view(),
{'action': 'deactivate'},
name='user_status_deactivate'),
url(r'^(?P<pk>\d+)/status/toggle/$',
views.SetUserStatusView.as_view(),
{'action': 'toggle'},
name='user_status_toggle'),
url(r'^csv_import/$', url(r'^csv_import/$',
views.UserCSVImportView.as_view(), views.UserCSVImportView.as_view(),
name='user_csv_import'), name='user_csv_import'),
# Group
url(r'^group/$',
views.GroupListView.as_view(),
name='group_list'),
url(r'^group/new/$',
views.GroupCreateView.as_view(),
name='group_create'),
url(r'^group/(?P<pk>\d+)/$',
views.GroupDetailView.as_view(),
name='group_detail'),
url(r'^group/(?P<pk>\d+)/edit/$',
views.GroupUpdateView.as_view(),
name='group_update'),
url(r'^group/(?P<pk>\d+)/del/$',
views.GroupDeleteView.as_view(),
name='group_delete'),
# PDF # PDF
url(r'^print/$', url(r'^print/$',
views.UsersListPDF.as_view(), views.UsersListPDF.as_view(),
@ -81,4 +16,17 @@ urlpatterns = patterns(
url(r'^passwords/print/$', url(r'^passwords/print/$',
views.UsersPasswordsPDF.as_view(), views.UsersPasswordsPDF.as_view(),
name='print_passwords'), name='print_passwords'),
# auth
url(r'^login/$',
views.UserLoginView.as_view(),
name='user_login'),
url(r'^logout/$',
views.UserLogoutView.as_view(),
name='user_logout'),
url(r'^whoami/$',
views.WhoAmIView.as_view(),
name='user_whoami'),
) )

View File

@ -1,179 +1,30 @@
from django.contrib import messages from django.contrib import messages
from django.contrib.auth.forms import PasswordChangeForm from django.contrib.auth import login as auth_login
from django.contrib.auth.hashers import make_password from django.contrib.auth import logout as auth_logout
from django.contrib.auth.views import login as django_login from django.contrib.auth.forms import AuthenticationForm, PasswordChangeForm
from django.core.urlresolvers import reverse from django.utils.translation import ugettext as _
from django.utils.translation import ugettext as _, ugettext_lazy, activate from django.utils.translation import activate, ugettext_lazy
from openslides.config.api import config
from openslides.utils.rest_api import ModelViewSet from openslides.utils.rest_api import ModelViewSet
from openslides.utils.utils import delete_default_permissions, html_strong
from openslides.utils.views import ( from openslides.utils.views import (
CreateView, CSVImportView, DeleteView, DetailView, FormView, ListView, CSVImportView,
PDFView, PermissionMixin, QuestionView, RedirectView, SingleObjectMixin, FormView,
UpdateView, LoginMixin) LoginMixin,
from openslides.utils.exceptions import OpenSlidesError PDFView,
UpdateView,
APIView
)
from .api import get_protected_perm
from .csv_import import import_users from .csv_import import import_users
from .forms import (GroupForm, UserCreateForm, UserMultipleCreateForm, from .forms import UsersettingsForm
UsersettingsForm, UserUpdateForm)
from .models import Group, User from .models import Group, User
from .pdf import users_to_pdf, users_passwords_to_pdf from .pdf import users_passwords_to_pdf, users_to_pdf
from .serializers import GroupSerializer, UserCreateUpdateSerializer, UserFullSerializer, UserShortSerializer from .serializers import (
GroupSerializer,
UserCreateUpdateSerializer,
class UserListView(ListView): UserFullSerializer,
""" UserShortSerializer
Show all users. )
"""
required_permission = 'users.can_see_extra_data'
context_object_name = 'users'
def get_queryset(self):
query = User.objects
if config['users_sort_users_by_first_name']:
query = query.order_by('first_name')
else:
query = query.order_by('last_name')
return query.all()
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
all_users = User.objects.count()
# context vars
context.update({
'allusers': all_users,
'request_user': self.request.user})
return context
class UserDetailView(DetailView, PermissionMixin):
"""
Classed based view to show a specific user in the interface.
"""
required_permission = 'users.can_see_extra_data'
model = User
context_object_name = 'shown_user'
class UserCreateView(CreateView):
"""
Create a new user.
"""
required_permission = 'users.can_manage'
model = User
context_object_name = 'edit_user'
form_class = UserCreateForm
success_url_name = 'user_list'
url_name_args = []
def manipulate_object(self, form):
self.object.username = User.objects.generate_username(
form.cleaned_data['first_name'], form.cleaned_data['last_name'])
if not self.object.default_password:
self.object.default_password = User.objects.generate_password()
self.object.set_password(self.object.default_password)
class UserMultipleCreateView(FormView):
"""
View to create multiple users at once using a big text field.
Sets the password with md5. It is the same password as in the
default_password field in cleartext. A stronger password hasher is used,
when the password is changed by the user.
"""
required_permission = 'users.can_manage'
template_name = 'users/user_form_multiple.html'
form_class = UserMultipleCreateForm
success_url_name = 'user_list'
def form_valid(self, form):
# TODO: Use bulk_create
for number, line in enumerate(form.cleaned_data['users_block'].splitlines()):
names_list = line.split()
first_name = ' '.join(names_list[:-1])
last_name = names_list[-1]
username = User.objects.generate_username(first_name, last_name)
default_password = User.objects.generate_password()
User.objects.create(
username=username,
first_name=first_name,
last_name=last_name,
default_password=default_password,
password=make_password(default_password, '', 'md5'))
messages.success(self.request, _('%(number)d users successfully created.') % {'number': number + 1})
return super(UserMultipleCreateView, self).form_valid(form)
class UserUpdateView(UpdateView):
"""
Update an existing users.
"""
required_permission = 'users.can_manage'
model = User
context_object_name = 'edit_user'
form_class = UserUpdateForm
success_url_name = 'user_list'
url_name_args = []
def get_form_kwargs(self, *args, **kwargs):
form_kwargs = super(UserUpdateView, self).get_form_kwargs(*args, **kwargs)
form_kwargs.update({'request': self.request})
return form_kwargs
class UserDeleteView(DeleteView):
"""
Delete a user.
"""
required_permission = 'users.can_manage'
model = User
success_url_name = 'user_list'
url_name_args = []
def pre_redirect(self, request, *args, **kwargs):
if self.get_object() == self.request.user:
messages.error(request, _("You can not delete yourself."))
else:
super().pre_redirect(request, *args, **kwargs)
def pre_post_redirect(self, request, *args, **kwargs):
if self.get_object() == self.request.user:
messages.error(self.request, _("You can not delete yourself."))
else:
super().pre_post_redirect(request, *args, **kwargs)
class SetUserStatusView(SingleObjectMixin, RedirectView):
"""
Activate or deactivate an user.
"""
required_permission = 'users.can_manage'
allow_ajax = True
url_name = 'user_list'
url_name_args = []
model = User
def pre_redirect(self, request, *args, **kwargs):
action = kwargs['action']
if action == 'activate':
self.get_object().is_active = True
elif action == 'deactivate':
if self.get_object().user == self.request.user:
messages.error(request, _("You can not deactivate yourself."))
else:
self.get_object().is_active = False
self.get_object().save()
return super(SetUserStatusView, self).pre_redirect(request, *args, **kwargs)
def get_ajax_context(self, **kwargs):
context = super(SetUserStatusView, self).get_ajax_context(**kwargs)
context['active'] = self.get_object().is_active
return context
class UsersListPDF(PDFView): class UsersListPDF(PDFView):
@ -213,30 +64,10 @@ class UserCSVImportView(CSVImportView):
""" """
Import users via CSV. Import users via CSV.
""" """
import_function = staticmethod(import_users)
required_permission = 'users.can_manage' required_permission = 'users.can_manage'
success_url_name = 'user_list' success_url_name = 'user_list'
template_name = 'users/user_form_csv_import.html' template_name = 'users/user_form_csv_import.html'
import_function = staticmethod(import_users)
class ResetPasswordView(SingleObjectMixin, QuestionView):
"""
Set the Passwort for a user to his default password.
"""
required_permission = 'users.can_manage'
model = User
allow_ajax = True
question_message = ugettext_lazy('Do you really want to reset the password?')
def get_redirect_url(self, **kwargs):
return self.get_object().get_absolute_url('update')
def on_clicked_yes(self):
self.get_object().reset_password()
self.get_object().save()
def get_final_message(self):
return _('The Password for %s was successfully reset.') % html_strong(self.get_object())
class UserViewSet(ModelViewSet): class UserViewSet(ModelViewSet):
@ -291,164 +122,13 @@ class GroupViewSet(ModelViewSet):
self.permission_denied(request) self.permission_denied(request)
class GroupListView(ListView):
"""
Overview over all groups.
"""
required_permission = 'users.can_manage'
template_name = 'users/group_list.html'
context_object_name = 'groups'
model = Group
class GroupDetailView(DetailView, PermissionMixin):
"""
Classed based view to show a specific group in the interface.
"""
required_permission = 'users.can_manage'
model = Group
template_name = 'users/group_detail.html'
context_object_name = 'group'
def get_context_data(self, *args, **kwargs):
context = super(GroupDetailView, self).get_context_data(*args, **kwargs)
query = User.objects
if config['users_sort_users_by_first_name']:
query = query.order_by('first_name')
else:
query = query.order_by('last_name')
context['group_members'] = query.filter(groups__in=[context['group']])
return context
class GroupCreateView(CreateView):
"""
Create a new group.
"""
required_permission = 'users.can_manage'
template_name = 'users/group_form.html'
context_object_name = 'group'
model = Group
form_class = GroupForm
success_url_name = 'group_list'
url_name_args = []
def get(self, request, *args, **kwargs):
delete_default_permissions()
return super(GroupCreateView, self).get(request, *args, **kwargs)
def get_apply_url(self):
"""
Returns the url when the user clicks on 'apply'.
"""
return self.get_url('group_update', args=[self.object.pk])
class GroupUpdateView(UpdateView):
"""
Update an existing group.
"""
required_permission = 'users.can_manage'
template_name = 'users/group_form.html'
model = Group
context_object_name = 'group'
form_class = GroupForm
url_name_args = []
success_url_name = 'group_list'
def get(self, request, *args, **kwargs):
delete_default_permissions()
return super().get(request, *args, **kwargs)
def get_form_kwargs(self, *args, **kwargs):
form_kwargs = super().get_form_kwargs(*args, **kwargs)
form_kwargs.update({'request': self.request})
return form_kwargs
def get_apply_url(self):
"""
Returns the url when the user clicks on 'apply'.
"""
return self.get_url('group_update', args=[self.object.pk])
class GroupDeleteView(DeleteView):
"""
Delete a group.
"""
required_permission = 'users.can_manage'
model = Group
success_url_name = 'group_list'
question_url_name = 'group_detail'
url_name_args = []
def pre_redirect(self, request, *args, **kwargs):
if not self.is_protected_from_deleting():
super().pre_redirect(request, *args, **kwargs)
def pre_post_redirect(self, request, *args, **kwargs):
if not self.is_protected_from_deleting():
super().pre_post_redirect(request, *args, **kwargs)
def is_protected_from_deleting(self):
"""
Checks whether the group is protected.
"""
if self.get_object().pk in [1, 2]:
messages.error(self.request, _('You can not delete this group.'))
return True
if (not self.request.user.is_superuser and
get_protected_perm() in self.get_object().permissions.all() and
not Group.objects.exclude(pk=self.get_object().pk).filter(
permissions__in=[get_protected_perm()],
user__pk=self.request.user.pk).exists()):
messages.error(
self.request,
_('You can not delete the last group containing the permission '
'to manage users you are in.'))
return True
return False
def get_url_name_args(self):
try:
answer = self.get_answer()
except OpenSlidesError:
answer = 'no'
if self.request.method == 'POST' and answer != 'no':
return []
else:
return [self.object.pk]
def login(request):
extra_content = {}
try:
admin = User.objects.get(pk=1)
if admin.check_password(admin.default_password):
user_data = {
'user': html_strong(admin.username),
'password': html_strong(admin.default_password)}
extra_content['first_time_message'] = _(
"Installation was successfully! Use %(user)s "
"(password: %(password)s) for first login.<br>"
"<strong>Important:</strong> Please change the password after "
"first login! Otherwise this message still appears for "
"everyone and could be a security risk.") % user_data
extra_content['next'] = reverse('password_change')
except User.DoesNotExist:
pass
return django_login(request, template_name='users/login.html', extra_context=extra_content)
class UserSettingsView(LoginMixin, UpdateView): class UserSettingsView(LoginMixin, UpdateView):
required_permission = None
template_name = 'users/settings.html'
success_url_name = 'user_settings'
model = User model = User
form_class = UsersettingsForm form_class = UsersettingsForm
success_url_name = 'user_settings'
url_name_args = [] url_name_args = []
template_name = 'users/settings.html'
def get_initial(self): def get_initial(self):
initial = super().get_initial() initial = super().get_initial()
@ -465,9 +145,10 @@ class UserSettingsView(LoginMixin, UpdateView):
class UserPasswordSettingsView(LoginMixin, FormView): class UserPasswordSettingsView(LoginMixin, FormView):
form_class = PasswordChangeForm required_permission = None
success_url_name = 'core_dashboard'
template_name = 'users/password_change.html' template_name = 'users/password_change.html'
success_url_name = 'core_dashboard'
form_class = PasswordChangeForm
def form_valid(self, form): def form_valid(self, form):
form.save() form.save()
@ -478,3 +159,54 @@ class UserPasswordSettingsView(LoginMixin, FormView):
kwargs = super().get_form_kwargs() kwargs = super().get_form_kwargs()
kwargs['user'] = self.request.user kwargs['user'] = self.request.user
return kwargs return kwargs
class UserLoginView(APIView):
"""
Login the user via ajax.
"""
http_method_names = ['post']
def post(self, *args, **kwargs):
form = AuthenticationForm(self.request, data=self.request.data)
if form.is_valid():
self.user = form.get_user()
auth_login(self.request, self.user)
self.success = True
else:
self.success = False
return super().post(*args, **kwargs)
def get_context_data(self, **context):
context['success'] = self.success
if self.success:
context['user_id'] = self.user.pk
return super().get_context_data(**context)
class UserLogoutView(APIView):
"""
Logout the user via ajax.
"""
http_method_names = ['post']
def post(self, *args, **kwargs):
auth_logout(self.request)
return super().post(*args, **kwargs)
class WhoAmIView(APIView):
"""
Returns the user id in the session.
"""
http_method_names = ['get']
def get_context_data(self, **context):
"""
Appends the user id into the context.
Uses None for the anonymous user.
"""
return super().get_context_data(
user_id=self.request.user.pk,
**context)

View File

@ -17,7 +17,6 @@ class UserWidget(Widget):
default_weight = 60 default_weight = 60
default_active = False default_active = False
template_name = 'users/widget_user.html' template_name = 'users/widget_user.html'
more_link_pattern_name = 'user_list'
def get_context_data(self, **context): def get_context_data(self, **context):
return super(UserWidget, self).get_context_data( return super(UserWidget, self).get_context_data(

View File

@ -13,6 +13,8 @@ from django.utils.translation import ugettext as _, ugettext_lazy
from django.views import generic as django_views from django.views import generic as django_views
from reportlab.lib.units import cm from reportlab.lib.units import cm
from reportlab.platypus import SimpleDocTemplate, Spacer from reportlab.platypus import SimpleDocTemplate, Spacer
from rest_framework.views import APIView as _APIView
from rest_framework.response import Response
from .exceptions import OpenSlidesError from .exceptions import OpenSlidesError
from .forms import CSVImportForm from .forms import CSVImportForm
@ -76,13 +78,19 @@ class AjaxMixin(object):
Mixin to response to an ajax request with an json object. Mixin to response to an ajax request with an json object.
""" """
def get_ajax_context(self, **kwargs): def get_ajax_context(self, **context):
""" """
Returns a dictonary with the context for the ajax response. Returns a dictonary with the context for the ajax response.
""" """
return kwargs return context
def ajax_get(self, request, *args, **kwargs): def ajax_get(self, request, *args, **kwargs):
"""
Deprecated. Use ajax_response instead.
"""
return self.ajax_response()
def ajax_response(self):
""" """
Returns the HttpResponse. Returns the HttpResponse.
""" """
@ -289,13 +297,15 @@ class ListView(PermissionMixin, ExtraContextMixin, django_views.ListView):
class AjaxView(PermissionMixin, AjaxMixin, View): class AjaxView(PermissionMixin, AjaxMixin, View):
""" """
View for ajax requests. View for ajax requests.
Deprecated. Use APIView instead.
""" """
def get(self, request, *args, **kwargs): def get(self, request, *args, **kwargs):
# TODO: Raise an error, if the request is not an ajax-request # TODO: Raise an error, if the request is not an ajax-request
return self.ajax_get(request, *args, **kwargs) return self.ajax_response()
def post(self, *args, **kwargs): def post(self, *args, **kwargs):
return self.get(*args, **kwargs) return self.ajax_response()
class RedirectView(PermissionMixin, AjaxMixin, UrlMixin, django_views.RedirectView): class RedirectView(PermissionMixin, AjaxMixin, UrlMixin, django_views.RedirectView):
@ -640,3 +650,33 @@ class CSVImportView(FormView):
messages.warning(self.request, warning) messages.warning(self.request, warning)
messages.error(self.request, error) messages.error(self.request, error)
return super(CSVImportView, self).form_valid(form) return super(CSVImportView, self).form_valid(form)
class APIView(_APIView):
"""
The Django Rest framework APIView with improvements for OpenSlides.
"""
http_method_names = []
"""
The allowed actions have to be explicitly defined.
Django allowes the following:
http_method_names = ['get', 'post', 'put', 'patch', 'delete', 'head', 'options', 'trace']
"""
def get_context_data(self, **context):
"""
Returns the context for the response.
"""
return context
def method_call(self, request, *args, **kwargs):
"""
Http method that returns the response object with the context data.
"""
return Response(self.get_context_data())
# Add the http-methods and delete the method "method_call"
get = post = put = patch = delete = head = options = trace = method_call
del method_call

View File

@ -0,0 +1,89 @@
import json
from rest_framework.test import APIClient
from openslides.utils.test import TestCase
class TestWhoAmIView(TestCase):
url = '/users/whoami/'
def test_get_anonymous(self):
response = self.client.get(self.url)
self.assertEqual(response.status_code, 200)
self.assertEqual(response.content, b'{"user_id":null}')
def test_get_authenticated_user(self):
self.client.login(username='admin', password='admin')
response = self.client.get(self.url)
self.assertEqual(response.status_code, 200)
self.assertEqual(response.content, b'{"user_id":1}')
def test_post(self):
response = self.client.post(self.url)
self.assertEqual(response.status_code, 405)
class TestUserLogoutView(TestCase):
url = '/users/logout/'
def test_get(self):
response = self.client.get(self.url)
self.assertEqual(response.status_code, 405)
def test_post_anonymous(self):
response = self.client.post(self.url)
self.assertEqual(response.status_code, 200)
def test_post_authenticated_user(self):
self.client.login(username='admin', password='admin')
self.client.session['test_key'] = 'test_value'
response = self.client.post(self.url)
self.assertEqual(response.status_code, 200)
self.assertFalse(hasattr(self.client.session, 'test_key'))
class TestUserLoginView(TestCase):
url = '/users/login/'
def setUp(self):
self.client = APIClient()
def test_get(self):
response = self.client.get(self.url)
self.assertEqual(response.status_code, 405)
def test_post_no_data(self):
response = self.client.post(self.url)
self.assertEqual(response.status_code, 200)
self.assertEqual(response.content, b'{"success":false}')
def test_post_correct_data(self):
response = self.client.post(
self.url,
{'username': 'admin', 'password': 'admin'})
self.assertEqual(response.status_code, 200)
self.assertEqual(
json.loads(response.content.decode('utf-8')),
{'success': True, 'user_id': 1})
def test_post_incorrect_data(self):
response = self.client.post(
self.url,
{'username': 'wrong', 'password': 'wrong'})
self.assertEqual(response.status_code, 200)
self.assertEqual(
json.loads(response.content.decode('utf-8')),
{'success': False})

View File

@ -120,10 +120,6 @@ class ConfigFormTest(TestCase):
response = self.client_manager.get('/config/') response = self.client_manager.get('/config/')
self.assertRedirects(response=response, expected_url='/config/general/', self.assertRedirects(response=response, expected_url='/config/general/',
status_code=302, target_status_code=200) status_code=302, target_status_code=200)
bad_client = Client()
response = bad_client.get('/config/', follow=True)
self.assertRedirects(response=response, expected_url='/login/?next=/config/general/',
status_code=302, target_status_code=200)
def test_get_config_form_testgroupedpage1_manager_client(self): def test_get_config_form_testgroupedpage1_manager_client(self):
response = self.client_manager.get('/config/testgroupedpage1/') response = self.client_manager.get('/config/testgroupedpage1/')
@ -141,10 +137,6 @@ class ConfigFormTest(TestCase):
def test_get_config_form_testgroupedpage1_other_clients(self): def test_get_config_form_testgroupedpage1_other_clients(self):
response = self.client_normal_user.get('/config/testgroupedpage1/') response = self.client_normal_user.get('/config/testgroupedpage1/')
self.assertEqual(response.status_code, 403) self.assertEqual(response.status_code, 403)
bad_client = Client()
response = bad_client.get('/config/testgroupedpage1/')
self.assertRedirects(response=response, expected_url='/login/?next=/config/testgroupedpage1/',
status_code=302, target_status_code=200)
def test_get_config_form_testsimplepage1_manager_client(self): def test_get_config_form_testsimplepage1_manager_client(self):
response = self.client_manager.get('/config/testsimplepage1/') response = self.client_manager.get('/config/testsimplepage1/')

View File

@ -64,9 +64,6 @@ class MediafileTest(TestCase):
response = client.get('/mediafile/') response = client.get('/mediafile/')
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertTemplateUsed(response, 'mediafile/mediafile_list.html') self.assertTemplateUsed(response, 'mediafile/mediafile_list.html')
bad_client = Client()
response = bad_client.get('/mediafile/')
self.assertRedirects(response, expected_url='/login/?next=/mediafile/', status_code=302, target_status_code=200)
def test_upload_mediafile_get_request(self): def test_upload_mediafile_get_request(self):
clients = self.login_clients() clients = self.login_clients()
@ -82,10 +79,6 @@ class MediafileTest(TestCase):
response = clients['client_normal_user'].get('/mediafile/new/') response = clients['client_normal_user'].get('/mediafile/new/')
self.assertEqual(response.status_code, 403) self.assertEqual(response.status_code, 403)
bad_client = Client()
response = bad_client.get('/mediafile/new/')
self.assertRedirects(response, expected_url='/login/?next=/mediafile/new/', status_code=302, target_status_code=200)
def test_upload_mediafile_post_request(self): def test_upload_mediafile_post_request(self):
# Test first user # Test first user
client_1 = self.login_clients()['client_manager'] client_1 = self.login_clients()['client_manager']
@ -139,10 +132,6 @@ class MediafileTest(TestCase):
response = clients['client_normal_user'].get('/mediafile/1/edit/') response = clients['client_normal_user'].get('/mediafile/1/edit/')
self.assertEqual(response.status_code, 403) self.assertEqual(response.status_code, 403)
bad_client = Client()
response = bad_client.get('/mediafile/1/edit/')
self.assertRedirects(response, expected_url='/login/?next=/mediafile/1/edit/', status_code=302, target_status_code=200)
def test_edit_mediafile_get_request_own_file(self): def test_edit_mediafile_get_request_own_file(self):
clients = self.login_clients() clients = self.login_clients()
self.object.uploader = self.vip_user self.object.uploader = self.vip_user
@ -204,9 +193,6 @@ class MediafileTest(TestCase):
self.assertEqual(response.status_code, 403) self.assertEqual(response.status_code, 403)
response = clients['client_normal_user'].get('/mediafile/1/del/') response = clients['client_normal_user'].get('/mediafile/1/del/')
self.assertEqual(response.status_code, 403) self.assertEqual(response.status_code, 403)
bad_client = Client()
response = bad_client.get('/mediafile/1/del/')
self.assertRedirects(response, expected_url='/login/?next=/mediafile/1/del/', status_code=302, target_status_code=200)
def test_delete_mediafile_get_request_own_file(self): def test_delete_mediafile_get_request_own_file(self):
self.object.uploader = self.vip_user self.object.uploader = self.vip_user

View File

@ -41,15 +41,9 @@ class UserGetAbsoluteUrlTest(TestCase):
""" """
user = User(pk=5) user = User(pk=5)
with patch('openslides.users.models.reverse') as mock_reverse:
mock_reverse.return_value = 'test url'
url = user.get_absolute_url()
self.assertEqual( self.assertEqual(
url, user.get_absolute_url(),
'test url', '/users/5/')
"User.get_absolute_url() does not return the result of reverse.")
mock_reverse.assert_called_once_with('user_detail', args=['5'])
def test_get_absolute_url_detail(self): def test_get_absolute_url_detail(self):
""" """
@ -57,15 +51,11 @@ class UserGetAbsoluteUrlTest(TestCase):
""" """
user = User(pk=5) user = User(pk=5)
with patch('openslides.users.models.reverse') as mock_reverse:
mock_reverse.return_value = 'test url'
url = user.get_absolute_url('detail') url = user.get_absolute_url('detail')
self.assertEqual( self.assertEqual(
url, url,
'test url', '/users/5/')
"User.get_absolute_url('detail') does not return the result of reverse.")
mock_reverse.assert_called_once_with('user_detail', args=['5'])
def test_get_absolute_url_update(self): def test_get_absolute_url_update(self):
""" """
@ -73,31 +63,11 @@ class UserGetAbsoluteUrlTest(TestCase):
""" """
user = User(pk=5) user = User(pk=5)
with patch('openslides.users.models.reverse') as mock_reverse:
mock_reverse.return_value = 'test url'
url = user.get_absolute_url('update') url = user.get_absolute_url('update')
self.assertEqual( self.assertEqual(
url, url,
'test url', '/users/5/edit/')
"User.get_absolute_url('update') does not return the result of reverse.")
mock_reverse.assert_called_once_with('user_update', args=['5'])
def test_get_absolute_url_delete(self):
"""
Tests get_absolute_url() with 'delete' as argument.
"""
user = User(pk=5)
with patch('openslides.users.models.reverse') as mock_reverse:
mock_reverse.return_value = 'test url'
url = user.get_absolute_url('delete')
self.assertEqual(
url,
'test url',
"User.get_absolute_url('delete') does not return the result of reverse.")
mock_reverse.assert_called_once_with('user_delete', args=['5'])
def test_get_absolute_url_other(self): def test_get_absolute_url_other(self):
""" """
@ -112,8 +82,7 @@ class UserGetAbsoluteUrlTest(TestCase):
self.assertEqual( self.assertEqual(
url, url,
'test url', 'test url')
"User.get_absolute_url(OTHER) does not return the result of reverse.")
mock_super().get_absolute_url.assert_called_once_with(dummy_argument) mock_super().get_absolute_url.assert_called_once_with(dummy_argument)

View File

@ -316,3 +316,18 @@ class SingleObjectMixinTest(TestCase):
self.assertTrue( self.assertTrue(
view.get_object.called, view.get_object.called,
"view.get_object() should be called") "view.get_object() should be called")
class TestAPIView(TestCase):
def test_class_creation(self):
"""
Tests that the APIView has all relevant methods
"""
http_methods = set(('get', 'post', 'put', 'patch', 'delete', 'head', 'options', 'trace'))
self.assertTrue(
http_methods.issubset(views.APIView.__dict__),
"All http methods should be defined in the APIView")
self.assertFalse(
hasattr(views.APIView, 'method_call'),
"The APIView should not have the method 'method_call'")