Changeset - 3d702d4d7154
[Not reviewed]
default
0 10 1
Branko Majic (branko) - 6 years ago 2017-12-21 17:59:32
branko@majic.rs
CONNT-25: Updating application and project to use Django 1.9.x:

- Updated the html_link tag implementation to produce safe output, and
added tests for it.
- Updated URL configuration in both the application and test project
according to deprecation warning.
- Updated views that return customized forms to ensure the form_class
passed in to get_form is optional.
- Bumped Django in both setup script and development requirements to
version 1.9.x.
- Updated test project configuration to match the version produced by
Django 1.9.x admin commands.
11 files changed with 283 insertions and 50 deletions:
0 comments (0 inline, 0 general)
conntrackt/templatetags/conntrackt_tags.py
Show inline comments
 
@@ -19,12 +19,13 @@
 
#
 

	
 

	
 
# 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.
 
register = template.Library()
 

	
 

	
 
@@ -51,32 +52,40 @@ def html_link(text, view, *args, **kwarg
 
        title - Title for the HTML <a> element.
 

	
 
        get - Additional GET parameter that should be appended to the URL.
 

	
 
    """
 

	
 
    # 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 = '<a href="%s" %s>%s</a>'
 
    if 'get' in kwargs:
 
        pattern = '<a href="{url}?{get}"'
 
    else:
 
        pattern = '<a href="{url}"'
 

	
 
    if 'class' in kwargs:
 
        pattern += ' class="{class}"'
 

	
 
    # Iterate over keyword arguments, and if they're supported, add them to
 
    # parameters.
 
    params = ""
 
    for key, value in kwargs.iteritems():
 
        if key in ("class", "title", "id"):
 
            params += '%s="%s" ' % (key, value)
 
        elif key == "get":
 
            url += "?%s" % value
 
        else:
 
            raise template.TemplateSyntaxError("Unknown argument for 'advhtml_link' tag: %r" % key)
 
    if 'title' in kwargs:
 
        pattern += ' title="{title}"'
 

	
 
    if 'id' in kwargs:
 
        pattern += ' id="{id}"'
 

	
 
    pattern += '>{text}</a>'
 

	
 
    # Render the tag.
 
    return pattern % (url, params, text)
 
    return format_html(pattern, url=url, text=text, **kwargs)
 

	
 

	
 
@register.simple_tag(takes_context=True)
 
def active_link(context, url_name, return_value='active', **kwargs):
 
    """
 
    This template tag can be used to check if the provided URL matches against
conntrackt/tests/test_tags.py
Show inline comments
 
new file 100644
 
# -*- coding: utf-8 -*-
 
#
 
# Copyright (C) 2017 Branko Majic
 
#
 
# This file is part of Django Conntrackt.
 
#
 
# Django Conntrackt is free software: you can redistribute it and/or modify it
 
# under the terms of the GNU General Public License as published by the Free
 
# Software Foundation, either version 3 of the License, or (at your option) any
 
# later version.
 
#
 
# Django Conntrackt is distributed in the hope that it will be useful, but
 
# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 
# FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more
 
# details.
 
#
 
# You should have received a copy of the GNU General Public License along with
 
# Django Conntrackt.  If not, see <http://www.gnu.org/licenses/>.
 
#
 

	
 

	
 
# 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, '<a href="/my/url?MyGetParameter=20" class="my_class" title="my_title" id="my_id">My link</a>')
 

	
 
    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, '<a href="/my/url?MyGetParameter=20" class="my_class" title="my_title" id="my_id">My link</a>')
 

	
 
    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 </a> link",
 
            "my_view",
 
            **{
 
                "id": "my</a>_id",
 
                "class": "my</a>_class",
 
                "title": "my</a>_title",
 
                "get": "MyGetParameter=</a>",
 
            }
 
        )
 

	
 
        self.assertEqual(link, '<a href="/my/url?MyGetParameter=&lt;/a&gt;" class="my&lt;/a&gt;_class" title="my&lt;/a&gt;_title" id="my&lt;/a&gt;_id">My &lt;/a&gt; link</a>')
conntrackt/urls.py
Show inline comments
 
@@ -17,27 +17,26 @@
 
# You should have received a copy of the GNU General Public License along with
 
# Django Conntrackt.  If not, see <http://www.gnu.org/licenses/>.
 
#
 

	
 

	
 
# 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.
 
from .views import IndexView, EntityView, entity_iptables, project_iptables, project_diagram
 
from .views import ProjectView, ProjectCreateView, ProjectUpdateView, ProjectDeleteView
 
from .views import LocationCreateView, LocationUpdateView, LocationDeleteView
 
from .views import EntityCreateView, EntityUpdateView, EntityDeleteView
 
from .views import InterfaceCreateView, InterfaceUpdateView, InterfaceDeleteView
 
from .views import CommunicationCreateView, CommunicationUpdateView, CommunicationDeleteView
 
from .views import SearchView, APISearchView
 

	
 

	
 
