diff --git a/adminsortable/admin.py b/adminsortable/admin.py
index ae24dd1..539d2b1 100644
--- a/adminsortable/admin.py
+++ b/adminsortable/admin.py
@@ -3,24 +3,11 @@ import json
from django import VERSION
from django.conf import settings
-
-try:
- from django.conf.urls import url
-except ImportError:
- # Django < 1.4
- from django.conf.urls.defaults import url
-
+from django.conf.urls import url
from django.contrib.admin import ModelAdmin, TabularInline, StackedInline
from django.contrib.admin.options import InlineModelAdmin
-
-try:
- from django.contrib.contenttypes.admin import (GenericStackedInline,
- GenericTabularInline)
-except:
- # Django < 1.7
- from django.contrib.contenttypes.generic import (GenericStackedInline,
- GenericTabularInline)
-
+from django.contrib.contenttypes.admin import (GenericStackedInline,
+ GenericTabularInline)
from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import PermissionDenied
from django.http import HttpResponse, Http404
@@ -51,13 +38,7 @@ class SortableAdminBase(object):
its sort order can be changed. This view adds a link to the
object_tools block to take people to the view to change the sorting.
"""
-
- try:
- qs_method = getattr(self, 'get_queryset', self.queryset)
- except AttributeError:
- qs_method = self.get_queryset
-
- if get_is_sortable(qs_method(request)):
+ if get_is_sortable(self.get_queryset(request)):
self.change_list_template = \
self.sortable_change_list_with_sort_link_template
self.is_sortable = True
@@ -101,12 +82,7 @@ class SortableAdmin(SortableAdminBase, ModelAdmin):
def get_urls(self):
urls = super(SortableAdmin, self).get_urls()
- opts = self.model._meta
- try:
- info = opts.app_label, opts.model_name
- except AttributeError:
- # Django < 1.7
- info = opts.app_label, opts.model_name
+ info = self.model._meta.app_label, self.model._meta.model_name
# this ajax view changes the order of instances of the model type
admin_do_sorting_url = url(
@@ -150,11 +126,7 @@ class SortableAdmin(SortableAdminBase, ModelAdmin):
pass
# Apply any sort filters to create a subset of sortable objects
- try:
- qs_method = getattr(self, 'get_queryset', self.queryset)
- except AttributeError:
- qs_method = self.get_queryset
- objects = qs_method(request).filter(**filters)
+ objects = self.get_queryset(request).filter(**filters)
# Determine if we need to regroup objects relative to a
# foreign key specified on the model class that is extending Sortable.
@@ -169,7 +141,11 @@ class SortableAdmin(SortableAdminBase, ModelAdmin):
for field in self.model._meta.fields:
if isinstance(field, SortableForeignKey):
- sortable_by_fk = field.rel.to
+ try:
+ sortable_by_fk = field.remote_field.model
+ except AttributeError:
+ # Django < 1.9
+ sortable_by_fk = field.rel.to
sortable_by_field_name = field.name.lower()
sortable_by_class_is_sortable = sortable_by_fk.objects.count() >= 2
@@ -206,9 +182,6 @@ class SortableAdmin(SortableAdminBase, ModelAdmin):
try:
order_field_name = opts.model._meta.ordering[0]
- except (AttributeError, IndexError):
- # for Django 1.5.x
- order_field_name = opts.ordering[0]
except (AttributeError, IndexError):
order_field_name = 'order'
@@ -328,27 +301,18 @@ class SortableInlineBase(SortableAdminBase, InlineModelAdmin):
' (or Sortable for legacy implementations)')
def get_queryset(self, request):
- if VERSION < (1, 6):
- qs = super(SortableInlineBase, self).queryset(request)
- else:
- qs = super(SortableInlineBase, self).get_queryset(request)
-
+ qs = super(SortableInlineBase, self).get_queryset(request)
if get_is_sortable(qs):
self.model.is_sortable = True
else:
self.model.is_sortable = False
return qs
- if VERSION < (1, 6):
- queryset = get_queryset
-
class SortableTabularInline(TabularInline, SortableInlineBase):
"""Custom template that enables sorting for tabular inlines"""
if VERSION >= (1, 10):
template = 'adminsortable/edit_inline/tabular-1.10.x.html'
- elif VERSION < (1, 6):
- template = 'adminsortable/edit_inline/tabular-1.5.x.html'
else:
template = 'adminsortable/edit_inline/tabular.html'
@@ -357,8 +321,6 @@ class SortableStackedInline(StackedInline, SortableInlineBase):
"""Custom template that enables sorting for stacked inlines"""
if VERSION >= (1, 10):
template = 'adminsortable/edit_inline/stacked-1.10.x.html'
- elif VERSION < (1, 6):
- template = 'adminsortable/edit_inline/stacked-1.5.x.html'
else:
template = 'adminsortable/edit_inline/stacked.html'
@@ -367,8 +329,6 @@ class SortableGenericTabularInline(GenericTabularInline, SortableInlineBase):
"""Custom template that enables sorting for tabular inlines"""
if VERSION >= (1, 10):
template = 'adminsortable/edit_inline/tabular-1.10.x.html'
- elif VERSION < (1, 6):
- template = 'adminsortable/edit_inline/tabular-1.5.x.html'
else:
template = 'adminsortable/edit_inline/tabular.html'
@@ -377,7 +337,5 @@ class SortableGenericStackedInline(GenericStackedInline, SortableInlineBase):
"""Custom template that enables sorting for stacked inlines"""
if VERSION >= (1, 10):
template = 'adminsortable/edit_inline/stacked-1.10.x.html'
- elif VERSION < (1, 6):
- template = 'adminsortable/edit_inline/stacked-1.5.x.html'
else:
template = 'adminsortable/edit_inline/stacked.html'
diff --git a/adminsortable/fields.py b/adminsortable/fields.py
index 380f555..3376f12 100644
--- a/adminsortable/fields.py
+++ b/adminsortable/fields.py
@@ -7,14 +7,4 @@ class SortableForeignKey(ForeignKey):
This field replaces previous functionality where `sortable_by` was
defined as a model property that specified another model class.
"""
-
- def south_field_triple(self):
- try:
- from south.modelsinspector import introspector
- cls_name = '{0}.{1}'.format(
- self.__class__.__module__,
- self.__class__.__name__)
- args, kwargs = introspector(self)
- return cls_name, args, kwargs
- except ImportError:
- pass
+ pass
diff --git a/adminsortable/templates/adminsortable/edit_inline/stacked-1.5.x.html b/adminsortable/templates/adminsortable/edit_inline/stacked-1.5.x.html
deleted file mode 100644
index c34182d..0000000
--- a/adminsortable/templates/adminsortable/edit_inline/stacked-1.5.x.html
+++ /dev/null
@@ -1,92 +0,0 @@
-{% load i18n admin_modify adminsortable_tags admin_urls %}
-{% load static from staticfiles %}
-
-
-
diff --git a/adminsortable/templates/adminsortable/edit_inline/tabular-1.5.x.html b/adminsortable/templates/adminsortable/edit_inline/tabular-1.5.x.html
deleted file mode 100644
index bdc8348..0000000
--- a/adminsortable/templates/adminsortable/edit_inline/tabular-1.5.x.html
+++ /dev/null
@@ -1,136 +0,0 @@
-{% load i18n admin_modify adminsortable_tags admin_urls %}
-{% load static from staticfiles %}
-
-
-
diff --git a/adminsortable/templatetags/django_template_additions.py b/adminsortable/templatetags/django_template_additions.py
index c05f349..1866b68 100644
--- a/adminsortable/templatetags/django_template_additions.py
+++ b/adminsortable/templatetags/django_template_additions.py
@@ -2,11 +2,6 @@ from itertools import groupby
import django
from django import template
-try:
- from django import TemplateSyntaxError
-except ImportError:
- #support for django 1.3
- from django.template.base import TemplateSyntaxError
register = template.Library()
@@ -64,14 +59,15 @@ def dynamic_regroup(parser, token):
"""
firstbits = token.contents.split(None, 3)
if len(firstbits) != 4:
- raise TemplateSyntaxError("'regroup' tag takes five arguments")
+ raise template.TemplateSyntaxError("'regroup' tag takes five arguments")
target = parser.compile_filter(firstbits[1])
if firstbits[2] != 'by':
- raise TemplateSyntaxError("second argument to 'regroup' tag must be 'by'")
+ raise template.TemplateSyntaxError(
+ "second argument to 'regroup' tag must be 'by'")
lastbits_reversed = firstbits[3][::-1].split(None, 2)
if lastbits_reversed[1][::-1] != 'as':
- raise TemplateSyntaxError("next-to-last argument to 'regroup' tag must"
- " be 'as'")
+ raise template.TemplateSyntaxError(
+ "next-to-last argument to 'regroup' tag must be 'as'")
expression = lastbits_reversed[2][::-1]
var_name = lastbits_reversed[0][::-1]
diff --git a/sample_project/app/admin.py b/sample_project/app/admin.py
index cba42f8..9da0c19 100644
--- a/sample_project/app/admin.py
+++ b/sample_project/app/admin.py
@@ -26,8 +26,8 @@ class ComponentInline(SortableStackedInline):
# )
model = Component
- def queryset(self, request):
- qs = super(ComponentInline, self).queryset(
+ def get_queryset(self, request):
+ qs = super(ComponentInline, self).get_queryset(
request).exclude(title__icontains='2')
if get_is_sortable(qs):
self.model.is_sortable = True
@@ -37,14 +37,14 @@ class ComponentInline(SortableStackedInline):
class WidgetAdmin(SortableAdmin):
- def queryset(self, request):
+ def get_queryset(self, request):
"""
A simple example demonstrating that adminsortable works even in
situations where you need to filter the queryset in admin. Here,
we are just filtering out `widget` instances with an pk higher
than 3
"""
- qs = super(WidgetAdmin, self).queryset(request)
+ qs = super(WidgetAdmin, self).get_queryset(request)
return qs.filter(id__lte=3)
inlines = [ComponentInline]
diff --git a/sample_project/app/models.py b/sample_project/app/models.py
index 6f1186f..4036064 100644
--- a/sample_project/app/models.py
+++ b/sample_project/app/models.py
@@ -1,10 +1,4 @@
-from django import VERSION
-
-if VERSION < (1, 9):
- from django.contrib.contenttypes.generic import GenericForeignKey
-else:
- from django.contrib.contenttypes.fields import GenericForeignKey
-
+from django.contrib.contenttypes.fields import GenericForeignKey
from django.contrib.contenttypes.models import ContentType
from django.db import models
from django.utils.encoding import python_2_unicode_compatible
@@ -51,7 +45,7 @@ class Project(SimpleModel, SortableMixin):
class Meta:
ordering = ['order']
- category = SortableForeignKey(Category)
+ category = SortableForeignKey(Category, on_delete=models.CASCADE)
description = models.TextField()
order = models.PositiveIntegerField(default=0, editable=False)
@@ -63,7 +57,7 @@ class Credit(SortableMixin):
class Meta:
ordering = ['order']
- project = models.ForeignKey(Project)
+ project = models.ForeignKey(Project, on_delete=models.CASCADE)
first_name = models.CharField(max_length=30, help_text="Given name")
last_name = models.CharField(max_length=30, help_text="Family name")
@@ -79,7 +73,7 @@ class Note(SortableMixin):
class Meta:
ordering = ['order']
- project = models.ForeignKey(Project)
+ project = models.ForeignKey(Project, on_delete=models.CASCADE)
text = models.CharField(max_length=100)
order = models.PositiveIntegerField(default=0, editable=False)
@@ -91,7 +85,7 @@ class Note(SortableMixin):
# Registered as a tabular inline on `Project` which can't be sorted
@python_2_unicode_compatible
class NonSortableCredit(models.Model):
- project = models.ForeignKey(Project)
+ project = models.ForeignKey(Project, on_delete=models.CASCADE)
first_name = models.CharField(max_length=30, help_text="Given name")
last_name = models.CharField(max_length=30, help_text="Family name")
@@ -102,7 +96,7 @@ class NonSortableCredit(models.Model):
# Registered as a stacked inline on `Project` which can't be sorted
@python_2_unicode_compatible
class NonSortableNote(models.Model):
- project = models.ForeignKey(Project)
+ project = models.ForeignKey(Project, on_delete=models.CASCADE)
text = models.CharField(max_length=100)
def __str__(self):
@@ -112,7 +106,7 @@ class NonSortableNote(models.Model):
# A generic bound model
@python_2_unicode_compatible
class GenericNote(SimpleModel, SortableMixin):
- content_type = models.ForeignKey(ContentType,
+ content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE,
verbose_name=u"Content type", related_name="generic_notes")
object_id = models.PositiveIntegerField(u"Content id")
content_object = GenericForeignKey(ct_field='content_type',
@@ -133,7 +127,7 @@ class Component(SimpleModel, SortableMixin):
class Meta:
ordering = ['order']
- widget = SortableForeignKey(Widget)
+ widget = SortableForeignKey(Widget, on_delete=models.CASCADE)
order = models.PositiveIntegerField(default=0, editable=False)
@@ -183,7 +177,8 @@ class SortableCategoryWidget(SimpleModel, SortableMixin):
verbose_name = 'Sortable Category Widget'
verbose_name_plural = 'Sortable Category Widgets'
- non_sortable_category = SortableForeignKey(NonSortableCategory)
+ non_sortable_category = SortableForeignKey(
+ NonSortableCategory, on_delete=models.CASCADE)
order = models.PositiveIntegerField(default=0, editable=False)
@@ -197,7 +192,8 @@ class SortableNonInlineCategory(SimpleModel, SortableMixin):
that is *not* sortable, and is also not defined as an inline of the
SortableForeignKey field."""
- non_sortable_category = SortableForeignKey(NonSortableCategory)
+ non_sortable_category = SortableForeignKey(
+ NonSortableCategory, on_delete=models.CASCADE)
order = models.PositiveIntegerField(default=0, editable=False)
@@ -229,7 +225,7 @@ class CustomWidget(SortableMixin, SimpleModel):
@python_2_unicode_compatible
class CustomWidgetComponent(SortableMixin, SimpleModel):
- custom_widget = models.ForeignKey(CustomWidget)
+ custom_widget = models.ForeignKey(CustomWidget, on_delete=models.CASCADE)
# custom field for ordering
widget_order = models.PositiveIntegerField(default=0, db_index=True,
diff --git a/sample_project/app/tests.py b/sample_project/app/tests.py
index e573ebb..0278331 100644
--- a/sample_project/app/tests.py
+++ b/sample_project/app/tests.py
@@ -1,16 +1,13 @@
try:
- import httplib
+ import httplib # Python 2
except ImportError:
- import http.client as httplib
-
-from django import VERSION
-
-if VERSION > (1, 8):
- import uuid
+ import http.client as httplib # Python 3
import json
+import uuid
+
+import django
-from django import VERSION
from django.contrib.auth.models import User
from django.db import models
from django.test import TestCase
@@ -33,13 +30,12 @@ class TestSortableModel(SortableMixin):
return self.title
-if VERSION > (1, 8):
- class TestNonAutoFieldModel(SortableMixin):
- id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
- order = models.PositiveIntegerField(editable=False, db_index=True)
+class TestNonAutoFieldModel(SortableMixin):
+ id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
+ order = models.PositiveIntegerField(editable=False, db_index=True)
- class Meta:
- ordering = ['order']
+ class Meta:
+ ordering = ['order']
class SortableTestCase(TestCase):
@@ -79,8 +75,12 @@ class SortableTestCase(TestCase):
return category
def test_new_user_is_authenticated(self):
- self.assertEqual(self.user.is_authenticated(), True,
- 'User is not authenticated')
+ if django.VERSION < (1, 10):
+ self.assertEqual(self.user.is_authenticated(), True,
+ 'User is not authenticated')
+ else:
+ self.assertEqual(self.user.is_authenticated, True,
+ 'User is not authenticated')
def test_new_user_is_staff(self):
self.assertEqual(self.user.is_staff, True, 'User is not staff')
@@ -114,7 +114,7 @@ class SortableTestCase(TestCase):
self.client.login(username=self.user.username,
password=self.user_raw_password)
response = self.client.get('/admin/app/category/sort/')
- self.assertEquals(response.status_code, httplib.OK,
+ self.assertEqual(response.status_code, httplib.OK,
'Unable to reach sort view.')
def make_test_categories(self):
@@ -257,7 +257,7 @@ class SortableTestCase(TestCase):
self.client.login(username=self.user.username,
password=self.user_raw_password)
response = self.client.get('/admin/app/project/sort/')
- self.assertEquals(response.status_code, httplib.OK,
+ self.assertEqual(response.status_code, httplib.OK,
'Unable to reach sort view.')
def test_adminsortable_change_list_view_permission_denied(self):
@@ -267,8 +267,8 @@ class SortableTestCase(TestCase):
self.client.login(username=self.staff.username,
password=self.staff_raw_password)
response = self.client.get('/admin/app/project/sort/')
- self.assertEquals(response.status_code, httplib.FORBIDDEN,
- 'Sort view must be forbidden.')
+ self.assertEqual(response.status_code, httplib.FORBIDDEN,
+ 'Sort view must be forbidden.')
def test_adminsortable_inline_changelist_success(self):
self.client.login(username=self.user.username,
@@ -317,8 +317,5 @@ class SortableTestCase(TestCase):
self.assertEqual(notes, expected_notes)
def test_save_non_auto_field_model(self):
- if VERSION > (1, 8):
- model = TestNonAutoFieldModel()
- model.save()
- else:
- pass
+ model = TestNonAutoFieldModel()
+ model.save()
diff --git a/sample_project/sample_project/settings.py b/sample_project/sample_project/settings.py
index ceb7009..7327e75 100644
--- a/sample_project/sample_project/settings.py
+++ b/sample_project/sample_project/settings.py
@@ -1,6 +1,8 @@
# Django settings for test_project project.
import os
+import django
+
def map_path(directory_name):
return os.path.join(os.path.dirname(__file__),
@@ -8,7 +10,6 @@ def map_path(directory_name):
DEBUG = True
-TEMPLATE_DEBUG = DEBUG
ADMINS = (
# ('Your Name', 'your_email@example.com'),
@@ -91,35 +92,31 @@ STATICFILES_FINDERS = (
# Make this unique, and don't share it with anybody.
SECRET_KEY = '8**a!c8$1x)p@j2pj0yq!*v+dzp24g*$918ws#x@k+gf%0%rct'
-# List of callables that know how to import templates from various sources.
-TEMPLATE_LOADERS = (
- 'django.template.loaders.filesystem.Loader',
- 'django.template.loaders.app_directories.Loader',
-)
-
-MIDDLEWARE_CLASSES = (
+MIDDLEWARE = [
'django.middleware.common.CommonMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
+ 'django.contrib.auth.middleware.SessionAuthenticationMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
# Uncomment the next line for simple clickjacking protection:
'django.middleware.clickjacking.XFrameOptionsMiddleware',
-)
+]
+
+if django.VERSION < (1, 10):
+ MIDDLEWARE_CLASSES = MIDDLEWARE
ROOT_URLCONF = 'sample_project.urls'
# Python dotted path to the WSGI application used by Django's runserver.
WSGI_APPLICATION = 'sample_project.wsgi.application'
-TEMPLATE_DIRS = (
- map_path('templates'),
-)
-
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
- 'DIRS': TEMPLATE_DIRS,
+ 'DIRS': [
+ map_path('templates')
+ ],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
diff --git a/sample_project/sample_project/urls.py b/sample_project/sample_project/urls.py
index 5abb2fc..3e2dcfe 100644
--- a/sample_project/sample_project/urls.py
+++ b/sample_project/sample_project/urls.py
@@ -14,5 +14,5 @@ urlpatterns = [
url(r'^admin/doc/', include('django.contrib.admindocs.urls')),
# Uncomment the next line to enable the admin:
- url(r'^admin/', include(admin.site.urls)),
+ url(r'^admin/', admin.site.urls),
]
diff --git a/sample_project/sample_project/utils.py b/sample_project/sample_project/utils.py
deleted file mode 100644
index 4178ad9..0000000
--- a/sample_project/sample_project/utils.py
+++ /dev/null
@@ -1,6 +0,0 @@
-import os
-
-
-def map_path(directory_name):
- return os.path.join(os.path.dirname(__file__),
- '../' + directory_name).replace('\\', '/')
diff --git a/tox.ini b/tox.ini
new file mode 100644
index 0000000..2f8f44f
--- /dev/null
+++ b/tox.ini
@@ -0,0 +1,36 @@
+[tox]
+envlist = django{1.8,1.9,1.10,1.11}-{py27,py34,py35},coverage
+
+[testenv]
+deps =
+ coverage
+ django1.8: Django>=1.8,<1.9
+ django1.9: Django>=1.9,<1.10
+ django1.10: Django>=1.10,<1.11
+ django1.11: Django>=1.11a1,<1.12
+whitelist_externals = cd
+setenv =
+ PYTHONPATH = {toxinidir}/sample_project
+ PYTHONWARNINGS = module
+ PYTHONDONTWRITEBYTECODE = 1
+commands =
+ coverage run -p sample_project/manage.py test app
+
+[testenv:coverage]
+deps = coverage
+skip_install = true
+commands =
+ coverage combine
+ coverage report
+ coverage html
+
+[coverage:run]
+branch = True
+parallel = True
+source =
+ adminsortable
+ sample_project
+
+[coverage:report]
+exclude_lines =
+ if __name__ == .__main__.: