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,))