Merge pull request #46 from safwanrahman/master

Update and add migration for django 1.8
This commit is contained in:
Jannis Leidel 2016-04-19 13:57:14 +02:00
commit c713eb5419
19 changed files with 180 additions and 90 deletions

View file

@ -2,16 +2,8 @@ language: python
python:
- "2.7"
env:
- TOX_ENV=py26-django14
- TOX_ENV=py26-django15
- TOX_ENV=py26-django16
- TOX_ENV=py27-django14
- TOX_ENV=py27-django15
- TOX_ENV=py27-django16
- TOX_ENV=py27-django17
- TOX_ENV=py33-django15
- TOX_ENV=py33-django16
- TOX_ENV=py33-django17
- TOX_ENV=py27-django18
- TOX_ENV=py33-django18
install:
- pip install tox
notifications:

View file

@ -56,6 +56,15 @@ html version using the setup.py::
Changelog:
==========
0.11 (2016-03-29):
-----------------
* Added Migration in order to support Django 1.8
* Dropped Support for Django 1.7 and lower
* Fix linter issues
0.10 (2015-12-14):
------------------

View file

@ -1,8 +1,11 @@
import sys
from authority.sites import site, get_check, get_choices_for, register, unregister
from authority.sites import site, get_check, get_choices_for, register, unregister # noqa
LOADING = False
def autodiscover():
"""
Goes and imports the permissions submodule of every app in INSTALLED_APPS

View file

@ -1,4 +1,3 @@
import django
from django import forms, template
from django.http import HttpResponseRedirect
from django.utils.translation import ugettext, ungettext, ugettext_lazy as _
@ -21,15 +20,11 @@ try:
except ImportError:
actions = False
# From 1.7 forward, Django consistenly uses the name "utils",
# not "util". We alias for backwards compatibility.
if django.VERSION[:2] < (1, 7):
forms.utils = forms.util
from authority.models import Permission
from authority.widgets import GenericForeignKeyRawIdWidget
from authority import get_choices_for
class PermissionInline(generic.GenericTabularInline):
model = Permission
raw_id_fields = ('user', 'group', 'creator')
@ -42,10 +37,12 @@ class PermissionInline(generic.GenericTabularInline):
kwargs['widget'] = forms.Select(choices=perm_choices)
return super(PermissionInline, self).formfield_for_dbfield(db_field, **kwargs)
class ActionPermissionInline(PermissionInline):
raw_id_fields = ()
template = 'admin/edit_inline/action_tabular.html'
class ActionErrorList(forms.utils.ErrorList):
def __init__(self, inline_formsets):
for inline_formset in inline_formsets:
@ -53,6 +50,7 @@ class ActionErrorList(forms.utils.ErrorList):
for errors_in_inline_form in inline_formset.errors:
self.extend(errors_in_inline_form.values())
def edit_permissions(modeladmin, request, queryset):
opts = modeladmin.model._meta
app_label = opts.app_label
@ -128,6 +126,7 @@ def edit_permissions(modeladmin, request, queryset):
context_instance=template.RequestContext(request))
edit_permissions.short_description = _("Edit permissions for selected %(verbose_name_plural)s")
class PermissionAdmin(admin.ModelAdmin):
list_display = ('codename', 'content_type', 'user', 'group', 'approved')
list_filter = ('approved', 'content_type')
@ -143,7 +142,10 @@ class PermissionAdmin(admin.ModelAdmin):
def formfield_for_dbfield(self, db_field, **kwargs):
# For generic foreign keys marked as generic_fields we use a special widget
if db_field.name in [f.fk_field for f in self.model._meta.virtual_fields if f.name in self.generic_fields]:
names = [f.fk_field
for f in self.model._meta.virtual_fields
if f.name in self.generic_fields]
if db_field.name in names:
for gfk in self.model._meta.virtual_fields:
if gfk.fk_field == db_field.name:
kwargs['widget'] = GenericForeignKeyRawIdWidget(
@ -161,7 +163,8 @@ class PermissionAdmin(admin.ModelAdmin):
def approve_permissions(self, request, queryset):
for permission in queryset:
permission.approve(request.user)
message = ungettext("%(count)d permission successfully approved.",
message = ungettext(
"%(count)d permission successfully approved.",
"%(count)d permissions successfully approved.", len(queryset))
self.message_user(request, message % {'count': len(queryset)})
approve_permissions.short_description = _("Approve selected permissions")

View file

@ -11,4 +11,6 @@ try:
from django.contrib.auth import get_user_model
except ImportError:
from django.contrib.auth.models import User
get_user_model = lambda: User
def get_user_model():
return User

View file

@ -10,6 +10,7 @@ from django.contrib.auth import REDIRECT_FIELD_NAME
from authority import get_check
from authority.views import permission_denied
def permission_required(perm, *lookup_variables, **kwargs):
"""
Decorator for views that checks whether a user has a particular permission
@ -18,6 +19,7 @@ def permission_required(perm, *lookup_variables, **kwargs):
login_url = kwargs.pop('login_url', settings.LOGIN_URL)
redirect_field_name = kwargs.pop('redirect_field_name', REDIRECT_FIELD_NAME)
redirect_to_login = kwargs.pop('redirect_to_login', True)
def decorate(view_func):
def decorated(request, *args, **kwargs):
if request.user.is_authenticated():
@ -60,6 +62,7 @@ def permission_required(perm, *lookup_variables, **kwargs):
return wraps(view_func)(decorated)
return decorate
def permission_required_or_403(perm, *args, **kwargs):
"""
Decorator that wraps the permission_required decorator and returns a

View file

@ -1,11 +1,13 @@
class AuthorityException(Exception):
pass
class NotAModel(AuthorityException):
def __init__(self, object):
super(NotAModel, self).__init__(
"Not a model class or instance")
class UnsavedModelInstance(AuthorityException):
def __init__(self, object):
super(UnsavedModelInstance, self).__init__(

View file

@ -95,7 +95,8 @@ class GroupPermissionForm(BasePermissionForm):
check = permissions.BasePermission(group=group)
if check.has_perm(self.perm, self.obj):
raise forms.ValidationError(mark_safe(
_("This group already has the permission '%(perm)s' for %(object_name)s '%(obj)s'") % {
_("This group already has the permission '%(perm)s' "
"for %(object_name)s '%(obj)s'") % {
'perm': self.perm,
'object_name': self.obj._meta.object_name.lower(),
'obj': self.obj,

View file

@ -0,0 +1,43 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
import datetime
from django.conf import settings
class Migration(migrations.Migration):
dependencies = [
('auth', '0001_initial'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('contenttypes', '0001_initial'),
]
operations = [
migrations.CreateModel(
name='Permission',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
('codename', models.CharField(max_length=100, verbose_name='codename')),
('object_id', models.PositiveIntegerField()),
('approved', models.BooleanField(default=False, help_text='Designates whether the permission has been approved and treated as active. Unselect this instead of deleting permissions.', verbose_name='approved')),
('date_requested', models.DateTimeField(default=datetime.datetime.now, verbose_name='date requested')),
('date_approved', models.DateTimeField(null=True, verbose_name='date approved', blank=True)),
('content_type', models.ForeignKey(related_name='row_permissions', to='contenttypes.ContentType')),
('creator', models.ForeignKey(related_name='created_permissions', blank=True, to=settings.AUTH_USER_MODEL, null=True)),
('group', models.ForeignKey(blank=True, to='auth.Group', null=True)),
('user', models.ForeignKey(related_name='granted_permissions', blank=True, to=settings.AUTH_USER_MODEL, null=True)),
],
options={
'verbose_name': 'permission',
'verbose_name_plural': 'permissions',
'permissions': (('change_foreign_permissions', 'Can change foreign permissions'), ('delete_foreign_permissions', 'Can delete foreign permissions'), ('approve_permission_requests', 'Can approve permission requests')),
},
bases=(models.Model,),
),
migrations.AlterUniqueTogether(
name='permission',
unique_together=set([('codename', 'object_id', 'content_type', 'user', 'group')]),
),
]

View file

View file

@ -20,11 +20,17 @@ class Permission(models.Model):
object_id = models.PositiveIntegerField()
content_object = generic.GenericForeignKey('content_type', 'object_id')
user = models.ForeignKey(user_model_label, null=True, blank=True, related_name='granted_permissions')
user = models.ForeignKey(
user_model_label, null=True, blank=True, related_name='granted_permissions')
group = models.ForeignKey(Group, null=True, blank=True)
creator = models.ForeignKey(user_model_label, null=True, blank=True, related_name='created_permissions')
creator = models.ForeignKey(
user_model_label, null=True, blank=True, related_name='created_permissions')
approved = models.BooleanField(_('approved'), default=False, help_text=_("Designates whether the permission has been approved and treated as active. Unselect this instead of deleting permissions."))
approved = models.BooleanField(
_('approved'),
default=False,
help_text=_("Designates whether the permission has been approved and treated as active. "
"Unselect this instead of deleting permissions."))
date_requested = models.DateTimeField(_('date requested'), default=datetime.now)
date_approved = models.DateTimeField(_('date approved'), blank=True, null=True)

View file

@ -6,12 +6,15 @@ from django.core.exceptions import ImproperlyConfigured
from authority.permissions import BasePermission
class AlreadyRegistered(Exception):
pass
class NotRegistered(Exception):
pass
class PermissionSite(object):
"""
A dictionary that contains permission instances and their labels.
@ -64,8 +67,8 @@ class PermissionSite(object):
if permission_class.label in self.get_labels():
raise ImproperlyConfigured(
"The name of %s conflicts with %s" % (permission_class,
self.get_permission_by_label(permission_class.label)))
"The name of %s conflicts with %s" % (
permission_class, self.get_permission_by_label(permission_class.label)))
for model in model_or_iterable:
if model in self._registry:
@ -73,8 +76,8 @@ class PermissionSite(object):
'The model %s is already registered' % model.__name__)
if options:
options['__module__'] = __name__
permission_class = type("%sPermission" % model.__name__,
(permission_class,), options)
permission_class = type(
"%sPermission" % model.__name__, (permission_class,), options)
permission_class.model = model
self.setup(model, permission_class)
@ -94,7 +97,9 @@ class PermissionSite(object):
if check_func is not None:
func = self.create_check(check_name, check_func)
func.__name__ = check_name
func.short_description = getattr(check_func, 'short_description',
func.short_description = getattr(
check_func,
'short_description',
_("%(object_name)s permission '%(check)s'") % {
'object_name': model._meta.object_name,
'check': check_name})
@ -122,6 +127,7 @@ class PermissionSite(object):
return granted
return check
class PermissionDescriptor(object):
def get_content_type(self, obj=None):
ContentType = models.get_model("contenttypes", "contenttype")

View file

@ -15,7 +15,7 @@
{% block breadcrumbs %}{% if not is_popup %}
<div class="breadcrumbs">
<a href="../../">{% trans "Home" %}</a> &rsaquo;
<a href="../">{{ app_label|capfirst|escape }}</a> &rsaquo;
<a href="../">{{ app_label|capfirst|escape }}</a> &rsaquo;
<a href="./">{{ opts.verbose_name_plural|capfirst }}</a> &rsaquo;
{% trans "Permissions" %}
</div>

View file

@ -145,7 +145,6 @@ class PermissionFormNode(ResolverNode):
@classmethod
def handle_token(cls, parser, token, approved):
bits = token.contents.split()
tag_name = bits[0]
kwargs = {
'obj': cls.next_bit_for(bits, 'for'),
'perm': cls.next_bit_for(bits, 'using', None),
@ -164,7 +163,7 @@ class PermissionFormNode(ResolverNode):
obj = self.resolve(self.obj, context)
perm = self.resolve(self.perm, context)
if self.template_name:
template_name = [self.resolve(obj, context) for obj in self.template_name.split(',')]
template_name = [self.resolve(o, context) for o in self.template_name.split(',')]
else:
template_name = 'authority/permission_form.html'
request = context['request']
@ -185,12 +184,15 @@ class PermissionFormNode(ResolverNode):
'form_url': url_for_obj('authority-add-permission-request', obj),
'next': request.build_absolute_uri(),
'approved': self.approved,
'form': UserPermissionForm(perm, obj,
approved=self.approved, initial=dict(
codename=perm, user=request.user.username)),
'form': UserPermissionForm(
perm,
obj,
approved=self.approved,
initial=dict(codename=perm, user=request.user.username)),
}
return template.loader.render_to_string(template_name, extra_context,
context_instance=template.RequestContext(request))
return template.loader.render_to_string(
template_name, extra_context, context_instance=template.RequestContext(request))
@register.tag
def permission_form(parser, token):
@ -206,6 +208,7 @@ def permission_form(parser, token):
"""
return PermissionFormNode.handle_token(parser, token, approved=True)
@register.tag
def permission_request_form(parser, token):
"""
@ -215,7 +218,8 @@ def permission_request_form(parser, token):
Syntax::
{% permission_request_form for OBJ and PERMISSION_LABEL.CHECK_NAME [with TEMPLATE] %}
{% permission_request_form for lesson using "lesson_permission.add_lesson" with "authority/permission_request_form.html" %}
{% permission_request_form for lesson using "lesson_permission.add_lesson"
with "authority/permission_request_form.html" %}
"""
return PermissionFormNode.handle_token(parser, token, approved=False)
@ -254,6 +258,7 @@ class PermissionsForObjectNode(ResolverNode):
context[var_name] = perms
return ''
@register.tag
def get_permissions(parser, token):
"""
@ -274,6 +279,7 @@ def get_permissions(parser, token):
return PermissionsForObjectNode.handle_token(parser, token, approved=True,
name='"permissions"')
@register.tag
def get_permission_requests(parser, token):
"""
@ -295,6 +301,7 @@ def get_permission_requests(parser, token):
approved=False,
name='"permission_requests"')
class PermissionForObjectNode(ResolverNode):
@classmethod
@ -337,6 +344,7 @@ class PermissionForObjectNode(ResolverNode):
context[var_name] = granted
return ''
@register.tag
def get_permission(parser, token):
"""
@ -347,8 +355,10 @@ def get_permission(parser, token):
{% get_permission PERMISSION_LABEL.CHECK_NAME for USER and *OBJS [as VARNAME] %}
{% get_permission "poll_permission.change_poll" for request.user and poll as "is_allowed" %}
{% get_permission "poll_permission.change_poll" for request.user and poll,second_poll as "is_allowed" %}
{% get_permission "poll_permission.change_poll"
for request.user and poll as "is_allowed" %}
{% get_permission "poll_permission.change_poll"
for request.user and poll,second_poll as "is_allowed" %}
{% if is_allowed %}
I've got ze power to change ze pollllllzzz. Muahahaa.
@ -361,6 +371,7 @@ def get_permission(parser, token):
approved=True,
name='"permission"')
@register.tag
def get_permission_request(parser, token):
"""
@ -371,8 +382,10 @@ def get_permission_request(parser, token):
{% get_permission_request PERMISSION_LABEL.CHECK_NAME for USER and *OBJS [as VARNAME] %}
{% get_permission_request "poll_permission.change_poll" for request.user and poll as "asked_for_permissio" %}
{% get_permission_request "poll_permission.change_poll" for request.user and poll,second_poll as "asked_for_permissio" %}
{% get_permission_request "poll_permission.change_poll"
for request.user and poll as "asked_for_permissio" %}
{% get_permission_request "poll_permission.change_poll"
for request.user and poll,second_poll as "asked_for_permissio" %}
{% if asked_for_permissio %}
Dude, you already asked for permission!
@ -381,16 +394,17 @@ def get_permission_request(parser, token):
{% endif %}
"""
return PermissionForObjectNode.handle_token(parser, token,
approved=False,
name='"permission_request"')
return PermissionForObjectNode.handle_token(
parser, token, approved=False, name='"permission_request"')
def base_link(context, perm, view_name):
return {
'next': context['request'].build_absolute_uri(),
'url': reverse(view_name, kwargs={'permission_pk': perm.pk,}),
'url': reverse(view_name, kwargs={'permission_pk': perm.pk}),
}
@register.inclusion_tag('authority/permission_delete_link.html', takes_context=True)
def permission_delete_link(context, perm):
"""
@ -400,11 +414,12 @@ def permission_delete_link(context, perm):
"""
user = context['request'].user
if user.is_authenticated():
if user.has_perm('authority.delete_foreign_permissions') \
or user.pk == perm.creator.pk:
if (user.has_perm('authority.delete_foreign_permissions') or
user.pk == perm.creator.pk):
return base_link(context, perm, 'authority-delete-permission')
return {'url': None}
@register.inclusion_tag('authority/permission_request_delete_link.html', takes_context=True)
def permission_request_delete_link(context, perm):
"""
@ -424,6 +439,7 @@ def permission_request_delete_link(context, perm):
return link_kwargs
return {'url': None}
@register.inclusion_tag('authority/permission_request_approve_link.html', takes_context=True)
def permission_request_approve_link(context, perm):
"""
@ -434,6 +450,5 @@ def permission_request_approve_link(context, perm):
user = context['request'].user
if user.is_authenticated():
if user.has_perm('authority.approve_permission_requests'):
return base_link(context, perm,
'authority-approve-permission-request')
return base_link(context, perm, 'authority-approve-permission-request')
return {'url': None}

