diff --git a/conntrackt/templates/conntrackt/index.html b/conntrackt/templates/conntrackt/index.html
--- a/conntrackt/templates/conntrackt/index.html
+++ b/conntrackt/templates/conntrackt/index.html
@@ -18,6 +18,7 @@
{% html_link "Add project" "project_create" class="btn btn-primary" %}
+ {% html_link "Add location" "location_create" class="btn btn-primary" %}
@@ -28,20 +29,20 @@
Projects
- {% if projects %}
-
- {% for project in projects %}
-
- {% html_link project.name "project" project.id class="btn btn-link" %} |
- {% html_link '' "project_iptables" project.id class="btn btn-link" %} |
- {% html_link '' "project_update" project.id class="btn btn-link" %} |
- {% html_link '' "project_delete" project.id class="btn btn-link" %} |
-
- {% endfor %}
-
- {% else %}
+ {% if projects %}
+
+ {% for project in projects %}
+
+ {% html_link project.name "project" project.id class="btn btn-link" %} |
+ {% html_link '' "project_iptables" project.id class="btn btn-link" %} |
+ {% html_link '' "project_update" project.id class="btn btn-link" %} |
+ {% html_link '' "project_delete" project.id class="btn btn-link" %} |
+
+ {% endfor %}
+
+ {% else %}
There are no projects defined.
- {% endif %}
+ {% endif %}
@@ -53,6 +54,8 @@
{% for location in locations %}
{{location.name}} |
+ {% html_link '' "location_update" location.id class="btn btn-link" %} |
+ {% html_link '' "location_delete" location.id class="btn btn-link" %} |
{% endfor %}
diff --git a/conntrackt/templates/conntrackt/location_confirm_delete.html b/conntrackt/templates/conntrackt/location_confirm_delete.html
new file mode 100644
--- /dev/null
+++ b/conntrackt/templates/conntrackt/location_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 location {{location.name}}
+
+
+{% endblock content %}
diff --git a/conntrackt/templates/conntrackt/location_create_form.html b/conntrackt/templates/conntrackt/location_create_form.html
new file mode 100644
--- /dev/null
+++ b/conntrackt/templates/conntrackt/location_create_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 %}
+
+
Add new location
+
+
+{% endblock content %}
diff --git a/conntrackt/templates/conntrackt/location_update_form.html b/conntrackt/templates/conntrackt/location_update_form.html
new file mode 100644
--- /dev/null
+++ b/conntrackt/templates/conntrackt/location_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 location {{location.name}}
+
+
+{% 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
@@ -94,7 +94,7 @@ class IndexViewTest(ViewTest):
def test_locations_available(self):
"""
- Tests if locations are show or not.
+ Tests if locations are shown or not.
"""
self.client.login(username="fullperms", password="fullperms")
@@ -568,3 +568,181 @@ class ProjectDeleteViewTest(TestCase):
follow=True)
self.assertContains(response, "Project Test Project 1 has been removed.")
+
+
+class LocationCreateViewTest(TestCase):
+
+ 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="add_location"))
+ 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("location_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.
+ """
+
+ self.client.login(username="fullperms", password="fullperms")
+
+ response = self.client.get(reverse("location_create"))
+
+ self.assertEqual(response.status_code, 200)
+
+ def test_form_styling(self):
+ """
+ Tests if proper form styling is being applied.
+ """
+
+ self.client.login(username="fullperms", password="fullperms")
+
+ response = self.client.get(reverse("location_create"))
+
+ self.assertContains(response, 'class="span6 textinput')
+ self.assertContains(response, 'class="span6 textarea')
+ self.assertContains(response, 'placeholder="New Location"')
+ self.assertContains(response, 'placeholder="Description for new location."')
+
+
+class LocationUpdateViewTest(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_location"))
+ 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("location_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("location_update", args=(1,)))
+
+ self.assertEqual(response.status_code, 200)
+
+ def test_form_styling(self):
+ """
+ Tests if proper form styling is being applied.
+ """
+
+ self.client.login(username="fullperms", password="fullperms")
+
+ response = self.client.get(reverse("location_update", args=(1,)))
+
+ self.assertContains(response, 'class="span6 textinput')
+ self.assertContains(response, 'class="span6 textarea')
+ self.assertContains(response, 'placeholder="Location name"')
+ self.assertContains(response, 'placeholder="Description for location."')
+
+ 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("location_update", args=(1,)))
+
+ self.assertContains(response, ">Edit location Test Location 1<")
+ self.assertContains(response, 'value="Test Location 1"')
+ self.assertContains(response, "This is a test location 1.")
+
+
+class LocationDeleteViewTest(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_location"))
+ 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("location_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("location_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("location_delete", args=(1,)))
+
+ self.assertContains(response, ">Remove location Test Location 1<")
+ self.assertContains(response, "Are you sure you want to remove this location?")
+
+ def test_message(self):
+ """
+ Tests if the message gets added when the location is deleted.
+ """
+
+ self.client.login(username="fullperms", password="fullperms")
+
+ response = self.client.get(reverse("location_delete", args=(1,)))
+
+ response = self.client.post(reverse("location_delete", args=(1,)),
+ {'csrfmiddlewaretoken': response.context['request'].META['CSRF_COOKIE']},
+ follow=True)
+
+ self.assertContains(response, "Location Test Location 1 has been removed.")
diff --git a/conntrackt/urls.py b/conntrackt/urls.py
--- a/conntrackt/urls.py
+++ b/conntrackt/urls.py
@@ -3,13 +3,16 @@ from django.conf.urls import patterns, u
from django.contrib.auth.views import login, logout
# Application imports.
-from .views import IndexView, ProjectView, ProjectCreateView, ProjectUpdateView, ProjectDeleteView, EntityView, entity_iptables, project_iptables
+from .views import IndexView, EntityView, entity_iptables, project_iptables
+from .views import ProjectView, ProjectCreateView, ProjectUpdateView, ProjectDeleteView
+from .views import LocationCreateView, LocationUpdateView, LocationDeleteView
urlpatterns = patterns(
'conntrackt.views',
# Homepage/index view.
url(r'^$', IndexView.as_view(), name="index"),
+
# View for showing information about a project.
url(r'^project/(?P\d+)/$', ProjectView.as_view(),
name='project'),
@@ -19,6 +22,15 @@ urlpatterns = patterns(
url(r'^project/(?P\d+)/edit/$', ProjectUpdateView.as_view(), name="project_update"),
# View for deleting a project.
url(r'^project/(?P\d+)/remove/$', ProjectDeleteView.as_view(), name="project_delete"),
+
+ # View for creating a new location.
+ url(r'^location/add/$', LocationCreateView.as_view(), name="location_create"),
+ # View for updating an existing location.
+ url(r'^location/(?P\d+)/edit/$', LocationUpdateView.as_view(), name="location_update"),
+ # View for deleting a location.
+ url(r'^location/(?P\d+)/remove/$', LocationDeleteView.as_view(), name="location_delete"),
+
+
# View for showing information about an entity.
url(r'^entity/(?P\d+)/$', EntityView.as_view(),
name='entity'),
diff --git a/conntrackt/views.py b/conntrackt/views.py
--- a/conntrackt/views.py
+++ b/conntrackt/views.py
@@ -323,3 +323,97 @@ class ProjectDeleteView(MultiplePermissi
messages.success(self.request, "Project %s has been removed." % self.get_object().name, extra_tags="alert alert-success")
return super(ProjectDeleteView, self).post(*args, **kwargs)
+
+
+class LocationCreateView(MultiplePermissionsRequiredMixin, CreateView):
+ """
+ View for creating a new location.
+ """
+
+ model = Location
+ template_name_suffix = "_create_form"
+
+ # Required permissions.
+ permissions = {
+ "all": ("conntrackt.add_location",),
+ }
+
+ # Raise authorisation denied exception for unmet permissions.
+ raise_exception = True
+
+ success_url = reverse_lazy("index")
+
+ def get_form(self, form_class):
+ """
+ Implements an override for the default form constructed for the create
+ view that includes some better styling of input widgets.
+ """
+
+ form = super(LocationCreateView, self).get_form(form_class)
+ form.fields["name"].widget.attrs["class"] = "span6"
+ form.fields["name"].widget.attrs["placeholder"] = "New Location"
+ form.fields["description"].widget.attrs["class"] = "span6"
+ form.fields["description"].widget.attrs["placeholder"] = "Description for new location."
+
+ return form
+
+
+class LocationUpdateView(MultiplePermissionsRequiredMixin, UpdateView):
+ """
+ View for modifying an existing location.
+ """
+
+ model = Location
+ template_name_suffix = "_update_form"
+
+ # Required permissions.
+ permissions = {
+ "all": ("conntrackt.change_location",),
+ }
+
+ # Raise authorisation denied exception for unmet permissions.
+ raise_exception = True
+
+ success_url = reverse_lazy("index")
+
+ def get_form(self, form_class):
+ """
+ Implements an override for the default form constructed for the create
+ view that includes some better styling of input widgets.
+ """
+
+ form = super(LocationUpdateView, self).get_form(form_class)
+ form.fields["name"].widget.attrs["class"] = "span6"
+ form.fields["name"].widget.attrs["placeholder"] = "Location name"
+ form.fields["description"].widget.attrs["class"] = "span6"
+ form.fields["description"].widget.attrs["placeholder"] = "Description for location."
+
+ return form
+
+
+class LocationDeleteView(MultiplePermissionsRequiredMixin, DeleteView):
+ """
+ View for deleting a location.
+ """
+
+ model = Location
+
+ # Required permissions.
+ permissions = {
+ "all": ("conntrackt.delete_location",),
+ }
+
+ # Raise authorisation denied exception for unmet permissions.
+ raise_exception = True
+
+ success_url = reverse_lazy("index")
+
+ def post(self, *args, **kwargs):
+ """
+ Add a success message that will be displayed to the user to confirm the
+ location deletion.
+ """
+
+ 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)
diff --git a/docs/conf.py b/docs/conf.py
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -11,7 +11,8 @@
# All configuration values have a default; values that are commented out
# serve to show the default.
-import sys, os
+import sys
+import os
# If extensions (or modules to document with autodoc) are in another directory,
# add these directories to sys.path here. If the directory is relative to the