# HG changeset patch # User Branko Majic # Date 2013-07-07 15:57:32 # Node ID 509ed963b5e1be24ed5d5e09dd3d4e3d7689455a # Parent 5d5ad00b179b373843976ec3587b777586a419a6 CONNT-2: Implemented project editing, and removal. Added tests for project add/edit/remove views. Added rendering of messages in the base template. Renamed the project_add view to project_create in order to follow the view naming schema. Cleaned-up some PEP8 errors (unrelated to issue). 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 @@
- {% block content %} - {% endblock %} + {% if messages %} + {% for message in messages %} +
{{ message }}
+ {% endfor %} + {% endif %} + {% block content %} + {% endblock %}
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 @@ -13,7 +13,7 @@
- {% html_link "Add project" "project_add" class="btn btn-primary" %} + {% html_link "Add project" "project_create" class="btn btn-primary" %}

diff --git a/conntrackt/templates/conntrackt/project_confirm_delete.html b/conntrackt/templates/conntrackt/project_confirm_delete.html new file mode 100644 --- /dev/null +++ b/conntrackt/templates/conntrackt/project_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 project {{project.name}}

+
+
+
+
+
+ {% csrf_token %} + {{ form }} + Are you sure you want to remove this project? +
+
+
+ +
+
+
+
+{% endblock content %} diff --git a/conntrackt/templates/conntrackt/project_create_form.html b/conntrackt/templates/conntrackt/project_create_form.html new file mode 100644 --- /dev/null +++ b/conntrackt/templates/conntrackt/project_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 project

+
+
+
+
+
+ {% csrf_token %} + {{ 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 @@ -14,6 +14,13 @@
{% endif %} +
+
+ {% html_link "Edit" "project_update" project.id class="btn btn-primary" %} + {% html_link "Remove" "project_delete" project.id class="btn btn-primary" %} +
+
+
{% if location_entities %}
{% 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 %} -
-
-
-
- {% csrf_token %} - {{ form | crispy }} -
-
- -
-
-
-
-{% 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}}

+
+
+
+
+
+ {% csrf_token %} + {{ form | crispy }} +
+
+ +
+
+
+
+{% 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',