diff --git a/conntrackt/templates/conntrackt/entity_detail.html b/conntrackt/templates/conntrackt/entity_detail.html
--- a/conntrackt/templates/conntrackt/entity_detail.html
+++ b/conntrackt/templates/conntrackt/entity_detail.html
@@ -54,11 +54,13 @@
- Interfaces |
+ Interfaces |
{% for interface in interfaces %}
- {{interface.name}} ({{interface.address}}/{{interface.netmask}}) |
+ {{interface.name}} ({{interface.address}}/{{interface.netmask}}) |
+ {% html_link '' 'interface_update' interface.id class="btn btn-link" %} |
+ {% html_link '' 'interface_delete' interface.id class="btn btn-link" %} |
{% endfor %}
diff --git a/conntrackt/templates/conntrackt/interface_confirm_delete.html b/conntrackt/templates/conntrackt/interface_confirm_delete.html
new file mode 100644
--- /dev/null
+++ b/conntrackt/templates/conntrackt/interface_confirm_delete.html
@@ -0,0 +1,27 @@
+{% extends "conntrackt/base.html" %}
+
+{# For html_link #}
+{% load conntrackt_tags %}
+{# For Bootstrapped forms #}
+{% load crispy_forms_tags %}
+
+{% block content %}
+
+
Remove interface {{interface.name}}
+
+
+{% endblock content %}
diff --git a/conntrackt/templates/conntrackt/interface_update_form.html b/conntrackt/templates/conntrackt/interface_update_form.html
new file mode 100644
--- /dev/null
+++ b/conntrackt/templates/conntrackt/interface_update_form.html
@@ -0,0 +1,25 @@
+{% extends "conntrackt/base.html" %}
+
+{# For html_link #}
+{% load conntrackt_tags %}
+{# For Bootstrapped forms #}
+{% load crispy_forms_tags %}
+
+{% block content %}
+
+
Edit interface {{interface.name}}
+
+
+{% endblock content %}
diff --git a/conntrackt/tests/test_views.py b/conntrackt/tests/test_views.py
--- a/conntrackt/tests/test_views.py
+++ b/conntrackt/tests/test_views.py
@@ -10,8 +10,9 @@ from django.test.client import Client
from django.contrib.auth.models import User, Permission
# Application imports
-from conntrackt.models import Project, Location, Entity
-from conntrackt.views import EntityCreateView, InterfaceCreateView
+from conntrackt.models import Project, Location, Entity, Interface
+from conntrackt.views import EntityCreateView
+from conntrackt.views import InterfaceCreateView, InterfaceUpdateView
class ViewTest(TestCase):
@@ -1064,3 +1065,175 @@ class InterfaceCreateViewTest(TestCase):
self.assertDictContainsSubset({"entity": "1"}, initial)
+
+class InterfaceUpdateViewTest(TestCase):
+
+ fixtures = ['test-data.json']
+
+ def setUp(self):
+ # Set-up web client.
+ self.client = Client()
+
+ # Set-up users with different view permissions.
+ self.user = {}
+ self.user["fullperms"] = User.objects.create_user("fullperms", "fullperms@example.com", "fullperms")
+ self.user["fullperms"].user_permissions.add(Permission.objects.get(codename="change_interface"))
+ self.user["noperms"] = User.objects.create_user("noperms", "noperms@example.com", "noperms")
+
+ def test_permission_denied(self):
+ """
+ Tests if permission will be denied for client without sufficient privileges.
+ """
+
+ self.client.login(username="noperms", password="noperms")
+
+ response = self.client.get(reverse("interface_update", args=(1,)))
+
+ self.assertContains(response, "You have insufficient privileges to access this resource. Please contact your local system administrator if you believe you should have been granted access.", status_code=403)
+
+ def test_permission_granted(self):
+ """
+ Tests if permission will be granted for user with correct privileges.
+ """
+
+ self.client.login(username="fullperms", password="fullperms")
+
+ response = self.client.get(reverse("interface_update", args=(1,)))
+
+ self.assertEqual(response.status_code, 200)
+
+ def test_content(self):
+ """
+ Tests if the form comes pre-populated with proper content.
+ """
+
+ self.client.login(username="fullperms", password="fullperms")
+
+ response = self.client.get(reverse("interface_update", args=(1,)))
+
+ self.assertContains(response, ">Edit interface eth0<")
+ self.assertContains(response, 'value="eth0"')
+ self.assertContains(response, "Main network interface.")
+
+ def test_form_entity_limit(self):
+ """
+ Tests if the queryset is properly limitted to specific project's
+ entities.
+ """
+
+ # Set-up the view.
+ view = InterfaceUpdateView()
+ view.request = RequestFactory().get("/fake-path/1")
+ view.object = Interface.objects.get(pk=1)
+
+ # Get the form.
+ form = view.get_form(view.get_form_class())
+
+ expected_entities = ["
",
+ "",
+ "",
+ ""]
+
+ self.assertQuerysetEqual(form.fields["entity"].queryset, expected_entities)
+
+ def test_success_url(self):
+ """
+ Validate that the success URL is set properly after update.
+ """
+
+ self.client.login(username="fullperms", password="fullperms")
+
+ interface = Interface.objects.get(pk=1)
+
+ response = self.client.get(reverse("interface_update", args=(1,)))
+
+ response = self.client.post(reverse("interface_update", args=(1,)),
+ {'csrfmiddlewaretoken': response.context['request'].META['CSRF_COOKIE'],
+ "name": interface.name,
+ "description": interface.name,
+ "entity": "1",
+ "address": "192.168.1.1",
+ "netmask": "255.255.255.255"},
+ follow=True)
+
+ self.assertEqual(response.context["request"].META["PATH_INFO"], reverse("entity", args=(1,)))
+
+
+class InterfaceDeleteViewTest(TestCase):
+
+ fixtures = ['test-data.json']
+
+ def setUp(self):
+ # Set-up web client.
+ self.client = Client()
+
+ # Set-up users with different view permissions.
+ self.user = {}
+ self.user["fullperms"] = User.objects.create_user("fullperms", "fullperms@example.com", "fullperms")
+ self.user["fullperms"].user_permissions.add(Permission.objects.get(codename="delete_interface"))
+ self.user["fullperms"].user_permissions.add(Permission.objects.get(codename="view"))
+ self.user["noperms"] = User.objects.create_user("noperms", "noperms@example.com", "noperms")
+
+ def test_permission_denied(self):
+ """
+ Tests if permission will be denied for client without sufficient privileges.
+ """
+
+ self.client.login(username="noperms", password="noperms")
+
+ response = self.client.get(reverse("interface_delete", args=(1,)))
+
+ self.assertContains(response, "You have insufficient privileges to access this resource. Please contact your local system administrator if you believe you should have been granted access.", status_code=403)
+
+ def test_permission_granted(self):
+ """
+ Tests if permission will be granted for user with correct privileges.
+ """
+
+ self.client.login(username="fullperms", password="fullperms")
+
+ response = self.client.get(reverse("interface_delete", args=(1,)))
+
+ self.assertEqual(response.status_code, 200)
+
+ def test_content(self):
+ """
+ Tests if the form comes pre-populated with proper content.
+ """
+
+ self.client.login(username="fullperms", password="fullperms")
+
+ response = self.client.get(reverse("interface_delete", args=(1,)))
+
+ self.assertContains(response, ">Remove interface eth0<")
+ self.assertContains(response, "Are you sure you want to remove this interface?")
+
+ def test_message(self):
+ """
+ Tests if the message gets added when the interface is deleted.
+ """
+
+ self.client.login(username="fullperms", password="fullperms")
+
+ response = self.client.get(reverse("interface_delete", args=(1,)))
+
+ response = self.client.post(reverse("interface_delete", args=(1,)),
+ {'csrfmiddlewaretoken': response.context['request'].META['CSRF_COOKIE']},
+ follow=True)
+
+ self.assertContains(response, "Interface eth0 has been removed.")
+
+ def test_success_url(self):
+ """
+ Validate that the success URL is set properly after delete.
+ """
+
+ self.client.login(username="fullperms", password="fullperms")
+
+ response = self.client.get(reverse("interface_delete", args=(1,)))
+
+ response = self.client.post(reverse("interface_delete", args=(1,)),
+ {'csrfmiddlewaretoken': response.context['request'].META['CSRF_COOKIE']},
+ follow=True)
+
+ self.assertEqual(response.context["request"].META["PATH_INFO"], reverse("entity", args=(1,)))
diff --git a/conntrackt/urls.py b/conntrackt/urls.py
--- a/conntrackt/urls.py
+++ b/conntrackt/urls.py
@@ -7,7 +7,7 @@ from .views import IndexView, EntityView
from .views import ProjectView, ProjectCreateView, ProjectUpdateView, ProjectDeleteView
from .views import LocationCreateView, LocationUpdateView, LocationDeleteView
from .views import EntityCreateView, EntityUpdateView, EntityDeleteView
-from .views import InterfaceCreateView
+from .views import InterfaceCreateView, InterfaceUpdateView, InterfaceDeleteView
urlpatterns = patterns(
@@ -45,6 +45,10 @@ urlpatterns = patterns(
# View for creating a new interface.
url(r'^interface/add/$', InterfaceCreateView.as_view(), name="interface_create"),
+ # View for updating an existing interface.
+ url(r'^interface/(?P\d+)/edit/$', InterfaceUpdateView.as_view(), name="interface_update"),
+ # View for deleting an interface.
+ url(r'^interface/(?P\d+)/remove/$', InterfaceDeleteView.as_view(), name="interface_delete"),
# View for rendering iptables rules for a specific entity.
url(r'^entity/(?P\d+)/iptables/$', entity_iptables, name="entity_iptables"),
diff --git a/conntrackt/views.py b/conntrackt/views.py
--- a/conntrackt/views.py
+++ b/conntrackt/views.py
@@ -598,3 +598,86 @@ class InterfaceCreateView(MultiplePermis
"""
return reverse("entity", args=(self.object.entity.pk,))
+
+
+class InterfaceUpdateView(MultiplePermissionsRequiredMixin, UpdateView):
+ """
+ View for updating an existing interface.
+ """
+
+ model = Interface
+ form_class = InterfaceForm
+ template_name_suffix = "_update_form"
+
+ # Required permissions.
+ permissions = {
+ "all": ("conntrackt.change_interface",),
+ }
+
+ # Raise authorisation denied exception for unmet permissions.
+ raise_exception = True
+
+ def get_form(self, form_class):
+ """
+ Returns an instance of form that can be used by the view.
+
+ The method will limit the entities that can be selected for the
+ interface to the ones that belong to the same project as the currently
+ set entity.
+ """
+
+ form = super(InterfaceUpdateView, self).get_form(form_class)
+
+ # Limit the entities to same project.
+ form.fields["entity"].queryset = Entity.objects.filter(project=self.object.entity.project)
+
+ return form
+
+ def get_success_url(self):
+ """
+ Returns the URL to which the user should be redirected after an
+ interface has been updated.
+
+ The URL in this case will be set to entity's details page.
+ """
+
+ return reverse("entity", args=(self.object.entity.pk,))
+
+
+class InterfaceDeleteView(MultiplePermissionsRequiredMixin, DeleteView):
+ """
+ View for deleting an interface.
+ """
+
+ model = Interface
+
+ # Required permissions.
+ permissions = {
+ "all": ("conntrackt.delete_interface",),
+ }
+
+ # Raise authorisation denied exception for unmet permissions.
+ raise_exception = True
+
+ def post(self, *args, **kwargs):
+ """
+ Add a success message that will be displayed to the user to confirm the
+ interface deletion.
+ """
+
+ messages.success(self.request, "Interface %s has been removed." % self.get_object().name, extra_tags="alert alert-success")
+
+ return super(InterfaceDeleteView, self).post(*args, **kwargs)
+
+ def delete(self, *args, **kwargs):
+ """
+ Deletes the object. This method is overridden in order to obtain the
+ entity ID for success URL.
+
+ @TODO: Fix this once Django 1.6 comes out with fix from ticket 19044.
+ """
+
+ self.success_url = reverse("entity", args=(self.get_object().entity.id,))
+
+ return super(InterfaceDeleteView, self).delete(*args, **kwargs)
+