diff --git a/conntrackt/models.py b/conntrackt/models.py
--- a/conntrackt/models.py
+++ b/conntrackt/models.py
@@ -105,7 +105,6 @@ class Entity(models.Model):
# Enforce uniqueness of entity name in a project.
unique_together = ("name", "project")
-
def __unicode__(self):
"""
Returns:
diff --git a/conntrackt/templates/conntrackt/base.html b/conntrackt/templates/conntrackt/base.html
--- a/conntrackt/templates/conntrackt/base.html
+++ b/conntrackt/templates/conntrackt/base.html
@@ -49,8 +49,13 @@
{% for location, entities in location_entities %}
diff --git a/conntrackt/templates/conntrackt/project_form.html b/conntrackt/templates/conntrackt/project_form.html
deleted file mode 100644
--- a/conntrackt/templates/conntrackt/project_form.html
+++ /dev/null
@@ -1,22 +0,0 @@
-{% extends "conntrackt/base.html" %}
-
-{# For html_link #}
-{% load conntrackt_tags %}
-{# For Bootstrapped forms #}
-{% load crispy_forms_tags %}
-
-{% block content %}
-
-{% endblock content %}
diff --git a/conntrackt/templates/conntrackt/project_update_form.html b/conntrackt/templates/conntrackt/project_update_form.html
new file mode 100644
--- /dev/null
+++ b/conntrackt/templates/conntrackt/project_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 project {{project.name}}
+
+
+{% endblock content %}
diff --git a/conntrackt/tests/test_models.py b/conntrackt/tests/test_models.py
--- a/conntrackt/tests/test_models.py
+++ b/conntrackt/tests/test_models.py
@@ -97,7 +97,6 @@ class EntityTest(TestCase):
self.assertEqual(str(ent), representation)
-
def test_unique_name(self):
"""
Test if unique entity name is enforced across same project.
@@ -131,11 +130,10 @@ class InterfaceTest(TestCase):
interface = entity.interface_set.get(pk=1)
- duplicate = Interface(name=interface.name, description = "Duplicate interface.", entity=entity, address="10.10.10.10", netmask="255.255.255.255")
+ duplicate = Interface(name=interface.name, description="Duplicate interface.", entity=entity, address="10.10.10.10", netmask="255.255.255.255")
self.assertRaises(IntegrityError, duplicate.save)
-
def test_representation_single(self):
"""
Test representation of single IP address.
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
@@ -379,16 +379,15 @@ class ProjectCreateViewTest(TestCase):
self.user["fullperms"].user_permissions.add(Permission.objects.get(codename="add_project"))
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("project_add"))
-
+
+ response = self.client.get(reverse("project_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):
@@ -398,7 +397,58 @@ class ProjectCreateViewTest(TestCase):
self.client.login(username="fullperms", password="fullperms")
- response = self.client.get(reverse("project_add"))
+ response = self.client.get(reverse("project_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("project_create"))
+
+ self.assertContains(response, 'class="span6 textinput')
+ self.assertContains(response, 'class="span6 textarea')
+ self.assertContains(response, 'placeholder="New Project"')
+ self.assertContains(response, 'placeholder="Description for new project."')
+
+
+class ProjectUpdateViewTest(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_project"))
+ 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("project_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("project_update", args=(1,)))
self.assertEqual(response.status_code, 200)
@@ -409,10 +459,87 @@ class ProjectCreateViewTest(TestCase):
self.client.login(username="fullperms", password="fullperms")
- response = self.client.get(reverse("project_add"))
+ response = self.client.get(reverse("project_update", args=(1,)))
self.assertContains(response, 'class="span6 textinput')
self.assertContains(response, 'class="span6 textarea')
- self.assertContains(response, 'placeholder="New Project"')
- self.assertContains(response, 'placeholder="Description for new project."')
+ self.assertContains(response, 'placeholder="Project name"')
+ self.assertContains(response, 'placeholder="Description for project."')
+
+ 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("project_update", args=(1,)))
+
+ self.assertContains(response, ">Edit project Test Project 1<")
+ self.assertContains(response, 'value="Test Project 1"')
+ self.assertContains(response, "This is a test project 1.")
+
+
+class ProjectDeleteViewTest(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_project"))
+ 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("project_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("project_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("project_delete", args=(1,)))
+
+ self.assertContains(response, ">Remove project Test Project 1<")
+ self.assertContains(response, "Are you sure you want to remove this project?")
+
+ def test_message(self):
+ """
+ Tests if the message gets added when the project is deleted.
+ """
+
+ self.client.login(username="fullperms", password="fullperms")
+
+ response = self.client.get(reverse("project_delete", args=(1,)))
+
+ response = self.client.post(reverse("project_delete", args=(1,)),
+ {'csrfmiddlewaretoken': response.context['request'].META['CSRF_COOKIE']},
+ follow=True)
+
+ self.assertContains(response, "Project Test Project 1 has been removed.")
diff --git a/conntrackt/urls.py b/conntrackt/urls.py
--- a/conntrackt/urls.py
+++ b/conntrackt/urls.py
@@ -3,7 +3,7 @@ from django.conf.urls import patterns, u
from django.contrib.auth.views import login, logout
# Application imports.
-from .views import IndexView, ProjectView, ProjectCreateView, EntityView, entity_iptables, project_iptables
+from .views import IndexView, ProjectView, ProjectCreateView, ProjectUpdateView, ProjectDeleteView, EntityView, entity_iptables, project_iptables
urlpatterns = patterns(
@@ -14,7 +14,11 @@ urlpatterns = patterns(
url(r'^project/(?P
\d+)/$', ProjectView.as_view(),
name='project'),
# View for creating a new project.
- url(r'^project/add/$', ProjectCreateView.as_view(), name="project_add"),
+ url(r'^project/add/$', ProjectCreateView.as_view(), name="project_create"),
+ # View for updating an existing project.
+ 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 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
@@ -4,9 +4,11 @@ from zipfile import ZipFile, ZIP_DEFLATE
# Django imports.
from django.contrib.auth.decorators import permission_required
+from django.contrib import messages
+from django.core.urlresolvers import reverse_lazy
from django.http import HttpResponse
from django.shortcuts import render_to_response, get_object_or_404
-from django.views.generic import TemplateView, DetailView, CreateView
+from django.views.generic import TemplateView, DetailView, CreateView, UpdateView, DeleteView
# Third-party application imports.
from braces.views import MultiplePermissionsRequiredMixin
@@ -237,7 +239,8 @@ class ProjectCreateView(MultiplePermissi
"""
model = Project
-
+ template_name_suffix = "_create_form"
+
# Required permissions.
permissions = {
"all": ("conntrackt.add_project",),
@@ -260,3 +263,61 @@ class ProjectCreateView(MultiplePermissi
return form
+
+class ProjectUpdateView(MultiplePermissionsRequiredMixin, UpdateView):
+ """
+ View for modifying an existing project.
+ """
+
+ model = Project
+ template_name_suffix = "_update_form"
+
+ # Required permissions.
+ permissions = {
+ "all": ("conntrackt.change_project",),
+ }
+
+ # Raise authorisation denied exception for unmet permissions.
+ raise_exception = True
+
+ 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(ProjectUpdateView, self).get_form(form_class)
+ form.fields["name"].widget.attrs["class"] = "span6"
+ form.fields["name"].widget.attrs["placeholder"] = "Project name"
+ form.fields["description"].widget.attrs["class"] = "span6"
+ form.fields["description"].widget.attrs["placeholder"] = "Description for project."
+
+ return form
+
+
+class ProjectDeleteView(MultiplePermissionsRequiredMixin, DeleteView):
+ """
+ View for deleting a project.
+ """
+
+ model = Project
+
+ # Required permissions.
+ permissions = {
+ "all": ("conntrackt.delete_project",),
+ }
+
+ # 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
+ project deletion.
+ """
+
+ 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)
diff --git a/projtest/projtest/settings.py b/projtest/projtest/settings.py
--- a/projtest/projtest/settings.py
+++ b/projtest/projtest/settings.py
@@ -10,7 +10,7 @@ MANAGERS = ADMINS
DATABASES = {
'default': {
- 'ENGINE': 'django.db.backends.sqlite3', # Add 'postgresql_psycopg2', 'mysql', 'sqlite3' or 'oracle'.
+ 'ENGINE': 'django.db.backends.sqlite3', # Add 'postgresql_psycopg2', 'mysql', 'sqlite3' or 'oracle'.
'NAME': 'projtest.db', # Or path to database file if using sqlite3.
'USER': '', # Not used with sqlite3.
'PASSWORD': '', # Not used with sqlite3.
@@ -167,7 +167,7 @@ LOGGING = {
}
# View that should be called for log-in action.
-LOGIN_URL="login"
+LOGIN_URL = "login"
# Use custom test runner.
TEST_RUNNER = 'discover_runner.DiscoverRunner'
diff --git a/projtest/projtest/urls.py b/projtest/projtest/urls.py
--- a/projtest/projtest/urls.py
+++ b/projtest/projtest/urls.py
@@ -13,7 +13,7 @@ urlpatterns = patterns(
# Examples:
# url(r'^$', 'projtest.views.home', name='home'),
# url(r'^projtest/', include('projtest.foo.urls')),
- url(r'^$', lambda r : HttpResponseRedirect('conntrackt/')),
+ url(r'^$', lambda r: HttpResponseRedirect('conntrackt/')),
url(r'^conntrackt/', include('conntrackt.urls')),
@@ -23,4 +23,3 @@ urlpatterns = patterns(
# Uncomment the next line to enable the admin:
url(r'^admin/', include(admin.site.urls)),
)
-
diff --git a/setup.py b/setup.py
--- a/setup.py
+++ b/setup.py
@@ -26,7 +26,7 @@ setup(
'Environment :: Web Environment',
'Framework :: Django',
'Intended Audience :: Developers',
- 'License :: OSI Approved :: GPLv3', # example license
+ 'License :: OSI Approved :: GPLv3',
'Operating System :: OS Independent',
'Programming Language :: Python',
'Programming Language :: Python :: 2.7',