# Python garbage
*.pyc
.coverage
+htmlcov
pip-log.txt
nosetests.xml
build
dist
*.egg-info
+.tox
# Mac OS X garbage
.DS_Store
#!/usr/bin/env python
+# -*- coding: utf-8
+# This file is part of django-ssify, licensed under GNU Affero GPLv3 or later.
+# Copyright © Fundacja Nowoczesna Polska. See README.md for more information.
+#
+"""
+Creates a simple Django configuration and runs tests for django-ssify.
+"""
+from __future__ import unicode_literals
import sys
import os
from os.path import dirname, abspath
from optparse import OptionParser
-from django.conf import settings, global_settings
+from django.conf import settings
# For convenience configure settings if they are not pre-configured or if we
# haven't been provided settings to use by environment variable.
'ssify',
'tests',
],
+ LANGUAGE_CODE='pl',
MEDIA_URL='/media/',
MIDDLEWARE_CLASSES=[
+ 'django.middleware.csrf.CsrfViewMiddleware',
'ssify.middleware.SsiMiddleware',
'django.middleware.cache.UpdateCacheMiddleware',
'ssify.middleware.PrepareForCacheMiddleware',
'django.middleware.common.CommonMiddleware',
+ 'django.contrib.sessions.middleware.SessionMiddleware',
+ 'ssify.middleware.LocaleMiddleware',
'django.middleware.cache.FetchFromCacheMiddleware',
],
STATIC_URL='/static/',
),
)
-from django.test.simple import DjangoTestSuiteRunner
+try:
+ from django.test.runner import DiscoverRunner
+except ImportError:
+ # Django < 1.6
+ from django.test.simple import DjangoTestSuiteRunner as DiscoverRunner
def runtests(*test_args, **kwargs):
- if 'south' in settings.INSTALLED_APPS:
- from south.management.commands import patch_for_test_db_setup
- patch_for_test_db_setup()
-
+ """Actual test suite entry point."""
if not test_args:
test_args = ['tests']
parent = dirname(abspath(__file__))
sys.path.insert(0, parent)
- test_runner = DjangoTestSuiteRunner(
+
+ # For Django 1.7+
+ try:
+ from django import setup
+ except ImportError:
+ pass
+ else:
+ setup()
+
+ test_runner = DiscoverRunner(
verbosity=kwargs.get('verbosity', 1),
interactive=kwargs.get('interactive', False),
failfast=kwargs.get('failfast'))
#!/usr/bin/env python
# -*- coding: utf-8 -*-
+# This file is part of django-ssify, licensed under GNU Affero GPLv3 or later.
+# Copyright © Fundacja Nowoczesna Polska. See README.md for more information.
#
from setuptools import setup, find_packages
license='LICENSE',
description='Two-phased rendering using SSI.',
long_description=open('README.md').read(),
+ install_requires=[
+ 'Django>=1.4',
+ ],
test_suite="runtests.runtests",
classifiers=[
"Development Status :: 3 - Alpha",
"Intended Audience :: System Administrators",
"License :: OSI Approved :: GNU Affero General Public License v3",
"Programming Language :: Python",
+ "Programming Language :: Python :: 2",
+ "Programming Language :: Python :: 2.6",
+ "Programming Language :: Python :: 2.7",
+ "Programming Language :: Python :: 3",
+ "Programming Language :: Python :: 3.2",
+ "Programming Language :: Python :: 3.3",
+ "Programming Language :: Python :: 3.4",
"Topic :: Internet :: WWW/HTTP",
"Topic :: Software Development :: Code Generators",
]
+# -*- coding: utf-8 -*-
+# This file is part of django-ssify, licensed under GNU Affero GPLv3 or later.
+# Copyright © Fundacja Nowoczesna Polska. See README.md for more information.
+#
"""
Implements two-phase rendering using SSI statements.
with `ssi_included` decorator.
"""
+from __future__ import unicode_literals
__version__ = '1.0'
__date__ = '2014-08-26'
SETTING = lazy(
lambda name, default: getattr(settings, name, default),
- bool, int, list, tuple, unicode)
+ bool, int, list, tuple, str)
INCLUDES_CACHES = SETTING('SSIFY_INCLUDES_CACHES', ('ssify',))
DEBUG = SETTING('SSIFY_DEBUG', False)
+# -*- coding: utf-8 -*-
+# This file is part of django-ssify, licensed under GNU Affero GPLv3 or later.
+# Copyright © Fundacja Nowoczesna Polska. See README.md for more information.
+#
+from __future__ import unicode_literals
import os
from django.core.cache.backends.filebased import FileBasedCache
+# -*- coding: utf-8 -*-
+# This file is part of django-ssify, licensed under GNU Affero GPLv3 or later.
+# Copyright © Fundacja Nowoczesna Polska. See README.md for more information.
+#
+"""
+Defines decorators for use in ssify-enabled projects.
+"""
+from __future__ import unicode_literals
import functools
from inspect import getargspec
import warnings
raise exceptions.NoLangFieldError(request)
current_lang = get_language()
activate(lang)
+ request.LANGUAGE_CODE = lang
response = view(request, *args, **kwargs)
if use_lang:
activate(current_lang)
if response.status_code == 200:
+ # We don't want this view to be cached in
+ # UpdateCacheMiddleware. We'll just cache the contents
+ # ourselves, and point the webserver to use this cache.
request._cache_update_cache = False
def _check_included_vars(response):
if get_ssi_vars:
# Remove the ssi vars that should be provided
# by the including view.
- pass_vars = set(get_ssi_vars(*args, **kwargs))
+ pass_vars = get_ssi_vars(*args, **kwargs)
for var in pass_vars:
if not isinstance(var, SsiVariable):
def ssi_variable(register, vary=None, name=None):
+ """
+ Creates a template tag representing an SSI variable from a function.
+
+ The function must take 'request' as its first argument.
+ It may take other arguments, which should be provided when using
+ the template tag.
+
+ """
# Cache control?
def dec(func):
+
# Find own path.
function_name = (name or
getattr(func, '_decorated_function', func).__name__)
-class SsifyError(BaseException):
+# -*- coding: utf-8 -*-
+# This file is part of django-ssify, licensed under GNU Affero GPLv3 or later.
+# Copyright © Fundacja Nowoczesna Polska. See README.md for more information.
+#
+"""
+Exception classes used in django-ssify.
+"""
+from __future__ import unicode_literals
+from django.utils.encoding import python_2_unicode_compatible
+
+
+class RequestMixin(object):
+ """Lets us print request and view data in the exceptions messages."""
+
+ def __init__(self, request, *args):
+ self.request = request
+ super(RequestMixin, self).__init__(*args)
+
+ def view_path(self):
+ """Returns full Python path to the view used in the request."""
+ try:
+ view = self.request.resolver_match.func
+ return "%s.%s" % (view.__module__, view.__name__)
+ except AttributeError:
+ return "<unknown>"
+
+
+class SsifyError(RequestMixin, BaseException):
+ """Base class for all the errors."""
pass
-class SsifyWarning(Warning):
+class SsifyWarning(RequestMixin, Warning):
+ """Base class for all the warnings."""
pass
+@python_2_unicode_compatible
class UndeclaredSsiVarsError(SsifyError):
+ """An ssi_included view used a SSI variable, but didn't declare it."""
+
def __init__(self, request, ssi_vars):
super(UndeclaredSsiVarsError, self).__init__(request, ssi_vars)
def __str__(self):
- request = self.args[0]
- view = request.resolver_match.func
- return "The view '%s.%s' at '%s' is marked as `ssi_included`, "\
+ return "The view '%s' at '%s' is marked as `ssi_included`, "\
"but it uses ssi variables not declared in `get_ssi_vars` "\
"argument: %s. " % (
- view.__module__, view.__name__, request.path,
- repr(self.args[1]))
+ self.view_path(), self.request.get_full_path(),
+ repr(self.args[0]))
+@python_2_unicode_compatible
class UnusedSsiVarsWarning(SsifyWarning):
+ """An ssi_included declared a SSI variable, but didn't use it."""
+
def __init__(self, request, ssi_vars):
super(UnusedSsiVarsWarning, self).__init__(request, ssi_vars)
def __str__(self):
- request = self.args[0]
- view = request.resolver_match.func
- return "The `ssi_included` view '%s.%s' at '%s' declares "\
+ return "The `ssi_included` view '%s' at '%s' declares "\
"using SSI variables %s but it looks like they're not "\
"really used. " % (
- view.__module__, view.__name__, request.path, self.args[1])
-
-
-class UndeclaredSsiRefError(SsifyError):
- def __init__(self, request, var, ref_name):
- super(UndeclaredSsiRefError, self).__init__(request, var, ref_name)
-
- def __str__(self):
- request = self.args[0]
- view = request.resolver_match.func
- return "Error while rendering ssi_included view '%s.%s' at '%s': "\
- "SSI variable %s references variable %s, which doesn't match "\
- "any variable declared in `get_ssi_vars`. " % (
- view.__module__, view.__name__, request.path,
- repr(self.args[1]), self.args[2])
+ self.view_path(), self.request.get_full_path(),
+ self.args[0])
+@python_2_unicode_compatible
class NoLangFieldError(SsifyError):
+ """ssi_included views should have a `lang` field in their URL patterns."""
+
def __init__(self, request):
super(NoLangFieldError, self).__init__(request)
def __str__(self):
- request = self.args[0]
- view = request.resolver_match.func
- return "The view '%s.%s' at '%s' is marked as `ssi_included` "\
+ return "The view '%s' at '%s' is marked as `ssi_included` "\
"with use_lang=True, but its URL match doesn't provide "\
"a 'lang' keyword argument for language. " % (
- view.__module__, view.__name__, request.path)
+ self.view_path(), self.request.get_full_path())
+@python_2_unicode_compatible
class SsiVarsDependencyCycleError(SsifyError):
- def __init__(self, ssi_vars):
- super(SsiVarsDependencyCycleError, self).__init__(ssi_vars)
+ """Looks like there's a dependency cycle in the SSI variables.
+
+ Yet to find an example of a configuration that triggers that.
+ """
+
+ def __init__(self, request, ssi_vars, resolved):
+ super(SsiVarsDependencyCycleError, self).__init__(
+ request, ssi_vars, resolved)
def __str__(self):
- return "Dependency cycle in SSI variables: %s." % self.args[0]
+ return "The view '%s' at '%s' has dependency cycle. "\
+ "Unresolved SSI variables:\n%s\n\n"\
+ "Resolved SSI variables:\n%s." % (
+ self.view_path(), self.request.get_full_path(),
+ self.args[0], self.args[1])
# -*- coding: utf-8 -*-
-# This file is part of Wolnelektury, licensed under GNU Affero GPLv3 or later.
-# Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
+# This file is part of django-ssify, licensed under GNU Affero GPLv3 or later.
+# Copyright © Fundacja Nowoczesna Polska. See README.md for more information.
#
+from __future__ import unicode_literals
from optparse import make_option
from django.core.management.base import BaseCommand
+# -*- coding: utf-8 -*-
+# This file is part of django-ssify, licensed under GNU Affero GPLv3 or later.
+# Copyright © Fundacja Nowoczesna Polska. See README.md for more information.
+#
+"""
+Middleware classes provide by django-ssify.
+
+The main middleware you should use is SsiMiddleware. It's responsible
+for providing the SSI variables needed for the SSI part of rendering.
+
+If you're using django's UpdateCacheMiddleware, add
+PrepareForCacheMiddleware *after it* also. It will add all the data
+needed by SsiMiddleware to the response.
+
+If you're using SessionMiddleware with LocaleMiddleware and your
+USE_I18N or USE_L10N is True, you should also use the provided
+LocaleMiddleware instead of the stock one.
+
+And, last but not least, if using CsrfViewMiddleware, move it to the
+top of MIDDLEWARE_CLASSES, even before SsiMiddleware, and use
+`csrf_token` from `ssify` tags library in your templates, this way
+your CSRF tokens will be set correctly.
+
+So, you should end up with something like this:
+
+ MIDDLEWARE_CLASSES = [
+ 'django.middleware.csrf.CsrfViewMiddleware',
+ 'ssify.middleware.SsiMiddleware',
+ 'django.middleware.cache.UpdateCacheMiddleware',
+ 'ssify.middleware.PrepareForCacheMiddleware',
+ ...
+ 'ssify.middleware.LocaleMiddleware',
+ ...
+ ]
+
+
+"""
+from __future__ import unicode_literals
from django.conf import settings
from django.utils.cache import patch_vary_headers
from django.middleware import locale
class PrepareForCacheMiddleware(object):
+ """
+ Patches the response object with all the data SsiMiddleware needs.
+
+ This should go after UpdateCacheMiddleware in MIDDLEWARE_CLASSES.
+ """
@staticmethod
def process_response(request, response):
+ """Adds a 'X-Ssi-Vars-Needed' header to the response."""
if getattr(request, 'ssi_vars_needed', None):
- vars_needed = {k: v.definition
- for (k, v) in request.ssi_vars_needed.items()}
+ vars_needed = {}
+ for (k, v) in request.ssi_vars_needed.items():
+ vars_needed[k] = v.definition
response['X-Ssi-Vars-Needed'] = json_encode(
vars_needed, sort_keys=True)
return response
class SsiMiddleware(object):
+ """
+ The main django-ssify middleware.
+
+ It prepends the response content with SSI set statements,
+ providing values for any SSI variables used in the templates.
+
+ It also patches the Vary header with the values given by
+ the SSI variables.
+
+ If SSIFY_DEBUG is set, it also passes the response through
+ DebugUnSsiMiddleware, which interprets and renders the SSI
+ statements, so you can see the output without an actual
+ SSI-enabled webserver.
+
+ """
def process_request(self, request):
request.ssi_vary = set()
#request.ssi_cache_control_after = set()
vars_needed = request.ssi_vars_needed
else:
vars_needed = json_decode(response.get('X-Ssi-Vars-Needed', '{}'))
- vars_needed = {k: SsiVariable(*v)
- for (k, v) in vars_needed.items()}
+ for k, v in vars_needed.items():
+ vars_needed[k] = SsiVariable(*v)
if vars_needed:
response.content = provide_vars(request, vars_needed) + \
patch_vary_headers(response, sorted(request.ssi_vary))
# TODO: cache control?
- # With a cached response, CsrfViewMiddleware.process_response
- # was never called, so if we used the csrf token, we must do
- # its job of setting the csrf token cookie on our own.
- if (not getattr(request, 'csrf_processing_done', False)
- and request.META.get("CSRF_COOKIE_USED", False)):
- response.set_cookie(settings.CSRF_COOKIE_NAME,
- request.META["CSRF_COOKIE"],
- max_age=getattr(settings, 'CSRF_COOKIE_AGE',
- 60 * 60 * 24 * 7 * 52),
- domain=settings.CSRF_COOKIE_DOMAIN,
- path=settings.CSRF_COOKIE_PATH,
- secure=settings.CSRF_COOKIE_SECURE,
- httponly=settings.CSRF_COOKIE_HTTPONLY
- )
- request.csrf_processing_done = True
-
def process_response(self, request, response):
if hasattr(response, 'render') and callable(response.render):
response.add_post_render_callback(
+# -*- coding: utf-8 -*-
+# This file is part of django-ssify, licensed under GNU Affero GPLv3 or later.
+# Copyright © Fundacja Nowoczesna Polska. See README.md for more information.
+#
"""
This module should only be used for debugging SSI statements.
support as a proxy (i.e. Nginx with ssi=on).
"""
+from __future__ import unicode_literals
import re
-import urlparse
+try:
+ from urllib.parse import urlparse
+except ImportError:
+ from urlparse import urlparse
from django.core.urlresolvers import resolve
from ssify import DEBUG_VERBOSE
"""Replaces SSI include with contents rendered by relevant view."""
path = process_value(match.group('path'))
func, args, kwargs = resolve(path)
- parsed = urlparse.urlparse(path)
+ parsed = urlparse(path)
+
+ # Reuse the original request, but reset some attributes.
request.META['PATH_INFO'] = request.path_info = \
request.path = parsed.path
request.META['QUERY_STRING'] = parsed.query
- content = func(request, *args, **kwargs).content
+ request.ssi_vars_needed = {}
+
+ content = func(request, *args, **kwargs).content.decode('ascii')
content = process_content(content)
if DEBUG_VERBOSE:
return "".join((
return content
variables = {}
- response.content = process_content(response.content)
+ response.content = process_content(
+ response.content.decode('ascii')).encode('ascii')
response['Content-Length'] = len(response.content)
def process_response(self, request, response):
+# -*- coding: utf-8 -*-
+# This file is part of django-ssify, licensed under GNU Affero GPLv3 or later.
+# Copyright © Fundacja Nowoczesna Polska. See README.md for more information.
+#
+from __future__ import unicode_literals
import json
from .variables import SsiVariable, SsiExpect
def _json_default(o):
if isinstance(o, SsiVariable):
- return {'__var__': o.name}
+ return {'__var__': o.definition}
if isinstance(o, SsiExpect):
return {'__expect__': o.name}
raise TypeError(o, 'not JSON serializable')
def _json_obj_hook(obj):
- if obj.keys() == ['__var__']:
- return SsiVariable(name=obj['__var__'])
- if obj.keys() == ['__expect__']:
+ keys = list(obj.keys())
+ if keys == ['__var__']:
+ return SsiVariable(*obj['__var__'])
+ if keys == ['__expect__']:
return SsiExpect(obj['__expect__'])
return obj
-from django.utils.cache import get_cache
+# -*- coding: utf-8 -*-
+# This file is part of django-ssify, licensed under GNU Affero GPLv3 or later.
+# Copyright © Fundacja Nowoczesna Polska. See README.md for more information.
+#
+from __future__ import unicode_literals
+from django.core.cache import get_cache
from ssify import INCLUDES_CACHES
# -*- coding: utf-8 -*-
-# This file is part of Wolnelektury, licensed under GNU Affero GPLv3 or later.
-# Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
+# This file is part of django-ssify, licensed under GNU Affero GPLv3 or later.
+# Copyright © Fundacja Nowoczesna Polska. See README.md for more information.
#
-from __future__ import absolute_import
+from __future__ import absolute_import, unicode_literals
from django.conf import settings
from django.core.urlresolvers import NoReverseMatch, reverse, resolve
-from django.middleware.csrf import get_token, rotate_token, _sanitize_token
+from django.middleware.csrf import get_token, _sanitize_token
from django import template
from django.utils.translation import get_language
from ssify.decorators import ssi_variable
from ssify.variables import SsiVariable
+try:
+ from django.middleware.csrf import rotate_token
+except ImportError:
+ from django.middleware.csrf import _get_new_csrf_key
+
+ # Missing in Django 1.4
+ def rotate_token(request):
+ request.META.update({
+ "CSRF_COOKIE_USED": True,
+ "CSRF_COOKIE": _get_new_csrf_key(),
+
+ })
+
register = template.Library()
@ssi_variable(register, vary=('Cookie',))
def get_csrf_token(request):
"""
- As CsrfViewMiddleware.process_view is never for a cached response,
- and we still need to provide a request-specific CSRF token as
- request-info ssi variable, we must make sure here that the
- CSRF token is in request.META['CSRF_COOKIE'].
+ CsrfViewMiddleware.process_view is never called for cached
+ responses, and we still need to provide a CSRF token as an
+ ssi variable, we must make sure here that the CSRF token
+ is in request.META['CSRF_COOKIE'].
"""
token = get_token(request)
+# -*- coding: utf-8 -*-
+# This file is part of django-ssify, licensed under GNU Affero GPLv3 or later.
+# Copyright © Fundacja Nowoczesna Polska. See README.md for more information.
+#
"""
Utilities for defining SSI variables.
at request time to the prerendered templates.
"""
+from __future__ import unicode_literals
from hashlib import md5
from django import template
-from django.utils.encoding import force_unicode
+from django.utils.encoding import force_text, python_2_unicode_compatible
from django.utils.functional import Promise
from django.utils.safestring import mark_safe
-from .exceptions import SsiVarsDependencyCycleError, UndeclaredSsiRefError
+from .exceptions import SsiVarsDependencyCycleError
+@python_2_unicode_compatible
class SsiVariable(object):
"""
Represents a variable computed by a template tag with given arguments.
so the user never has to deal with it directly.
"""
- ret_types = 'bool', 'int', 'unicode'
-
def __init__(self, tagpath=None, args=None, kwargs=None, name=None):
self.tagpath = tagpath
self.args = list(args or [])
def name(self):
"""Variable name is a hash of its definition."""
if self._name is None:
- self._name = 'v' + md5(json_encode(self.definition)).hexdigest()
+ self._name = 'v' + md5(json_encode(self.definition).encode('ascii')).hexdigest()
return self._name
def rehash(self):
return template.get_library(taglib).tags[tagname].get_value(
request, *self.args, **self.kwargs)
- def __unicode__(self):
+ def __str__(self):
return mark_safe("<!--#echo var='%s' encoding='none'-->" % self.name)
def as_var(self):
"""This class says: I want the real value of this variable here."""
def __init__(self, name):
self.name = name
+ def __repr__(self):
+ return "SsiExpect(%s)" % (self.name,)
def ssi_expect(var, type_):
value = ''
return "<!--#set var='%s' value='%s'-->" % (
var,
- force_unicode(value).replace(u'\\', u'\\\\').replace(u"'", u"\\'"))
+ force_text(value).replace('\\', '\\\\').replace("'", "\\'"))
def provide_vars(request, ssi_vars):
The main purpose of this function is to by called by SsifyMiddleware.
"""
+ def resolve_expects(var):
+ if not hasattr(var, 'hash_dirty'):
+ var.hash_dirty = False
+
+ for i, arg in enumerate(var.args):
+ if isinstance(arg, SsiExpect):
+ var.args[i] = resolved[arg.name]
+ var.hash_dirty = True
+ for k, arg in var.kwargs.items():
+ if isinstance(arg, SsiExpect):
+ var.kwargs[k] = resolved[arg.name]
+ var.hash_dirty = True
+
+ for arg in var.args + list(var.kwargs.values()):
+ if isinstance(arg, SsiVariable):
+ var.hash_dirty = resolve_expects(arg) or var.hash_dirty
+
+ hash_dirty = var.hash_dirty
+ if var.hash_dirty:
+ # Rehash after calculating the SsiExpects with real
+ # values, because that's what the included views expect.
+ var.rehash()
+ var.hash_dirty = False
+
+ return hash_dirty
+
+ def resolve_args(var):
+ kwargs = {}
+ for k, arg in var.kwargs.items():
+ kwargs[k] = resolved[arg.name] if isinstance(arg, SsiVariable) else arg
+ new_var = SsiVariable(var.tagpath,
+ [resolved[arg.name] if isinstance(arg, SsiVariable) else arg for arg in var.args],
+ kwargs)
+ return new_var
+
resolved = {}
- queue = ssi_vars.items()
+ queue = list(ssi_vars.values())
+
unresolved_streak = 0
while queue:
- var_name, var = queue.pop(0)
- hash_dirty = False
- new_name = var_name
-
+ var = queue.pop(0)
try:
- for i, arg in enumerate(var.args):
- if isinstance(arg, SsiExpect):
- var.args[i] = resolved[arg.name]
- hash_dirty = True
- for k, arg in var.kwargs.items():
- if isinstance(arg, SsiExpect):
- var.args[k] = resolved[arg.name]
- hash_dirty = True
-
- if hash_dirty:
- # Rehash after calculating the SsiExpects with real
- # values, because that's what the included views expect.
- new_name = var.rehash()
-
- for i, arg in enumerate(var.args):
- if isinstance(arg, SsiVariable):
- var.args[i] = resolved[arg.name]
- for k, arg in var.kwargs.items():
- if isinstance(arg, SsiVariable):
- var.args[k] = resolved[arg.name]
-
- except KeyError:
- queue.append((var_name, var))
+ resolve_expects(var)
+ rv = resolve_args(var)
+ except KeyError as e:
+ queue.append(var)
unresolved_streak += 1
- if unresolved_streak == len(queue):
- if arg.name in ssi_vars:
- raise SsiVarsDependencyCycleError(queue)
- else:
- raise UndeclaredSsiRefError(request, var, arg.name)
+ if unresolved_streak > len(queue):
+ raise SsiVarsDependencyCycleError(request, queue, resolved)
continue
- resolved[new_name] = var.get_value(request)
+ resolved[var.name] = rv.get_value(request)
unresolved_streak = 0
- output = u"".join(ssi_set_statement(var, value)
+ output = "".join(ssi_set_statement(var, value)
for (var, value) in resolved.items()
).encode('utf-8')
return output
+++ /dev/null
-{% load ssify %}
-
-{% ssi_include 'random_quote' %}
\ No newline at end of file
+++ /dev/null
-{% load test_tags %}{% random_number 1 %}
\ No newline at end of file
+++ /dev/null
-{% load test_tags %}
-{% quote_len_odd number as odd %}
-{{ quote }}
-Line {{ number }} of {% number_of_quotes %}
-{{ odd.if }}Odd number of characters.
-{{ odd.else }}Even number of characters.
-{{ odd.endif }}
+++ /dev/null
-{% load ssify test_tags %}
-{% number_of_quotes as qlen %}
-{% random_number qlen as number %}
-{% ssi_include 'quote' number=number %}
\ No newline at end of file
--- /dev/null
+{% load ssify test_tags %}
+
+{# Django 1.4 compatibility: TemplateView sets `params` from the URL. #}
+{% if params.limit %}
+ {% random_number limit=params.limit as a %}
+{% else %}
+ {% random_number limit=limit as a %}
+{% endif %}
+{% random_number a as b %}
+{% random_number limit=b %}
\ No newline at end of file
--- /dev/null
+{% load ssify test_tags %}
+{% random_number 4 as a %}
+{% ssi_include 'args' limit=a %}
\ No newline at end of file
--- /dev/null
+{% load ssify %}
+
+{% ssi_include 'language_with_lang' lang='pl' %}
\ No newline at end of file
--- /dev/null
+{% load ssify %}
+
+{% ssi_include 'random_quote' %}
\ No newline at end of file
--- /dev/null
+{% load ssify test_tags %}
+{% random_number 1 %}
--- /dev/null
+{% load test_tags %}
+{% quote_len_odd number as odd %}
+{{ quote }}
+Line {{ number }} of {% number_of_quotes %}
+{{ odd.if }}Odd number of characters.
+{{ odd.else }}Even number of characters.
+{{ odd.endif }}
--- /dev/null
+{% load ssify test_tags %}
+{% number_of_quotes as qlen %}
+{% random_number limit=qlen as number %}
+{% ssi_include 'quote' number=number %}
\ No newline at end of file
--- /dev/null
+{% load ssify %}
+
+{% csrf_token %}
\ No newline at end of file
--- /dev/null
+{% load ssify %}
+
+{% ssi_include 'bad_language_with_lang' %}
\ No newline at end of file
--- /dev/null
+{% load ssify %}
+
+{% ssi_include 'language_with_lang' %}
\ No newline at end of file
--- /dev/null
+{% load ssify %}
+
+{% ssi_include 'language_without_lang' %}
\ No newline at end of file
+# -*- coding: utf-8 -*-
+# This file is part of django-ssify, licensed under GNU Affero GPLv3 or later.
+# Copyright © Fundacja Nowoczesna Polska. See README.md for more information.
+#
+from __future__ import unicode_literals
from django import template
from ssify import ssi_variable
from tests.views import QUOTES
+++ /dev/null
-from django.test import Client, TestCase
-from django.test.utils import override_settings
-
-
-class SsifyTestCase(TestCase):
- def setUp(self):
- self.client = Client()
-
- def test_zero(self):
- self.assertEqual(
- self.client.get('/number_zero').content,
- "<!--#set var='ve023a08d2c2075118e25b5f4339438dc' value='0'-->"
- "<!--#echo var='ve023a08d2c2075118e25b5f4339438dc' "
- "encoding='none'-->"
- )
-
- def test_single_quote(self):
- self.assertEqual(
- self.client.get('/quote/3').content.strip(),
- """Explicit is better than implicit.
-Line 3 of <!--#echo var='va50d914691ecf9b421c680d93ba1263e' encoding='none'-->
-<!--#if expr='${vddc386e120ab274a980ab67384391a1a}'-->Odd number of characters.
-<!--#else-->Even number of characters.
-<!--#endif-->"""
- )
-
- def test_random_quote(self):
- self.assertEqual(
- self.client.get('/').content.strip(),
- "<!--#set var='vda0df841702ea993b36d101460264364' value='4'-->"
- "<!--#set var='va50d914691ecf9b421c680d93ba1263e' value='22'-->"
- "<!--#set var='vafe010f2e683908fee32c48d01bb2650' value=''-->"
- "\n\n<!--#include file='/random_quote'-->"
- )
-
- # Do it again, this time from cache.
- self.assertEqual(
- self.client.get('/').content.strip(),
- "<!--#set var='vda0df841702ea993b36d101460264364' value='4'-->"
- "<!--#set var='va50d914691ecf9b421c680d93ba1263e' value='22'-->"
- "<!--#set var='vafe010f2e683908fee32c48d01bb2650' value=''-->"
- "\n\n<!--#include file='/random_quote'-->"
- )
- self.assertEqual(
- self.client.get('/random_quote').content.strip(),
- "<!--#include "
- "file='/quote/${vda0df841702ea993b36d101460264364}'-->"
- )
-
- @override_settings(SSIFY_DEBUG=True)
- def test_debug_render_random_quote(self):
- """Renders the complete view using the DebugSsiMiddleware."""
- response = self.client.get('/')
- if hasattr(response, 'render') and callable(response.render):
- response.render()
- self.assertEqual(
- response.content.strip(),
- """Simple is better than complex.
-Line 4 of 22
-Even number of characters."""
- )
--- /dev/null
+# -*- coding: utf-8 -*-
+# This file is part of django-ssify, licensed under GNU Affero GPLv3 or later.
+# Copyright © Fundacja Nowoczesna Polska. See README.md for more information.
+#
+"""
+This file works only for django.test.simple.DjangoTestSuiteRunner
+in Django<1.6. The newer django.test.runner.DiscoverRunner finds
+test_* modules by itself.
+
+"""
+from __future__ import unicode_literals
+
+from .test_args import *
+from .test_basic import *
+from .test_csrf import *
+from .test_locale import *
\ No newline at end of file
--- /dev/null
+# -*- coding: utf-8 -*-
+# This file is part of django-ssify, licensed under GNU Affero GPLv3 or later.
+# Copyright © Fundacja Nowoczesna Polska. See README.md for more information.
+#
+from __future__ import unicode_literals
+
+import re
+from django.test import TestCase
+from django.test.utils import override_settings
+from tests.tests_utils import split_ssi
+
+
+class ArgsTestCase(TestCase):
+ def test_args(self):
+ self.assertEqual(
+ sorted(split_ssi(self.client.get('/args').content)),
+ sorted([b"<!--#set var='vff80027f1d552d08d46c8b603948d85c' value='2'-->",
+ b"<!--#set var='veeb5ec4364971b409c48e36bd1428d03' value='1'-->",
+ b"<!--#set var='v05a1f9ec205c5aa84197f6b326c518a2' value='0'-->",
+ b"<!--#echo var='v05a1f9ec205c5aa84197f6b326c518a2' encoding='none'-->",
+ ])
+ )
+
+ def test_args_included(self):
+ self.assertEqual(
+ self.client.get('/args/3').content.strip(),
+ b"<!--#echo var='v05a1f9ec205c5aa84197f6b326c518a2' encoding='none'-->"
+ )
+
+ def test_include_args(self):
+ self.assertEqual(
+ sorted(split_ssi(self.client.get('/include_args').content)),
+ sorted([b"<!--#set var='vf6aba0780227af845107c046f336cc8a' value='3'-->",
+ b"<!--#set var='vff80027f1d552d08d46c8b603948d85c' value='2'-->",
+ b"<!--#set var='veeb5ec4364971b409c48e36bd1428d03' value='1'-->",
+ b"<!--#set var='v05a1f9ec205c5aa84197f6b326c518a2' value='0'-->",
+ b"<!--#include file='/args/${vf6aba0780227af845107c046f336cc8a}'-->",
+ ]),
+ )
+
+ # Test a second time, this time from cache.
+ self.assertEqual(
+ sorted(split_ssi(self.client.get('/include_args').content)),
+ sorted([b"<!--#set var='vf6aba0780227af845107c046f336cc8a' value='3'-->",
+ b"<!--#set var='vff80027f1d552d08d46c8b603948d85c' value='2'-->",
+ b"<!--#set var='veeb5ec4364971b409c48e36bd1428d03' value='1'-->",
+ b"<!--#set var='v05a1f9ec205c5aa84197f6b326c518a2' value='0'-->",
+ b"<!--#include file='/args/${vf6aba0780227af845107c046f336cc8a}'-->",
+ ]),
+ )
+
+ @override_settings(SSIFY_DEBUG=True)
+ def test_debug_render_include_args(self):
+ pass
+ """Renders the complete view using the DebugSsiMiddleware."""
+ response = self.client.get('/include_args')
+ if hasattr(response, 'render') and callable(response.render):
+ response.render()
+ self.assertEqual(
+ response.content.strip(),
+ b"""0"""
+ )
--- /dev/null
+# -*- coding: utf-8 -*-
+# This file is part of django-ssify, licensed under GNU Affero GPLv3 or later.
+# Copyright © Fundacja Nowoczesna Polska. See README.md for more information.
+#
+from __future__ import unicode_literals
+
+import re
+import warnings
+from django.test import TestCase
+from django.test.utils import override_settings
+from ssify.exceptions import UndeclaredSsiVarsError, UnusedSsiVarsWarning
+from tests.tests_utils import split_ssi
+
+
+class BasicTestCase(TestCase):
+ def test_zero(self):
+ self.assertEqual(
+ self.client.get('/number_zero').content.strip(),
+ b"<!--#set var='ve023a08d2c2075118e25b5f4339438dc' value='0'-->\n"
+ b"<!--#echo var='ve023a08d2c2075118e25b5f4339438dc' "
+ b"encoding='none'-->"
+ )
+
+ def test_basic_include(self):
+ self.assertEqual(
+ self.client.get('/basic_include').content.strip(),
+ b"<!--#include file='/language/pl'-->"
+ )
+
+ def test_single_quote(self):
+ self.assertEqual(
+ self.client.get('/quote/3').content.strip(),
+ b"""Explicit is better than implicit.
+Line 3 of <!--#echo var='va50d914691ecf9b421c680d93ba1263e' encoding='none'-->
+<!--#if expr='${vddc386e120ab274a980ab67384391a1a}'-->Odd number of characters.
+<!--#else-->Even number of characters.
+<!--#endif-->"""
+ )
+
+ def test_undeclared_vars(self):
+ self.assertRaises(UndeclaredSsiVarsError,
+ self.client.get,
+ '/quote_undeclared/3')
+
+ def test_overdeclared_vars(self):
+ with warnings.catch_warnings(record=True) as w:
+ response = self.client.get('/quote_overdeclared/3')
+ self.assertIs(w[-1].category, UnusedSsiVarsWarning)
+
+ def test_random_quote(self):
+ self.assertEqual(
+ sorted(split_ssi(self.client.get('/').content)),
+ sorted([b"<!--#set var='va50d914691ecf9b421c680d93ba1263e' value='22'-->",
+ b"<!--#set var='v3e7f638af74c9f420b6d2c5fe4dda51d' value='4'-->",
+ b"<!--#set var='vafe010f2e683908fee32c48d01bb2650' value=''-->",
+ b"<!--#include file='/random_quote'-->"])
+ )
+
+ # Do it again, this time from cache.
+ self.assertEqual(
+ sorted(split_ssi(self.client.get('/').content)),
+ sorted([b"<!--#set var='va50d914691ecf9b421c680d93ba1263e' value='22'-->",
+ b"<!--#set var='v3e7f638af74c9f420b6d2c5fe4dda51d' value='4'-->",
+ b"<!--#set var='vafe010f2e683908fee32c48d01bb2650' value=''-->",
+ b"<!--#include file='/random_quote'-->"])
+ )
+ self.assertEqual(
+ self.client.get('/random_quote').content.strip(),
+ b"<!--#include "
+ b"file='/quote/${v3e7f638af74c9f420b6d2c5fe4dda51d}'-->"
+ )
+
+ @override_settings(SSIFY_DEBUG=True)
+ def test_debug_render_random_quote(self):
+ """Renders the complete view using the DebugSsiMiddleware."""
+ response = self.client.get('/')
+ if hasattr(response, 'render') and callable(response.render):
+ response.render()
+ self.assertEqual(
+ response.content.strip(),
+ b"""Simple is better than complex.
+Line 4 of 22
+Even number of characters."""
+ )
--- /dev/null
+# -*- coding: utf-8 -*-
+# This file is part of django-ssify, licensed under GNU Affero GPLv3 or later.
+# Copyright © Fundacja Nowoczesna Polska. See README.md for more information.
+#
+from __future__ import unicode_literals
+
+from django.conf import settings
+from django.test import Client, TestCase
+
+
+class CsrfTestCase(TestCase):
+ def setUp(self):
+ self.client = Client(enforce_csrf_checks=True)
+
+ def assertCsrfTokenOk(self, response):
+ token = response.cookies[settings.CSRF_COOKIE_NAME].value
+ self.assertTrue(token)
+ self.assertEqual(
+ response.content.strip(),
+ ("<!--#set var='vd07f6920655622adc90dd591c545bb2a' value='%s'-->\n\n"
+ "<input type='hidden' name='csrfmiddlewaretoken' value='"
+ "<!--#echo var='vd07f6920655622adc90dd591c545bb2a' "
+ "encoding='none'-->' />" % token).encode('ascii')
+ )
+ return token
+
+ def test_csrf_token(self):
+ response = self.client.get('/csrf')
+ token = self.assertCsrfTokenOk(response)
+
+ # And now for a second request, with the token cookie.
+ response = self.client.get('/csrf')
+ new_token = self.assertCsrfTokenOk(response)
+ self.assertEqual(new_token, token)
+
+ # Make a bad request to see that CSRF protection works.
+ response = self.client.post('/csrf_check', {
+ 'test': 'some data',
+ })
+ self.assertEqual(response.status_code, 403)
+
+ # Make a good request.
+ response = self.client.post('/csrf_check', {
+ 'test': 'some data',
+ 'csrfmiddlewaretoken': token,
+ })
+ self.assertEqual(response.status_code, 200)
+ self.assertEqual(response.content, b'some data')
+
+ def test_new_csrf_token_in_cached_response(self):
+ Client().get('/csrf')
+ response = Client().get('/csrf')
+ token = self.assertCsrfTokenOk(response)
--- /dev/null
+# -*- coding: utf-8 -*-
+# This file is part of django-ssify, licensed under GNU Affero GPLv3 or later.
+# Copyright © Fundacja Nowoczesna Polska. See README.md for more information.
+#
+from __future__ import unicode_literals
+
+from django.conf import settings
+from django.test import Client, TestCase
+from django.test.utils import override_settings
+from django.utils import translation
+from ssify import exceptions
+from ssify.middleware import SsiMiddleware
+
+
+class LocaleTestCase(TestCase):
+ def setUp(self):
+ self.ssi_process_response = SsiMiddleware.process_response
+ SsiMiddleware.process_response = lambda self, req, res: res
+
+ def tearDown(self):
+ SsiMiddleware.process_response = self.ssi_process_response
+
+ def test_locale_middleware(self):
+ index = settings.MIDDLEWARE_CLASSES.index(
+ 'ssify.middleware.LocaleMiddleware')
+ stock_middleware = settings.MIDDLEWARE_CLASSES[:index] + \
+ ['django.middleware.locale.LocaleMiddleware'] + \
+ settings.MIDDLEWARE_CLASSES[index + 1:]
+
+ for use_stock_middleware in False, True:
+ for with_lang in False, True:
+ for with_i18n in False, True:
+ override = {'USE_I18N': with_i18n}
+
+ if use_stock_middleware:
+ override['MIDDLEWARE_CLASSES'] = stock_middleware
+
+ if use_stock_middleware and with_i18n:
+ expected_vary = 'Accept-Language, Cookie'
+ else:
+ expected_vary = 'Accept-Language'
+
+ if with_lang:
+ url = '/include_language_with_lang'
+ else:
+ url = '/include_language_without_lang'
+
+ with self.settings(**override):
+ # Changed USE_I18N, must reload translation mechanism.
+ translation._trans.__dict__.clear()
+ response = Client().get(url)
+ self.assertEqual(
+ response['Vary'],
+ expected_vary,
+ 'Wrong Vary with: use_stock_middleware=%s, '
+ 'with_lang=%s, with_i18n=%s; '
+ 'expected: %s, got: %s' % (
+ use_stock_middleware, with_lang, with_i18n,
+ expected_vary, response['Vary'])
+ )
+
+ def test_lang_arg(self):
+ self.assertEqual(
+ self.client.get('/language/uk').content.strip(), b'uk')
+ self.assertEqual(
+ self.client.get('/language').content.strip(), b'pl')
+
+ def test_lang_arg_missing(self):
+ self.assertRaises(
+ exceptions.NoLangFieldError,
+ lambda: self.client.get('/bad_language'))
+
+ def test_locale_middleware_without_session(self):
+ index = settings.MIDDLEWARE_CLASSES.index(
+ 'django.contrib.sessions.middleware.SessionMiddleware')
+ middleware = settings.MIDDLEWARE_CLASSES[:index] + \
+ settings.MIDDLEWARE_CLASSES[index + 1:]
+ with self.settings(MIDDLEWARE_CLASSES=middleware):
+ self.assertEqual(
+ self.client.get('/include_language_with_lang')['Vary'],
+ 'Accept-Language')
+
--- /dev/null
+# -*- coding: utf-8 -*-
+# This file is part of django-ssify, licensed under GNU Affero GPLv3 or later.
+# Copyright © Fundacja Nowoczesna Polska. See README.md for more information.
+#
+from __future__ import unicode_literals
+
+import re
+
+
+splitter = re.compile(br'(?<=-->)\s*(?=<!--#)')
+
+def split_ssi(ssi_text):
+ """re.split won't split on empty sequence, so we need that."""
+ ssi_text = ssi_text.strip()
+ start = 0
+ for match in re.finditer(splitter, ssi_text):
+ yield ssi_text[start:match.start()]
+ start = match.end()
+ yield ssi_text[start:]
+# -*- coding: utf-8 -*-
+# This file is part of django-ssify, licensed under GNU Affero GPLv3 or later.
+# Copyright © Fundacja Nowoczesna Polska. See README.md for more information.
+#
+from __future__ import unicode_literals
+
from django.conf.urls import patterns, url
from django.views.generic import TemplateView
urlpatterns = patterns(
'tests.views',
+ # tests.basic
url(r'^$',
- TemplateView.as_view(template_name='tests/main.html')
+ TemplateView.as_view(template_name='tests_basic/main.html')
),
url(r'^number_zero$',
- TemplateView.as_view(template_name='tests/number_zero.html')
+ TemplateView.as_view(template_name='tests_basic/number_zero.html')
+ ),
+ url(r'^basic_include$',
+ TemplateView.as_view(template_name='tests_basic/basic_include.html')
),
url(r'^random_quote$', 'random_quote', name='random_quote'),
url(r'^quote/(?P<number>.+)$', 'quote', name='quote'),
+
+ url(r'^quote_undeclared/(?P<number>.+)$', 'quote_undeclared'),
+ url(r'^quote_overdeclared/(?P<number>.+)$', 'quote_overdeclared'),
+
+ # tests.args
+ url(r'^include_args$',
+ TemplateView.as_view(template_name='tests_args/include_args.html'),
+ ),
+ url(r'^args$',
+ TemplateView.as_view(template_name='tests_args/args.html'),
+ {'limit': 3}
+ ),
+ url(r'^args/(?P<limit>\d+)$', 'args', name='args'),
+
+ # tests.csrf
+ url(r'^csrf$',
+ TemplateView.as_view(template_name='tests_csrf/csrf_token.html'),
+ ),
+ url(r'^csrf_check$', 'csrf_check'),
+
+ # tests.locale
+ url(r'^include_language_with_lang$',
+ TemplateView.as_view(template_name='tests_locale/include_language_with_lang.html')
+ ),
+ url(r'^include_language_without_lang$',
+ TemplateView.as_view(template_name='tests_locale/include_language_without_lang.html')
+ ),
+ url(r'^language/(?P<lang>.+)$', 'language_with_lang', name='language_with_lang'),
+ url(r'^language$', 'language_without_lang', name='language_without_lang'),
+ url(r'^bad_language$', 'language_with_lang', name='bad_language_with_lang'),
)
+# -*- coding: utf-8 -*-
+# This file is part of django-ssify, licensed under GNU Affero GPLv3 or later.
+# Copyright © Fundacja Nowoczesna Polska. See README.md for more information.
+#
+from __future__ import unicode_literals
+
+from django.http import HttpResponse
from django.shortcuts import render
+from django.utils import translation
from ssify import ssi_included, ssi_expect, SsiVariable as V
@ssi_included(use_lang=False, get_ssi_vars=lambda number: [
('test_tags.number_of_quotes',),
- ('test_tags.quote_len_odd', (ssi_expect(number, int),))
+ ('test_tags.quote_len_odd', [ssi_expect(number, int)])
])
def quote(request, number):
number = int(number)
- return render(request, 'tests/quote.html', {
+ return render(request, 'tests_basic/quote.html', {
+ 'number': number,
+ 'quote': QUOTES[number]
+ })
+
+
+@ssi_included(use_lang=False)
+def quote_undeclared(request, number):
+ number = int(number)
+ return render(request, 'tests_basic/quote.html', {
'number': number,
'quote': QUOTES[number]
})
+@ssi_included(use_lang=False, get_ssi_vars=lambda number: [
+ ('test_tags.number_of_quotes',),
+ ('test_tags.quote_len_odd', [ssi_expect(number, int)]),
+ ('test_tags.quote_len_odd', [V('nonexistent')]),
+])
+def quote_overdeclared(request, number):
+ number = int(number)
+ return render(request, 'tests_basic/quote.html', {
+ 'number': number,
+ 'quote': QUOTES[number]
+ })
+
+
+
@ssi_included(use_lang=False, get_ssi_vars=lambda: (
lambda number: [number] + quote.get_ssi_vars(number))(
- number=V('test_tags.random_number', [V('test_tags.number_of_quotes')])
+ number=V('test_tags.random_number', (), {'limit': V('test_tags.number_of_quotes')})
))
def random_quote(request):
"""
then sets a third saying if the length of the selected quote is odd.
"""
- return render(request, 'tests/random_quote.html')
+ return render(request, 'tests_basic/random_quote.html')
+
+
+def language(request):
+ assert request.LANGUAGE_CODE == translation.get_language()
+ return HttpResponse(request.LANGUAGE_CODE)
+
+language_without_lang = ssi_included(use_lang=False)(language)
+language_with_lang = ssi_included(language)
+
+
+
+
+@ssi_included(use_lang=False, get_ssi_vars=lambda limit: (
+ ('test_tags.random_number', [], {'limit': ssi_expect(limit, int)}),
+ ('test_tags.random_number', [V(
+ 'test_tags.random_number', [], {'limit': ssi_expect(limit, int)}
+ )]),
+ ('test_tags.random_number', [], {'limit': V(
+ 'test_tags.random_number', [V(
+ 'test_tags.random_number', [], {'limit': ssi_expect(limit, int)}
+ )]
+ )}),
+ ))
+def args(request, limit):
+ return render(request, 'tests_args/args.html', {'limit': int(limit)})
+
+
+def csrf_check(request):
+ return HttpResponse(request.POST['test'])
# Nothing interesting here.
def _quotes():
import sys
- import cStringIO
stdout_backup = sys.stdout
- sys.stdout = cStringIO.StringIO()
+ try:
+ # Python 2
+ from cStringIO import StringIO
+ sys.stdout = StringIO()
+ except ImportError:
+ # Python 3
+ from io import BytesIO, TextIOWrapper
+ sys.stdout = TextIOWrapper(BytesIO(), sys.stdout.encoding)
import this
- this_string = sys.stdout.getvalue()
+ sys.stdout.seek(0)
+ this_string = sys.stdout.read()
sys.stdout.close()
sys.stdout = stdout_backup
return this_string.split('\n')
--- /dev/null
+# This file is part of django-ssify, licensed under GNU Affero GPLv3 or later.
+# Copyright © Fundacja Nowoczesna Polska. See README.md for more information.
+#
+[tox]
+envlist=clear,
+ d14-py26,
+ d15-py26, d15-py27, d15-py32, d15-py33,
+ d16-py26, d16-py27, d16-py32, d16-py33,
+ d17-py27, d17-py32, d17-py33, d17-py34,
+ dd-py27, dd-py32, dd-py33, dd-py34,
+ stats
+
+[testenv]
+indexserver=http://py.mdrn.pl
+commands=coverage run --source=ssify --append --branch runtests.py
+deps=coverage
+
+[testenv:clear]
+commands=coverage erase
+
+[testenv:stats]
+commands=coverage html
+
+[base]
+
+[testenv:d14-py26]
+basepython=python2.6
+deps=
+ Django>=1.4,<1.5
+ {[testenv]deps}
+
+[testenv:d15-py26]
+basepython=python2.6
+deps=
+ Django>=1.5,<1.6
+ {[testenv]deps}
+
+[testenv:d15-py27]
+basepython=python2.7
+deps=
+ Django>=1.5,<1.6
+ {[testenv]deps}
+
+[testenv:d15-py32]
+basepython=python3.2
+deps=
+ Django>=1.5,<1.6
+ {[testenv]deps}
+
+[testenv:d15-py33]
+basepython=python3.3
+deps=
+ Django>=1.5,<1.6
+ {[testenv]deps}
+
+[testenv:d16-py26]
+basepython=python2.6
+deps=
+ Django>=1.6,<1.7
+ {[testenv]deps}
+
+[testenv:d16-py27]
+basepython=python2.7
+deps=
+ Django>=1.6,<1.7
+ {[testenv]deps}
+
+[testenv:d16-py32]
+basepython=python3.2
+deps=
+ Django>=1.6,<1.7
+ {[testenv]deps}
+
+[testenv:d16-py33]
+basepython=python3.3
+deps=
+ Django>=1.6,<1.7
+ {[testenv]deps}
+
+[testenv:d17-py27]
+basepython=python2.7
+deps=
+ https://www.djangoproject.com/download/1.7c3/tarball/
+ {[testenv]deps}
+
+[testenv:d17-py32]
+basepython=python3.2
+deps=
+ https://www.djangoproject.com/download/1.7c3/tarball/
+ {[testenv]deps}
+
+[testenv:d17-py33]
+basepython=python3.3
+deps=
+ https://www.djangoproject.com/download/1.7c3/tarball/
+ {[testenv]deps}
+
+[testenv:d17-py34]
+basepython=python3.4
+deps=
+ https://www.djangoproject.com/download/1.7c3/tarball/
+ {[testenv]deps}
+
+[testenv:dd-py27]
+basepython=python2.7
+deps=
+ https://github.com/django/django/zipball/master
+ {[testenv]deps}
+
+[testenv:dd-py32]
+basepython=python3.2
+deps=
+ https://github.com/django/django/zipball/master
+ {[testenv]deps}
+
+[testenv:dd-py33]
+basepython=python3.3
+deps=
+ https://github.com/django/django/zipball/master
+ {[testenv]deps}
+
+[testenv:dd-py34]
+basepython=python3.4
+deps=
+ https://github.com/django/django/zipball/master
+ {[testenv]deps}