Merge pull request #1479 from normanjaeckel/FixCommitAngularAuth

Implemented auth via AngularJS.
This commit is contained in:
Norman Jäckel 2015-02-16 11:47:14 +01:00
commit 00a9709bf8
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'")