View file

@ -1,10 +1,9 @@
from django import VERSION
from django.conf import settings
from django.contrib.auth.models import Permission as DjangoPermission
from django.contrib.auth.models import Group
from django.db.models import Q
from django.test import TestCase
from django.contrib.contenttypes.models import ContentType
from django.db.models import Q
import authority
from authority import permissions
@ -17,13 +16,8 @@ from authority.forms import UserPermissionForm # noqa
User = get_user_model()
if VERSION >= (1, 5):
FIXTURES = ['tests_custom.json']
QUERY = Q(email="jezdez@github.com")
else:
FIXTURES = ['tests.json']
QUERY = Q(username="jezdez")
FIXTURES = ['tests_custom.json']
QUERY = Q(email="jezdez@github.com")
class UserPermission(permissions.BasePermission):
checks = ('browse',)

View file

@ -1,31 +1,24 @@
try:
from django.conf.urls import *
except ImportError: # django < 1.4
from django.conf.urls.defaults import *
from django.conf.urls import patterns, url
urlpatterns = patterns('authority.views',
urlpatterns = patterns(
'authority.views',
url(r'^permission/add/(?P<app_label>[\w\-]+)/(?P<module_name>[\w\-]+)/(?P<pk>\d+)/$',
view='add_permission',
name="authority-add-permission",
kwargs={'approved': True}
),
kwargs={'approved': True}),
url(r'^permission/delete/(?P<permission_pk>\d+)/$',
view='delete_permission',
name="authority-delete-permission",
kwargs={'approved': True}
),
kwargs={'approved': True}),
url(r'^request/add/(?P<app_label>[\w\-]+)/(?P<module_name>[\w\-]+)/(?P<pk>\d+)/$',
view='add_permission',
name="authority-add-permission-request",
kwargs={'approved': False}
),
kwargs={'approved': False}),
url(r'^request/approve/(?P<permission_pk>\d+)/$',
view='approve_permission_request',
name="authority-approve-permission-request"
),
name="authority-approve-permission-request"),
url(r'^request/delete/(?P<permission_pk>\d+)/$',
view='delete_permission',
name="authority-delete-permission-request",
kwargs={'approved': False}
),
kwargs={'approved': False}),
)

