diff --git a/conntrackt/templatetags/conntrackt_tags.py b/conntrackt/templatetags/conntrackt_tags.py --- a/conntrackt/templatetags/conntrackt_tags.py +++ b/conntrackt/templatetags/conntrackt_tags.py @@ -22,6 +22,7 @@ # Django imports. from django import template from django.core import urlresolvers +from django.utils.html import format_html # Get an instance of Django's template library. @@ -54,26 +55,34 @@ def html_link(text, view, *args, **kwarg """ + # Verify the passed-in keyword arguments first. + for key in kwargs.keys(): + if key not in ("get", "class", "title", "id"): + raise template.TemplateSyntaxError("Unknown argument for 'html_link' tag: %r" % key) + # Generate the URL by using the supplied view name and arguments that should # be passed to the view. url = urlresolvers.reverse(view, args=args) # Set-up the base pattern (url, parameters, text). - pattern = '%s' + if 'get' in kwargs: + pattern = '. +# + + +# Standard library imports. +import json +from StringIO import StringIO +from zipfile import ZipFile, ZIP_DEFLATED + +# Python third-party library imports. +import mock + +# Django imports. +from django.template import Context, Template, TemplateSyntaxError +from django.test import TestCase + +# Application imports +from conntrackt.templatetags.conntrackt_tags import html_link, active_link, current_url_equals + + +@mock.patch('conntrackt.templatetags.conntrackt_tags.urlresolvers.reverse') +class HtmlLinkTest(TestCase): + + def test_url_reverse_called_with_passed_in_args(self, mock_reverse): + """ + Tests if URL reversing is performed using the correct set of + passed-in arguments. + """ + + html_link("My link", "my_view", 'arg1', 'arg2', 'arg3') + + mock_reverse.assert_called_once_with("my_view", args=('arg1', 'arg2', 'arg3')) + + def test_url_reverse_called_without_args_if_they_are_not_passed_in(self, mock_reverse): + """ + Tests if URL reverse is performed without using any positional + arguments if they are not specified. + """ + + kwargs = { + "id": "myid", + "class": "myclass", + "title": "mytitle", + } + + + html_link("My link", "my_view", **kwargs) + + mock_reverse.assert_called_once_with("my_view", args=()) + + def test_html_id_applied_to_output_element(self, mock_reverse): + """ + Tests if id attribute is filled-in correctly in the HTML tag. + """ + + # Mock a view we want to reverse. + mock_reverse.return_value = "/my/url" + kwargs = { + 'id': "my_id", + } + + link = html_link("My link", "my_view", **kwargs) + + self.assertIn('id="my_id"', link) + + def test_html_class_applied_to_output_element(self, mock_reverse): + """ + Tests if class attribute is filled-in correctly in the HTML tag. + """ + + # Mock a view we want to reverse. + mock_reverse.return_value = "/my/url" + kwargs = { + 'class': "class1,class2,class3", + } + + link = html_link("My link", "my_view", **kwargs) + + self.assertIn('class="class1,class2,class3"', link) + + def test_html_title_applied_to_output_element(self, mock_reverse): + """ + Tests if title attribute is filled-in correctly in the HTML tag. + """ + + # Mock a view we want to reverse. + mock_reverse.return_value = "/my/url" + kwargs = { + 'title': "My title", + } + + link = html_link("My link", "my_view", **kwargs) + + self.assertIn('title="My title"', link) + + def test_get_parameter_applied_to_output_element_link(self, mock_reverse): + """ + Tests if generated URL contains the passed-in get argument. + """ + + # Mock a view we want to reverse. + mock_reverse.return_value = "/my/url" + kwargs = { + 'get': "MyGetParameter", + } + + link = html_link("My link", "my_view", **kwargs) + + self.assertIn('href="/my/url?MyGetParameter"', link) + + def test_rendered_output_format(self, mock_reverse): + """ + Tests if the rendered output format is correct. + """ + + mock_reverse.return_value = "/my/url" + + link = html_link( + "My link", + "my_view", + **{ + "id": "my_id", + "class": "my_class", + "title": "my_title", + "get": "MyGetParameter=20", + } + ) + + self.assertEqual(link, 'My link') + + def test_invalid_passed_in_keyword_argument_raises_exception(self, mock_reverse): + """ + Tests if passing-in a non-supported keyword argument raises an + exception (if parameter validation works correctly). + """ + + with self.assertRaises(TemplateSyntaxError): + html_link("My link", "my_view", invalid_keyword="some-value") + + def test_rendered_output_not_escaped(self, mock_reverse): + """ + Tests if rendered output is not double-escaped by Django. + """ + + mock_reverse.return_value = "/my/url" + + template = Template('{% load conntrackt_tags %}{% html_link "My link" "my_view" class="my_class" title="my_title" id="my_id" get="MyGetParameter=20" %}') + context = Context() + rendered_output = template.render(context) + + self.assertEqual(rendered_output, 'My link') + + def test_html_escapes_passed_in_values(self, mock_reverse): + """ + Tests if values passed-in as keyword arguments are escaped within + resulting output. + """ + + mock_reverse.return_value = "/my/url" + + link = html_link( + "My link", + "my_view", + **{ + "id": "my_id", + "class": "my_class", + "title": "my_title", + "get": "MyGetParameter=", + } + ) + + self.assertEqual(link, 'My </a> link') diff --git a/conntrackt/urls.py b/conntrackt/urls.py --- a/conntrackt/urls.py +++ b/conntrackt/urls.py @@ -20,7 +20,7 @@ # Django imports. -from django.conf.urls import patterns, url +from django.conf.urls import url from django.contrib.auth.views import login, logout # Application imports. @@ -33,8 +33,7 @@ from .views import CommunicationCreateVi from .views import SearchView, APISearchView -urlpatterns = patterns( - 'conntrackt.views', +urlpatterns = [ # Homepage/index view. url(r'^$', IndexView.as_view(), name="index"), @@ -98,4 +97,4 @@ urlpatterns = patterns( # View for getting the search results in JSON format. url(r'^api/search/(?P.*)$', APISearchView.as_view(), name="api_search"), -) +] diff --git a/conntrackt/views.py b/conntrackt/views.py --- a/conntrackt/views.py +++ b/conntrackt/views.py @@ -516,7 +516,7 @@ class EntityCreateView(RedirectToNextMix # Raise authorisation denied exception for unmet permissions. raise_exception = True - def get_form(self, form_class): + def get_form(self, form_class=EntityForm): """ Returns an instance of form that can be used by the view. @@ -643,7 +643,7 @@ class InterfaceCreateView(RedirectToNext # Raise authorisation denied exception for unmet permissions. raise_exception = True - def get_form(self, form_class): + def get_form(self, form_class=InterfaceForm): """ Returns an instance of form that can be used by the view. @@ -701,7 +701,7 @@ class InterfaceUpdateView(RedirectToNext # Raise authorisation denied exception for unmet permissions. raise_exception = True - def get_form(self, form_class): + def get_form(self, form_class=InterfaceForm): """ Returns an instance of form that can be used by the view. @@ -799,7 +799,7 @@ class CommunicationCreateView(RedirectTo # Raise authorisation denied exception for unmet permissions. raise_exception = True - def get_form(self, form_class): + def get_form(self, form_class=CommunicationForm): """ Returns an instance of form that can be used by the view. @@ -890,7 +890,7 @@ class CommunicationUpdateView(RedirectTo # Raise authorisation denied exception for unmet permissions. raise_exception = True - def get_form(self, form_class): + def get_form(self, form_class=CommunicationForm): """ Returns an instance of form that can be used by the view. diff --git a/requirements/base.in b/requirements/base.in --- a/requirements/base.in +++ b/requirements/base.in @@ -24,7 +24,7 @@ django-braces~=1.12.0 django-crispy-forms~=1.6.0 # Web framework used by application. -django~=1.8.0 +django~=1.9.0 # Library for programatic calculation of colours (contrasts, # inversions etc). diff --git a/requirements/development.txt b/requirements/development.txt --- a/requirements/development.txt +++ b/requirements/development.txt @@ -11,7 +11,7 @@ chardet==3.0.4 # via requests coverage==4.4.2 django-braces==1.12.0 django-crispy-forms==1.6.1 -django==1.8.18 +django==1.9.13 docutils==0.14 # via sphinx factory-boy==2.1.2 funcsigs==1.0.2 # via mock diff --git a/requirements/test.txt b/requirements/test.txt --- a/requirements/test.txt +++ b/requirements/test.txt @@ -11,7 +11,7 @@ chardet==3.0.4 # via requests coverage==4.4.2 django-braces==1.12.0 django-crispy-forms==1.6.1 -django==1.8.18 +django==1.9.13 docutils==0.14 # via sphinx factory-boy==2.1.2 funcsigs==1.0.2 # via mock diff --git a/setup.py b/setup.py --- a/setup.py +++ b/setup.py @@ -26,7 +26,7 @@ README = open(os.path.join(os.path.dirna INSTALL_REQUIREMENTS = [ "django-braces~=1.12.0", "django-crispy-forms~=1.6.0", - "django~=1.8.0", + "django~=1.9.0", "palette~=0.2.0", "pydot~=1.2.0", ] diff --git a/testproject/testproject/settings.py b/testproject/testproject/settings.py --- a/testproject/testproject/settings.py +++ b/testproject/testproject/settings.py @@ -22,19 +22,20 @@ Django settings for testproject project. For more information on this file, see -https://docs.djangoproject.com/en/1.7/topics/settings/ +https://docs.djangoproject.com/en/1.9/topics/settings/ For the full list of settings and their values, see -https://docs.djangoproject.com/en/1.7/ref/settings/ +https://docs.djangoproject.com/en/1.9/ref/settings/ """ +import os + # Build paths inside the project like this: os.path.join(BASE_DIR, ...) -import os -BASE_DIR = os.path.dirname(os.path.dirname(__file__)) +BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) # Quick-start development settings - unsuitable for production -# See https://docs.djangoproject.com/en/1.7/howto/deployment/checklist/ +# See https://docs.djangoproject.com/en/1.9/howto/deployment/checklist/ # SECURITY WARNING: keep the secret key used in production secret! SECRET_KEY = '%s-x^wskhxu#5%o)0ck71g7o@7p18has!9_#(h(f@j@$97pcaw' @@ -42,14 +43,12 @@ SECRET_KEY = '%s-x^wskhxu#5%o)0ck71g7o@7 # SECURITY WARNING: don't run with debug turned on in production! DEBUG = True -TEMPLATE_DEBUG = True - ALLOWED_HOSTS = [] # Application definition -INSTALLED_APPS = ( +INSTALLED_APPS = [ 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', @@ -62,9 +61,10 @@ INSTALLED_APPS = ( 'braces', # Better forms, including styling functions. 'crispy_forms', -) +] -MIDDLEWARE_CLASSES = ( +MIDDLEWARE_CLASSES = [ + 'django.middleware.security.SecurityMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', 'django.middleware.common.CommonMiddleware', 'django.middleware.csrf.CsrfViewMiddleware', @@ -72,15 +72,31 @@ MIDDLEWARE_CLASSES = ( 'django.contrib.auth.middleware.SessionAuthenticationMiddleware', 'django.contrib.messages.middleware.MessageMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware', -) +] ROOT_URLCONF = 'testproject.urls' +TEMPLATES = [ + { + 'BACKEND': 'django.template.backends.django.DjangoTemplates', + 'DIRS': [], + 'APP_DIRS': True, + 'OPTIONS': { + 'context_processors': [ + 'django.template.context_processors.debug', + 'django.template.context_processors.request', + 'django.contrib.auth.context_processors.auth', + 'django.contrib.messages.context_processors.messages', + ], + }, + }, +] + WSGI_APPLICATION = 'testproject.wsgi.application' # Database -# https://docs.djangoproject.com/en/1.7/ref/settings/#databases +# https://docs.djangoproject.com/en/1.9/ref/settings/#databases DATABASES = { 'default': { @@ -89,8 +105,15 @@ DATABASES = { } } + +# Password validation +# https://docs.djangoproject.com/en/1.9/ref/settings/#auth-password-validators +# Keep empty for test project to make life a bit easier. +AUTH_PASSWORD_VALIDATORS = [] + + # Internationalization -# https://docs.djangoproject.com/en/1.7/topics/i18n/ +# https://docs.djangoproject.com/en/1.9/topics/i18n/ LANGUAGE_CODE = 'en-us' @@ -104,7 +127,7 @@ USE_TZ = True # Static files (CSS, JavaScript, Images) -# https://docs.djangoproject.com/en/1.7/howto/static-files/ +# https://docs.djangoproject.com/en/1.9/howto/static-files/ STATIC_URL = '/static/' diff --git a/testproject/testproject/urls.py b/testproject/testproject/urls.py --- a/testproject/testproject/urls.py +++ b/testproject/testproject/urls.py @@ -19,19 +19,30 @@ # +"""testproject URL Configuration + +The `urlpatterns` list routes URLs to views. For more information please see: + https://docs.djangoproject.com/en/1.9/topics/http/urls/ +Examples: +Function views + 1. Add an import: from my_app import views + 2. Add a URL to urlpatterns: url(r'^$', views.home, name='home') +Class-based views + 1. Add an import: from other_app.views import Home + 2. Add a URL to urlpatterns: url(r'^$', Home.as_view(), name='home') +Including another URLconf + 1. Import the include() function: from django.conf.urls import url, include + 2. Add a URL to urlpatterns: url(r'^blog/', include('blog.urls')) +""" + # Django imports -from django.conf.urls import patterns, include, url +from django.conf.urls import include, url from django.contrib import admin from django.http import HttpResponseRedirect -urlpatterns = patterns( - '', - # Examples: - # url(r'^$', 'testproject.views.home', name='home'), - # url(r'^blog/', include('blog.urls')), - +urlpatterns = [ url(r'^$', lambda r: HttpResponseRedirect('conntrackt/')), url(r'^conntrackt/', include('conntrackt.urls')), - url(r'^admin/', include(admin.site.urls)), -) + url(r'^admin/', admin.site.urls), +] diff --git a/testproject/testproject/wsgi.py b/testproject/testproject/wsgi.py --- a/testproject/testproject/wsgi.py +++ b/testproject/testproject/wsgi.py @@ -24,11 +24,13 @@ WSGI config for testproject project. It exposes the WSGI callable as a module-level variable named ``application``. For more information on this file, see -https://docs.djangoproject.com/en/1.7/howto/deployment/wsgi/ +https://docs.djangoproject.com/en/1.9/howto/deployment/wsgi/ """ import os + +from django.core.wsgi import get_wsgi_application + os.environ.setdefault("DJANGO_SETTINGS_MODULE", "testproject.settings") -from django.core.wsgi import get_wsgi_application application = get_wsgi_application()