diff --git a/conntrackt/forms.py b/conntrackt/forms.py new file mode 100644 --- /dev/null +++ b/conntrackt/forms.py @@ -0,0 +1,32 @@ +# Django imports. +from django.forms import ModelForm +from django.forms.models import inlineformset_factory + +# Application imports. +from .models import Entity, Interface + + +class EntityForm(ModelForm): + """ + Implements a custom model form for entities with some styling changes. + """ + + class Meta: + model = Entity + + def __init__(self, *args, **kwargs): + """ + Initialises the form instance. Sets-up some bootstrap CSS classes for + widgets. + """ + + 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" + 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" diff --git a/conntrackt/templates/conntrackt/entity_create_form.html b/conntrackt/templates/conntrackt/entity_create_form.html new file mode 100644 --- /dev/null +++ b/conntrackt/templates/conntrackt/entity_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 entity

+
+ +
+
+
+
+ {% csrf_token %} + {{ form | crispy }} + {{ interface_form | crispy }} +
+
+ +
+
+
+
+{% endblock content %} diff --git a/conntrackt/templates/conntrackt/project_detail.html b/conntrackt/templates/conntrackt/project_detail.html --- a/conntrackt/templates/conntrackt/project_detail.html +++ b/conntrackt/templates/conntrackt/project_detail.html @@ -1,9 +1,12 @@ {% extends "conntrackt/base.html" %} +{# For html_link #} {% load conntrackt_tags %} {% block content %} -

{{project.name}}

+
+

{{project.name}}

+
{% if project.description %}
@@ -18,6 +21,9 @@
{% html_link "Edit" "project_update" project.id class="btn btn-primary" %} {% html_link "Remove" "project_delete" project.id class="btn btn-primary" %} + {% with project_id=project.id|slugify %} + {% html_link "Add entity" "entity_create" class="btn btn-primary" get="project="|add:project_id %} + {% endwith %}

diff --git a/conntrackt/tests/test_forms.py b/conntrackt/tests/test_forms.py --- a/conntrackt/tests/test_forms.py +++ b/conntrackt/tests/test_forms.py @@ -0,0 +1,23 @@ +# Django imports. +from django.test import TestCase + +# Application imports. +from conntrackt.forms import EntityForm + + +class EntityFormTest(TestCase): + """ + Tests for the custom Entity model form. + """ + + def test_styling(self): + """ + Test that the form styling is set-up correctly. + """ + + form = EntityForm() + + self.assertIn("span6", form.fields["name"].widget.attrs["class"]) + self.assertIn("span6", form.fields["description"].widget.attrs["class"]) + self.assertIn("span6", form.fields["project"].widget.attrs["class"]) + self.assertIn("span6", form.fields["location"].widget.attrs["class"]) 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 @@ -4,12 +4,14 @@ from zipfile import ZipFile, ZIP_DEFLATE # Django imports. from django.core.urlresolvers import reverse +from django.test import RequestFactory from django.test import TestCase 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 class ViewTest(TestCase): @@ -746,3 +748,104 @@ class LocationDeleteViewTest(TestCase): follow=True) self.assertContains(response, "Location Test Location 1 has been removed.") + + +class EntityCreateViewTest(TestCase): + + def setUp(self): + """ + Sets-up some data necessary for testing. + """ + + # Set-up some data for testing. + Project.objects.create(name="Test Project 1", description="This is test project 1.") + Project.objects.create(name="Test Project 2", description="This is test project 2.") + Location.objects.create(name="Test Location 1", description="This is test location 1.") + Location.objects.create(name="Test Location 2", description="This is test location 2.") + + 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("entity_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_entity")) + + self.client.login(username="fullperms", password="fullperms") + + response = self.client.get(reverse("entity_create")) + + self.assertEqual(response.status_code, 200) + + def test_form_project_limit(self): + """ + Tests if the queryset is properly limitted to specific project if GET + parameters is passed. + """ + + # Set-up the view. + view = EntityCreateView() + view.request = RequestFactory().get("/fake-path?project=1") + view.object = None + + # Get the form. + form = view.get_form(view.get_form_class()) + + self.assertQuerysetEqual(form.fields["project"].queryset, [""]) + + def test_form_location_limit(self): + """ + Tests if the queryset is properly limitted to specific location if GET + parameters is passed. + """ + + # Set-up the view. + view = EntityCreateView() + view.request = RequestFactory().get("/fake-path?location=1") + view.object = None + + # Get the form. + form = view.get_form(view.get_form_class()) + + self.assertQuerysetEqual(form.fields["location"].queryset, [""]) + + def test_initial_project(self): + """ + Tests if the choice field for project is defaulted to project passed as + part of GET parameters. + """ + + view = EntityCreateView() + view.request = RequestFactory().get("/fake-path?project=1") + view.object = None + + initial = view.get_initial() + + self.assertDictContainsSubset({"project": "1"}, initial) + + def test_initial_location(self): + """ + Tests if the choice field for location is defaulted to location passed + as part of GET parameters. + """ + + view = EntityCreateView() + view.request = RequestFactory().get("/fake-path?location=1") + view.object = None + + initial = view.get_initial() + + self.assertDictContainsSubset({"location": "1"}, initial) diff --git a/conntrackt/urls.py b/conntrackt/urls.py --- a/conntrackt/urls.py +++ b/conntrackt/urls.py @@ -6,6 +6,7 @@ from django.contrib.auth.views import lo from .views import IndexView, EntityView, entity_iptables, project_iptables from .views import ProjectView, ProjectCreateView, ProjectUpdateView, ProjectDeleteView from .views import LocationCreateView, LocationUpdateView, LocationDeleteView +from .views import EntityCreateView urlpatterns = patterns( @@ -34,6 +35,9 @@ urlpatterns = patterns( # View for showing information about an entity. url(r'^entity/(?P\d+)/$', EntityView.as_view(), name='entity'), + # View for creating a new entity. + url(r'^entity/add/$', EntityCreateView.as_view(), name="entity_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,6 +14,7 @@ from django.views.generic import Templat from braces.views import MultiplePermissionsRequiredMixin # Application imports. +from .forms import EntityForm from .models import Project, Entity, Location from .utils import generate_entity_iptables @@ -417,3 +418,58 @@ class LocationDeleteView(MultiplePermiss messages.success(self.request, "Location %s has been removed." % self.get_object().name, extra_tags="alert alert-success") return super(LocationDeleteView, self).post(*args, **kwargs) + + +class EntityCreateView(MultiplePermissionsRequiredMixin, CreateView): + """ + View for creating a new entity. + """ + + model = Entity + form_class = EntityForm + template_name_suffix = "_create_form" + + # Required permissions. + permissions = { + "all": ("conntrackt.add_entity",), + } + + # 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 project or location select inputs if request + contained this information. + """ + + form = super(EntityCreateView, self).get_form(form_class) + + # Limit the project selection if required. + project_id = self.request.GET.get("project", None) + if project_id: + form.fields["project"].queryset = Project.objects.filter(pk=project_id) + form.fields["project"].widget.attrs["readonly"] = True + + # Limit the location selection if required. + location_id = self.request.GET.get("location", None) + if location_id: + form.fields["location"].queryset = Location.objects.filter(pk=location_id) + form.fields["location"].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(EntityCreateView, self).get_initial() + + initial["project"] = self.request.GET.get("project", None) + initial["location"] = self.request.GET.get("location", None) + + return initial