View file

@ -1,6 +1,4 @@
from datetime import datetime
from django.shortcuts import render_to_response, get_object_or_404
from django.views.decorators.http import require_POST
from django.http import HttpResponseRedirect, HttpResponseForbidden
from django.db.models.loading import get_model
from django.utils.translation import ugettext as _
@ -12,6 +10,7 @@ from authority.models import Permission
from authority.forms import UserPermissionForm
from authority.templatetags.permissions import url_for_obj
def get_next(request, obj=None):
next = request.REQUEST.get('next')
if not next:
@ -21,9 +20,10 @@ def get_next(request, obj=None):
next = '/'
return next
@login_required
def add_permission(request, app_label, module_name, pk, approved=False,
template_name = 'authority/permission_form.html',
template_name='authority/permission_form.html',
extra_context=None, form_class=UserPermissionForm):
codename = request.POST.get('codename', None)
model = get_model(app_label, module_name)
@ -47,7 +47,7 @@ def add_permission(request, app_label, module_name, pk, approved=False,
# Limit permission request to current user
form.data['user'] = request.user
if form.is_valid():
permission = form.save(request)
form.save(request)
request.user.message_set.create(
message=_('You added a permission request.'))
return HttpResponseRedirect(next)
@ -66,6 +66,7 @@ def add_permission(request, app_label, module_name, pk, approved=False,
return render_to_response(template_name, context,
context_instance=RequestContext(request))
@login_required
def approve_permission_request(request, permission_pk):
requested_permission = get_object_or_404(Permission, pk=permission_pk)
@ -76,12 +77,13 @@ def approve_permission_request(request, permission_pk):
next = get_next(request, requested_permission)
return HttpResponseRedirect(next)
@login_required
def delete_permission(request, permission_pk, approved):
permission = get_object_or_404(Permission, pk=permission_pk,
approved=approved)
if (request.user.has_perm('authority.delete_foreign_permissions')
or request.user == permission.creator):
if (request.user.has_perm('authority.delete_foreign_permissions') or
request.user == permission.creator):
permission.delete()
if approved:
msg = _('You removed the permission.')
@ -91,6 +93,7 @@ def delete_permission(request, permission_pk, approved):
next = get_next(request)
return HttpResponseRedirect(next)
def permission_denied(request, template_name=None, extra_context=None):
"""
Default 403 handler.

View file

@ -4,6 +4,7 @@ from django.utils.safestring import mark_safe
from django.utils.translation import ugettext_lazy as _
from django.contrib.admin.widgets import ForeignKeyRawIdWidget
generic_script = """
<script type="text/javascript">
function showGenericRelatedObjectLookupPopup(ct_select, triggering_link, url_base) {
@ -17,6 +18,7 @@ function showGenericRelatedObjectLookupPopup(ct_select, triggering_link, url_bas
</script>
"""
class GenericForeignKeyRawIdWidget(ForeignKeyRawIdWidget):
def __init__(self, ct_field, cts=[], attrs=None):
self.ct_field = ct_field
@ -35,10 +37,23 @@ class GenericForeignKeyRawIdWidget(ForeignKeyRawIdWidget):
if 'class' not in attrs:
attrs['class'] = 'vForeignKeyRawIdAdminField'
output = [forms.TextInput.render(self, name, value, attrs)]
output.append("""%(generic_script)s
<a href="%(related)s%(url)s" class="related-lookup" id="lookup_id_%(name)s" onclick="return showGenericRelatedObjectLookupPopup(document.getElementById('id_%(ct_field)s'), this, '%(related)s%(url)s');"> """
% {'generic_script': generic_script, 'related': related_url, 'url': url, 'name': name, 'ct_field': self.ct_field})
output.append('<img src="%s/admin/img/selector-search.gif" width="16" height="16" alt="%s" /></a>' % (settings.STATIC_URL, _('Lookup')))
output.append(
"""%(generic_script)s
<a href="%(related)s%(url)s"
class="related-lookup"
id="lookup_id_%(name)s"
onclick="return showGenericRelatedObjectLookupPopup(
document.getElementById('id_%(ct_field)s'), this, '%(related)s%(url)s');">
""" % {
'generic_script': generic_script,
'related': related_url,
'url': url,
'name': name,
'ct_field': self.ct_field
})
output.append(
'<img src="%s/admin/img/selector-search.gif" width="16" height="16" alt="%s" /></a>'
% (settings.STATIC_URL, _('Lookup')))
from django.contrib.contenttypes.models import ContentType
content_types = """
@ -46,7 +61,12 @@ class GenericForeignKeyRawIdWidget(ForeignKeyRawIdWidget):
var content_types = new Array();
%s
</script>
""" % ('\n'.join(["content_types[%s] = '%s/%s/';" % (ContentType.objects.get_for_model(ct).id, ct._meta.app_label, ct._meta.object_name.lower()) for ct in self.cts]))
""" % ('\n'.join([
"content_types[%s] = '%s/%s/';" % (
ContentType.objects.get_for_model(ct).id,
ct._meta.app_label,
ct._meta.object_name.lower()
) for ct in self.cts]))
return mark_safe(u''.join(output) + content_types)
def url_parameters(self):

View file

@ -1,14 +1,9 @@
[tox]
envlist =
py26-django{14,15,16},
py27-django{14,15,16,17,18},
py33-django{15,16,17,18}
[testenv]
commands = python example/manage.py test authority
deps =
django14: Django>=1.4, <1.5
django15: Django>=1.5, <1.6
django16: Django>=1.6, <1.7
django17: Django>=1.7, <1.8
django18: Django>=1.8, <1.9