# HG changeset patch # User Branko Majic # Date 2013-07-17 21:35:51 # Node ID 72d37f8490534018de603fd923df768ec46e3547 # Parent 2a0c8cb4797c3eac6a49a86b6893a7635b9f9b65 CONNT-5: Implemented views for updating and removing interfaces. Includes tests. 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 @@
- + {% for interface in interfaces %} - + + + {% endfor %}
InterfacesInterfaces
{{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" %}
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}}

+
+
+
+
+
+ {% csrf_token %} + {{ form }} + Are you sure you want to remove this interface? +
+
+
+ +
+
+
+
+{% 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}}

+
+
+
+
+
+ {% csrf_token %} + {{ form | crispy }} +
+
+ +
+
+
+
+{% 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) +