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 ...@@ -59,7 +59,7 @@ from django.utils.translation import string_concat
from .validators import domain_validator 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], ")")) LANGUAGES_WITH_CODE = ((l[0], string_concat(l[1], " (", l[0], ")"))
for l in LANGUAGES) for l in LANGUAGES)
...@@ -1257,10 +1257,19 @@ class CirclePasswordChangeForm(PasswordChangeForm): ...@@ -1257,10 +1257,19 @@ class CirclePasswordChangeForm(PasswordChangeForm):
class UserCreationForm(OrgUserCreationForm): 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: class Meta:
model = User model = User
fields = ("username", 'email', 'first_name', 'last_name') fields = ("username", 'email', 'first_name', 'last_name', 'groups')
@property @property
def helper(self): def helper(self):
...@@ -1275,8 +1284,39 @@ class UserCreationForm(OrgUserCreationForm): ...@@ -1275,8 +1284,39 @@ class UserCreationForm(OrgUserCreationForm):
user.set_password(self.cleaned_data["password1"]) user.set_password(self.cleaned_data["password1"])
if commit: if commit:
user.save() 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 return user
@property
def helper(self):
helper = FormHelper()
helper.add_input(Submit("submit", _("Save")))
return helper
class AclUserOrGroupAddForm(forms.Form): class AclUserOrGroupAddForm(forms.Form):
name = forms.CharField(widget=autocomplete_light.TextWidget( name = forms.CharField(widget=autocomplete_light.TextWidget(
...@@ -1497,3 +1537,10 @@ class TemplateListSearchForm(forms.Form): ...@@ -1497,3 +1537,10 @@ class TemplateListSearchForm(forms.Form):
data = self.data.copy() data = self.data.copy()
data['stype'] = "owned" data['stype'] = "owned"
self.data = data self.data = data
class UserListSearchForm(forms.Form):
s = forms.CharField(widget=forms.TextInput(attrs={
'class': "form-control input-tags",
'placeholder': _("Search...")
}))
...@@ -154,156 +154,70 @@ $(function () { ...@@ -154,156 +154,70 @@ $(function () {
addSliderMiscs(); addSliderMiscs();
/* search for vms */ /* search */
function register_search(form, list, generateHTML) {
var my_vms = []; var my_vms = [];
$("#dashboard-vm-search-input").keyup(function(e) { var search_in_progress = false;
form.find('input').keyup(function(e) {
if (search_in_progress) {
return;
}
// if my_vms is empty get a list of our vms // if my_vms is empty get a list of our vms
if(my_vms.length < 1) { if(my_vms.length < 1) {
$("#dashboard-vm-search-form button i").addClass("fa-spinner fa-spin"); search_in_progress = true;
var btn = form.find('button');
$.get("/dashboard/vm/list/", function(result) { btn.find('i').addClass("fa-spinner fa-spin");
for(var i in result) {
my_vms.push({ $.get(form.prop('action'), function(result) {
'pk': result[i].pk, search_in_progress = false;
'name': result[i].name, my_vms = result;
'state': result[i].state, $(this).trigger("keyup");
'fav': result[i].fav, btn.find('i').removeClass("fa-spinner fa-spin").addClass("fa-search");
'host': result[i].host,
'icon': result[i].icon,
'status': result[i].status,
'owner': result[i].owner,
});
}
$("#dashboard-vm-search-input").trigger("keyup");
$("#dashboard-vm-search-form button i").removeClass("fa-spinner fa-spin").addClass("fa-search");
}); });
return; return;
} }
input = $("#dashboard-vm-search-input").val().toLowerCase(); input = $(this).val().toLowerCase();
var search_result = []; var search_result = [];
var html = '';
for(var i in my_vms) { for(var i in my_vms) {
if(my_vms[i].name.toLowerCase().indexOf(input) != -1 || my_vms[i].host.indexOf(input) != -1) { if(my_vms[i].name.toLowerCase().indexOf(input) != -1 ||
(my_vms[i].host && my_vms[i].host.indexOf(input) != -1) ||
(my_vms[i].org_id && my_vms[i].org_id.toLowerCase().indexOf(input) != -1)
) {
search_result.push(my_vms[i]); search_result.push(my_vms[i]);
} }
} }
search_result.sort(compareVmByFav); search_result.sort(compareVmByFav);
var html = '';
var is_last = search_result.length < 5;
for(i=0; i<5 && i<search_result.length; i++) for(i=0; i<5 && i<search_result.length; i++)
html += generateVmHTML(search_result[i].pk, search_result[i].name, html += generateHTML(search_result[i], is_last);
search_result[i].owner ? search_result[i].owner : search_result[i].host, search_result[i].icon,
search_result[i].status, search_result[i].fav,
(search_result.length < 5));
if(search_result.length === 0) if(search_result.length === 0)
html += '<div class="list-group-item list-group-item-last">' + gettext("No result") + '</div>'; html += '<div class="list-group-item list-group-item-last">' + gettext("No result") + '</div>';
$("#dashboard-vm-list").html(html);
list.html(html);
$('.title-favourite').tooltip({'placement': 'right'}); $('.title-favourite').tooltip({'placement': 'right'});
}); });
$("#dashboard-vm-search-form").submit(function() { form.submit(function() {
var vm_list_items = $("#dashboard-vm-list .list-group-item"); var vm_list_items = list.find(".list-group-item");
if(vm_list_items.length == 1 && vm_list_items.first().prop("href")) { if(vm_list_items.length == 1 && vm_list_items.first().prop("href")) {
window.location.href = vm_list_items.first().prop("href"); window.location.href = vm_list_items.first().prop("href");
return false; return false;
} }
return true; return true;
}); });
/* search for nodes */
var my_nodes = [];
$("#dashboard-node-search-input").keyup(function(e) {
// if my_nodes is empty get a list of our nodes
if(my_nodes.length < 1) {
$.ajaxSetup( { "async": false } );
$.get("/dashboard/node/list/", function(result) {
for(var i in result) {
my_nodes.push({
'name': result[i].name,
'icon': result[i].icon,
'status': result[i].status,
'label': result[i].label,
'url': result[i].url,
});
}
});
$.ajaxSetup( { "async": true } );
}
input = $("#dashboard-node-search-input").val().toLowerCase();
var search_result = [];
var html = '';
for(var i in my_nodes) {
if(my_nodes[i].name.toLowerCase().indexOf(input) != -1) {
search_result.push(my_nodes[i]);
}
}
for(i=0; i<5 && i<search_result.length; i++)
html += generateNodeHTML(search_result[i].name,
search_result[i].icon, search_result[i].status,
search_result[i].url,
(search_result.length < 5));
if(search_result.length === 0)
html += '<div class="list-group-item list-group-item-last">' + gettext("No result") + '</div>';
$("#dashboard-node-list").html(html);
html = '';
for(i=0; i<5 && i<search_result.length; i++)
html += generateNodeTagHTML(search_result[i].name,
search_result[i].icon, search_result[i].status,
search_result[i].label, search_result[i].url);
if(search_result.length === 0)
html += '<div class="list-group-item list-group-item-last">' + gettext("No result") + '</div>';
$("#dashboard-node-taglist").html(html);
// if there is only one result and ENTER is pressed redirect
if(e.keyCode == 13 && search_result.length == 1) {
window.location.href = search_result[0].url;
}
if(e.keyCode == 13 && search_result.length > 1 && input.length > 0) {
window.location.href = "/dashboard/node/list/?s=" + input;
}
});
/* search for groups */
var my_groups = [];
$("#dashboard-group-search-input").keyup(function(e) {
// if my_groups is empty get a list of our groups
if(my_groups.length < 1) {
$.ajaxSetup( { "async": false } );
$.get("/dashboard/group/list/", function(result) {
for(var i in result) {
my_groups.push({
'url': result[i].url,
'name': result[i].name,
});
}
});
$.ajaxSetup( { "async": true } );
}
input = $("#dashboard-group-search-input").val().toLowerCase();
var search_result = [];
var html = '';
for(var i in my_groups) {
if(my_groups[i].name.toLowerCase().indexOf(input) != -1) {
search_result.push(my_groups[i]);
} }
}
for(i=0; i<5 && i<search_result.length; i++)
html += generateGroupHTML(search_result[i].url, search_result[i].name, search_result.length < 5);
if(search_result.length === 0)
html += '<div class="list-group-item list-group-item-last">' + gettext("No result") + '</div>';
$("#dashboard-group-list").html(html);
// if there is only one result and ENTER is pressed redirect register_search($("#dashboard-vm-search-form"), $("#dashboard-vm-list"), generateVmHTML);
if(e.keyCode == 13 && search_result.length == 1) { register_search($("#dashboard-node-search-form"), $("#dashboard-node-list"), generateNodeHTML);
window.location.href = search_result[0].url; register_search($("#dashboard-group-search-form"), $("#dashboard-group-list"), generateGroupHTML);
} register_search($("#dashboard-user-search-form"), $("#dashboard-user-list"), generateUserHTML);
if(e.keyCode == 13 && search_result.length > 1 && input.length > 0) { register_search($("#dashboard-template-search-form"), $("#dashboard-template-list"), generateTemplateHTML);
window.location.href = "/dashboard/group/list/?s=" + input;
}
});
/* notification message toggle */ /* notification message toggle */
$(document).on('click', ".notification-message-subject", function() { $(document).on('click', ".notification-message-subject", function() {
...@@ -364,42 +278,54 @@ $(function () { ...@@ -364,42 +278,54 @@ $(function () {
}); });
}); });
function generateVmHTML(pk, name, host, icon, _status, fav, is_last) { function generateVmHTML(data, is_last) {
return '<a href="/dashboard/vm/' + pk + '/" class="list-group-item' + return '<a href="' + data.url + '" class="list-group-item' +
(is_last ? ' list-group-item-last' : '') + '">' + (is_last ? ' list-group-item-last' : '') + '">' +
'<span class="index-vm-list-name">' + '<span class="index-vm-list-name">' +
'<i class="fa ' + icon + '" title="' + _status + '"></i> ' + safe_tags_replace(name) + '<i class="fa ' + data.icon + '" title="' + data.status + '"></i> ' + safe_tags_replace(data.name) +
'</span>' + '</span>' +
'<small class="text-muted index-vm-list-host"> ' + host + '</small>' + '<small class="text-muted index-vm-list-host"> ' + data.host + '</small>' +
'<div class="pull-right dashboard-vm-favourite" data-vm="' + pk + '">' + '<div class="pull-right dashboard-vm-favourite" data-vm="' + data.pk + '">' +
(fav ? '<i class="fa fa-star text-primary title-favourite" title="' + gettext("Unfavourite") + '"></i>' : (data.fav ? '<i class="fa fa-star text-primary title-favourite" title="' + gettext("Unfavourite") + '"></i>' :
'<i class="fa fa-star-o text-primary title-favourite" title="' + gettext("Mark as favorite") + '"></i>' ) + '<i class="fa fa-star-o text-primary title-favourite" title="' + gettext("Mark as favorite") + '"></i>' ) +
'</div>' + '</div>' +
'<div style="clear: both;"></div>' + '<div style="clear: both;"></div>' +
'</a>'; '</a>';
} }
function generateGroupHTML(url, name, is_last) { function generateGroupHTML(data, is_last) {
return '<a href="' + url + '" class="list-group-item real-link' + (is_last ? " list-group-item-last" : "") +'">'+ return '<a href="' + data.url + '" class="list-group-item real-link' + (is_last ? " list-group-item-last" : "") +'">'+
'<i class="fa fa-users"></i> '+ safe_tags_replace(name) + '<i class="fa fa-users"></i> '+ safe_tags_replace(data.name) +
'</a>'; '</a>';
} }
function generateNodeHTML(name, icon, _status, url, is_last) { function generateUserHTML(data, is_last) {
return '<a href="' + url + '" class="list-group-item real-link' + (is_last ? ' list-group-item-last' : '') + '">' + return '<a href="' + data.url + '" class="list-group-item real-link' + (is_last ? " list-group-item-last" : "") +'">'+
'<span class="index-user-list-name"><i class="fa fa-user"></i> '+ safe_tags_replace(data.name) + '</span>' +
'<span class="index-user-list-org">' +
'<small class="text-muted"> ' + (data.org_id ? safe_tags_replace(data.org_id) : "") + '</small>' +
'</span></a>';
}
function generateTemplateHTML(data, is_last) {
return '<a href="' + data.url + '" class="list-group-item real-link' + (is_last ? " list-group-item-last" : "") +'">'+
' <span class="index-template-list-name">' +
' <i class="fa fa-' + data.icon + '"></i> '+ safe_tags_replace(data.name) +
' </span>' +
' <small class="text-muted index-template-list-system">' + safe_tags_replace(data.system) + '</small>' +
' <div class="clearfix"></div>' +
'</a>';
}
function generateNodeHTML(data, is_last) {
return '<a href="' + data.url + '" class="list-group-item real-link' + (is_last ? ' list-group-item-last' : '') + '">' +
'<span class="index-node-list-name">' + '<span class="index-node-list-name">' +
'<i class="fa ' + icon + '" title="' + _status + '"></i> ' + safe_tags_replace(name) + '<i class="fa ' + data.icon + '" title="' + data.status + '"></i> ' + safe_tags_replace(data.name) +
'</span>' + '</span>' +
'<div style="clear: both;"></div>' + '<div style="clear: both;"></div>' +
'</a>'; '</a>';
} }
function generateNodeTagHTML(name, icon, _status, label , url) {
return '<a href="' + url + '" class="label ' + label + '" >' +
'<i class="fa ' + icon + '" title="' + _status + '"></i> ' + safe_tags_replace(name) +
'</a> ';
}
/* copare vm-s by fav, pk order */ /* copare vm-s by fav, pk order */
function compareVmByFav(a, b) { function compareVmByFav(a, b) {
if(a.fav && b.fav) { if(a.fav && b.fav) {
......
...@@ -562,7 +562,7 @@ footer a, footer a:hover, footer a:visited { ...@@ -562,7 +562,7 @@ footer a, footer a:hover, footer a:visited {
} }
#dashboard-vm-list, #dashboard-node-list, #dashboard-group-list, #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; min-height: 200px;
} }
...@@ -1168,6 +1168,28 @@ textarea[name="new_members"] { ...@@ -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-12 {
/* fa-fw is too wide */ /* fa-fw is too wide */
width: 12px; width: 12px;
......
...@@ -18,13 +18,16 @@ ...@@ -18,13 +18,16 @@
from __future__ import absolute_import from __future__ import absolute_import
from django.contrib.auth.models import Group, User 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 import Table, A
from django_tables2.columns import (TemplateColumn, Column, LinkColumn, from django_tables2.columns import (
BooleanColumn) TemplateColumn, Column, LinkColumn, BooleanColumn
)
from django_sshkey.models import UserKey
from vm.models import Node, InstanceTemplate, Lease 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 from dashboard.models import ConnectCommand
...@@ -123,26 +126,30 @@ class GroupListTable(Table): ...@@ -123,26 +126,30 @@ class GroupListTable(Table):
class UserListTable(Table): class UserListTable(Table):
pk = TemplateColumn( username = LinkColumn(
template_name='dashboard/vm-list/column-id.html', 'dashboard.views.profile',
verbose_name="ID", args=[A('username')],
attrs={'th': {'class': 'vm-list-table-thin'}},
) )
profile__org_id = LinkColumn(
username = TemplateColumn( 'dashboard.views.profile',
template_name="dashboard/group-list/column-username.html" accessor='profile.org_id',
args=[A('username')],
verbose_name=_('Organization ID')
) )
class Meta: is_superuser = BooleanColumn(
model = User verbose_name=mark_safe(
attrs = {'class': ('table table-bordered table-striped table-hover ' _('<abbr data-placement="left" title="Superuser status">SU</abbr>')
'vm-list-table')} )
fields = ('pk', 'username', ) )
is_active = BooleanColumn()
class UserListTablex(Table):
class Meta: class Meta:
model = User 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): class TemplateListTable(Table):
......
...@@ -78,7 +78,7 @@ ...@@ -78,7 +78,7 @@
<h3> <h3>
{% trans "User list" %} {% trans "User list" %}
{% if perms.auth.add_user %} {% 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" %} {% trans "Create user" %}
</a> </a>
{% endif %} {% endif %}
......
...@@ -37,7 +37,7 @@ ...@@ -37,7 +37,7 @@
id="dashboard-node-search-form"> id="dashboard-node-search-form">
<div class="input-group input-group-sm"> <div class="input-group input-group-sm">
<input id="dashboard-node-search-input" type="text" class="form-control" <input id="dashboard-node-search-input" type="text" class="form-control"
placeholder="{% trans "Search..." %}" /> name="s" placeholder="{% trans "Search..." %}" />
<div class="input-group-btn"> <div class="input-group-btn">
<button type="submit" class="btn btn-primary" title="{% trans "Search" %}" data-container="body"> <button type="submit" class="btn btn-primary" title="{% trans "Search" %}" data-container="body">
<i class="fa fa-search"></i> <i class="fa fa-search"></i>
......
...@@ -33,7 +33,18 @@ ...@@ -33,7 +33,18 @@
{% endfor %} {% endfor %}
</div> </div>
<div class="list-group-item list-group-footer"> <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"> <a href="{% url "dashboard.views.template-list" %}" class="btn btn-primary btn-xs">
<i class="fa fa-chevron-circle-right"></i> {% trans "show all" %} <i class="fa fa-chevron-circle-right"></i> {% trans "show all" %}
</a> </a>
...@@ -43,4 +54,5 @@ ...@@ -43,4 +54,5 @@
</div> </div>
</div> </div>
</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 @@ ...@@ -48,6 +48,12 @@
{% include "dashboard/index-nodes.html" %} {% include "dashboard/index-nodes.html" %}
</div> </div>
{% endif %} {% endif %}
{% if perms.auth.change_user %}
<div class="col-lg-4 col-sm-6">
{% include "dashboard/index-users.html" %}
</div>
{% endif %}
</div> </div>
</div> </div>
{% endblock %} {% endblock %}
...@@ -8,7 +8,7 @@ ...@@ -8,7 +8,7 @@
{% block content %} {% block content %}
<div class="row"> <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 panel-default">
<div class="panel-heading"> <div class="panel-heading">
{% if request.user.is_superuser %} {% if request.user.is_superuser %}
...@@ -17,7 +17,7 @@ ...@@ -17,7 +17,7 @@
title="{% trans "Log in as this user. Recommended to open in an incognito window." %}"> title="{% trans "Log in as this user. Recommended to open in an incognito window." %}">
{% trans "Login as this user" %}</a> {% trans "Login as this user" %}</a>
{% endif %} {% 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"> <h3 class="no-margin">
<i class="fa fa-user"></i> <i class="fa fa-user"></i>
{% include "dashboard/_display-name.html" with user=profile show_org=True %} {% include "dashboard/_display-name.html" with user=profile show_org=True %}
...@@ -109,6 +109,23 @@ ...@@ -109,6 +109,23 @@
</div> </div>
</div> </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> </div>
{% endblock %} {% endblock %}
{% extends "dashboard/base.html" %}
{% load i18n %} {% load i18n %}
{% load crispy_forms_tags %} {% load crispy_forms_tags %}
{% block content %} {% block content %}
{% crispy form %} {% crispy form %}
{% endblock %} {% 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): ...@@ -1293,24 +1293,26 @@ class GroupDetailTest(LoginMixin, TestCase):
self.login(c, 'user1') self.login(c, 'user1')
self.u1.user_permissions.add(Permission.objects.get( self.u1.user_permissions.add(Permission.objects.get(
name='Can add user')) name='Can add user'))
response = c.post('/dashboard/group/%d/create/' % self.g1.pk, response = c.post('/dashboard/profile/create/',
{'username': 'userx1', {'username': 'userx1',
'groups': self.g1.pk,
'password1': 'test123', 'password1': 'test123',
'password2': 'test123'}) 'password2': 'test123'})
self.assertEqual(response.status_code, 403) self.assertEqual(response.status_code, 200)
self.assertEqual(user_count, self.g1.user_set.count()) self.assertEqual(user_count, self.g1.user_set.count())
def test_permitted_user_add_wo_can_add_user_perm(self): def test_permitted_user_add_wo_can_add_user_perm(self):
user_count = self.g1.user_set.count() user_count = self.g1.user_set.count()
c = Client() c = Client()
self.login(c, 'user0') self.login(c, 'user0')
response = c.post('/dashboard/group/%d/create/' % self.g1.pk, response = c.post('/dashboard/profile/create/',
{'username': 'userx2', {'username': 'userx2',
'groups': self.g1.pk,
'password1': 'test123', 'password1': 'test123',
'password2': 'test123'}) 'password2': 'test123'})
self.assertRedirects( self.assertRedirects(
response, 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(response.status_code, 302)
self.assertEqual(user_count, self.g1.user_set.count()) self.assertEqual(user_count, self.g1.user_set.count())
...@@ -1320,11 +1322,12 @@ class GroupDetailTest(LoginMixin, TestCase): ...@@ -1320,11 +1322,12 @@ class GroupDetailTest(LoginMixin, TestCase):
name='Can add user')) name='Can add user'))
c = Client() c = Client()
self.login(c, 'user0') self.login(c, 'user0')
response = c.post('/dashboard/group/%d/create/' % self.g1.pk, response = c.post('/dashboard/profile/create/',
{'username': 'userx2', {'username': 'userx2',
'groups': self.g1.pk,
'password1': 'test123', 'password1': 'test123',
'password2': '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(user_count + 1, self.g1.user_set.count())
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
......
...@@ -52,6 +52,7 @@ from .views import ( ...@@ -52,6 +52,7 @@ from .views import (
TransferTemplateOwnershipView, TransferTemplateOwnershipConfirmView, TransferTemplateOwnershipView, TransferTemplateOwnershipConfirmView,
OpenSearchDescriptionView, OpenSearchDescriptionView,
NodeActivityView, NodeActivityView,
UserList,
) )
from .views.vm import vm_ops, vm_mass_ops from .views.vm import vm_ops, vm_mass_ops
from .views.node import node_ops from .views.node import node_ops
...@@ -61,6 +62,11 @@ autocomplete_light.autodiscover() ...@@ -61,6 +62,11 @@ autocomplete_light.autodiscover()
urlpatterns = patterns( urlpatterns = patterns(
'', '',
url(r'^$', IndexView.as_view(), name="dashboard.index"), 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(), url(r'^lease/(?P<pk>\d+)/$', LeaseDetail.as_view(),
name="dashboard.views.lease-detail"), name="dashboard.views.lease-detail"),
url(r'^lease/create/$', LeaseCreate.as_view(), url(r'^lease/create/$', LeaseCreate.as_view(),
...@@ -174,9 +180,6 @@ urlpatterns = patterns( ...@@ -174,9 +180,6 @@ urlpatterns = patterns(
name="dashboard.views.remove-future-user"), name="dashboard.views.remove-future-user"),
url(r'^group/create/$', GroupCreate.as_view(), url(r'^group/create/$', GroupCreate.as_view(),
name='dashboard.views.group-create'), 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/$', url(r'^group/(?P<group_pk>\d+)/permissions/$',
GroupPermissionsView.as_view(), GroupPermissionsView.as_view(),
name="dashboard.views.group-permissions"), name="dashboard.views.group-permissions"),
......
...@@ -21,7 +21,7 @@ import logging ...@@ -21,7 +21,7 @@ import logging
from django.core.cache import get_cache from django.core.cache import get_cache
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from django.conf import settings 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 django.views.generic import TemplateView
from braces.views import LoginRequiredMixin from braces.views import LoginRequiredMixin
...@@ -86,6 +86,14 @@ class IndexView(LoginRequiredMixin, TemplateView): ...@@ -86,6 +86,14 @@ class IndexView(LoginRequiredMixin, TemplateView):
'more_groups': groups.count() - len(groups[:5]), '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 # template
if user.has_perm('vm.create_template'): if user.has_perm('vm.create_template'):
context['templates'] = InstanceTemplate.get_objects_with_level( context['templates'] = InstanceTemplate.get_objects_with_level(
......
...@@ -207,6 +207,18 @@ class TemplateList(LoginRequiredMixin, FilterMixin, SingleTableView): ...@@ -207,6 +207,18 @@ class TemplateList(LoginRequiredMixin, FilterMixin, SingleTableView):
def get(self, *args, **kwargs): def get(self, *args, **kwargs):
self.search_form = TemplateListSearchForm(self.request.GET) self.search_form = TemplateListSearchForm(self.request.GET)
self.search_form.full_clean() 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) return super(TemplateList, self).get(*args, **kwargs)
def create_acl_queryset(self, model): def create_acl_queryset(self, model):
......
...@@ -31,25 +31,31 @@ from django.core.exceptions import ( ...@@ -31,25 +31,31 @@ from django.core.exceptions import (
) )
from django.core.urlresolvers import reverse, reverse_lazy from django.core.urlresolvers import reverse, reverse_lazy
from django.core.paginator import Paginator, InvalidPage from django.core.paginator import Paginator, InvalidPage
from django.db.models import Q
from django.http import HttpResponse, HttpResponseRedirect, Http404 from django.http import HttpResponse, HttpResponseRedirect, Http404
from django.shortcuts import redirect, get_object_or_404 from django.shortcuts import redirect, get_object_or_404
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
from django.views.decorators.http import require_POST from django.views.decorators.http import require_POST
from django.views.generic import ( from django.views.generic import (
TemplateView, DetailView, View, UpdateView, CreateView, TemplateView, View, UpdateView, CreateView,
) )
from django_sshkey.models import UserKey from django_sshkey.models import UserKey
from braces.views import LoginRequiredMixin, PermissionRequiredMixin from braces.views import LoginRequiredMixin, PermissionRequiredMixin
from django_tables2 import SingleTableView
from vm.models import Instance, InstanceTemplate from vm.models import Instance, InstanceTemplate
from ..forms import ( from ..forms import (
CircleAuthenticationForm, MyProfileForm, UserCreationForm, UnsubscribeForm, CircleAuthenticationForm, MyProfileForm, UserCreationForm, UnsubscribeForm,
UserKeyForm, CirclePasswordChangeForm, ConnectCommandForm, 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 from .util import saml_available, DeleteViewBase
...@@ -267,36 +273,46 @@ class UserCreationView(LoginRequiredMixin, PermissionRequiredMixin, ...@@ -267,36 +273,46 @@ class UserCreationView(LoginRequiredMixin, PermissionRequiredMixin,
template_name = 'dashboard/user-create.html' template_name = 'dashboard/user-create.html'
permission_required = "auth.add_user" permission_required = "auth.add_user"
def get_success_url(self): def get_template_names(self):
reverse('dashboard.views.group-detail', args=[self.group.pk]) return ['dashboard/nojs-wrapper.html']
def get_group(self, group_pk): def get_context_data(self, *args, **kwargs):
self.group = get_object_or_404(Group, pk=group_pk) context = super(UserCreationView, self).get_context_data(*args,
if not self.group.profile.has_level(self.request.user, 'owner'): **kwargs)
raise PermissionDenied() context.update({
'template': self.template_name,
'box_title': _('Create a User'),
})
return context
def get(self, *args, **kwargs): def get_success_url(self):
self.get_group(kwargs.pop('group_pk')) return reverse('dashboard.views.profile', args=[self.object.username])
return super(UserCreationView, self).get(*args, **kwargs)
def get_form_kwargs(self):
def post(self, *args, **kwargs): profiles = GroupProfile.get_objects_with_level(
group_pk = kwargs.pop('group_pk') 'owner', self.request.user)
self.get_group(group_pk) choices = Group.objects.filter(groupprofile__in=profiles)
ret = super(UserCreationView, self).post(*args, **kwargs) group_pk = self.request.GET.get('group_pk')
if self.object: if group_pk:
create_profile(self.object) try:
self.object.groups.add(self.group) default = choices.get(pk=group_pk)
return redirect( except (ValueError, Group.DoesNotExist):
reverse('dashboard.views.group-detail', args=[group_pk])) raise Http404()
else: 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" template_name = "dashboard/profile.html"
model = User model = User
slug_field = "username" slug_field = "username"
slug_url_kwarg = "username" slug_url_kwarg = "username"
form_class = UserEditForm
success_message = _("Successfully modified user.")
def get(self, *args, **kwargs): def get(self, *args, **kwargs):
user = self.request.user user = self.request.user
...@@ -357,6 +373,15 @@ class ProfileView(LoginRequiredMixin, DetailView): ...@@ -357,6 +373,15 @@ class ProfileView(LoginRequiredMixin, DetailView):
user, self.request.user) user, self.request.user)
return context 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 @require_POST
def toggle_use_gravatar(request, **kwargs): def toggle_use_gravatar(request, **kwargs):
...@@ -480,3 +505,48 @@ class ConnectCommandCreate(LoginRequiredMixin, SuccessMessageMixin, ...@@ -480,3 +505,48 @@ class ConnectCommandCreate(LoginRequiredMixin, SuccessMessageMixin,
kwargs = super(ConnectCommandCreate, self).get_form_kwargs() kwargs = super(ConnectCommandCreate, self).get_form_kwargs()
kwargs['user'] = self.request.user kwargs['user'] = self.request.user
return kwargs 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): ...@@ -950,6 +950,7 @@ class VmList(LoginRequiredMixin, FilterMixin, ListView):
destroyed_at=None).all() destroyed_at=None).all()
instances = [{ instances = [{
'pk': i.pk, 'pk': i.pk,
'url': reverse('dashboard.views.detail', args=[i.pk]),
'name': i.name, 'name': i.name,
'icon': i.get_status_icon(), 'icon': i.get_status_icon(),
'host': i.short_hostname, '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