urlpatterns = patterns(
 
    'conntrackt.views',
 
urlpatterns = [
 
    # Homepage/index view.
 
    url(r'^$', IndexView.as_view(), name="index"),
 

	
 
    # View for showing information about a project.
 
    url(r'^project/(?P<pk>\d+)/$', ProjectView.as_view(),
 
        name='project'),
 
@@ -95,7 +94,7 @@ urlpatterns = patterns(
 

	
 
    # View for displaying the search page.
 
    url(r'^search/$', SearchView.as_view(), name="search"),
 

	
 
    # View for getting the search results in JSON format.
 
    url(r'^api/search/(?P<search_term>.*)$', APISearchView.as_view(), name="api_search"),
 
)
 
]
conntrackt/views.py
Show inline comments
 
@@ -513,13 +513,13 @@ class EntityCreateView(RedirectToNextMix
 
        "all": ("conntrackt.add_entity",),
 
        }
 

	
 
    # 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.
 

	
 
        The method will limit the project or location select inputs if request
 
        contained this information.
 
        """
 
@@ -640,13 +640,13 @@ class InterfaceCreateView(RedirectToNext
 
        "all": ("conntrackt.add_interface",),
 
        }
 

	
 
    # 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.
 

	
 
        The method will limit the entity select input if request contained this
 
        information.
 
        """
 
@@ -698,13 +698,13 @@ class InterfaceUpdateView(RedirectToNext
 
        "all": ("conntrackt.change_interface",),
 
        }
 

	
 
    # 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.
 

	
 
        The method will limit the entities that can be selected for the
 
        interface to the ones that belong to the same project as the currently
 
        set entity.
 
@@ -796,13 +796,13 @@ class CommunicationCreateView(RedirectTo
 
        "all": ("conntrackt.add_communication",),
 
        }
 

	
 
    # 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.
 

	
 
        The method will limit the source and destination interface selection to
 
        interfaces belonging to the same project as provided entity ID (if any
 
        was provided).
 
@@ -887,13 +887,13 @@ class CommunicationUpdateView(RedirectTo
 
        "all": ("conntrackt.change_communication",),
 
        }
 

	
 
    # 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.
 

	
 
        The method will limit the source and destination interfaces that can be
 
        selected for the communication. Both will be limited to interfaces
 
        coming from entities that belong to the same project as current
requirements/base.in
Show inline comments
 
@@ -21,13 +21,13 @@
 
django-braces~=1.12.0
 

	
 
# Convenience tools for controlling rendering of forms while embracing DRY.
 
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).
 
palette~=0.2.0
 

	
 
# Interaface towards Graphviz for chart generation.
requirements/development.txt
Show inline comments
 
@@ -8,13 +8,13 @@ alabaster==0.7.10         # via sphinx
 
babel==2.5.1              # via sphinx
 
certifi==2017.11.5        # via requests
 
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
 
idna==2.6                 # via requests
 
imagesize==0.7.1          # via sphinx
 
jinja2==2.10              # via sphinx
requirements/test.txt
Show inline comments
 
@@ -8,13 +8,13 @@ alabaster==0.7.10         # via sphinx
 
babel==2.5.1              # via sphinx
 
certifi==2017.11.5        # via requests
 
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
 
idna==2.6                 # via requests
 
imagesize==0.7.1          # via sphinx
 
jinja2==2.10              # via sphinx
setup.py
Show inline comments
 
@@ -23,13 +23,13 @@ import os
 
from setuptools import setup, find_packages
 

	
 
README = open(os.path.join(os.path.dirname(__file__), 'README.rst')).read()
 
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",
 
]
 

	
 
# allow setup.py to be run from any path
 
os.chdir(os.path.normpath(os.path.join(os.path.abspath(__file__), os.pardir)))
testproject/testproject/settings.py
Show inline comments
 
@@ -19,81 +19,104 @@
 
#
 

	
 
"""
 
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'
 

	
 
# 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',
 
    'django.contrib.sessions',
 
    'django.contrib.messages',
 
    'django.contrib.staticfiles',
 
     # Enable the conntrackt application
 
     'conntrackt',
 
     # Generic mixins for Django.
 
     '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',
 
    'django.contrib.auth.middleware.AuthenticationMiddleware',
 
    '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': {
 
        'ENGINE': 'django.db.backends.sqlite3',
 
        'NAME': os.path.join(BASE_DIR, 'testproject.db'),
 
    }
 
}
 

	
 

	
 
# 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'
 

	
 
TIME_ZONE = 'Europe/Stockholm'
 

	
 
USE_I18N = True
 
@@ -101,13 +124,13 @@ USE_I18N = True
 
USE_L10N = True
 

	
 
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/'
 

	
 
# Extend the default TEMPLATE_CONTEXT_PROCESSORS to include the
 
# request as part of context (used throughout tests).
 
from django.conf.global_settings import TEMPLATE_CONTEXT_PROCESSORS
testproject/testproject/urls.py
Show inline comments
 
@@ -16,22 +16,33 @@
 
#
 
# You should have received a copy of the GNU General Public License along with
 
# Django Conntrackt.  If not, see <http://www.gnu.org/licenses/>.
 
#
 

	
 

	
 
"""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),
 
]
testproject/testproject/wsgi.py
Show inline comments
 
@@ -21,14 +21,16 @@
 
"""
 
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()
0 comments (0 inline, 0 general)