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
+
+
+
+{% 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,))