Commit 1e514d5b by Kálmán Viktor

Merge branch 'feature-useradmin' into 'master'

Useradmin 

Closes #372

See merge request !286
parents 8c7786a6 ca4fcef5
......@@ -59,7 +59,7 @@ from django.utils.translation import string_concat
from .validators import domain_validator
from dashboard.models import ConnectCommand
from dashboard.models import ConnectCommand, create_profile
LANGUAGES_WITH_CODE = ((l[0], string_concat(l[1], " (", l[0], ")"))
for l in LANGUAGES)
......@@ -1257,10 +1257,19 @@ class CirclePasswordChangeForm(PasswordChangeForm):
class UserCreationForm(OrgUserCreationForm):
def __init__(self, *args, **kwargs):
choices = kwargs.pop('choices')
group = kwargs.pop('default')
super(UserCreationForm, self).__init__(*args, **kwargs)
self.fields['groups'] = forms.ModelMultipleChoiceField(
queryset=choices, initial=[group], required=False,
label=_('Groups'))
class Meta:
model = User
fields = ("username", 'email', 'first_name', 'last_name')
fields = ("username", 'email', 'first_name', 'last_name', 'groups')
@property
def helper(self):
......@@ -1275,8 +1284,39 @@ class UserCreationForm(OrgUserCreationForm):
user.set_password(self.cleaned_data["password1"])
if commit:
user.save()
create_profile(user)
user.groups.add(*self.cleaned_data["groups"])
return user
class UserEditForm(forms.ModelForm):
instance_limit = forms.IntegerField(
label=_('Instance limit'),
min_value=0, widget=NumberInput)
def __init__(self, *args, **kwargs):
super(UserEditForm, self).__init__(*args, **kwargs)
self.fields["instance_limit"].initial = (
self.instance.profile.instance_limit)
class Meta:
model = User
fields = ('email', 'first_name', 'last_name', 'instance_limit',
'is_active')
def save(self, commit=True):
user = super(UserEditForm, self).save()
user.profile.instance_limit = (
self.cleaned_data['instance_limit'] or None)
user.profile.save()
return user
@property
def helper(self):
helper = FormHelper()
helper.add_input(Submit("submit", _("Save")))
return helper
class AclUserOrGroupAddForm(forms.Form):
name = forms.CharField(widget=autocomplete_light.TextWidget(
......@@ -1497,3 +1537,10 @@ class TemplateListSearchForm(forms.Form):
data = self.data.copy()
data['stype'] = "owned"
self.data = data
class UserListSearchForm(forms.Form):
s = forms.CharField(widget=forms.TextInput(attrs={
'class': "form-control input-tags",
'placeholder': _("Search...")
}))
......@@ -562,7 +562,7 @@ footer a, footer a:hover, footer a:visited {
}
#dashboard-vm-list, #dashboard-node-list, #dashboard-group-list,
#dashboard-template-list, #dashboard-files-toplist {
#dashboard-template-list, #dashboard-files-toplist, #dashboard-user-list {
min-height: 200px;
}
......@@ -1168,6 +1168,28 @@ textarea[name="new_members"] {
}
}
#dashboard-user-list {
.list-group-item {
display: flex;
}
.index-user-list-name, .index-user-list-org {
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
}
.index-user-list-name {
max-width: 80%;
}
.index-user-list-org {
padding-left: 5px;
flex: 1;
}
}
.fa-fw-12 {
/* fa-fw is too wide */
width: 12px;
......
......@@ -18,13 +18,16 @@
from __future__ import absolute_import
from django.contrib.auth.models import Group, User
from django.utils.translation import ugettext_lazy as _
from django.utils.html import mark_safe
from django_tables2 import Table, A
from django_tables2.columns import (TemplateColumn, Column, LinkColumn,
BooleanColumn)
from django_tables2.columns import (
TemplateColumn, Column, LinkColumn, BooleanColumn
)
from django_sshkey.models import UserKey
from vm.models import Node, InstanceTemplate, Lease
from django.utils.translation import ugettext_lazy as _
from django_sshkey.models import UserKey
from dashboard.models import ConnectCommand
......@@ -123,26 +126,30 @@ class GroupListTable(Table):
class UserListTable(Table):
pk = TemplateColumn(
template_name='dashboard/vm-list/column-id.html',
verbose_name="ID",
attrs={'th': {'class': 'vm-list-table-thin'}},
username = LinkColumn(
'dashboard.views.profile',
args=[A('username')],
)
username = TemplateColumn(
template_name="dashboard/group-list/column-username.html"
profile__org_id = LinkColumn(
'dashboard.views.profile',
accessor='profile.org_id',
args=[A('username')],
verbose_name=_('Organization ID')
)
class Meta:
model = User
attrs = {'class': ('table table-bordered table-striped table-hover '
'vm-list-table')}
fields = ('pk', 'username', )
is_superuser = BooleanColumn(
verbose_name=mark_safe(
_('<abbr data-placement="left" title="Superuser status">SU</abbr>')
)
)
is_active = BooleanColumn()
class UserListTablex(Table):
class Meta:
model = User
template = "django_tables2/table_no_page.html"
attrs = {'class': ('table table-bordered table-striped table-hover')}
fields = ('username', 'last_name', 'first_name', 'profile__org_id',
'email', 'is_active', 'is_superuser')
class TemplateListTable(Table):
......
......@@ -78,7 +78,7 @@
<h3>
{% trans "User list" %}
{% if perms.auth.add_user %}
<a href="{% url "dashboard.views.create-user" group.pk %}" class="btn btn-success pull-right">
<a href="{% url "dashboard.views.user-create" %}?group_pk={{ group.pk}}" class="btn btn-success pull-right">
{% trans "Create user" %}
</a>
{% endif %}
......
......@@ -37,7 +37,7 @@
id="dashboard-node-search-form">
<div class="input-group input-group-sm">
<input id="dashboard-node-search-input" type="text" class="form-control"
placeholder="{% trans "Search..." %}" />
name="s" placeholder="{% trans "Search..." %}" />
<div class="input-group-btn">
<button type="submit" class="btn btn-primary" title="{% trans "Search" %}" data-container="body">
<i class="fa fa-search"></i>
......
......@@ -33,7 +33,18 @@
{% endfor %}
</div>
<div class="list-group-item list-group-footer">
<div class="text-right">
<div class="row">
<div class="col-xs-6">
<form action="{% url "dashboard.views.template-list" %}" method="GET" id="dashboard-template-search-form">
<div class="input-group input-group-sm">
<input id="dashboard-group-search-input" name="s" type="text" class="form-control" placeholder="{% trans "Search..." %}" />
<div class="input-group-btn">
<button type="submit" class="btn btn-primary"><i class="fa fa-search"></i></button>
</div>
</div>
</form>
</div>
<div class="col-xs-6 text-right">
<a href="{% url "dashboard.views.template-list" %}" class="btn btn-primary btn-xs">
<i class="fa fa-chevron-circle-right"></i> {% trans "show all" %}
</a>
......@@ -43,4 +54,5 @@
</div>
</div>
</div>
</div>
</div>
{% load i18n %}
<div class="panel panel-default">
<div class="panel-heading">
<div class="pull-right toolbar">
<span class="btn btn-default btn-xs infobtn" data-container="body" title="{% trans "List of CIRCLE users." %}"><i class="fa fa-info-circle"></i></span>
</div>
<h3 class="no-margin"><i class="fa fa-users"></i> {% trans "Users" %}</h3>
</div>
<div class="list-group" id="user-list-view">
<div id="dashboard-user-list">
{% for i in users %}
<a href="{% url "dashboard.views.profile" username=i.username %}" class="list-group-item real-link
{% if forloop.last and users|length < 5 %} list-group-item-last{% endif %}">
<span class="index-user-list-name">
<i class="fa fa-user"></i> {% firstof i.get_full_name|safe i.username|safe %}
</span>
<span class="index-user-list-org">
<small class="text-muted"> {{ i.profile.org_id|default:"" }}</small>
</span>
</a>
{% endfor %}
</div>
<div class="list-group-item list-group-footer text-right">
<div class="row">
<div class="col-xs-6">
<form action="{% url "dashboard.views.user-list" %}" method="GET" id="dashboard-user-search-form">
<div class="input-group input-group-sm">
<input id="dashboard-group-search-input" name="s" type="text" class="form-control" placeholder="{% trans "Search..." %}" />
<div class="input-group-btn">
<button type="submit" class="btn btn-primary"><i class="fa fa-search"></i></button>
</div>
</div>
</form>
</div>
<div class="col-xs-6 text-right">
<a class="btn btn-primary btn-xs" href="{% url "dashboard.views.user-list" %}">
<i class="fa fa-chevron-circle-right"></i>
{% if more_users > 0 %}
{% blocktrans count more=more_users %}
<strong>{{ more }}</strong> more
{% plural %}
<strong>{{ more }}</strong> more
{% endblocktrans %}
{% else %}
{% trans "list" %}
{% endif %}
</a>
<a class="btn btn-success btn-xs user-create" href="{% url "dashboard.views.user-create" %}"><i class="fa fa-plus-circle"></i> {% trans "new" %} </a>
</div>
</div>
</div>
</div>
</div>
......@@ -48,6 +48,12 @@
{% include "dashboard/index-nodes.html" %}
</div>
{% endif %}
{% if perms.auth.change_user %}
<div class="col-lg-4 col-sm-6">
{% include "dashboard/index-users.html" %}
</div>
{% endif %}
</div>
</div>
{% endblock %}
......@@ -8,7 +8,7 @@
{% block content %}
<div class="row">
<div class="col-md-12">
<div class="col-md-{% if perms.auth.change_user %}8{% else %}12{% endif %}">
<div class="panel panel-default">
<div class="panel-heading">
{% if request.user.is_superuser %}
......@@ -17,7 +17,7 @@
title="{% trans "Log in as this user. Recommended to open in an incognito window." %}">
{% trans "Login as this user" %}</a>
{% endif %}
<a class="pull-right btn btn-default btn-xs" href="{% url "dashboard.index" %}">{% trans "Back" %}</a>
<a class="pull-right btn btn-default btn-xs" href="{% url "dashboard.views.user-list" %}">{% trans "Back" %}</a>
<h3 class="no-margin">
<i class="fa fa-user"></i>
{% include "dashboard/_display-name.html" with user=profile show_org=True %}
......@@ -109,6 +109,23 @@
</div>
</div>
</div>
{% if perms.auth.change_user %}
<div class="col-md-4">
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="no-margin">
<i class="fa fa-user"></i>
{% trans "Edit user" %}
</h3>
</div>
<div class="panel-body">
{% crispy form %}
</div>
</div>
</div>
{% endif %}
</div>
{% endblock %}
{% extends "dashboard/base.html" %}
{% load i18n %}
{% load crispy_forms_tags %}
{% block content %}
{% crispy form %}
{% crispy form %}
{% endblock %}
{% extends "dashboard/base.html" %}
{% load staticfiles %}
{% load i18n %}
{% load render_table from django_tables2 %}
{% block title-page %}{% trans "Users" %}{% endblock %}
{% block content %}
<div class="row">
<div class="col-md-12">
<div class="panel panel-default">
<div class="panel-heading">
<a href="{% url "dashboard.views.user-create" %}" class="pull-right btn btn-success btn-xs">
<i class="fa fa-plus"></i> {% trans "new user" %}
</a>
<h3 class="no-margin"><i class="fa fa-user"></i> {% trans "Users" %}</h3>
</div>
<div class="panel-body">
<div class="row">
<div class="col-md-offset-8 col-md-4" id="user-list-search">
<form action="" method="GET">
<div class="input-group">
{{ search_form.s }}
<div class="input-group-btn">
{{ search_form.stype }}
<button type="submit" class="btn btn-primary input-tags">
<i class="fa fa-search"></i>
</button>
</div>
</div><!-- .input-group -->
</form>
</div><!-- .col-md-4 #user-list-search -->
</div>
</div>
<div class="panel-body">
<div class="table-responsive">
{% render_table table %}
</div>
</div>
</div>
</div>
</div>
{% endblock %}
......@@ -1293,24 +1293,26 @@ class GroupDetailTest(LoginMixin, TestCase):
self.login(c, 'user1')
self.u1.user_permissions.add(Permission.objects.get(
name='Can add user'))
response = c.post('/dashboard/group/%d/create/' % self.g1.pk,
response = c.post('/dashboard/profile/create/',
{'username': 'userx1',
'groups': self.g1.pk,
'password1': 'test123',
'password2': 'test123'})
self.assertEqual(response.status_code, 403)
self.assertEqual(response.status_code, 200)
self.assertEqual(user_count, self.g1.user_set.count())
def test_permitted_user_add_wo_can_add_user_perm(self):
user_count = self.g1.user_set.count()
c = Client()
self.login(c, 'user0')
response = c.post('/dashboard/group/%d/create/' % self.g1.pk,
response = c.post('/dashboard/profile/create/',
{'username': 'userx2',
'groups': self.g1.pk,
'password1': 'test123',
'password2': 'test123'})
self.assertRedirects(
response,
'/accounts/login/?next=/dashboard/group/%d/create/' % self.g1.pk)
'/accounts/login/?next=/dashboard/profile/create/')
self.assertEqual(response.status_code, 302)
self.assertEqual(user_count, self.g1.user_set.count())
......@@ -1320,11 +1322,12 @@ class GroupDetailTest(LoginMixin, TestCase):
name='Can add user'))
c = Client()
self.login(c, 'user0')
response = c.post('/dashboard/group/%d/create/' % self.g1.pk,
response = c.post('/dashboard/profile/create/',
{'username': 'userx2',
'groups': self.g1.pk,
'password1': 'test123',
'password2': 'test123'})
self.assertRedirects(response, '/dashboard/group/%d/' % self.g1.pk)
self.assertRedirects(response, '/dashboard/profile/userx2/')
self.assertEqual(user_count + 1, self.g1.user_set.count())
self.assertEqual(response.status_code, 302)
......
......@@ -52,6 +52,7 @@ from .views import (
TransferTemplateOwnershipView, TransferTemplateOwnershipConfirmView,
OpenSearchDescriptionView,
NodeActivityView,
UserList,
)
from .views.vm import vm_ops, vm_mass_ops
from .views.node import node_ops
......@@ -61,6 +62,11 @@ autocomplete_light.autodiscover()
urlpatterns = patterns(
'',
url(r'^$', IndexView.as_view(), name="dashboard.index"),
url(r"^profile/list/$", UserList.as_view(),
name="dashboard.views.user-list"),
url(r'^profile/create/$',
UserCreationView.as_view(),
name="dashboard.views.user-create"),
url(r'^lease/(?P<pk>\d+)/$', LeaseDetail.as_view(),
name="dashboard.views.lease-detail"),
url(r'^lease/create/$', LeaseCreate.as_view(),
......@@ -174,9 +180,6 @@ urlpatterns = patterns(
name="dashboard.views.remove-future-user"),
url(r'^group/create/$', GroupCreate.as_view(),
name='dashboard.views.group-create'),
url(r'^group/(?P<group_pk>\d+)/create/$',
UserCreationView.as_view(),
name="dashboard.views.create-user"),
url(r'^group/(?P<group_pk>\d+)/permissions/$',
GroupPermissionsView.as_view(),
name="dashboard.views.group-permissions"),
......
......@@ -21,7 +21,7 @@ import logging
from django.core.cache import get_cache
from django.core.urlresolvers import reverse
from django.conf import settings
from django.contrib.auth.models import Group
from django.contrib.auth.models import Group, User
from django.views.generic import TemplateView
from braces.views import LoginRequiredMixin
......@@ -86,6 +86,14 @@ class IndexView(LoginRequiredMixin, TemplateView):
'more_groups': groups.count() - len(groups[:5]),
})
# users
if user.has_module_perms('auth.change_user'):
users = User.objects.all()
context.update({
'users': users[:5],
'more_users': users.count() - len(users[:5]),
})
# template
if user.has_perm('vm.create_template'):
context['templates'] = InstanceTemplate.get_objects_with_level(
......
......@@ -207,6 +207,18 @@ class TemplateList(LoginRequiredMixin, FilterMixin, SingleTableView):
def get(self, *args, **kwargs):
self.search_form = TemplateListSearchForm(self.request.GET)
self.search_form.full_clean()
if self.request.is_ajax():
templates = [{
'icon': i.os_type,
'system': i.system,
'url': reverse("dashboard.views.template-detail",
kwargs={'pk': i.pk}),
'name': i.name} for i in self.get_queryset()]
return HttpResponse(
json.dumps(templates),
content_type="application/json",
)
else:
return super(TemplateList, self).get(*args, **kwargs)
def create_acl_queryset(self, model):
......
......@@ -31,25 +31,31 @@ from django.core.exceptions import (
)
from django.core.urlresolvers import reverse, reverse_lazy
from django.core.paginator import Paginator, InvalidPage
from django.db.models import Q
from django.http import HttpResponse, HttpResponseRedirect, Http404
from django.shortcuts import redirect, get_object_or_404
from django.utils.translation import ugettext as _
from django.views.decorators.http import require_POST
from django.views.generic import (
TemplateView, DetailView, View, UpdateView, CreateView,
TemplateView, View, UpdateView, CreateView,
)
from django_sshkey.models import UserKey
from braces.views import LoginRequiredMixin, PermissionRequiredMixin
from django_tables2 import SingleTableView
from vm.models import Instance, InstanceTemplate
from ..forms import (
CircleAuthenticationForm, MyProfileForm, UserCreationForm, UnsubscribeForm,
UserKeyForm, CirclePasswordChangeForm, ConnectCommandForm,
UserListSearchForm, UserEditForm,
)
from ..models import Profile, GroupProfile, ConnectCommand
from ..tables import (
UserKeyListTable, ConnectCommandListTable, UserListTable,
)
from ..models import Profile, GroupProfile, ConnectCommand, create_profile
from ..tables import UserKeyListTable, ConnectCommandListTable
from .util import saml_available, DeleteViewBase
......@@ -267,36 +273,46 @@ class UserCreationView(LoginRequiredMixin, PermissionRequiredMixin,
template_name = 'dashboard/user-create.html'
permission_required = "auth.add_user"
def get_success_url(self):
reverse('dashboard.views.group-detail', args=[self.group.pk])
def get_template_names(self):
return ['dashboard/nojs-wrapper.html']
def get_group(self, group_pk):
self.group = get_object_or_404(Group, pk=group_pk)
if not self.group.profile.has_level(self.request.user, 'owner'):
raise PermissionDenied()
def get_context_data(self, *args, **kwargs):
context = super(UserCreationView, self).get_context_data(*args,
**kwargs)
context.update({
'template': self.template_name,
'box_title': _('Create a User'),
})
return context
def get(self, *args, **kwargs):
self.get_group(kwargs.pop('group_pk'))
return super(UserCreationView, self).get(*args, **kwargs)
def post(self, *args, **kwargs):
group_pk = kwargs.pop('group_pk')
self.get_group(group_pk)
ret = super(UserCreationView, self).post(*args, **kwargs)
if self.object:
create_profile(self.object)
self.object.groups.add(self.group)
return redirect(
reverse('dashboard.views.group-detail', args=[group_pk]))
def get_success_url(self):
return reverse('dashboard.views.profile', args=[self.object.username])
def get_form_kwargs(self):
profiles = GroupProfile.get_objects_with_level(
'owner', self.request.user)
choices = Group.objects.filter(groupprofile__in=profiles)
group_pk = self.request.GET.get('group_pk')
if group_pk:
try:
default = choices.get(pk=group_pk)
except (ValueError, Group.DoesNotExist):
raise Http404()
else:
return ret
default = None
val = super(UserCreationView, self).get_form_kwargs()
val.update({'choices': choices, 'default': default})
return val
class ProfileView(LoginRequiredMixin, DetailView):
class ProfileView(LoginRequiredMixin, SuccessMessageMixin, UpdateView):
template_name = "dashboard/profile.html"
model = User
slug_field = "username"
slug_url_kwarg = "username"
form_class = UserEditForm
success_message = _("Successfully modified user.")
def get(self, *args, **kwargs):
user = self.request.user
......@@ -357,6 +373,15 @@ class ProfileView(LoginRequiredMixin, DetailView):
user, self.request.user)
return context
def post(self, request, *args, **kwargs):
if not request.user.has_perm('auth.change_user'):
raise PermissionDenied()
return super(ProfileView, self).post(self, request, *args, **kwargs)
def get_success_url(self):
return reverse('dashboard.views.profile',
kwargs=self.kwargs)
@require_POST
def toggle_use_gravatar(request, **kwargs):
......@@ -480,3 +505,48 @@ class ConnectCommandCreate(LoginRequiredMixin, SuccessMessageMixin,
kwargs = super(ConnectCommandCreate, self).get_form_kwargs()
kwargs['user'] = self.request.user
return kwargs
class UserList(LoginRequiredMixin, PermissionRequiredMixin, SingleTableView):
template_name = "dashboard/user-list.html"
permission_required = "auth.change_user"
model = User
table_class = UserListTable
table_pagination = True
def get_context_data(self, *args, **kwargs):
context = super(UserList, self).get_context_data(*args, **kwargs)
context['search_form'] = self.search_form
return context
def get(self, *args, **kwargs):
self.search_form = UserListSearchForm(self.request.GET)
self.search_form.full_clean()
if self.request.is_ajax():
users = [
{'url': reverse("dashboard.views.profile", args=[i.username]),
'name': i.get_full_name() or i.username,
'org_id': i.profile.org_id,
}
for i in self.get_queryset()]
return HttpResponse(
json.dumps(users), content_type="application/json")
else:
return super(UserList, self).get(*args, **kwargs)
def get_queryset(self):
logger.debug('UserList.get_queryset() called. User: %s',
unicode(self.request.user))
qs = User.objects.all().order_by("-pk")
q = self.search_form.cleaned_data.get('s')
if q:
filters = (Q(username__icontains=q) | Q(email__icontains=q)
| Q(profile__org_id__icontains=q))
for w in q.split()[:3]:
filters |= (
Q(first_name__icontains=w) | Q(last_name__icontains=w))
qs = qs.filter(filters)
return qs.select_related("profile")
......@@ -950,6 +950,7 @@ class VmList(LoginRequiredMixin, FilterMixin, ListView):
destroyed_at=None).all()
instances = [{
'pk': i.pk,
'url': reverse('dashboard.views.detail', args=[i.pk]),
'name': i.name,
'icon': i.get_status_icon(),
'host': i.short_hostname,
......
This source diff could not be displayed because it is too large. You can view the blob instead.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment