Commit a4d01e2b by Kálmán Viktor

Merge branch 'issue-vm-detail-fixes' into 'master'

Issue Vm Detail Fixes

- Rename virtual machines and update description
- Remove interfaces

🚧
parents 1be19049 44aed6e2
......@@ -187,7 +187,7 @@ html {
height: 300px;
}
#vm-details-rename, #vm-details-rename *, #vm-details-h1-name, #vm-list-rename, #vm-list-rename *,
#vm-details-rename, #vm-details-h1-name, #vm-details-rename ,
#node-details-rename, #node-details-rename *, #node-details-h1-name, #node-list-rename, #node-list-rename *#group-details-rename, #group-details-rename *, #group-details-h1-name, #group-list-rename, #group-list-rename * {
display: inline;
......@@ -197,7 +197,11 @@ html {
display: none;
}
#vm-details-rename-name, #node-details-rename-name, #group-details-rename-name {
.vm-details-home-name {
max-width: 401px;
}
#node-details-rename-name, #group-details-rename-name {
max-width: 160px;
}
......@@ -467,3 +471,23 @@ footer a, footer a:hover, footer a:visited {
overflow: hidden;
padding-left: 10px;
}
#vm-details-home-description {
display: inline-block;
position: relative;
}
#vm-details-home-description textarea {
min-width: 240px;
min-height: 250px;
}
.vm-details-home-edit-description-click, .vm-details-home-edit-name-click {
cursor: pointer;
}
.vm-details-description-submit {
position: absolute;
bottom: 10px;
right: 20px;
}
......@@ -303,7 +303,8 @@ function deleteObject(data) {
// no need to remove them from DOM
$('a[data-disk-pk="' + data.pk + '"]').parent("li").fadeOut();
$('a[data-disk-pk="' + data.pk + '"]').parent("h4").fadeOut();
} else {
}
else {
$('a[data-'+data['type']+'-pk="' + data['pk'] + '"]').closest('tr').fadeOut(function() {
$(this).remove();
});
......
......@@ -31,33 +31,6 @@ $(function() {
return false;
});
/* rename */
$("#vm-details-h1-name, .vm-details-rename-button").click(function() {
$("#vm-details-h1-name").hide();
$("#vm-details-rename").css('display', 'inline');
$("#vm-details-rename-name").focus();
});
/* rename ajax */
$('#vm-details-rename-submit').click(function() {
var name = $('#vm-details-rename-name').val();
$.ajax({
method: 'POST',
url: location.href,
data: {'new_name': name},
headers: {"X-CSRFToken": getCookie('csrftoken')},
success: function(data, textStatus, xhr) {
$("#vm-details-h1-name").html(data['new_name']).show();
$('#vm-details-rename').hide();
// addMessage(data['message'], "success");
},
error: function(xhr, textStatus, error) {
addMessage("Error during renaming!", "danger");
}
});
return false;
});
/* remove tag */
$('.vm-details-remove-tag').click(function() {
var to_remove = $.trim($(this).parent('div').text());
......@@ -150,8 +123,7 @@ $(function() {
/* add network button */
$("#vm-details-network-add").click(function() {
$("#vm-details-network-add-for-form").html($("#vm-details-network-add-form").html());
$('input[name="new_network_managed"]').tooltip();
$("#vm-details-network-add-form").toggle();
return false;
});
......@@ -165,6 +137,126 @@ $(function() {
$(".vm-details-help-button").click(function() {
$(".vm-details-help").stop().slideToggle();
});
/* for interface remove buttons */
$('.interface-remove').click(function() {
var interface_pk = $(this).data('interface-pk');
addModalConfirmation(removeInterface,
{ 'url': '/dashboard/interface/' + interface_pk + '/delete/',
'data': [],
'pk': interface_pk,
'type': "interface",
});
return false;
});
/* removing interface post */
function removeInterface(data) {
$.ajax({
type: 'POST',
url: data['url'],
headers: {"X-CSRFToken": getCookie('csrftoken')},
success: function(re, textStatus, xhr) {
/* remove the html element */
$('a[data-interface-pk="' + data.pk + '"]').closest("div").fadeOut();
/* add the removed element to the list */
network_select = $('select[name="new_network_vlan"]');
name_html = (re.removed_network.managed ? "": "") + " " + re.removed_network.vlan;
option_html = '<option value="' + re.removed_network.vlan_pk + '">' + name_html + '</option>';
// if it's -1 then it's a dummy placeholder so we can use .html
if($("option", network_select)[0].value === "-1") {
network_select.html(option_html);
network_select.next("div").children("button").prop("disabled", false);
} else {
network_select.append(option_html);
}
},
error: function(xhr, textStatus, error) {
addMessage('Uh oh :(', 'danger')
}
});
}
/* rename */
$("#vm-details-h1-name, .vm-details-rename-button").click(function() {
$("#vm-details-h1-name").hide();
$("#vm-details-rename").css('display', 'inline');
$("#vm-details-rename-name").focus();
});
/* rename in home tab */
$(".vm-details-home-edit-name-click").click(function() {
$(".vm-details-home-edit-name-click").hide();
$("#vm-details-home-rename").show();
$("input", $("#vm-details-home-rename")).focus();
});
/* rename ajax */
$('.vm-details-rename-submit').click(function() {
var name = $(this).parent("span").prev("input").val();
$.ajax({
method: 'POST',
url: location.href,
data: {'new_name': name},
headers: {"X-CSRFToken": getCookie('csrftoken')},
success: function(data, textStatus, xhr) {
$(".vm-details-home-edit-name").text(data['new_name']).show();
$(".vm-details-home-edit-name").parent("div").show();
$(".vm-details-home-edit-name-click").show();
$(".vm-details-home-rename-form-div").hide();
// update the inputs too
$(".vm-details-rename-submit").parent("span").prev("input").val(data['new_name']);
},
error: function(xhr, textStatus, error) {
addMessage("Error during renaming!", "danger");
}
});
return false;
});
/* update description click */
$(".vm-details-home-edit-description-click").click(function() {
$(".vm-details-home-edit-description-click").hide();
$("#vm-details-home-description").show();
return false;
});
/* description update ajax */
$('.vm-details-description-submit').click(function() {
var description = $(this).prev("textarea").val();
$.ajax({
method: 'POST',
url: location.href,
data: {'new_description': description},
headers: {"X-CSRFToken": getCookie('csrftoken')},
success: function(data, textStatus, xhr) {
var new_desc = data['new_description'];
/* we can't simply use $.text, because we need new lines */
var tagsToReplace = {
'&': "&amp;",
'<': "&lt;",
'>': "&gt;",
};
new_desc = new_desc.replace(/[&<>]/g, function(tag) {
return tagsToReplace[tag] || tag;
});
$(".vm-details-home-edit-description")
.html(new_desc.replace(/\n/g, "<br />"));
$(".vm-details-home-edit-description-click").show();
$("#vm-details-home-description").hide();
// update the textareia
$("vm-details-home-description textarea").text(data['new_description']);
},
error: function(xhr, textStatus, error) {
addMessage("Error during renaming!", "danger");
}
});
return false;
});
});
......
......@@ -10,14 +10,18 @@
{% include "dashboard/vm-detail/_operations.html" %}
</div>
<h1>
<div id="vm-details-rename">
<div id="vm-details-rename" class="vm-details-home-rename-form-div">
<form action="" method="POST" id="vm-details-rename-form">
{% csrf_token %}
<input id="vm-details-rename-name" class="form-control" name="new_name" type="text" value="{{ instance.name }}"/>
<button type="submit" id="vm-details-rename-submit" class="btn">{% trans "Rename" %}</button>
<div class="input-group vm-details-home-name">
<input id="vm-details-rename-name" class="form-control input-sm" name="new_name" type="text" value="{{ instance.name }}"/>
<span class="input-group-btn">
<button type="submit" class="btn btn-sm vm-details-rename-submit">{% trans "Rename" %}</button>
</span>
</div>
</form>
</div>
<div id="vm-details-h1-name">
<div id="vm-details-h1-name" class="vm-details-home-edit-name">
{{ instance.name }}
</div>
<small>{{ instance.primary_host.get_fqdn }}</small>
......
......@@ -4,8 +4,46 @@
<dl>
<dt>{% trans "System" %}:</dt>
<dd><i class="icon-{{ os_type_icon }}"></i> {{ instance.system }}</dd>
<dt style="margin-top: 5px;">{% trans "Description" %}:</dt>
<dd><small>{{ instance.description }}</small></dd>
<dt style="margin-top: 5px;">
{% trans "Name" %}:
<a href="#" class="vm-details-home-edit-name-click"><i class="icon-pencil"></i></a>
</dt>
<dd>
<div class="vm-details-home-edit-name-click">
<small class="vm-details-home-edit-name">{{ instance.name }}</small>
</div>
<div class="js-hidden vm-details-home-rename-form-div" id="vm-details-home-rename">
<form method="POST">
{% csrf_token %}
<div class="input-group">
<input type="text" name="new_name" value="{{ instance.name }}" class="form-control input-sm"/>
<span class="input-group-btn">
<button type="submit" class="btn btn-success btn-sm vm-details-rename-submit">
<i class="icon-pencil"></i> {% trans "Rename" %}
</button>
</span>
</div>
</form>
</div>
</dd>
<dt style="margin-top: 5px;">
{% trans "Description" %}:
<a href="#" class="vm-details-home-edit-description-click"><i class="icon-pencil"></i></a>
</dt>
<dd>
{% csrf_token %}
<div class="vm-details-home-edit-description-click">
<small class="vm-details-home-edit-description">{{ instance.description|linebreaks }}</small>
</div>
<div id="vm-details-home-description" class="js-hidden">
<form method="POST">
<textarea name="new_description" class="form-control">{{ instance.description }}</textarea>
<button type="submit" class="btn btn-xs btn-success vm-details-description-submit">
<i class="icon-pencil"></i> {% trans "Update" %}
</button>
</form>
</div>
</dd>
</dl>
<h4>{% trans "Expiration" %} {% if instance.is_expiring %}<i class="icon-warning-sign text-danger"></i>{% endif %}
......
......@@ -6,13 +6,50 @@
{% trans "Interfaces" %}
</h2>
<div class="row" id="vm-details-network-add-for-form">
<div class="js-hidden row" id="vm-details-network-add-form">
<div class="col-md-12">
<div>
<hr />
<h3>
{% trans "Add new network interface!" %}
</h3>
<form method="POST" action="">
{% csrf_token %}
<div class="input-group" style="max-width: 330px;">
<select name="new_network_vlan" class="form-control font-awesome-font">
{% for v in vlans %}
<option value="{{ v.pk }}">
{% if v.managed %}
&#xf0ac;
{% else %}
&#xf0c1;
{% endif %}
{{ v.name }}
</option>
{% empty %}
<option value="-1">No more networks!</option>
{% endfor %}
</select>
<div class="input-group-btn">
<button {% if vlans|length == 0 %}disabled{% endif %}
type="submit" class="btn btn-success"><i class="icon-plus-sign"></i></button>
</div>
</div>
</form>
<hr />
</div>
</div>
</div>
{% for i in instance.interface_set.all %}
<div>
<h3 class="list-group-item-heading dashboard-vm-details-network-h3">
<i class="icon-{% if i.host %}globe{% else %}link{% endif %}"></i> {{ i.vlan.name }}
{% if not i.host %}(unmanaged) <a href="#" class="btn btn-danger btn-xs">{% trans "remove" %}</a>{% endif %}
{% if not i.host%}({% trans "unmanaged" %}){% endif %}
<a href="{% url "dashboard.views.interface-delete" pk=i.pk %}?next={{ request.path }}" class="btn btn-danger btn-xs interface-remove"
data-interface-pk="{{ i.pk }}">
{% trans "remove" %}
</a>
</h3>
{% if i.host %}
<div class="row">
......@@ -109,31 +146,5 @@
</div>
</div>
{% endif %}
{% endfor %}
<div class="js-hidden row" id="vm-details-network-add-form">
<div class="col-md-12">
<div>
<hr />
<h3>
{% trans "Add new network interface!" %}
</h3>
<form method="POST" action="">
{% csrf_token %}
<div class="input-group" style="max-width: 330px;">
<select name="new_network_vlan" class="form-control">
{% if vlans|length == 0 %}
<option value="-1">No more networks!</option>
{% endif %}
{% for v in vlans %}
<option value="{{ v.pk }}">{{ v.name }}</option>
{% endfor %}
</select>
<div class="input-group-btn">
<button type="submit" class="btn btn-success"><i class="icon-plus-sign"></i></button>
</div>
</div>
</form>
<hr />
</div>
</div>
</div>
{% endfor %}
import json
from unittest import skip
from django.test import TestCase
from django.test.client import Client
......@@ -174,6 +176,46 @@ class VmDetailTest(LoginMixin, TestCase):
self.assertEqual(response.status_code, 302)
self.assertEqual(inst.interface_set.count(), interface_count + 1)
def test_permitted_network_delete(self):
c = Client()
self.login(c, "user1")
inst = Instance.objects.get(pk=1)
inst.set_level(self.u1, 'owner')
inst.add_interface(vlan=Vlan.objects.get(pk=1), user=self.us)
iface_count = inst.interface_set.count()
c.post("/dashboard/interface/1/delete/")
self.assertEqual(inst.interface_set.count(), iface_count - 1)
def test_permitted_network_delete_w_ajax(self):
c = Client()
self.login(c, "user1")
inst = Instance.objects.get(pk=1)
inst.set_level(self.u1, 'owner')
vlan = Vlan.objects.get(pk=1)
inst.add_interface(vlan=vlan, user=self.us)
iface_count = inst.interface_set.count()
response = c.post("/dashboard/interface/1/delete/",
HTTP_X_REQUESTED_WITH='XMLHttpRequest')
removed_network = json.loads(response.content)['removed_network']
self.assertEqual(removed_network['vlan'], vlan.name)
self.assertEqual(removed_network['vlan_pk'], vlan.pk)
self.assertEqual(removed_network['managed'], vlan.managed)
self.assertEqual(inst.interface_set.count(), iface_count - 1)
def test_unpermitted_network_delete(self):
c = Client()
self.login(c, "user1")
inst = Instance.objects.get(pk=1)
inst.set_level(self.u1, 'user')
inst.add_interface(vlan=Vlan.objects.get(pk=1), user=self.us)
iface_count = inst.interface_set.count()
response = c.post("/dashboard/interface/1/delete/")
self.assertEqual(iface_count, inst.interface_set.count())
self.assertEqual(response.status_code, 403)
def test_create_vm_w_unpermitted_network(self):
c = Client()
self.login(c, 'user2')
......@@ -559,6 +601,41 @@ class VmDetailTest(LoginMixin, TestCase):
self.assertEqual(response.status_code, 302)
self.assertEqual(instance_count + 2, Instance.objects.all().count())
def test_unpermitted_description_update(self):
c = Client()
self.login(c, "user1")
inst = Instance.objects.get(pk=1)
inst.set_level(self.u2, 'owner')
inst.set_level(self.u1, 'user')
old_desc = inst.description
response = c.post("/dashboard/vm/1/", {'new_description': 'test1234'})
self.assertEqual(response.status_code, 403)
self.assertEqual(Instance.objects.get(pk=1).description, old_desc)
def test_permitted_description_update_w_ajax(self):
c = Client()
self.login(c, "user1")
inst = Instance.objects.get(pk=1)
inst.set_level(self.u1, 'owner')
response = c.post("/dashboard/vm/1/", {'new_description': "naonyo"},
HTTP_X_REQUESTED_WITH='XMLHttpRequest')
self.assertEqual(response.status_code, 200)
self.assertEqual(json.loads(response.content)['new_description'],
"naonyo")
self.assertEqual(Instance.objects.get(pk=1).description, "naonyo")
def test_permitted_description_update(self):
c = Client()
self.login(c, "user1")
inst = Instance.objects.get(pk=1)
inst.set_level(self.u1, 'owner')
response = c.post("/dashboard/vm/1/", {'new_description': "naonyo"})
self.assertEqual(response.status_code, 302)
self.assertEqual(Instance.objects.get(pk=1).description, "naonyo")
class NodeDetailTest(LoginMixin, TestCase):
fixtures = ['test-vm-fixture.json', 'node.json']
......
......@@ -12,7 +12,7 @@ from .views import (
TemplateDelete, TemplateDetail, TemplateList, TransferOwnershipConfirmView,
TransferOwnershipView, vm_activity, VmCreate, VmDelete, VmDetailView,
VmDetailVncTokenView, VmGraphView, VmList, VmMassDelete, VmMigrateView,
VmRenewView, DiskRemoveView, get_disk_download_status,
VmRenewView, DiskRemoveView, get_disk_download_status, InterfaceDeleteView,
)
urlpatterns = patterns(
......@@ -108,6 +108,9 @@ urlpatterns = patterns(
url(r'^disk/(?P<pk>\d+)/status/$', get_disk_download_status,
name="dashboard.views.disk-status"),
url(r'^interface/(?P<pk>\d+)/delete/$', InterfaceDeleteView.as_view(),
name="dashboard.views.interface-delete"),
url(r'^profile/$', MyPreferencesView.as_view(),
name="dashboard.views.profile"),
)
......@@ -216,7 +216,7 @@ class VmDetailView(CheckedDetailView):
context['vlans'] = Vlan.get_objects_with_level(
'user', self.request.user
).exclude(
).exclude( # exclude already added interfaces
pk__in=Interface.objects.filter(
instance=self.get_object()).values_list("vlan", flat=True)
).all()
......@@ -239,6 +239,7 @@ class VmDetailView(CheckedDetailView):
options = {
'change_password': self.__change_password,
'new_name': self.__set_name,
'new_description': self.__set_description,
'new_tag': self.__add_tag,
'to_remove': self.__remove_tag,
'port': self.__add_port,
......@@ -309,8 +310,30 @@ class VmDetailView(CheckedDetailView):
)
else:
messages.success(request, success_message)
return redirect(reverse_lazy("dashboard.views.detail",
kwargs={'pk': self.object.pk}))
return redirect(self.object.get_absolute_url())
def __set_description(self, request):
self.object = self.get_object()
if not self.object.has_level(request.user, 'owner'):
raise PermissionDenied()
new_description = request.POST.get("new_description")
Instance.objects.filter(pk=self.object.pk).update(
**{'description': new_description})
success_message = _("VM description successfully updated!")
if request.is_ajax():
response = {
'message': success_message,
'new_description': new_description,
}
return HttpResponse(
json.dumps(response),
content_type="application/json"
)
else:
messages.success(request, success_message)
return redirect(self.object.get_absolute_url())
def __add_tag(self, request):
new_tag = request.POST.get('new_tag')
......@@ -393,7 +416,7 @@ class VmDetailView(CheckedDetailView):
if not self.object.has_level(request.user, 'owner'):
raise PermissionDenied()
vlan = Vlan.objects.get(pk=request.POST.get("new_network_vlan"))
vlan = get_object_or_404(Vlan, pk=request.POST.get("new_network_vlan"))
if not vlan.has_level(request.user, 'user'):
raise PermissionDenied()
try:
......@@ -2260,3 +2283,53 @@ class InstanceActivityDetail(SuperuserRequiredMixin, DetailView):
order_by('-started').select_related('user').
prefetch_related('children'))
return ctx
class InterfaceDeleteView(DeleteView):
model = Interface
def get_template_names(self):
if self.request.is_ajax():
return ['dashboard/confirm/ajax-delete.html']
else:
return ['dashboard/confirm/base-delete.html']
def get_context_data(self, **kwargs):
context = super(InterfaceDeleteView, self).get_context_data(**kwargs)
interface = self.get_object()
context['text'] = _("Are you sure you want to remove this interface "
"from <strong>%(vm)s</strong>?" %
{'vm': interface.instance.name})
return context
def delete(self, request, *args, **kwargs):
self.object = self.get_object()
instance = self.object.instance
if not instance.has_level(request.user, "owner"):
raise PermissionDenied()
instance.remove_interface(interface=self.object, user=request.user)
success_url = self.get_success_url()
success_message = _("Interface successfully deleted!")
if request.is_ajax():
return HttpResponse(
json.dumps(
{'message': success_message,
'removed_network': {
'vlan': self.object.vlan.name,
'vlan_pk': self.object.vlan.pk,
'managed': self.object.host is not None,
}}),
content_type="application/json",
)
else:
messages.success(request, success_message)
return HttpResponseRedirect("%s#network" % success_url)
def get_success_url(self):
redirect = self.request.POST.get("next")
if redirect:
return redirect
self.object.instance.get_absolute_url()
......@@ -53,6 +53,7 @@ class Interface(Model):
class Meta:
app_label = 'vm'
db_table = 'vm_interface'
ordering = ("-vlan__managed", )
def __unicode__(self):
return 'cloud-' + str(self.instance.id) + '-' + str(self.vlan.vid)
......
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