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 %} -
-

{{ inline_admin_formset.opts.verbose_name_plural|title }} {% if inline_admin_formset.formset.initial_form_count > 1 %} - {% trans "drag and drop to change order" %}{% endif %}

-{{ inline_admin_formset.formset.management_form }} -{{ inline_admin_formset.formset.non_form_errors }} - -{% for inline_admin_form in inline_admin_formset %}
-

- {% if inline_admin_form.original %} - {% with initial_forms_count=inline_admin_formset.formset.management_form.INITIAL_FORMS.value %} - - {% endwith %} - {% endif %} - {{ inline_admin_formset.opts.verbose_name|title }}: {% if inline_admin_form.original %}{{ inline_admin_form.original }}{% else %}#{{ forloop.counter }}{% endif %} - {% if inline_admin_form.show_url %}{% trans "View on site" %}{% endif %} - {% if inline_admin_formset.formset.can_delete and inline_admin_form.original %}{{ inline_admin_form.deletion_field.field }} {{ inline_admin_form.deletion_field.label_tag }}{% endif %} -

- {% if inline_admin_form.form.non_field_errors %}{{ inline_admin_form.form.non_field_errors }}{% endif %} - {% for fieldset in inline_admin_form %} - {% include "admin/includes/fieldset.html" %} - {% endfor %} - {% if inline_admin_form.has_auto_field %}{{ inline_admin_form.pk_field.field }}{% endif %} - {{ inline_admin_form.fk_field.field }} - {% if inline_admin_form.original %} - - {% endif %} -
{% endfor %} -
- - 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__.: