# HG changeset patch # User Branko Majic # Date 2013-07-17 20:21:42 # Node ID 2a0c8cb4797c3eac6a49a86b6893a7635b9f9b65 # Parent 079e7f2b06804ccaaaf55a5c3a3a955bad22bd6f CONNT-5: Added custom model form for Interface. Applied small improvement to EntityForm for styling. Implemented adding interfaces to entities from the entity details page. Updated representation of interfaces on entity details page. Updated view tests for the new functionality. Minor fix to EntityView. diff --git a/conntrackt/forms.py b/conntrackt/forms.py --- a/conntrackt/forms.py +++ b/conntrackt/forms.py @@ -22,11 +22,37 @@ class EntityForm(ModelForm): super(EntityForm, self).__init__(*args, **kwargs) - # Update the widgets to be wider, and set-up placeholder values for text - # boxes. - self.fields["name"].widget.attrs["class"] = "span6" + # Update the widgets to be wider. + for field_name, field in self.fields.iteritems(): + field.widget.attrs["class"] = "span6" + + # Set-up some placeholders. self.fields["name"].widget.attrs["placeholder"] = "Entity name" - self.fields["description"].widget.attrs["class"] = "span6" - self.fields["description"].widget.attrs["placeholder"] = "Description for new entity." - self.fields["project"].widget.attrs["class"] = "span6" - self.fields["location"].widget.attrs["class"] = "span6" + self.fields["description"].widget.attrs["placeholder"] = "Entity description" + + +class InterfaceForm(ModelForm): + """ + Implements a custom model form for interfaces with some styling changes. + """ + + class Meta: + model = Interface + + def __init__(self, *args, **kwargs): + """ + Initialises the form instance. Sets-up some bootstrap CSS classes for + widgets. + """ + + super(InterfaceForm, self).__init__(*args, **kwargs) + + # Update the widgets to be wider. + for field_name, field in self.fields.iteritems(): + field.widget.attrs["class"] = "span6" + + # Set-up some placeholders. + self.fields["name"].widget.attrs["placeholder"] = "Interface name" + self.fields["description"].widget.attrs["placeholder"] = "Interface description" + self.fields["address"].widget.attrs["placeholder"] = "IP address of interface" + self.fields["netmask"].widget.attrs["placeholder"] = "IP address netmask" 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 @@ -24,6 +24,9 @@ {% html_link "Edit" "entity_update" entity.id class="btn btn-primary" %} {% html_link "Remove" "entity_delete" entity.id class="btn btn-primary" %} {% html_link "Get Iptables" 'entity_iptables' entity.id class="btn btn-primary" %} + {% with entity_id=entity.id|slugify %} + {% html_link "Add interface" "interface_create" class="btn btn-primary" get="entity="|add:entity_id %} + {% endwith %} @@ -53,11 +56,11 @@ Interfaces - - {% for interface in interfaces %} + {% for interface in interfaces %} + {{interface.name}} ({{interface.address}}/{{interface.netmask}}) - {% endfor %} - + + {% endfor %} diff --git a/conntrackt/templates/conntrackt/interface_create_form.html b/conntrackt/templates/conntrackt/interface_create_form.html new file mode 100644 --- /dev/null +++ b/conntrackt/templates/conntrackt/interface_create_form.html @@ -0,0 +1,27 @@ +{% extends "conntrackt/base.html" %} + +{# For html_link #} +{% load conntrackt_tags %} +{# For Bootstrapped forms #} +{% load crispy_forms_tags %} + +{% block content %} +
+

Add new interface

+
+ +
+
+
+
+ {% csrf_token %} + {{ form | crispy }} + {{ interface_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,8 @@ from django.test.client import Client from django.contrib.auth.models import User, Permission # Application imports -from conntrackt.models import Project, Location -from conntrackt.views import EntityCreateView +from conntrackt.models import Project, Location, Entity +from conntrackt.views import EntityCreateView, InterfaceCreateView class ViewTest(TestCase): @@ -992,3 +992,75 @@ class EntityUpdateViewTest(TestCase): self.assertContains(response, ">Edit entity Test Entity 1<") self.assertContains(response, 'value="Test Entity 1"') self.assertContains(response, "This is a test entity 1.") + + +class InterfaceCreateViewTest(TestCase): + + def setUp(self): + """ + Sets-up some data necessary for testing. + """ + + # Set-up some data for testing. + project = Project.objects.create(name="Test Project", description="This is test project.") + location = Location.objects.create(name="Test Location", description="This is test location.") + Entity.objects.create(name="Test Entity 1", description="This is test entity 1.", project=project, location=location) + Entity.objects.create(name="Test Entity 2", description="This is test entity 2.", project=project, location=location) + + def test_permission_denied(self): + """ + Tests if permission will be denied for client without sufficient privileges. + """ + + User.objects.create_user("noperms", "noperms@example.com", "noperms") + + self.client.login(username="noperms", password="noperms") + + response = self.client.get(reverse("interface_create")) + + 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. + """ + + user = User.objects.create_user("fullperms", "fullperms@example.com", "fullperms") + user.user_permissions.add(Permission.objects.get(codename="add_interface")) + + self.client.login(username="fullperms", password="fullperms") + + response = self.client.get(reverse("interface_create")) + + self.assertEqual(response.status_code, 200) + + def test_form_entity_limit(self): + """ + Tests if the queryset is properly limitted to specific entity if GET + parameter is passed. + """ + + # Set-up the view. + view = InterfaceCreateView() + view.request = RequestFactory().get("/fake-path?entity=1") + view.object = None + + # Get the form. + form = view.get_form(view.get_form_class()) + + self.assertQuerysetEqual(form.fields["entity"].queryset, [""]) + + def test_initial_project(self): + """ + Tests if the choice field for entity is defaulted to entity passed as + part of GET parameters. + """ + + view = InterfaceCreateView() + view.request = RequestFactory().get("/fake-path?entity=1") + view.object = None + + initial = view.get_initial() + + self.assertDictContainsSubset({"entity": "1"}, initial) + diff --git a/conntrackt/urls.py b/conntrackt/urls.py --- a/conntrackt/urls.py +++ b/conntrackt/urls.py @@ -7,6 +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 urlpatterns = patterns( @@ -42,6 +43,9 @@ urlpatterns = patterns( # View for deleting an entity. url(r'^entity/(?P\d+)/remove/$', EntityDeleteView.as_view(), name="entity_delete"), + # View for creating a new interface. + url(r'^interface/add/$', InterfaceCreateView.as_view(), name="interface_create"), + # View for rendering iptables rules for a specific entity. url(r'^entity/(?P\d+)/iptables/$', entity_iptables, name="entity_iptables"), # View for rendering zip file with iptables rules for all entities in a project. diff --git a/conntrackt/views.py b/conntrackt/views.py --- a/conntrackt/views.py +++ b/conntrackt/views.py @@ -14,8 +14,8 @@ from django.views.generic import Templat from braces.views import MultiplePermissionsRequiredMixin # Application imports. -from .forms import EntityForm -from .models import Project, Entity, Location +from .forms import EntityForm, InterfaceForm +from .models import Project, Entity, Location, Interface from .utils import generate_entity_iptables @@ -126,7 +126,7 @@ class EntityView(MultiplePermissionsRequ """ # Call the parent class method. - context = super(DetailView, self).get_context_data(**kwargs) + context = super(EntityView, self).get_context_data(**kwargs) # Add the rendered iptables rules to the context. context['entity_iptables'] = generate_entity_iptables(self.object) @@ -529,7 +529,6 @@ class EntityDeleteView(MultiplePermissio return super(EntityDeleteView, self).post(*args, **kwargs) - def delete(self, *args, **kwargs): """ Deletes the object. This method is overridden in order to obtain the @@ -541,3 +540,61 @@ class EntityDeleteView(MultiplePermissio self.success_url = reverse("project", args=(self.get_object().project.id,)) return super(EntityDeleteView, self).delete(*args, **kwargs) + + +class InterfaceCreateView(MultiplePermissionsRequiredMixin, CreateView): + """ + View for creating a new interface. + """ + + model = Interface + form_class = InterfaceForm + template_name_suffix = "_create_form" + + # Required permissions + permissions = { + "all": ("conntrackt.add_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 entity select input if request contained this + information. + """ + + form = super(InterfaceCreateView, self).get_form(form_class) + + # Limit the entity selection if required. + entity_id = self.request.GET.get("entity", None) + if entity_id: + form.fields["entity"].queryset = Entity.objects.filter(pk=entity_id) + form.fields["entity"].widget.attrs["readonly"] = True + + return form + + def get_initial(self): + """ + Returns initial values that should be pre-selected (if they were + specified through a GET parameter). + """ + + initial = super(InterfaceCreateView, self).get_initial() + + initial["entity"] = self.request.GET.get("entity", None) + + return initial + + def get_success_url(self): + """ + Returns the URL to which the user should be redirected after an + interface has been created. + + The URL in this case will be set to entity's details page. + """ + + return reverse("entity", args=(self.object.entity.pk,))