New Upstream Snapshot - python-django-adminsortable

Ready changes

Summary

Merged new upstream version: 2.3.0+git20220312.1.b9c1f3e (was: 2.0.10).

Resulting package

Built on 2023-01-19T21:37 (took 4m17s)

The resulting binary packages can be installed (if you have the apt repository enabled) by running one of:

apt install -t fresh-snapshots python3-django-adminsortable

Diff

diff --git a/AUTHORS b/AUTHORS
new file mode 100644
index 0000000..ca8dbc6
--- /dev/null
+++ b/AUTHORS
@@ -0,0 +1,2 @@
+This software is maintained by:
+Brandon Taylor <alsoicode@gmail.com>
diff --git a/PKG-INFO b/PKG-INFO
index 84cebe5..636125d 100644
--- a/PKG-INFO
+++ b/PKG-INFO
@@ -1,19 +1,796 @@
-Metadata-Version: 1.1
+Metadata-Version: 2.1
 Name: django-admin-sortable
-Version: 2.0.10
+Version: 2.3
 Summary: Drag and drop sorting for models and inline models in Django admin.
 Home-page: https://github.com/iambrandontaylor/django-admin-sortable
 Author: Brandon Taylor
 Author-email: alsoicode@gmail.com
 License: APL
-Description: UNKNOWN
-Platform: UNKNOWN
 Classifier: Development Status :: 5 - Production/Stable
 Classifier: Environment :: Web Environment
 Classifier: Framework :: Django
 Classifier: Intended Audience :: Developers
 Classifier: License :: OSI Approved :: Apache Software License
 Classifier: Operating System :: OS Independent
-Classifier: Programming Language :: Python :: 2
 Classifier: Programming Language :: Python :: 3
 Classifier: Topic :: Utilities
+License-File: AUTHORS
+
+Django Admin Sortable
+=====================
+
+|PyPI version| |Python versions| |Build Status|
+
+This project makes it easy to add drag-and-drop ordering to any model in
+Django admin. Inlines for a sortable model may also be made sortable,
+enabling individual items or groups of items to be sortable.
+
+If you find Django Admin Sortable to be helpful, consider `buying me a
+coffee <https://www.buymeacoffee.com/NY9TUAEwF>`__!
+
+Sorting model instances with a sortable parent:
+
+.. figure:: http://res.cloudinary.com/alsoicode/image/upload/v1451237555/django-admin-sortable/sortable-models.jpg
+   :alt: sortable-models
+
+   sortable-models
+
+Sorting inlines:
+
+.. figure:: http://res.cloudinary.com/alsoicode/image/upload/v1451237555/django-admin-sortable/sortable-inlines.jpg
+   :alt: sortable-inlines
+
+   sortable-inlines
+
+Supported Django Versions
+-------------------------
+
+For Django 4 use the latest version
+
+For Django 3 use 2.2.4
+
+For Django 1.8.x < 3.0, use 2.1.8.
+
+For Django 1.5.x to 1.7.x, use version 2.0.18.
+
+Other notes of interest regarding versions
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+django-admin-sortable 1.5.2 introduced backward-incompatible changes for
+Django 1.4.x
+
+django-admin-sortable 1.6.6 introduced a backward-incompatible change
+for the ``sorting_filters`` attribute. Please convert your attributes to
+the new tuple-based format if you haven’t already.
+
+django-admin-sortable 1.7.1 and higher are compatible with Python 3.
+
+django-admin-sortable 2.1.6 has a bug. Please don’t use it :)
+
+Installation
+------------
+
+1. ``$ pip install django-admin-sortable``
+
+–or–
+
+Download django-admin-sortable from
+`source <https://github.com/iambrandontaylor/django-admin-sortable/archive/master.zip>`__
+
+1. Unzip the directory and cd into the uncompressed project directory
+
+2.
+
+   -  Optional: Enable your virtualenv
+
+3. Run ``$ python setup.py install`` or add ``adminsortable`` to your
+   PYTHONPATH.
+
+Configuration
+-------------
+
+1. Add ``adminsortable`` to your ``INSTALLED_APPS``.
+2. Ensure ``django.template.context_processors.static`` is in your
+   ``TEMPLATES["OPTIONS"]["context_processors"]``.
+
+   -  (In older versions of Django, ensure
+      ``django.core.context_processors.static`` is in
+      ``TEMPLATE_CONTEXT_PROCESSORS`` instead.)
+
+3. Ensure that ``CSRF_COOKIE_HTTPONLY`` has not been set to ``True``, as
+   django-admin-sortable is currently incompatible with that setting.
+
+Static Media
+~~~~~~~~~~~~
+
+Preferred: Use the `staticfiles
+app <https://docs.djangoproject.com/en/3.0/howto/static-files/>`__
+
+Alternate: Copy the ``adminsortable`` folder from the ``static`` folder
+to the location you serve static files from.
+
+Testing
+~~~~~~~
+
+Have a look at the included sample_project to see working examples. The
+login credentials for admin are: admin/admin
+
+When a model is sortable, a tool-area link will be added that says
+“Change Order”. Click this link, and you will be taken to the custom
+view where you can drag-and-drop the records into order.
+
+Inlines may be drag-and-dropped into any order directly from the change
+form.
+
+Usage
+-----
+
+Models
+~~~~~~
+
+To add “sortability” to a model, you need to inherit ``SortableMixin``
+and at minimum, define:
+
+-  The field which should be used for ``Meta.ordering``, which must
+   resolve to one of the integer fields defined in Django’s ORM:
+
+-  ``PositiveIntegerField``
+
+-  ``IntegerField``
+
+-  ``PositiveSmallIntegerField``
+
+-  ``SmallIntegerField``
+
+-  ``BigIntegerField``
+
+-  ``Meta.ordering`` **must only contain one value**, otherwise, your
+   objects will not be sorted correctly.
+
+-  **IMPORTANT**: You must name the field you use for ordering something
+   other than “order_field” as this name is reserved by the
+   ``SortableMixin`` class.
+
+-  It is recommended that you set ``editable=False`` and
+   ``db_index=True`` on the field defined in ``Meta.ordering`` for a
+   seamless Django admin experience and faster lookups on the objects.
+
+Sample Model:
+
+.. code:: python
+
+   # models.py
+   from adminsortable.models import SortableMixin
+
+   class MySortableClass(SortableMixin):
+       title = models.CharField(max_length=50)
+
+       class Meta:
+           verbose_name = 'My Sortable Class'
+           verbose_name_plural = 'My Sortable Classes'
+           ordering = ['the_order']
+
+
+       # define the field the model should be ordered by
+       the_order = models.PositiveIntegerField(default=0, editable=False, db_index=True)
+
+       def __unicode__(self):
+           return self.title
+
+Support for models that don’t use an ``AutoField`` for their primary key
+are also supported in version 2.0.20 or higher.
+
+Common Use Case
+^^^^^^^^^^^^^^^
+
+A common use case is to have child objects that are sortable relative to
+a parent. If your parent object is also sortable, here’s how you would
+set up your models and admin options:
+
+.. code:: python
+
+   # models.py
+   from adminsortable.fields import SortableForeignKey
+
+   class Category(SortableMixin):
+       class Meta:
+           ordering = ['category_order']
+           verbose_name_plural = 'Categories'
+
+       title = models.CharField(max_length=50)
+
+       # ordering field
+       category_order = models.PositiveIntegerField(default=0, editable=False, db_index=True)
+
+   class Project(SortableMixin):
+       class Meta:
+           ordering = ['project_order']
+
+       category = SortableForeignKey(Category)
+       title = models.CharField(max_length=50)
+
+       # ordering field
+       project_order = models.PositiveIntegerField(default=0, editable=False, db_index=True)
+
+       def __unicode__(self):
+           return self.title
+
+   # admin.py
+   from adminsortable.admin import SortableAdmin
+
+   from your_app.models import Category, Project
+
+   admin.site.register(Category, SortableAdmin)
+   admin.site.register(Project, SortableAdmin)
+
+Sometimes you might have a parent model that is not sortable, but has
+child models that are. In that case define your models and admin options
+as such:
+
+.. code:: python
+
+   from adminsortable.fields import SortableForeignKey
+
+   # models.py
+   class Category(models.Model):
+       class Meta:
+           verbose_name_plural = 'Categories'
+
+       title = models.CharField(max_length=50)
+       ...
+
+   class Project(SortableMixin):
+       class Meta:
+           ordering = ['project_order']
+
+       category = SortableForeignKey(Category)
+       title = models.CharField(max_length=50)
+
+       # ordering field
+       project_order = models.PositiveIntegerField(default=0, editable=False, db_index=True)
+
+       def __unicode__(self):
+           return self.title
+
+   # admin
+   from adminsortable.admin import NonSortableParentAdmin, SortableStackedInline
+
+   from your_app.models import Category, Project
+
+   class ProjectInline(SortableStackedInline):
+       model = Project
+       extra = 1
+
+   class CategoryAdmin(NonSortableParentAdmin):
+       inlines = [ProjectInline]
+
+   admin.site.register(Category, CategoryAdmin)
+
+The ``NonSortableParentAdmin`` class is necessary to wire up the
+additional URL patterns and JavaScript that Django Admin Sortable needs
+to make your models sortable. The child model does not have to be an
+inline model, it can be wired directly to Django admin and the objects
+will be grouped by the non-sortable foreign key when sorting.
+
+Backwards Compatibility
+~~~~~~~~~~~~~~~~~~~~~~~
+
+If you previously used Django Admin Sortable, **DON’T PANIC** -
+everything will still work exactly as before **without any changes to
+your code**. Going forward, it is recommended that you use the new
+``SortableMixin`` on your models, as pre-2.0 compatibility might not be
+a permanent thing.
+
+Please note however that the ``Sortable`` class still contains the
+hard-coded ``order`` field, and meta inheritance requirements:
+
+.. code:: python
+
+   # legacy model definition
+
+   from adminsortable.models import Sortable
+
+   class Project(Sortable):
+       class Meta(Sortable.Meta):
+           pass
+       title = models.CharField(max_length=50)
+
+       def __unicode__(self):
+           return self.title
+
+Model Instance Methods
+^^^^^^^^^^^^^^^^^^^^^^
+
+Each instance of a sortable model has two convenience methods to get the
+next or previous instance:
+
+.. code:: python
+
+       .get_next()
+       .get_previous()
+
+By default, these methods will respect their order in relation to a
+``SortableForeignKey`` field, if present. Meaning, that given the
+following data:
+
+::
+
+   | Parent Model 1 |               |
+   |                | Child Model 1 |
+   |                | Child Model 2 |
+   | Parent Model 2 |               |
+   |                | Child Model 3 |
+   |                | Child Model 4 |
+   |                | Child Model 5 |
+
+“Child Model 2” ``get_next()`` would return ``None`` “Child Model 3”
+``get_previous`` would return ``None``
+
+If you wish to override this behavior, pass in:
+``filter_on_sortable_fk=False``:
+
+.. code:: python
+
+       your_instance.get_next(filter_on_sortable_fk=False)
+
+You may also pass in additional ORM “filer_args” as a list, or
+“filter_kwargs” as a dictionary, should you need to:
+
+.. code:: python
+
+       your_instance.get_next(
+           filter_args=[Q(field1=True) | Q(field2=True)],
+           filter_kwargs={'title__icontains': 'blue'}
+       )
+
+Deprecation Warning
+^^^^^^^^^^^^^^^^^^^
+
+Previously “filter_kwargs” was named “extra_filters”. With the addition
+of “filter_args”, “extra_filters” was renamed for consistency.
+
+Adding Sorting to an existing model
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Django 1.5.x to 1.6.x
+^^^^^^^^^^^^^^^^^^^^^
+
+If you’re adding Sorting to an existing model, it is recommended that
+you use `django-south <http://south.areacode.com/>`__ to create a schema
+migration to add the “order” field to your model. You will also need to
+create a data migration in order to add the appropriate values for the
+“order” column.
+
+Example assuming a model named “Category”:
+
+.. code:: python
+
+   def forwards(self, orm):
+       for index, category in enumerate(orm.Category.objects.all()):
+           category.order = index + 1
+           category.save()
+
+See: `this
+link <http://south.readthedocs.org/en/latest/tutorial/part3.html>`__ for
+more information on South Data Migrations.
+
+Django 1.7.x or higher
+^^^^^^^^^^^^^^^^^^^^^^
+
+Since schema migrations are built into Django 1.7, you don’t have to use
+south, but the process of adding and running migrations is nearly
+identical. Take a look at the
+`Migrations <https://docs.djangoproject.com/en/1.7/topics/migrations/>`__
+documentation to get started.
+
+Django Admin Integration
+~~~~~~~~~~~~~~~~~~~~~~~~
+
+To enable sorting in the admin, you need to inherit from
+``SortableAdmin``:
+
+.. code:: python
+
+   from django.contrib import admin
+   from myapp.models import MySortableClass
+   from adminsortable.admin import SortableAdmin
+
+   class MySortableAdminClass(SortableAdmin):
+       """Any admin options you need go here"""
+
+   admin.site.register(MySortableClass, MySortableAdminClass)
+
+To enable sorting on TabularInline models, you need to inherit from
+SortableTabularInline:
+
+.. code:: python
+
+   from adminsortable.admin import SortableTabularInline
+
+   class MySortableTabularInline(SortableTabularInline):
+       """Your inline options go here"""
+
+To enable sorting on StackedInline models, you need to inherit from
+SortableStackedInline:
+
+.. code:: python
+
+   from adminsortable.admin import SortableStackedInline
+
+   class MySortableStackedInline(SortableStackedInline):
+      """Your inline options go here"""
+
+There are also generic equivalents that you can inherit from:
+
+.. code:: python
+
+   from adminsortable.admin import (SortableGenericTabularInline,
+       SortableGenericStackedInline)
+       """Your generic inline options go here"""
+
+If your parent model is *not* sortable, but has child inlines that are,
+your parent model needs to inherit from ``NonSortableParentAdmin``:
+
+.. code:: python
+
+   from adminsortable.admin import (NonSortableParentAdmin,
+       SortableTabularInline)
+
+   class ChildTabularInline(SortableTabularInline):
+       model = YourModel
+
+   class ParentAdmin(NonSortableParentAdmin):
+       inlines = [ChildTabularInline]
+
+Overriding ``queryset()``
+^^^^^^^^^^^^^^^^^^^^^^^^^
+
+django-admin-sortable supports custom queryset overrides on admin models
+and inline models in Django admin!
+
+If you’re providing an override of a SortableAdmin or Sortable inline
+model, you don’t need to do anything extra. django-admin-sortable will
+automatically honor your queryset.
+
+Have a look at the WidgetAdmin class in the sample project for an
+example of an admin class with a custom ``queryset()`` override.
+
+Overriding ``queryset()`` for an inline model
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+This is a special case, which requires a few lines of extra code to
+properly determine the sortability of your model. Example:
+
+.. code:: python
+
+   # add this import to your admin.py
+   from adminsortable.utils import get_is_sortable
+
+
+   class ComponentInline(SortableStackedInline):
+       model = Component
+
+       def queryset(self, request):
+           qs = super(ComponentInline, self).queryset(request).filter(
+               title__icontains='foo')
+
+           # You'll need to add these lines to determine if your model
+           # is sortable once we hit the change_form() for the parent model.
+
+           if get_is_sortable(qs):
+               self.model.is_sortable = True
+           else:
+               self.model.is_sortable = False
+           return qs
+
+If you override the queryset of an inline, the number of objects present
+may change, and adminsortable won’t be able to automatically determine
+if the inline model is sortable from here, which is why we have to set
+the ``is_sortable`` property of the model in this method.
+
+Sorting subsets of objects
+^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+It is also possible to sort a subset of objects in your model by adding
+a ``sorting_filters`` tuple. This works exactly the same as
+``.filter()`` on a QuerySet, and is applied *after* ``get_queryset()``
+on the admin class, allowing you to override the queryset as you would
+normally in admin but apply additional filters for sorting. The text
+“Change Order of” will appear before each filter in the Change List
+template, and the filter groups are displayed from left to right in the
+order listed. If no ``sorting_filters`` are specified, the text “Change
+Order” will be displayed for the link.
+
+Self-Referential SortableForeignKey
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+You can specify a self-referential SortableForeignKey field, however the
+admin interface will currently show a model that is a grandchild at the
+same level as a child. I’m working to resolve this issue.
+
+Important!
+''''''''''
+
+django-admin-sortable 1.6.6 introduced a backwards-incompatible change
+for ``sorting_filters``. Previously this attribute was defined as a
+dictionary, so you’ll need to change your values over to the new
+tuple-based format.
+
+An example of sorting subsets would be a “Board of Directors”. In this
+use case, you have a list of “People” objects. Some of these people are
+on the Board of Directors and some not, and you need to sort them
+independently.
+
+.. code:: python
+
+   class Person(Sortable):
+       class Meta(Sortable.Meta):
+           verbose_name_plural = 'People'
+
+       first_name = models.CharField(max_length=50)
+       last_name = models.CharField(max_length=50)
+       is_board_member = models.BooleanField('Board Member', default=False)
+
+       sorting_filters = (
+           ('Board Members', {'is_board_member': True}),
+           ('Non-Board Members', {'is_board_member': False}),
+       )
+
+       def __unicode__(self):
+           return '{} {}'.format(self.first_name, self.last_name)
+
+Extending custom templates
+^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+By default, adminsortable’s change form and change list views inherit
+from Django admin’s standard templates. Sometimes you need to have a
+custom change form or change list, but also need adminsortable’s CSS and
+JavaScript for inline models that are sortable for example.
+
+SortableAdmin has two attributes you can override for this use case:
+
+.. code:: python
+
+   change_form_template_extends
+   change_list_template_extends
+
+These attributes have default values of:
+
+.. code:: python
+
+   change_form_template_extends = 'admin/change_form.html'
+   change_list_template_extends = 'admin/change_list.html'
+
+If you need to extend the inline change form templates, you’ll need to
+select the right one, depending on your version of Django. For 1.10.x or
+below, you’ll need to extend one of the following:
+
+::
+
+   templates/adminsortable/edit_inline/stacked-1.10.x.html
+   templates/adminsortable/edit_inline/tabular-inline-1.10.x.html
+
+otherwise, extend:
+
+::
+
+   templates/adminsortable/edit_inline/stacked.html
+   templates/adminsortable/edit_inline/tabular.html
+
+A Special Note About Stacked Inlines…
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+The height of a stacked inline model can dynamically increase, which can
+make them difficult to sort. If you anticipate the height of a stacked
+inline is going to be very tall, I would suggest using
+SortableTabularInline instead.
+
+Custom JS callbacks after sorting is complete
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+If you need to define a custom event or other callback to be executed
+after sorting is completed, you’ll need to:
+
+1. Create a custom template for to add your JavaScript
+2. Populate the ``after_sorting_js_callback_name`` on your model admin
+
+An example of this can be found in the “samples” application in the
+source. Here’s a model admin for a model called “Project”:
+
+.. code:: python
+
+   class ProjectAdmin(SortableAdmin):
+       inlines = [
+           CreditInline, NoteInline, GenericNoteInline,
+           NonSortableCreditInline, NonSortableNoteInline
+       ]
+       list_display = ['__str__', 'category']
+
+       after_sorting_js_callback_name = 'afterSortCallback'  # do not include () - just function name
+       sortable_change_list_template = 'adminsortable/custom_change_list.html'
+       sortable_change_form_template = "adminsortable/custom_change_form.html"
+
+This example is going to add a custom callback on the parent model, and
+it’s inlines. Here is the JavaScript added to the custom change list:
+
+.. code:: html+django
+
+   {% extends 'adminsortable/change_list.html' %}
+
+   {% block extrahead %}
+     {{ block.super }}
+
+     <script>
+       django.jQuery(document).on('order:changed', function(event) {
+         console.log(event.message);
+         // your code here
+       });
+
+       window['{{ after_sorting_js_callback_name }}'] = function() {
+         django.jQuery(document).trigger({ type: 'order:changed', message: 'Order changed', time: new Date() });
+       };
+     </script>
+   {% endblock %}
+
+and the custom change form, for the inline models:
+
+.. code:: html+django
+
+   {% extends "adminsortable/change_form.html" %}
+
+   {% block extrahead %}
+     {{ block.super }}
+
+     <script>
+       django.jQuery(document).on('order:changed', function(event) {
+         console.log(event.message);
+         // your code here
+       });
+
+       window['{{ after_sorting_js_callback_name }}'] = function() {
+         django.jQuery(document).trigger({ type: 'order:changed', message: 'Order changed', time: new Date() });
+       };
+     </script>
+   {% endblock %}
+
+Ideally, you’d pull in a shared piece of code for your callback to keep
+your code DRY.
+
+Django-CMS integration
+~~~~~~~~~~~~~~~~~~~~~~
+
+Django-CMS plugins use their own change form, and thus won’t
+automatically include the necessary JavaScript for django-admin-sortable
+to work. Fortunately, this is easy to resolve, as the ``CMSPlugin``
+class allows a change form template to be specified:
+
+.. code:: python
+
+   # example plugin
+   from cms.plugin_base import CMSPluginBase
+
+   class CMSCarouselPlugin(CMSPluginBase):
+       admin_preview = False
+       change_form_template = 'cms/sortable-stacked-inline-change-form.html'
+       inlines = [SlideInline]
+       model = Carousel
+       name = _('Carousel')
+       render_template = 'carousels/carousel.html'
+
+       def render(self, context, instance, placeholder):
+           context.update({
+               'carousel': instance,
+               'placeholder': placeholder
+           })
+           return context
+
+   plugin_pool.register_plugin(CMSCarouselPlugin)
+
+The contents of ``sortable-stacked-inline-change-form.html`` at a
+minimum need to extend the extrahead block with:
+
+.. code:: html+django
+
+   {% extends "admin/cms/page/plugin_change_form.html" %}
+   {% load static from staticfiles %}
+
+   {% block extrahead %}
+       {{ block.super }}
+       <script src="{% static 'adminsortable/js/jquery-ui-django-admin.min.js' %}"></script>
+       <script src="{% static 'adminsortable/js/jquery.ui.touch-punch.min.js' %}"></script>
+       <script src="{% static 'adminsortable/js/jquery.django-csrf.js' %}"></script>
+       <script src="{% static 'adminsortable/js/admin.sortable.stacked.inlines.js' %}"></script>
+
+       <link rel="stylesheet" type="text/css" href="{% static 'adminsortable/css/admin.sortable.inline.css' %}" />
+   {% endblock extrahead %}
+
+Sorting within Django-CMS is really only feasible for inline models of a
+plugin as Django-CMS already includes sorting for plugin instances. For
+tabular inlines, just substitute:
+
+.. code:: html+django
+
+   <script src="{% static 'adminsortable/js/admin.sortable.stacked.inlines.js' %}"></script>
+
+with:
+
+.. code:: html+django
+
+   <script src="{% static 'adminsortable/js/admin.sortable.tabular.inlines.js' %}"></script>
+
+Notes
+~~~~~
+
+From ``django-cms 3.x`` the path of change_form.html has changed.
+Replace the follwing line:
+
+.. code:: html+django
+
+   {% extends "admin/cms/page/plugin_change_form.html" %}
+
+with
+
+.. code:: html+django
+
+   {% extends "admin/cms/page/plugin/change_form.html" %}
+
+From ``django-admin-sortable 2.0.13`` the ``jquery.django-csrf.js`` was
+removed and you have to include the snippet-template. Change the
+following line:
+
+.. code:: html+django
+
+   <script type="text/javascript" src="{% static 'adminsortable/js/jquery.django-csrf.js' %}"></script>
+
+to
+
+.. code:: html+django
+
+   {% include 'adminsortable/csrf/jquery.django-csrf.html' with csrf_cookie_name='csrftoken' %}
+
+Please note, if you change the ``CSRF_COOKIE_NAME`` you have to adjust
+``csrf_cookie_name='YOUR_CSRF_COOKIE_NAME'``
+
+Rationale
+~~~~~~~~~
+
+Other projects have added drag-and-drop ordering to the ChangeList view,
+however this introduces a couple of problems…
+
+-  The ChangeList view supports pagination, which makes drag-and-drop
+   ordering across pages impossible.
+-  The ChangeList view by default, does not order records based on a
+   foreign key, nor distinguish between rows that are associated with a
+   foreign key. This makes ordering the records grouped by a foreign key
+   impossible.
+-  The ChangeList supports in-line editing, and adding drag-and-drop
+   ordering on top of that just seemed a little much in my opinion.
+
+Status
+~~~~~~
+
+django-admin-sortable is currently used in production.
+
+What’s new in 2.3.0?
+~~~~~~~~~~~~~~~~~~~~
+
+-  Django 4 compatibility
+
+Future
+~~~~~~
+
+-  Better template support for foreign keys that are self referential.
+   If someone would like to take on rendering recursive sortables, that
+   would be super.
+
+License
+~~~~~~~
+
+django-admin-sortable is released under the Apache Public License v2.
+
+.. |PyPI version| image:: https://img.shields.io/pypi/v/django-admin-sortable.svg
+   :target: https://pypi.python.org/pypi/django-admin-sortable
+.. |Python versions| image:: https://img.shields.io/pypi/pyversions/django-admin-sortable.svg
+   :target: https://pypi.python.org/pypi/django-admin-sortable
+.. |Build Status| image:: https://travis-ci.org/alsoicode/django-admin-sortable.svg?branch=master
+   :target: https://travis-ci.org/alsoicode/django-admin-sortable
diff --git a/README.rst b/README.rst
index 9aa2fd1..3d08a92 100644
--- a/README.rst
+++ b/README.rst
@@ -1,14 +1,15 @@
 Django Admin Sortable
 =====================
 
-|Build Status|
-
-Current version: 2.0.10
+|PyPI version| |Python versions| |Build Status|
 
 This project makes it easy to add drag-and-drop ordering to any model in
 Django admin. Inlines for a sortable model may also be made sortable,
 enabling individual items or groups of items to be sortable.
 
+If you find Django Admin Sortable to be helpful, consider `buying me a
+coffee <https://www.buymeacoffee.com/NY9TUAEwF>`__!
+
 Sorting model instances with a sortable parent:
 
 .. figure:: http://res.cloudinary.com/alsoicode/image/upload/v1451237555/django-admin-sortable/sortable-models.jpg
@@ -26,31 +27,41 @@ Sorting inlines:
 Supported Django Versions
 -------------------------
 
-If you're using Django 1.4.x, use django-admin-sortable 1.4.9 or below.
-For Django 1.5.x or higher, use the latest version of
-django-admin-sortable.
+For Django 4 use the latest version
+
+For Django 3 use 2.2.4
+
+For Django 1.8.x < 3.0, use 2.1.8.
+
+For Django 1.5.x to 1.7.x, use version 2.0.18.
+
+Other notes of interest regarding versions
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 
 django-admin-sortable 1.5.2 introduced backward-incompatible changes for
 Django 1.4.x
 
 django-admin-sortable 1.6.6 introduced a backward-incompatible change
 for the ``sorting_filters`` attribute. Please convert your attributes to
-the new tuple-based format.
+the new tuple-based format if you haven’t already.
 
 django-admin-sortable 1.7.1 and higher are compatible with Python 3.
 
+django-admin-sortable 2.1.6 has a bug. Please don’t use it :)
+
 Installation
 ------------
 
 1. ``$ pip install django-admin-sortable``
 
---or--
+–or–
 
 Download django-admin-sortable from
 `source <https://github.com/iambrandontaylor/django-admin-sortable/archive/master.zip>`__
 
 1. Unzip the directory and cd into the uncompressed project directory
-2. 
+
+2.
 
    -  Optional: Enable your virtualenv
 
@@ -61,14 +72,21 @@ Configuration
 -------------
 
 1. Add ``adminsortable`` to your ``INSTALLED_APPS``.
-2. Ensure ``django.core.context_processors.static`` is in your
-   ``TEMPLATE_CONTEXT_PROCESSORS``.
+2. Ensure ``django.template.context_processors.static`` is in your
+   ``TEMPLATES["OPTIONS"]["context_processors"]``.
+
+   -  (In older versions of Django, ensure
+      ``django.core.context_processors.static`` is in
+      ``TEMPLATE_CONTEXT_PROCESSORS`` instead.)
+
+3. Ensure that ``CSRF_COOKIE_HTTPONLY`` has not been set to ``True``, as
+   django-admin-sortable is currently incompatible with that setting.
 
 Static Media
 ~~~~~~~~~~~~
 
 Preferred: Use the `staticfiles
-app <https://docs.djangoproject.com/en/1.6/ref/contrib/staticfiles/>`__
+app <https://docs.djangoproject.com/en/3.0/howto/static-files/>`__
 
 Alternate: Copy the ``adminsortable`` folder from the ``static`` folder
 to the location you serve static files from.
@@ -76,11 +94,11 @@ to the location you serve static files from.
 Testing
 ~~~~~~~
 
-Have a look at the included sample\_project to see working examples. The
+Have a look at the included sample_project to see working examples. The
 login credentials for admin are: admin/admin
 
 When a model is sortable, a tool-area link will be added that says
-"Change Order". Click this link, and you will be taken to the custom
+“Change Order”. Click this link, and you will be taken to the custom
 view where you can drag-and-drop the records into order.
 
 Inlines may be drag-and-dropped into any order directly from the change
@@ -92,129 +110,143 @@ Usage
 Models
 ~~~~~~
 
-To add "sortability" to a model, you need to inherit ``SortableMixin``
+To add “sortability” to a model, you need to inherit ``SortableMixin``
 and at minimum, define:
 
 -  The field which should be used for ``Meta.ordering``, which must
-   resolve to one of the integer fields defined in Django's ORM:
+   resolve to one of the integer fields defined in Django’s ORM:
+
 -  ``PositiveIntegerField``
+
 -  ``IntegerField``
+
 -  ``PositiveSmallIntegerField``
+
 -  ``SmallIntegerField``
+
 -  ``BigIntegerField``
 
 -  ``Meta.ordering`` **must only contain one value**, otherwise, your
    objects will not be sorted correctly.
+
+-  **IMPORTANT**: You must name the field you use for ordering something
+   other than “order_field” as this name is reserved by the
+   ``SortableMixin`` class.
+
 -  It is recommended that you set ``editable=False`` and
    ``db_index=True`` on the field defined in ``Meta.ordering`` for a
    seamless Django admin experience and faster lookups on the objects.
 
 Sample Model:
 
-::
+.. code:: python
+
+   # models.py
+   from adminsortable.models import SortableMixin
+
+   class MySortableClass(SortableMixin):
+       title = models.CharField(max_length=50)
 
-    # models.py
-    from adminsortable.models import SortableMixin
+       class Meta:
+           verbose_name = 'My Sortable Class'
+           verbose_name_plural = 'My Sortable Classes'
+           ordering = ['the_order']
 
-    class MySortableClass(SortableMixin):
-        class Meta:
-            verbose_name = 'My Sortable Class'
-            verbose_name_plural = 'My Sortable Classes'
-            ordering = ['the_order']
 
-        title = models.CharField(max_length=50)
+       # define the field the model should be ordered by
+       the_order = models.PositiveIntegerField(default=0, editable=False, db_index=True)
 
-        # define the field the model should be ordered by
-        the_order = models.PositiveIntegerField(default=0, editable=False, db_index=True)
+       def __unicode__(self):
+           return self.title
 
-        def __unicode__(self):
-            return self.title
+Support for models that don’t use an ``AutoField`` for their primary key
+are also supported in version 2.0.20 or higher.
 
 Common Use Case
 ^^^^^^^^^^^^^^^
 
 A common use case is to have child objects that are sortable relative to
-a parent. If your parent object is also sortable, here's how you would
+a parent. If your parent object is also sortable, here’s how you would
 set up your models and admin options:
 
-::
+.. code:: python
 
-    # models.py
-    from adminsortable.fields import SortableForeignKey
+   # models.py
+   from adminsortable.fields import SortableForeignKey
 
-    class Category(SortableMixin):
-        class Meta:
-            ordering = ['category_order']
-            verbose_name_plural = 'Categories'
+   class Category(SortableMixin):
+       class Meta:
+           ordering = ['category_order']
+           verbose_name_plural = 'Categories'
 
-        title = models.CharField(max_length=50)
+       title = models.CharField(max_length=50)
 
-        # ordering field
-        category_order = models.PositiveIntegerField(default=0, editable=False, db_index=True)
+       # ordering field
+       category_order = models.PositiveIntegerField(default=0, editable=False, db_index=True)
 
-    class Project(SortableMixin):
-        class Meta:
-            ordering = ['project_order']
+   class Project(SortableMixin):
+       class Meta:
+           ordering = ['project_order']
 
-        category = SortableForeignKey(Category)
-        title = models.CharField(max_length=50)
+       category = SortableForeignKey(Category)
+       title = models.CharField(max_length=50)
 
-        # ordering field
-        project_order = models.PositiveIntegerField(default=0, editable=False, db_index=True)
+       # ordering field
+       project_order = models.PositiveIntegerField(default=0, editable=False, db_index=True)
 
-        def __unicode__(self):
-            return self.title
+       def __unicode__(self):
+           return self.title
 
-    # admin.py
-    from adminsortable.admin import SortableAdmin
+   # admin.py
+   from adminsortable.admin import SortableAdmin
 
-    from your_app.models import Category, Project
+   from your_app.models import Category, Project
 
-    admin.site.register(Category, SortableAdmin)
-    admin.site.register(Project, SortableAdmin)
+   admin.site.register(Category, SortableAdmin)
+   admin.site.register(Project, SortableAdmin)
 
 Sometimes you might have a parent model that is not sortable, but has
 child models that are. In that case define your models and admin options
 as such:
 
-::
+.. code:: python
 
-    from adminsortable.fields import SortableForeignKey
+   from adminsortable.fields import SortableForeignKey
 
-    # models.py
-    class Category(models.Model):
-        class Meta:
-            verbose_name_plural = 'Categories'
+   # models.py
+   class Category(models.Model):
+       class Meta:
+           verbose_name_plural = 'Categories'
 
-        title = models.CharField(max_length=50)
-        ...
+       title = models.CharField(max_length=50)
+       ...
 
-    class Project(SortableMixin):
-        class Meta:
-            ordering = ['project_order']
+   class Project(SortableMixin):
+       class Meta:
+           ordering = ['project_order']
 
-        category = SortableForeignKey(Category)
-        title = models.CharField(max_length=50)
+       category = SortableForeignKey(Category)
+       title = models.CharField(max_length=50)
 
-        # ordering field
-        project_order = models.PositiveIntegerField(default=0, editable=False, db_index=True)
+       # ordering field
+       project_order = models.PositiveIntegerField(default=0, editable=False, db_index=True)
 
-        def __unicode__(self):
-            return self.title
+       def __unicode__(self):
+           return self.title
 
-    # admin
-    from adminsortable.admin import NonSortableParentAdmin, SortableStackedInline
+   # admin
+   from adminsortable.admin import NonSortableParentAdmin, SortableStackedInline
 
-    from your_app.models import Category, Project
+   from your_app.models import Category, Project
 
-    class ProjectInline(SortableStackedInline):
-        model = Project
-        extra = 1
+   class ProjectInline(SortableStackedInline):
+       model = Project
+       extra = 1
 
-    class CategoryAdmin(NonSortableParentAdmin):
-        inlines = [ProjectInline]
+   class CategoryAdmin(NonSortableParentAdmin):
+       inlines = [ProjectInline]
 
-    admin.site.register(Category, CategoryAdmin)
+   admin.site.register(Category, CategoryAdmin)
 
 The ``NonSortableParentAdmin`` class is necessary to wire up the
 additional URL patterns and JavaScript that Django Admin Sortable needs
@@ -225,28 +257,28 @@ will be grouped by the non-sortable foreign key when sorting.
 Backwards Compatibility
 ~~~~~~~~~~~~~~~~~~~~~~~
 
-If you previously used Django Admin Sortable, **DON'T PANIC** -
-everything will still work exactly as before ***without any changes to
-your code***. Going forward, it is recommended that you use the new
+If you previously used Django Admin Sortable, **DON’T PANIC** -
+everything will still work exactly as before **without any changes to
+your code**. Going forward, it is recommended that you use the new
 ``SortableMixin`` on your models, as pre-2.0 compatibility might not be
 a permanent thing.
 
 Please note however that the ``Sortable`` class still contains the
 hard-coded ``order`` field, and meta inheritance requirements:
 
-::
+.. code:: python
 
-    # legacy model definition
+   # legacy model definition
 
-    from adminsortable.models import Sortable
+   from adminsortable.models import Sortable
 
-    class Project(Sortable):
-        class Meta(Sortable.Meta):
-            pass
-        title = models.CharField(max_length=50)
+   class Project(Sortable):
+       class Meta(Sortable.Meta):
+           pass
+       title = models.CharField(max_length=50)
 
-        def __unicode__(self):
-            return self.title
+       def __unicode__(self):
+           return self.title
 
 Model Instance Methods
 ^^^^^^^^^^^^^^^^^^^^^^
@@ -254,10 +286,10 @@ Model Instance Methods
 Each instance of a sortable model has two convenience methods to get the
 next or previous instance:
 
-::
+.. code:: python
 
-    .get_next()
-    .get_previous()
+       .get_next()
+       .get_previous()
 
 By default, these methods will respect their order in relation to a
 ``SortableForeignKey`` field, if present. Meaning, that given the
@@ -265,51 +297,60 @@ following data:
 
 ::
 
-    | Parent Model 1 |               |
-    |                | Child Model 1 |
-    |                | Child Model 2 |
-    | Parent Model 2 |               |
-    |                | Child Model 3 |
-    |                | Child Model 4 |
-    |                | Child Model 5 |
+   | Parent Model 1 |               |
+   |                | Child Model 1 |
+   |                | Child Model 2 |
+   | Parent Model 2 |               |
+   |                | Child Model 3 |
+   |                | Child Model 4 |
+   |                | Child Model 5 |
 
-"Child Model 2" ``get_next()`` would return ``None`` "Child Model 3"
+“Child Model 2” ``get_next()`` would return ``None`` “Child Model 3”
 ``get_previous`` would return ``None``
 
 If you wish to override this behavior, pass in:
 ``filter_on_sortable_fk=False``:
 
-::
+.. code:: python
 
-    your_instance.get_next(filter_on_sortable_fk=False)
+       your_instance.get_next(filter_on_sortable_fk=False)
 
-You may also pass in additional ORM "extra\_filters" as a dictionary,
-should you need to:
+You may also pass in additional ORM “filer_args” as a list, or
+“filter_kwargs” as a dictionary, should you need to:
 
-::
+.. code:: python
+
+       your_instance.get_next(
+           filter_args=[Q(field1=True) | Q(field2=True)],
+           filter_kwargs={'title__icontains': 'blue'}
+       )
+
+Deprecation Warning
+^^^^^^^^^^^^^^^^^^^
 
-    your_instance.get_next(extra_filters={'title__icontains': 'blue'})
+Previously “filter_kwargs” was named “extra_filters”. With the addition
+of “filter_args”, “extra_filters” was renamed for consistency.
 
 Adding Sorting to an existing model
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 
-Django 1.6.x or below
+Django 1.5.x to 1.6.x
 ^^^^^^^^^^^^^^^^^^^^^
 
-If you're adding Sorting to an existing model, it is recommended that
+If you’re adding Sorting to an existing model, it is recommended that
 you use `django-south <http://south.areacode.com/>`__ to create a schema
-migration to add the "order" field to your model. You will also need to
+migration to add the “order” field to your model. You will also need to
 create a data migration in order to add the appropriate values for the
-"order" column.
+“order” column.
 
-Example assuming a model named "Category":
+Example assuming a model named “Category”:
 
-::
+.. code:: python
 
-    def forwards(self, orm):
-        for index, category in enumerate(orm.Category.objects.all()):
-            category.order = index + 1
-            category.save()
+   def forwards(self, orm):
+       for index, category in enumerate(orm.Category.objects.all()):
+           category.order = index + 1
+           category.save()
 
 See: `this
 link <http://south.readthedocs.org/en/latest/tutorial/part3.html>`__ for
@@ -318,7 +359,7 @@ more information on South Data Migrations.
 Django 1.7.x or higher
 ^^^^^^^^^^^^^^^^^^^^^^
 
-Since schema migrations are built into Django 1.7, you don't have to use
+Since schema migrations are built into Django 1.7, you don’t have to use
 south, but the process of adding and running migrations is nearly
 identical. Take a look at the
 `Migrations <https://docs.djangoproject.com/en/1.7/topics/migrations/>`__
@@ -330,58 +371,58 @@ Django Admin Integration
 To enable sorting in the admin, you need to inherit from
 ``SortableAdmin``:
 
-::
+.. code:: python
 
-    from django.contrib import admin
-    from myapp.models import MySortableClass
-    from adminsortable.admin import SortableAdmin
+   from django.contrib import admin
+   from myapp.models import MySortableClass
+   from adminsortable.admin import SortableAdmin
 
-    class MySortableAdminClass(SortableAdmin):
-        """Any admin options you need go here"""
+   class MySortableAdminClass(SortableAdmin):
+       """Any admin options you need go here"""
 
-    admin.site.register(MySortableClass, MySortableAdminClass)
+   admin.site.register(MySortableClass, MySortableAdminClass)
 
 To enable sorting on TabularInline models, you need to inherit from
 SortableTabularInline:
 
-::
+.. code:: python
 
-    from adminsortable.admin import SortableTabularInline
+   from adminsortable.admin import SortableTabularInline
 
-    class MySortableTabularInline(SortableTabularInline):
+   class MySortableTabularInline(SortableTabularInline):
        """Your inline options go here"""
 
 To enable sorting on StackedInline models, you need to inherit from
 SortableStackedInline:
 
-::
+.. code:: python
 
-    from adminsortable.admin import SortableStackedInline
+   from adminsortable.admin import SortableStackedInline
 
-    class MySortableStackedInline(SortableStackedInline):
-       """Your inline options go here"""
+   class MySortableStackedInline(SortableStackedInline):
+      """Your inline options go here"""
 
 There are also generic equivalents that you can inherit from:
 
-::
+.. code:: python
 
-    from adminsortable.admin import (SortableGenericTabularInline,
-        SortableGenericStackedInline)
-        """Your generic inline options go here"""
+   from adminsortable.admin import (SortableGenericTabularInline,
+       SortableGenericStackedInline)
+       """Your generic inline options go here"""
 
 If your parent model is *not* sortable, but has child inlines that are,
 your parent model needs to inherit from ``NonSortableParentAdmin``:
 
-::
+.. code:: python
 
-    from adminsortable.admin import (NonSortableParentAdmin,
-        SortableTabularInline)
+   from adminsortable.admin import (NonSortableParentAdmin,
+       SortableTabularInline)
 
-    class ChildTabularInline(SortableTabularInline):
-        model = YourModel
+   class ChildTabularInline(SortableTabularInline):
+       model = YourModel
 
-    class ParentAdmin(NonSortableParentAdmin):
-        inlines = [ChildTabularInline]
+   class ParentAdmin(NonSortableParentAdmin):
+       inlines = [ChildTabularInline]
 
 Overriding ``queryset()``
 ^^^^^^^^^^^^^^^^^^^^^^^^^
@@ -389,8 +430,8 @@ Overriding ``queryset()``
 django-admin-sortable supports custom queryset overrides on admin models
 and inline models in Django admin!
 
-If you're providing an override of a SortableAdmin or Sortable inline
-model, you don't need to do anything extra. django-admin-sortable will
+If you’re providing an override of a SortableAdmin or Sortable inline
+model, you don’t need to do anything extra. django-admin-sortable will
 automatically honor your queryset.
 
 Have a look at the WidgetAdmin class in the sample project for an
@@ -402,30 +443,30 @@ Overriding ``queryset()`` for an inline model
 This is a special case, which requires a few lines of extra code to
 properly determine the sortability of your model. Example:
 
-::
+.. code:: python
 
-    # add this import to your admin.py
-    from adminsortable.utils import get_is_sortable
+   # add this import to your admin.py
+   from adminsortable.utils import get_is_sortable
 
 
-    class ComponentInline(SortableStackedInline):
-        model = Component
+   class ComponentInline(SortableStackedInline):
+       model = Component
 
-        def queryset(self, request):
-            qs = super(ComponentInline, self).queryset(request).filter(
-                title__icontains='foo')
+       def queryset(self, request):
+           qs = super(ComponentInline, self).queryset(request).filter(
+               title__icontains='foo')
 
-            # You'll need to add these lines to determine if your model
-            # is sortable once we hit the change_form() for the parent model.
+           # You'll need to add these lines to determine if your model
+           # is sortable once we hit the change_form() for the parent model.
 
-            if get_is_sortable(qs):
-                self.model.is_sortable = True
-            else:
-                self.model.is_sortable = False
-            return qs
+           if get_is_sortable(qs):
+               self.model.is_sortable = True
+           else:
+               self.model.is_sortable = False
+           return qs
 
 If you override the queryset of an inline, the number of objects present
-may change, and adminsortable won't be able to automatically determine
+may change, and adminsortable won’t be able to automatically determine
 if the inline model is sortable from here, which is why we have to set
 the ``is_sortable`` property of the model in this method.
 
@@ -437,161 +478,266 @@ a ``sorting_filters`` tuple. This works exactly the same as
 ``.filter()`` on a QuerySet, and is applied *after* ``get_queryset()``
 on the admin class, allowing you to override the queryset as you would
 normally in admin but apply additional filters for sorting. The text
-"Change Order of" will appear before each filter in the Change List
+“Change Order of” will appear before each filter in the Change List
 template, and the filter groups are displayed from left to right in the
-order listed. If no ``sorting_filters`` are specified, the text "Change
-Order" will be displayed for the link.
+order listed. If no ``sorting_filters`` are specified, the text “Change
+Order” will be displayed for the link.
 
 Self-Referential SortableForeignKey
 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
 
 You can specify a self-referential SortableForeignKey field, however the
 admin interface will currently show a model that is a grandchild at the
-same level as a child. I'm working to resolve this issue.
+same level as a child. I’m working to resolve this issue.
 
 Important!
 ''''''''''
 
 django-admin-sortable 1.6.6 introduced a backwards-incompatible change
 for ``sorting_filters``. Previously this attribute was defined as a
-dictionary, so you'll need to change your values over to the new
+dictionary, so you’ll need to change your values over to the new
 tuple-based format.
 
-An example of sorting subsets would be a "Board of Directors". In this
-use case, you have a list of "People" objects. Some of these people are
+An example of sorting subsets would be a “Board of Directors”. In this
+use case, you have a list of “People” objects. Some of these people are
 on the Board of Directors and some not, and you need to sort them
 independently.
 
-::
+.. code:: python
 
-    class Person(Sortable):
-        class Meta(Sortable.Meta):
-            verbose_name_plural = 'People'
+   class Person(Sortable):
+       class Meta(Sortable.Meta):
+           verbose_name_plural = 'People'
 
-        first_name = models.CharField(max_length=50)
-        last_name = models.CharField(max_length=50)
-        is_board_member = models.BooleanField('Board Member', default=False)
+       first_name = models.CharField(max_length=50)
+       last_name = models.CharField(max_length=50)
+       is_board_member = models.BooleanField('Board Member', default=False)
 
-        sorting_filters = (
-            ('Board Members', {'is_board_member': True}),
-            ('Non-Board Members', {'is_board_member': False}),
-        )
+       sorting_filters = (
+           ('Board Members', {'is_board_member': True}),
+           ('Non-Board Members', {'is_board_member': False}),
+       )
 
-        def __unicode__(self):
-            return '{} {}'.format(self.first_name, self.last_name)
+       def __unicode__(self):
+           return '{} {}'.format(self.first_name, self.last_name)
 
 Extending custom templates
 ^^^^^^^^^^^^^^^^^^^^^^^^^^
 
-By default, adminsortable's change form and change list views inherit
-from Django admin's standard templates. Sometimes you need to have a
-custom change form or change list, but also need adminsortable's CSS and
+By default, adminsortable’s change form and change list views inherit
+from Django admin’s standard templates. Sometimes you need to have a
+custom change form or change list, but also need adminsortable’s CSS and
 JavaScript for inline models that are sortable for example.
 
 SortableAdmin has two attributes you can override for this use case:
 
-::
+.. code:: python
 
-    change_form_template_extends
-    change_list_template_extends
+   change_form_template_extends
+   change_list_template_extends
 
 These attributes have default values of:
 
-::
+.. code:: python
 
-    change_form_template_extends = 'admin/change_form.html'
-    change_list_template_extends = 'admin/change_list.html'
+   change_form_template_extends = 'admin/change_form.html'
+   change_list_template_extends = 'admin/change_list.html'
 
-If you need to extend the inline change form templates, you'll need to
-select the right one, depending on your version of Django. For Django
-1.5.x or below, you'll need to extend one of the following:
+If you need to extend the inline change form templates, you’ll need to
+select the right one, depending on your version of Django. For 1.10.x or
+below, you’ll need to extend one of the following:
 
 ::
 
-    templates/adminsortable/edit_inline/stacked-1.5.x.html
-    templates/adminsortable/edit_inline/tabular-inline-1.5.x.html
+   templates/adminsortable/edit_inline/stacked-1.10.x.html
+   templates/adminsortable/edit_inline/tabular-inline-1.10.x.html
 
-For Django 1.6.x, extend:
+otherwise, extend:
 
 ::
 
-    templates/adminsortable/edit_inline/stacked.html
-    templates/adminsortable/edit_inline/tabular.html
+   templates/adminsortable/edit_inline/stacked.html
+   templates/adminsortable/edit_inline/tabular.html
 
-A Special Note About Stacked Inlines...
-^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+A Special Note About Stacked Inlines…
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
 
 The height of a stacked inline model can dynamically increase, which can
 make them difficult to sort. If you anticipate the height of a stacked
 inline is going to be very tall, I would suggest using
 SortableTabularInline instead.
 
+Custom JS callbacks after sorting is complete
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+If you need to define a custom event or other callback to be executed
+after sorting is completed, you’ll need to:
+
+1. Create a custom template for to add your JavaScript
+2. Populate the ``after_sorting_js_callback_name`` on your model admin
+
+An example of this can be found in the “samples” application in the
+source. Here’s a model admin for a model called “Project”:
+
+.. code:: python
+
+   class ProjectAdmin(SortableAdmin):
+       inlines = [
+           CreditInline, NoteInline, GenericNoteInline,
+           NonSortableCreditInline, NonSortableNoteInline
+       ]
+       list_display = ['__str__', 'category']
+
+       after_sorting_js_callback_name = 'afterSortCallback'  # do not include () - just function name
+       sortable_change_list_template = 'adminsortable/custom_change_list.html'
+       sortable_change_form_template = "adminsortable/custom_change_form.html"
+
+This example is going to add a custom callback on the parent model, and
+it’s inlines. Here is the JavaScript added to the custom change list:
+
+.. code:: html+django
+
+   {% extends 'adminsortable/change_list.html' %}
+
+   {% block extrahead %}
+     {{ block.super }}
+
+     <script>
+       django.jQuery(document).on('order:changed', function(event) {
+         console.log(event.message);
+         // your code here
+       });
+
+       window['{{ after_sorting_js_callback_name }}'] = function() {
+         django.jQuery(document).trigger({ type: 'order:changed', message: 'Order changed', time: new Date() });
+       };
+     </script>
+   {% endblock %}
+
+and the custom change form, for the inline models:
+
+.. code:: html+django
+
+   {% extends "adminsortable/change_form.html" %}
+
+   {% block extrahead %}
+     {{ block.super }}
+
+     <script>
+       django.jQuery(document).on('order:changed', function(event) {
+         console.log(event.message);
+         // your code here
+       });
+
+       window['{{ after_sorting_js_callback_name }}'] = function() {
+         django.jQuery(document).trigger({ type: 'order:changed', message: 'Order changed', time: new Date() });
+       };
+     </script>
+   {% endblock %}
+
+Ideally, you’d pull in a shared piece of code for your callback to keep
+your code DRY.
+
 Django-CMS integration
 ~~~~~~~~~~~~~~~~~~~~~~
 
-Django-CMS plugins use their own change form, and thus won't
+Django-CMS plugins use their own change form, and thus won’t
 automatically include the necessary JavaScript for django-admin-sortable
 to work. Fortunately, this is easy to resolve, as the ``CMSPlugin``
 class allows a change form template to be specified:
 
-::
+.. code:: python
 
-    # example plugin
-    from cms.plugin_base import CMSPluginBase
+   # example plugin
+   from cms.plugin_base import CMSPluginBase
 
-    class CMSCarouselPlugin(CMSPluginBase):
-        admin_preview = False
-        change_form_template = 'cms/sortable-stacked-inline-change-form.html'
-        inlines = [SlideInline]
-        model = Carousel
-        name = _('Carousel')
-        render_template = 'carousels/carousel.html'
+   class CMSCarouselPlugin(CMSPluginBase):
+       admin_preview = False
+       change_form_template = 'cms/sortable-stacked-inline-change-form.html'
+       inlines = [SlideInline]
+       model = Carousel
+       name = _('Carousel')
+       render_template = 'carousels/carousel.html'
 
-        def render(self, context, instance, placeholder):
-            context.update({
-                'carousel': instance,
-                'placeholder': placeholder
-            })
-            return context
+       def render(self, context, instance, placeholder):
+           context.update({
+               'carousel': instance,
+               'placeholder': placeholder
+           })
+           return context
 
-    plugin_pool.register_plugin(CMSCarouselPlugin)
+   plugin_pool.register_plugin(CMSCarouselPlugin)
 
 The contents of ``sortable-stacked-inline-change-form.html`` at a
 minimum need to extend the extrahead block with:
 
-::
+.. code:: html+django
 
-    {% extends "admin/cms/page/plugin_change_form.html" %}
-    {% load static from staticfiles %}
+   {% extends "admin/cms/page/plugin_change_form.html" %}
+   {% load static from staticfiles %}
 
-    {% block extrahead %}
-        {{ block.super }}
-        <script type="text/javascript" src="{% static 'adminsortable/js/jquery-ui-django-admin.min.js' %}"></script>
-        <script type="text/javascript" src="{% static 'adminsortable/js/jquery.django-csrf.js' %}"></script>
-        <script type="text/javascript" src="{% static 'adminsortable/js/admin.sortable.stacked.inlines.js' %}"></script>
+   {% block extrahead %}
+       {{ block.super }}
+       <script src="{% static 'adminsortable/js/jquery-ui-django-admin.min.js' %}"></script>
+       <script src="{% static 'adminsortable/js/jquery.ui.touch-punch.min.js' %}"></script>
+       <script src="{% static 'adminsortable/js/jquery.django-csrf.js' %}"></script>
+       <script src="{% static 'adminsortable/js/admin.sortable.stacked.inlines.js' %}"></script>
 
-        <link rel="stylesheet" type="text/css" href="{% static 'adminsortable/css/admin.sortable.inline.css' %}" />
-    {% endblock extrahead %}
+       <link rel="stylesheet" type="text/css" href="{% static 'adminsortable/css/admin.sortable.inline.css' %}" />
+   {% endblock extrahead %}
 
 Sorting within Django-CMS is really only feasible for inline models of a
 plugin as Django-CMS already includes sorting for plugin instances. For
 tabular inlines, just substitute:
 
-::
+.. code:: html+django
 
-    <script src="{% static 'adminsortable/js/admin.sortable.stacked.inlines.js' %}"></script>
+   <script src="{% static 'adminsortable/js/admin.sortable.stacked.inlines.js' %}"></script>
 
 with:
 
-::
+.. code:: html+django
+
+   <script src="{% static 'adminsortable/js/admin.sortable.tabular.inlines.js' %}"></script>
+
+Notes
+~~~~~
+
+From ``django-cms 3.x`` the path of change_form.html has changed.
+Replace the follwing line:
+
+.. code:: html+django
+
+   {% extends "admin/cms/page/plugin_change_form.html" %}
+
+with
+
+.. code:: html+django
+
+   {% extends "admin/cms/page/plugin/change_form.html" %}
+
+From ``django-admin-sortable 2.0.13`` the ``jquery.django-csrf.js`` was
+removed and you have to include the snippet-template. Change the
+following line:
+
+.. code:: html+django
+
+   <script type="text/javascript" src="{% static 'adminsortable/js/jquery.django-csrf.js' %}"></script>
+
+to
+
+.. code:: html+django
+
+   {% include 'adminsortable/csrf/jquery.django-csrf.html' with csrf_cookie_name='csrftoken' %}
 
-    <script src="{% static 'adminsortable/js/admin.sortable.tabular.inlines.js' %}"></script>
+Please note, if you change the ``CSRF_COOKIE_NAME`` you have to adjust
+``csrf_cookie_name='YOUR_CSRF_COOKIE_NAME'``
 
 Rationale
 ~~~~~~~~~
 
 Other projects have added drag-and-drop ordering to the ChangeList view,
-however this introduces a couple of problems...
+however this introduces a couple of problems…
 
 -  The ChangeList view supports pagination, which makes drag-and-drop
    ordering across pages impossible.
@@ -607,11 +753,10 @@ Status
 
 django-admin-sortable is currently used in production.
 
-What's new in 2.0.10?
-~~~~~~~~~~~~~~~~~~~~~
+What’s new in 2.3.0?
+~~~~~~~~~~~~~~~~~~~~
 
--  Bugfix for accessing custom ``order`` property of model. Thanks
-   [@theithec](https://github.com/theithec) for reporting the issue.
+-  Django 4 compatibility
 
 Future
 ~~~~~~
@@ -625,5 +770,9 @@ License
 
 django-admin-sortable is released under the Apache Public License v2.
 
-.. |Build Status| image:: https://travis-ci.org/iambrandontaylor/django-admin-sortable.svg?branch=master
-   :target: https://travis-ci.org/iambrandontaylor/django-admin-sortable
+.. |PyPI version| image:: https://img.shields.io/pypi/v/django-admin-sortable.svg
+   :target: https://pypi.python.org/pypi/django-admin-sortable
+.. |Python versions| image:: https://img.shields.io/pypi/pyversions/django-admin-sortable.svg
+   :target: https://pypi.python.org/pypi/django-admin-sortable
+.. |Build Status| image:: https://travis-ci.org/alsoicode/django-admin-sortable.svg?branch=master
+   :target: https://travis-ci.org/alsoicode/django-admin-sortable
diff --git a/adminsortable/__init__.py b/adminsortable/__init__.py
index 25cbabc..fbbe635 100644
--- a/adminsortable/__init__.py
+++ b/adminsortable/__init__.py
@@ -1,4 +1,4 @@
-VERSION = (2, 0, 10)
+VERSION = (2, 3, 0)
 DEV_N = None
 
 
diff --git a/adminsortable/admin.py b/adminsortable/admin.py
old mode 100755
new mode 100644
index 123e9a7..0943186
--- a/adminsortable/admin.py
+++ b/adminsortable/admin.py
@@ -1,31 +1,22 @@
 import json
-
-from django import VERSION
+from urllib.parse import urlencode
 
 from django.conf import settings
-
-if VERSION > (1, 7):
-    from django.conf.urls import url
-elif VERSION > (1, 5):
-    from django.conf.urls import patterns, url
-else:
-    from django.conf.urls.defaults import patterns, url
-
 from django.contrib.admin import ModelAdmin, TabularInline, StackedInline
 from django.contrib.admin.options import InlineModelAdmin
-
-if VERSION >= (1, 8):
-    from django.contrib.auth import get_permission_codename
-    from django.contrib.contenttypes.admin import (GenericStackedInline,
-        GenericTabularInline)
-else:
-    from django.contrib.contenttypes.generic import (GenericStackedInline,
-        GenericTabularInline)
-
+from django.contrib.admin.views.main import IGNORED_PARAMS, PAGE_VAR
+from django.contrib.contenttypes.admin import (GenericStackedInline,
+                                               GenericTabularInline)
 from django.contrib.contenttypes.models import ContentType
-from django.http import HttpResponse
+from django.core.exceptions import PermissionDenied
+from django.db import transaction
+from django.http import JsonResponse
 from django.shortcuts import render
 from django.template.defaultfilters import capfirst
+from django.urls import re_path
+from django.utils.decorators import method_decorator
+from django.utils.translation import gettext as _
+from django.views.decorators.http import require_POST
 
 from adminsortable.fields import SortableForeignKey
 from adminsortable.models import SortableMixin
@@ -43,6 +34,17 @@ class SortableAdminBase(object):
     change_form_template_extends = 'admin/change_form.html'
     change_list_template_extends = 'admin/change_list.html'
 
+    after_sorting_js_callback_name = None
+
+    def get_querystring_filters(self, request):
+        filters = {}
+
+        for k, v in request.GET.items():
+            if k not in IGNORED_PARAMS and k != PAGE_VAR:
+                filters[k] = v
+
+        return filters
+
     def changelist_view(self, request, extra_context=None):
         """
         If the model that inherits Sortable has more than one object,
@@ -50,12 +52,15 @@ class SortableAdminBase(object):
         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
+        # apply any filters via the querystring
+        filters = self.get_querystring_filters(request)
+
+        # Check if the filtered queryset contains more than 1 item
+        # to enable sort link
+        queryset = self.get_queryset(request).filter(**filters)
+        self.is_sortable = False
 
-        if get_is_sortable(qs_method(request)):
+        if get_is_sortable(queryset):
             self.change_list_template = \
                 self.sortable_change_list_with_sort_link_template
             self.is_sortable = True
@@ -66,11 +71,18 @@ class SortableAdminBase(object):
         extra_context.update({
             'change_list_template_extends': self.change_list_template_extends,
             'sorting_filters': [sort_filter[0] for sort_filter
-                in getattr(self.model, 'sorting_filters', [])]
+                in getattr(self.model, 'sorting_filters', [])],
+            'is_sortable': self.is_sortable
         })
+
         return super(SortableAdminBase, self).changelist_view(request,
             extra_context=extra_context)
 
+    # override this function in your SortableAdmin if you need to do something
+    # after sorting has occurred
+    def after_sorting(self):
+        pass
+
 
 class SortableAdmin(SortableAdminBase, ModelAdmin):
     """
@@ -81,53 +93,55 @@ class SortableAdmin(SortableAdminBase, ModelAdmin):
     class Meta:
         abstract = True
 
+    @property
+    def has_sortable_tabular_inlines(self):
+        base_classes = (SortableTabularInline, SortableGenericTabularInline)
+        return any(issubclass(klass, base_classes) for klass in self.inlines)
+
+    @property
+    def has_sortable_stacked_inlines(self):
+        base_classes = (SortableStackedInline, SortableGenericStackedInline)
+        return any(issubclass(klass, base_classes) for klass in self.inlines)
+
+    @property
+    def change_form_template(self):
+        if self.has_sortable_tabular_inlines or self.has_sortable_stacked_inlines:
+            return self.sortable_change_form_template
+        return super(SortableAdmin, self).change_form_template
+
     def get_urls(self):
         urls = super(SortableAdmin, self).get_urls()
+        info = self.model._meta.app_label, self.model._meta.model_name
 
-        # this ajax view changes the order
-        admin_do_sorting_url = url(r'^sorting/do-sorting/(?P<model_type_id>\d+)/$',
+        # this ajax view changes the order of instances of the model type
+        admin_do_sorting_url = re_path(
+            r'^sort/do-sorting/(?P<model_type_id>\d+)/$',
             self.admin_site.admin_view(self.do_sorting_view),
-            name='admin_do_sorting')
+            name='%s_%s_do_sorting' % info)
 
         # this view displays the sortable objects
-        admin_sort_url = url(r'^sort/$',
+        admin_sort_url = re_path(
+            r'^sort/$',
             self.admin_site.admin_view(self.sort_view),
-            name='admin_sort')
-
-        if VERSION > (1, 7):
-            admin_urls = [
-                admin_do_sorting_url,
-                admin_sort_url
-            ]
-        else:
-            admin_urls = patterns('',
-                admin_do_sorting_url,
-                admin_sort_url,)
+            name='%s_%s_sort' % info)
 
-        return admin_urls + urls
+        urls = [
+            admin_do_sorting_url,
+            admin_sort_url
+        ] + urls
+        return urls
 
-    def sort_view(self, request):
+    def get_sort_view_queryset(self, request, sortable_by_expression):
         """
-        Custom admin view that displays the objects as a list whose sort
-        order can be changed via drag-and-drop.
+        Return a queryset, optionally filtered based on request and
+        `sortable_by_expression` to be used in the sort view.
         """
-
-        opts = self.model._meta
-        if VERSION >= (1, 8):
-            codename = get_permission_codename('change', opts)
-            has_perm = request.user.has_perm('{0}.{1}'.format(opts.app_label,
-                codename))
-        else:
-            has_perm = request.user.has_perm('{0}.{1}'.format(opts.app_label,
-                opts.get_change_permission()))
-
-        jquery_lib_path = 'admin/js/jquery.js' if VERSION < (1, 9) \
-            else 'admin/js/vendor/jquery/jquery.js'
-
         # get sort group index from querystring if present
         sort_filter_index = request.GET.get('sort_filter')
 
-        filters = {}
+        # apply any filters via the querystring
+        filters = self.get_querystring_filters(request)
+
         if sort_filter_index:
             try:
                 filters = self.model.sorting_filters[int(sort_filter_index)][1]
@@ -135,11 +149,19 @@ 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)
+        return self.get_queryset(request).filter(**filters)
+
+    def sort_view(self, request):
+        """
+        Custom admin view that displays the objects as a list whose sort
+        order can be changed via drag-and-drop.
+        """
+        if not self.has_change_permission(request):
+            raise PermissionDenied
+
+        opts = self.model._meta
+
+        jquery_lib_path = 'admin/js/vendor/jquery/jquery.js'
 
         # Determine if we need to regroup objects relative to a
         # foreign key specified on the model class that is extending Sortable.
@@ -154,19 +176,15 @@ class SortableAdmin(SortableAdminBase, ModelAdmin):
 
         for field in self.model._meta.fields:
             if isinstance(field, SortableForeignKey):
-                sortable_by_fk = field.rel.to
+                sortable_by_fk = field.remote_field.model
                 sortable_by_field_name = field.name.lower()
-                sortable_by_class_is_sortable = sortable_by_fk.objects.count() >= 2
+                sortable_by_class_is_sortable = \
+                    isinstance(sortable_by_fk, SortableMixin) and \
+                    sortable_by_fk.objects.count() >= 2
 
         if sortable_by_property:
-            # backwards compatibility for < 1.1.1, where sortable_by was a
-            # classmethod instead of a property
-            try:
-                sortable_by_class, sortable_by_expression = \
-                    sortable_by_property()
-            except (TypeError, ValueError):
-                sortable_by_class = self.model.sortable_by
-                sortable_by_expression = sortable_by_class.__name__.lower()
+            sortable_by_class = self.model.sortable_by
+            sortable_by_expression = sortable_by_class.__name__.lower()
 
             sortable_by_class_display_name = sortable_by_class._meta \
                 .verbose_name_plural
@@ -184,6 +202,8 @@ class SortableAdmin(SortableAdminBase, ModelAdmin):
                 sortable_by_class_display_name = \
                 sortable_by_class_is_sortable = None
 
+        objects = self.get_sort_view_queryset(request, sortable_by_expression)
+
         if sortable_by_property or sortable_by_fk:
             # Order the objects by the property they are sortable by,
             # then by the order, otherwise the regroup
@@ -192,9 +212,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]
-            finally:
                 order_field_name = 'order'
 
             objects = objects.order_by(sortable_by_expression, order_field_name)
@@ -204,19 +221,26 @@ class SortableAdmin(SortableAdminBase, ModelAdmin):
         except AttributeError:
             verbose_name_plural = opts.verbose_name_plural
 
-        context = {
+        context = self.admin_site.each_context(request)
+
+        filters = urlencode(self.get_querystring_filters(request))
+
+        context.update({
             'title': u'Drag and drop {0} to change display order'.format(
                 capfirst(verbose_name_plural)),
             'opts': opts,
-            'app_label': opts.app_label,
-            'has_perm': has_perm,
+            'has_perm': True,
             'objects': objects,
             'group_expression': sortable_by_expression,
             'sortable_by_class': sortable_by_class,
             'sortable_by_class_is_sortable': sortable_by_class_is_sortable,
             'sortable_by_class_display_name': sortable_by_class_display_name,
-            'jquery_lib_path': jquery_lib_path
-        }
+            'filters': filters,
+            'jquery_lib_path': jquery_lib_path,
+            'csrf_cookie_name': getattr(settings, 'CSRF_COOKIE_NAME', 'csrftoken'),
+            'csrf_header_name': getattr(settings, 'CSRF_HEADER_NAME', 'X-CSRFToken'),
+            'after_sorting_js_callback_name': self.after_sorting_js_callback_name
+        })
         return render(request, self.sortable_change_list_template, context)
 
     def add_view(self, request, form_url='', extra_context=None):
@@ -230,82 +254,84 @@ class SortableAdmin(SortableAdminBase, ModelAdmin):
             extra_context=extra_context)
 
     def change_view(self, request, object_id, form_url='', extra_context=None):
-        self.has_sortable_tabular_inlines = False
-        self.has_sortable_stacked_inlines = False
 
         if extra_context is None:
             extra_context = {}
 
         extra_context.update({
-            'change_form_template_extends': self.change_form_template_extends
+            'change_form_template_extends': self.change_form_template_extends,
+            'has_sortable_tabular_inlines': self.has_sortable_tabular_inlines,
+            'has_sortable_stacked_inlines': self.has_sortable_stacked_inlines,
+            'csrf_cookie_name': getattr(settings, 'CSRF_COOKIE_NAME', 'csrftoken'),
+            'csrf_header_name': getattr(settings, 'CSRF_HEADER_NAME', 'X-CSRFToken'),
+            'after_sorting_js_callback_name': self.after_sorting_js_callback_name
         })
 
-        for klass in self.inlines:
-            if issubclass(klass, SortableTabularInline) or issubclass(klass,
-                    SortableGenericTabularInline):
-                self.has_sortable_tabular_inlines = True
-            if issubclass(klass, SortableStackedInline) or issubclass(klass,
-                    SortableGenericStackedInline):
-                self.has_sortable_stacked_inlines = True
-
-        if self.has_sortable_tabular_inlines or \
-                self.has_sortable_stacked_inlines:
-
-            self.change_form_template = self.sortable_change_form_template
-
-            extra_context.update({
-                'has_sortable_tabular_inlines':
-                self.has_sortable_tabular_inlines,
-                'has_sortable_stacked_inlines':
-                self.has_sortable_stacked_inlines
-            })
-
         return super(SortableAdmin, self).change_view(request, object_id,
             form_url='', extra_context=extra_context)
 
+    @method_decorator(require_POST)
     def do_sorting_view(self, request, model_type_id=None):
         """
         This view sets the ordering of the objects for the model type
         and primary keys passed in. It must be an Ajax POST.
         """
+        if not self.has_change_permission(request):
+            raise PermissionDenied
+
         response = {'objects_sorted': False}
 
-        if request.is_ajax() and request.method == 'POST':
-            try:
-                indexes = list(map(str,
-                    request.POST.get('indexes', []).split(',')))
-                klass = ContentType.objects.get(
-                    id=model_type_id).model_class()
-                objects_dict = dict([(str(obj.pk), obj) for obj in
-                    klass.objects.filter(pk__in=indexes)])
+        if request.META.get('HTTP_X_REQUESTED_WITH') == 'XMLHttpRequest':
+            klass = ContentType.objects.get(id=model_type_id).model_class()
+
+            indexes = [str(idx) for idx in request.POST.get('indexes', []).split(',')]
+
+            # apply any filters via the querystring
+            filters = self.get_querystring_filters(request)
+
+            filters['pk__in'] = indexes
+
+            # Lock rows that we might update
+            qs = klass.objects.select_for_update().filter(**filters)
 
+            with transaction.atomic():
+                objects_dict = {str(obj.pk): obj for obj in qs}
+                objects_list = [*objects_dict.keys()]
+                if len(indexes) != len(objects_dict):
+                    return JsonResponse({
+                            'objects_sorted': False,
+                            'reason': _("An object has been added or removed "
+                                        "since the last load. Please refresh "
+                                        "the page and try reordering again."),
+                        }, status_code=400)
                 order_field_name = klass._meta.ordering[0]
 
                 if order_field_name.startswith('-'):
                     order_field_name = order_field_name[1:]
                     step = -1
-                    start_object = max(objects_dict.values(),
-                        key=lambda x: getattr(x, order_field_name))
+                    start_object = objects_dict[objects_list[-1]]
+
                 else:
                     step = 1
-                    start_object = min(objects_dict.values(),
-                        key=lambda x: getattr(x, order_field_name))
+                    start_object = objects_dict[objects_list[0]]
 
                 start_index = getattr(start_object, order_field_name,
                     len(indexes))
-
+                objects_to_update = []
                 for index in indexes:
                     obj = objects_dict.get(index)
-                    setattr(obj, order_field_name, start_index)
-                    obj.save()
+                    # perform the update only if the order field has changed
+                    if getattr(obj, order_field_name) != start_index:
+                        setattr(obj, order_field_name, start_index)
+                        objects_to_update.append(obj)
                     start_index += step
+
+                qs.bulk_update(objects_to_update, [order_field_name])
                 response = {'objects_sorted': True}
-            except (KeyError, IndexError, klass.DoesNotExist,
-                    AttributeError, ValueError):
-                pass
 
-        return HttpResponse(json.dumps(response, ensure_ascii=False),
-            content_type='application/json')
+        self.after_sorting()
+
+        return JsonResponse(response)
 
 
 class NonSortableParentAdmin(SortableAdmin):
@@ -324,48 +350,29 @@ 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, 6):
-        template = 'adminsortable/edit_inline/tabular-1.5.x.html'
-    else:
-        template = 'adminsortable/edit_inline/tabular.html'
+    template = 'adminsortable/edit_inline/tabular.html'
 
 
 class SortableStackedInline(StackedInline, SortableInlineBase):
     """Custom template that enables sorting for stacked inlines"""
-    if VERSION < (1, 6):
-        template = 'adminsortable/edit_inline/stacked-1.5.x.html'
-    else:
-        template = 'adminsortable/edit_inline/stacked.html'
+    template = 'adminsortable/edit_inline/stacked.html'
 
 
 class SortableGenericTabularInline(GenericTabularInline, SortableInlineBase):
     """Custom template that enables sorting for tabular inlines"""
-    if VERSION < (1, 6):
-        template = 'adminsortable/edit_inline/tabular-1.5.x.html'
-    else:
-        template = 'adminsortable/edit_inline/tabular.html'
+    template = 'adminsortable/edit_inline/tabular.html'
 
 
 class SortableGenericStackedInline(GenericStackedInline, SortableInlineBase):
     """Custom template that enables sorting for stacked inlines"""
-    if VERSION < (1, 6):
-        template = 'adminsortable/edit_inline/stacked-1.5.x.html'
-    else:
-        template = 'adminsortable/edit_inline/stacked.html'
+    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/locale/hr/LC_MESSAGES/django.mo b/adminsortable/locale/hr/LC_MESSAGES/django.mo
new file mode 100644
index 0000000..45fd7e3
Binary files /dev/null and b/adminsortable/locale/hr/LC_MESSAGES/django.mo differ
diff --git a/adminsortable/locale/hr/LC_MESSAGES/django.po b/adminsortable/locale/hr/LC_MESSAGES/django.po
new file mode 100644
index 0000000..3e18fbf
--- /dev/null
+++ b/adminsortable/locale/hr/LC_MESSAGES/django.po
@@ -0,0 +1,50 @@
+#, fuzzy
+msgid ""
+msgstr ""
+"Project-Id-Version: adminsortable\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2018-03-22 18:35+0100\n"
+"PO-Revision-Date: 2018-03-22 18:35+0100\n"
+"Last-Translator: Luka Matijevic <lumatijev@gmail.com>\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: hr\n"
+
+#: templates/adminsortable/change_list.html:15
+#, python-format
+msgid "Drag and drop %(model)s to change display order"
+msgstr "Povuci i ispusti %(model)s kako bi promijenio redoslijed prikaza"
+
+#: templates/adminsortable/change_list.html:25
+#, python-format
+msgid "Reorder"
+msgstr "Promijeni redoslijed"
+
+#: templates/adminsortable/change_list.html:32
+#, python-format
+msgid "Drag and drop %(sort_type)s %(model)s to change their order."
+msgstr "Povuci i ispusti %(sort_type)s %(model)s kako bi promijenio njihov redoslijed."
+
+#: templates/adminsortable/change_list.html:34
+#, python-format
+msgid "Drag and drop %(model)s to change their order."
+msgstr "Povuci i ispusti %(model)s kako bi promijenio njihov redoslijed."
+
+#: templates/adminsortable/change_list.html:39
+#, python-format
+msgid ""
+"You may also drag and drop %(sortable_by_class_display_name)s to change "
+"their order."
+msgstr ""
+"Možeš povući i ispustiti %(sortable_by_class_display_name)s kako bi promijenio "
+"njihov redoslijed."
+
+#: templates/adminsortable/change_list.html:50
+#, python-format
+msgid "Return to %(model)s"
+msgstr "Povratak na %(model)s"
+
+#: templates/adminsortable/change_list_with_sort_link.html:6
+msgid "Change Order"
+msgstr "Promjena redoslijeda"
\ No newline at end of file
diff --git a/adminsortable/locale/lv/LC_MESSAGES/django.po b/adminsortable/locale/lv/LC_MESSAGES/django.po
new file mode 100644
index 0000000..4ed9940
--- /dev/null
+++ b/adminsortable/locale/lv/LC_MESSAGES/django.po
@@ -0,0 +1,51 @@
+msgid ""
+msgstr ""
+"Project-Id-Version: adminsortable\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2011-09-22 15:42+0200\n"
+"PO-Revision-Date: 2018-02-08 22:47+0200\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Last-Translator: Pēteris <pb@sungis.lv>\n"
+"Language-Team: \n"
+"Language: lv\n"
+"X-Generator: Poedit 1.8.7.1\n"
+
+#: templates/adminsortable/change_list.html:15
+#, python-format
+msgid "Drag and drop %(model)s to change display order"
+msgstr "Velc un novieto %(model)s, lai mainītu attēlošanas secību"
+
+#: templates/adminsortable/change_list.html:25
+#, python-format
+msgid "Reorder"
+msgstr "Pārkārtot"
+
+#: templates/adminsortable/change_list.html:32
+#, python-format
+msgid "Drag and drop %(sort_type)s %(model)s to change their order."
+msgstr "Velc un novieto %(sort_type)s %(model)s, lai mainītu to secību."
+
+#: templates/adminsortable/change_list.html:34
+#, python-format
+msgid "Drag and drop %(model)s to change their order."
+msgstr "Velc un novieto %(model)s, lai mainītu attēlošanas secību."
+
+#: templates/adminsortable/change_list.html:39
+#, python-format
+msgid ""
+"You may also drag and drop %(sortable_by_class_display_name)s to change "
+"their order."
+msgstr ""
+"Tu vari arī vilt un pārvieetot %(sortable_by_class_display_name)s, lai "
+"mainītu secību."
+
+#: templates/adminsortable/change_list.html:50
+#, python-format
+msgid "Return to %(model)s"
+msgstr "Atgriezties pie %(model)s"
+
+#: templates/adminsortable/change_list_with_sort_link.html:6
+msgid "Change Order"
+msgstr "Mainīt secību"
diff --git a/adminsortable/locale/nb/LC_MESSAGES/django.mo b/adminsortable/locale/nb/LC_MESSAGES/django.mo
new file mode 100644
index 0000000..092453a
Binary files /dev/null and b/adminsortable/locale/nb/LC_MESSAGES/django.mo differ
diff --git a/adminsortable/locale/nb/LC_MESSAGES/django.po b/adminsortable/locale/nb/LC_MESSAGES/django.po
new file mode 100644
index 0000000..4f6366c
--- /dev/null
+++ b/adminsortable/locale/nb/LC_MESSAGES/django.po
@@ -0,0 +1,51 @@
+msgid ""
+msgstr ""
+"Project-Id-Version: adminsortable\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2011-09-22 15:42+0200\n"
+"PO-Revision-Date: 2018-02-05 17:24+0100\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Last-Translator: Simen Heggestøyl <simenheg@gmail.com>\n"
+"Language-Team: \n"
+"Language: nb\n"
+"X-Generator: Poedit 2.0.5\n"
+
+#: templates/adminsortable/change_list.html:15
+#, python-format
+msgid "Drag and drop %(model)s to change display order"
+msgstr "Dra og slipp %(model)s for å endre visningsrekkefølgen"
+
+#: templates/adminsortable/change_list.html:25
+#, python-format
+msgid "Reorder"
+msgstr "Endre rekkefølge"
+
+#: templates/adminsortable/change_list.html:32
+#, python-format
+msgid "Drag and drop %(sort_type)s %(model)s to change their order."
+msgstr "Dra og slipp %(sort_type)s %(model)s for å endre rekkefølgen deres."
+
+#: templates/adminsortable/change_list.html:34
+#, python-format
+msgid "Drag and drop %(model)s to change their order."
+msgstr "Dra og slipp %(model)s for å endre rekkefølgen deres."
+
+#: templates/adminsortable/change_list.html:39
+#, python-format
+msgid ""
+"You may also drag and drop %(sortable_by_class_display_name)s to change "
+"their order."
+msgstr ""
+"Du kan også dra og slippe %(sortable_by_class_display_name)s for å endre "
+"rekkefølgen deres."
+
+#: templates/adminsortable/change_list.html:50
+#, python-format
+msgid "Return to %(model)s"
+msgstr "Tilbake til %(model)s"
+
+#: templates/adminsortable/change_list_with_sort_link.html:6
+msgid "Change Order"
+msgstr "Endre rekkefølge"
diff --git a/adminsortable/locale/uk/LC_MESSAGES/django.mo b/adminsortable/locale/uk/LC_MESSAGES/django.mo
new file mode 100644
index 0000000..04b6a46
Binary files /dev/null and b/adminsortable/locale/uk/LC_MESSAGES/django.mo differ
diff --git a/adminsortable/locale/uk/LC_MESSAGES/django.po b/adminsortable/locale/uk/LC_MESSAGES/django.po
new file mode 100644
index 0000000..f2b0696
--- /dev/null
+++ b/adminsortable/locale/uk/LC_MESSAGES/django.po
@@ -0,0 +1,110 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the PACKAGE package.
+# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
+#
+#, fuzzy
+msgid ""
+msgstr ""
+"Project-Id-Version: PACKAGE VERSION\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2019-06-21 23:37+0300\n"
+"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: LANGUAGE <LL@li.org>\n"
+"Language: \n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=4; plural=(n % 1 == 0 && n % 10 == 1 && n % 100 != "
+"11 ? 0 : n % 1 == 0 && n % 10 >= 2 && n % 10 <= 4 && (n % 100 < 12 || n % "
+"100 > 14) ? 1 : n % 1 == 0 && (n % 10 ==0 || (n % 10 >=5 && n % 10 <=9) || "
+"(n % 100 >=11 && n % 100 <=14 )) ? 2: 3);\n"
+
+#: templates/adminsortable/change_form.html:33
+msgid ""
+"There are unsaved changes on this page. Please save your changes before "
+"reordering."
+msgstr "На цій сторінці є незбережені зміни. Будь ласка, збережіть ваші зміни перед зміною порядку."
+
+#: templates/adminsortable/change_list.html:53
+#, python-format
+msgid "Drag and drop %(model)s to change display order"
+msgstr "Перетягніть %(model)s, щоб змінити порядок відображення"
+
+#: templates/adminsortable/change_list.html:53
+msgid "Django site admin"
+msgstr "Django адміністрування"
+
+#: templates/adminsortable/change_list.html:59
+msgid "Home"
+msgstr "Домівка"
+
+#: templates/adminsortable/change_list.html:70
+msgid "Reorder"
+msgstr "Зміна порядку"
+
+#: templates/adminsortable/change_list.html:78
+#, python-format
+msgid "Drag and drop %(sort_type)s %(model)s to change their order."
+msgstr "Перетягніть %(sort_type)s %(model)s, щоб змінити їх порядок"
+
+#: templates/adminsortable/change_list.html:80
+#, python-format
+msgid "Drag and drop %(model)s to change their order."
+msgstr "Перетягніть %(model)s, щоб змінити їх порядок"
+
+#: templates/adminsortable/change_list.html:85
+#, python-format
+msgid ""
+"You may also drag and drop %(sortable_by_class_display_name)s to change "
+"their order."
+msgstr "Ви також можете перетягнути %(sortable_by_class_display_name)s, щоб змінити їх порядок"
+
+#: templates/adminsortable/change_list.html:98
+#, python-format
+msgid "Return to %(model)s"
+msgstr "Повернутись до %(model)s"
+
+#: templates/adminsortable/change_list_with_sort_link.html:24
+msgid "Change Order of"
+msgstr "Змінити порядок"
+
+#: templates/adminsortable/change_list_with_sort_link.html:29
+msgid "Change Order"
+msgstr "Змінити порядок"
+
+#: templates/adminsortable/edit_inline/stacked-1.10.x.html:12
+#: templates/adminsortable/edit_inline/stacked.html:15
+#: templates/adminsortable/edit_inline/tabular-1.10.x.html:34
+#: templates/adminsortable/edit_inline/tabular.html:35
+msgid "Change"
+msgstr "Змінити"
+
+#: templates/adminsortable/edit_inline/stacked-1.10.x.html:14
+#: templates/adminsortable/edit_inline/stacked.html:17
+#: templates/adminsortable/edit_inline/tabular-1.10.x.html:36
+#: templates/adminsortable/edit_inline/tabular.html:37
+msgid "View on site"
+msgstr "Дивитися на сайті"
+
+#: templates/adminsortable/edit_inline/stacked.html:4
+#: templates/adminsortable/edit_inline/tabular.html:7
+msgid "drag and drop to change order"
+msgstr "перетягніть, щоб змінити порядок"
+
+#: templates/adminsortable/edit_inline/stacked.html:37
+#: templates/adminsortable/edit_inline/tabular.html:86
+msgid "Remove"
+msgstr "Видалити"
+
+#: templates/adminsortable/edit_inline/stacked.html:38
+#: templates/adminsortable/edit_inline/tabular.html:85
+#, python-format
+msgid "Add another %(verbose_name)s"
+msgstr "Додати ще %(verbose_name)s"
+
+#: templates/adminsortable/edit_inline/tabular-1.10.x.html:20
+#: templates/adminsortable/edit_inline/tabular.html:18
+msgid "Delete?"
+msgstr "Видалити?"
diff --git a/adminsortable/models.py b/adminsortable/models.py
index 9157f62..94e15f7 100644
--- a/adminsortable/models.py
+++ b/adminsortable/models.py
@@ -1,3 +1,4 @@
+from django import VERSION
 from django.contrib.contenttypes.models import ContentType
 from django.db import models
 
@@ -60,7 +61,7 @@ class SortableMixin(models.Model):
         # check that the order field is an integer type
         if not self.order_field or not isinstance(self.order_field,
                 integer_fields):
-            raise NotImplemented(u'You must define the field '
+            raise NotImplementedError(u'You must define the field '
                 '`Meta.ordering` refers to, and it must be of type: '
                 'PositiveIntegerField, IntegerField, '
                 'PositiveSmallIntegerField, SmallIntegerField, '
@@ -87,7 +88,8 @@ class SortableMixin(models.Model):
             'typecast to an integer.'
 
     def save(self, *args, **kwargs):
-        if not self.id:
+        needs_default = (self._state.adding if VERSION >= (1, 8) else not self.pk)
+        if not getattr(self, self.order_field_name) and needs_default:
             try:
                 current_max = self.__class__.objects.aggregate(
                     models.Max(self.order_field_name))[self.order_field_name + '__max'] or 0
@@ -98,36 +100,47 @@ class SortableMixin(models.Model):
 
         super(SortableMixin, self).save(*args, **kwargs)
 
-    def _filter_objects(self, filters, extra_filters, filter_on_sortable_fk):
+    def _filter_objects(self, filters, filter_args, extra_filters, filter_kwargs, filter_on_sortable_fk):
+        # DEPRECATION WARNING: `extra_filters` will be replaced by `filter_kwargs` in the next release
+
         if extra_filters:
             filters.update(extra_filters)
 
+        if filter_kwargs:
+            filters.update(filter_kwargs)
+
         if self.sortable_foreign_key and filter_on_sortable_fk:
             # sfk_obj == sortable foreign key instance
             sfk_obj = getattr(self, self.sortable_foreign_key.name)
-            filters.update(
-                {self.sortable_foreign_key.name: sfk_obj.id})
+            filters.update({ self.sortable_foreign_key.name: sfk_obj.id })
 
         try:
             order_by = '-{0}'.format(self.order_field_name) \
                 if '{0}__lt'.format(self.order_field_name) in filters.keys() \
                 else self.order_field_name
-            obj = self.__class__.objects.filter(
-                **filters).order_by(order_by)[:1][0]
+            obj = self.__class__.objects.filter(*filter_args, **filters).order_by(order_by)[:1][0]
         except IndexError:
             obj = None
 
         return obj
 
-    def get_next(self, extra_filters={}, filter_on_sortable_fk=True):
+    def get_next(self, filter_args=[], extra_filters={}, filter_kwargs={}, filter_on_sortable_fk=True):
         return self._filter_objects(
             {'{0}__gt'.format(self.order_field_name): self._get_order_field_value()},
-            extra_filters, filter_on_sortable_fk)
+            filter_args,
+            extra_filters,
+            filter_kwargs,
+            filter_on_sortable_fk
+        )
 
-    def get_previous(self, extra_filters={}, filter_on_sortable_fk=True):
+    def get_previous(self, filter_args=[], extra_filters={}, filter_kwargs={}, filter_on_sortable_fk=True):
         return self._filter_objects(
             {'{0}__lt'.format(self.order_field_name): self._get_order_field_value()},
-            extra_filters, filter_on_sortable_fk)
+            filter_args,
+            extra_filters,
+            filter_kwargs,
+            filter_on_sortable_fk
+        )
 
 
 # for legacy support of existing implementations
diff --git a/adminsortable/static/adminsortable/css/admin.sortable.css b/adminsortable/static/adminsortable/css/admin.sortable.css
index e03ef81..4311972 100644
--- a/adminsortable/static/adminsortable/css/admin.sortable.css
+++ b/adminsortable/static/adminsortable/css/admin.sortable.css
@@ -10,8 +10,8 @@
 	margin-left: 1em;
 }
 
-#sortable ul li,
-#sortable ul li a
+#sortable ul.sortable li,
+#sortable ul.sortable li a
 {
     cursor: move;
 }
@@ -19,10 +19,15 @@
 #sortable ul li
 {
 	overflow: auto;
+	margin-bottom: 8px;
 	margin-left: 0;
 	display: block;
 }
 
+#sortable ul li:last-child {
+	margin-bottom: 0;
+}
+
 #sortable .sortable
 {
 	list-style: none;
diff --git a/adminsortable/static/adminsortable/css/admin.sortable.inline.css b/adminsortable/static/adminsortable/css/admin.sortable.inline.css
index 4783fad..bc5cd22 100644
--- a/adminsortable/static/adminsortable/css/admin.sortable.inline.css
+++ b/adminsortable/static/adminsortable/css/admin.sortable.inline.css
@@ -2,24 +2,7 @@
     cursor: move;
 }
 
-.sortable .inline-related .module.aligned .fa,
+.sortable .inline-related h3 .fa,
 .sortable.inline-group .module .fa {
-    display: block;
-    float: left;
-}
-
-.sortable .inline-related .module.aligned .fa {
-    margin: 5px 10px 0 0;
-}
-
-.sortable .inline-related.flat-admin .module.aligned .fa {
-    margin: 9px 10px 0 0;
-}
-
-.sortable.inline-group .module .fa {
-	margin: 26px -10px 0 10px;
-}
-
-.sortable.inline-group.flat-admin .module .fa {
-	margin: 34px -10px 0 10px;
+	margin-right: 5px;
 }
diff --git a/adminsortable/static/adminsortable/js/jquery.django-csrf.js b/adminsortable/static/adminsortable/js/jquery.django-csrf.js
deleted file mode 100644
index 280f398..0000000
--- a/adminsortable/static/adminsortable/js/jquery.django-csrf.js
+++ /dev/null
@@ -1,32 +0,0 @@
-// using jQuery
-function getCookie(name) {
-    var cookieValue = null;
-    if (document.cookie && document.cookie !== '') {
-        var cookies = document.cookie.split(';');
-        for (var i = 0; i < cookies.length; i++) {
-            var cookie = django.jQuery.trim(cookies[i]);
-            // Does this cookie string begin with the name we want?
-            if (cookie.substring(0, name.length + 1) === (name + '=')) {
-                cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
-                break;
-            }
-        }
-    }
-    return cookieValue;
-}
-
-var csrftoken = getCookie('csrftoken');
-
-function csrfSafeMethod(method) {
-    // these HTTP methods do not require CSRF protection
-    return (/^(GET|HEAD|OPTIONS|TRACE)$/.test(method));
-}
-
-django.jQuery.ajaxSetup({
-    crossDomain: false, // obviates need for sameOrigin test
-    beforeSend: function(xhr, settings) {
-        if (!csrfSafeMethod(settings.type)) {
-            xhr.setRequestHeader("X-CSRFToken", csrftoken);
-        }
-    }
-});
diff --git a/adminsortable/static/adminsortable/js/jquery.ui.touch-punch.min.js b/adminsortable/static/adminsortable/js/jquery.ui.touch-punch.min.js
new file mode 100644
index 0000000..31272ce
--- /dev/null
+++ b/adminsortable/static/adminsortable/js/jquery.ui.touch-punch.min.js
@@ -0,0 +1,11 @@
+/*!
+ * jQuery UI Touch Punch 0.2.3
+ *
+ * Copyright 2011–2014, Dave Furfero
+ * Dual licensed under the MIT or GPL Version 2 licenses.
+ *
+ * Depends:
+ *  jquery.ui.widget.js
+ *  jquery.ui.mouse.js
+ */
+!function(a){function f(a,b){if(!(a.originalEvent.touches.length>1)){a.preventDefault();var c=a.originalEvent.changedTouches[0],d=document.createEvent("MouseEvents");d.initMouseEvent(b,!0,!0,window,1,c.screenX,c.screenY,c.clientX,c.clientY,!1,!1,!1,!1,0,null),a.target.dispatchEvent(d)}}if(a.support.touch="ontouchend"in document,a.support.touch){var e,b=a.ui.mouse.prototype,c=b._mouseInit,d=b._mouseDestroy;b._touchStart=function(a){var b=this;!e&&b._mouseCapture(a.originalEvent.changedTouches[0])&&(e=!0,b._touchMoved=!1,f(a,"mouseover"),f(a,"mousemove"),f(a,"mousedown"))},b._touchMove=function(a){e&&(this._touchMoved=!0,f(a,"mousemove"))},b._touchEnd=function(a){e&&(f(a,"mouseup"),f(a,"mouseout"),this._touchMoved||f(a,"click"),e=!1)},b._mouseInit=function(){var b=this;b.element.bind({touchstart:a.proxy(b,"_touchStart"),touchmove:a.proxy(b,"_touchMove"),touchend:a.proxy(b,"_touchEnd")}),c.call(b)},b._mouseDestroy=function(){var b=this;b.element.unbind({touchstart:a.proxy(b,"_touchStart"),touchmove:a.proxy(b,"_touchMove"),touchend:a.proxy(b,"_touchEnd")}),d.call(b)}}}(jQuery);
\ No newline at end of file
diff --git a/adminsortable/static/adminsortable/js/admin.sortable.js b/adminsortable/templates/adminsortable/admin.sortable.html
similarity index 68%
rename from adminsortable/static/adminsortable/js/admin.sortable.js
rename to adminsortable/templates/adminsortable/admin.sortable.html
index cca28cb..dc55915 100644
--- a/adminsortable/static/adminsortable/js/admin.sortable.js
+++ b/adminsortable/templates/adminsortable/admin.sortable.html
@@ -1,50 +1,54 @@
-(function($){
-
-    $(function() {
-        jQuery('.sortable').sortable({
-            axis : 'y',
-            containment : 'parent',
-            tolerance : 'pointer',
-            items : 'li',
-            stop : function(event, ui)
-            {
-                var indexes = [],
-                    lineItems = ui.item.parent().find('> li');
-
-                lineItems.each(function(i)
-                {
-                    indexes.push($(this).find(':hidden[name="pk"]').val());
-                });
-
-                $.ajax({
-                    url: ui.item.find('a.admin_sorting_url').attr('href'),
-                    type: 'POST',
-                    data: { indexes: indexes.join(',') },
-                    success: function()
-                    {
-                        // set icons based on position
-                        lineItems.each(function(index, element) {
-                            var icon = $(element).find('> a .fa');
-                            icon.removeClass('fa-sort-desc fa-sort-asc fa-sort');
-
-                            if (index === 0) {
-                                icon.addClass('fa fa-sort-desc');
-                            }
-                            else if (index == lineItems.length - 1) {
-                                icon.addClass('fa fa-sort-asc');
-                            }
-                            else  {
-                                icon.addClass('fa fa-sort');
-                            }
-                        });
-
-                        ui.item.effect('highlight', {}, 1000);
-                    }
-                });
-            }
-        }).click(function(e){
-            e.preventDefault();
-        });
-    });
-
-})(django.jQuery);
+<script>
+  (function($){
+
+    $(function() {
+        jQuery('.sortable').sortable({
+            axis : 'y',
+            containment : 'parent',
+            tolerance : 'pointer',
+            items : 'li',
+            stop : function(event, ui) {
+                var indexes = [],
+                    lineItems = ui.item.parent().find('> li');
+
+                lineItems.each(function(i) {
+                    indexes.push($(this).find(':hidden[name="pk"]').val());
+                });
+
+                $.ajax({
+                    url: ui.item.find('a.admin_sorting_url').attr('href'),
+                    type: 'POST',
+                    data: { indexes: indexes.join(','), csrfmiddlewaretoken: window.csrftoken },
+                    success: function() {
+                        // set icons based on position
+                        lineItems.each(function(index, element) {
+                            var icon = $(element).find('a.admin_sorting_url .fa');
+                            icon.removeClass('fa-sort-desc fa-sort-asc fa-sort');
+
+                            if (index === 0) {
+                                icon.addClass('fa fa-sort-desc');
+                            }
+                            else if (index == lineItems.length - 1) {
+                                icon.addClass('fa fa-sort-asc');
+                            }
+                            else  {
+                                icon.addClass('fa fa-sort');
+                            }
+                        });
+
+                        ui.item.effect('highlight', {}, 1000);
+
+                        {% if after_sorting_js_callback_name %}
+                        {# if a callback is defined in a custom template, execute it #}
+                        window['{{ after_sorting_js_callback_name }}']();
+                        {% endif %}
+                    }
+                });
+            }
+        }).click(function(e){
+            e.preventDefault();
+        });
+    });
+
+  })(django.jQuery);
+</script>
diff --git a/adminsortable/templates/adminsortable/change_form.html b/adminsortable/templates/adminsortable/change_form.html
index 0f3fd76..7ed51f8 100644
--- a/adminsortable/templates/adminsortable/change_form.html
+++ b/adminsortable/templates/adminsortable/change_form.html
@@ -1,22 +1,23 @@
 {% extends change_form_template_extends %}
 {% load i18n admin_modify %}
-{% load static from staticfiles %}
+{% load static %}
 
 {% block extrahead %}
     {{ block.super }}
     {% url 'admin:jsi18n' as jsi18nurl %}
 
     {% if has_sortable_tabular_inlines or has_sortable_stacked_inlines %}
-        <script type="text/javascript" src="{% static 'adminsortable/js/jquery-ui-django-admin.min.js' %}"></script>
-        <script type="text/javascript" src="{% static 'adminsortable/js/jquery.django-csrf.js' %}"></script>
+        <script src="{% static 'adminsortable/js/jquery-ui-django-admin.min.js' %}"></script>
+        <script src="{% static 'adminsortable/js/jquery.ui.touch-punch.min.js' %}"></script>
+        {% include 'adminsortable/csrf/jquery.django-csrf.html' with csrf_cookie_name=csrf_cookie_name %}
     {% endif %}
 
     {% if has_sortable_tabular_inlines %}
-        <script type="text/javascript" src="{% static 'adminsortable/js/admin.sortable.tabular.inlines.js' %}"></script>
+        {% include 'adminsortable/edit_inline/admin.sortable.tabular.inlines.html' with after_sorting_js_callback_name=after_sorting_js_callback_name %}
     {% endif %}
 
     {% if has_sortable_stacked_inlines %}
-        <script type="text/javascript" src="{% static 'adminsortable/js/admin.sortable.stacked.inlines.js' %}"></script>
+        {% include 'adminsortable/edit_inline/admin.sortable.stacked.inlines.html' with after_sorting_js_callback_name=after_sorting_js_callback_name %}
     {% endif %}
 {% endblock %}
 
@@ -24,7 +25,7 @@
     {{ block.super }}
     {% if has_sortable_tabular_inlines or has_sortable_stacked_inlines %}
         <link rel="stylesheet" type="text/css" href="{% static 'adminsortable/css/admin.sortable.inline.css' %}" />
-        <link rel="stylesheet" href="//maxcdn.bootstrapcdn.com/font-awesome/4.5.0/css/font-awesome.min.css">
+        <link rel="stylesheet" href="//maxcdn.bootstrapcdn.com/font-awesome/4.6.3/css/font-awesome.min.css">
     {% endif %}
 {% endblock %}
 
diff --git a/adminsortable/templates/adminsortable/change_list.html b/adminsortable/templates/adminsortable/change_list.html
index 7b6e83e..2cbbc34 100644
--- a/adminsortable/templates/adminsortable/change_list.html
+++ b/adminsortable/templates/adminsortable/change_list.html
@@ -1,37 +1,76 @@
-{% extends 'admin/change_list.html' %}
-{% load admin_list i18n adminsortable_tags %}
-{% load static from staticfiles %}
+{% extends "admin/base_site.html" %}
+{% load i18n admin_urls static admin_list %}
 
 {% block extrastyle %}
-	{{ block.super }}
-	<link rel="stylesheet" href="//maxcdn.bootstrapcdn.com/font-awesome/4.5.0/css/font-awesome.min.css">
+  {{ block.super }}
+  <link rel="stylesheet" type="text/css" href="{% static "admin/css/changelists.css" %}" />
+  {% if cl.formset %}
+    <link rel="stylesheet" type="text/css" href="{% static "admin/css/forms.css" %}" />
+  {% endif %}
+  {% if cl.formset or action_form %}
+    <script type="text/javascript" src="{% url 'admin:jsi18n' %}"></script>
+  {% endif %}
+  {{ media.css }}
+  {% if not actions_on_top and not actions_on_bottom %}
+    <style>
+      #changelist table thead th:first-child {width: inherit}
+    </style>
+  {% endif %}
+  <link rel="stylesheet" href="//maxcdn.bootstrapcdn.com/font-awesome/4.6.3/css/font-awesome.min.css">
 	<link rel="stylesheet" href="{% static 'adminsortable/css/admin.sortable.css' %}" />
 {% endblock %}
 
 {% block extrahead %}
-	{{ block.super }}
-    <script src="{% static jquery_lib_path %}"></script>
-    <script src="{% static 'admin/js/jquery.init.js' %}"></script>
-	<script src="{% static 'adminsortable/js/jquery-ui-django-admin.min.js' %}"></script>
-    <script src="{% static 'adminsortable/js/jquery.django-csrf.js' %}"></script>
-    <script src="{% static 'adminsortable/js/admin.sortable.js' %}"></script>
+<script src="{% static jquery_lib_path %}"></script>
+<script src="{% static 'admin/js/jquery.init.js' %}"></script>
+{{ block.super }}
+{{ media.js }}
+<script src="{% static 'adminsortable/js/jquery-ui-django-admin.min.js' %}"></script>
+<script src="{% static 'adminsortable/js/jquery.ui.touch-punch.min.js' %}"></script>
+{% include 'adminsortable/csrf/jquery.django-csrf.html' with csrf_cookie_name=csrf_cookie_name %}
+{% include 'adminsortable/admin.sortable.html' with after_sorting_js_callback_name=after_sorting_js_callback_name %}
+
+<script type="text/javascript">
+(function($) {
+    $(document).ready(function($) {
+        var url = window.location.href;
+        var urlParts = url.split('?');
+        var changeListUrl = $('a#return-to-changelist').attr('href');
+
+        if (urlParts.length === 2) {
+            $('a#return-to-changelist').attr(
+                'href',
+                changeListUrl + '?' + urlParts[1].replace('filter_expression=', '')
+            );
+        }
+    });
+})(django.jQuery);
+</script>
 {% endblock %}
 
+{% block bodyclass %}{{ block.super }} app-{{ opts.app_label }} model-{{ opts.model_name }} change-list{% endblock %}
+
 {% block title %}{% blocktrans with opts.verbose_name_plural|capfirst as model %}Drag and drop {{ model }} to change display order{% endblocktrans %} | {% trans 'Django site admin' %}{% endblock %}
 
+{% if not is_popup %}
 {% block breadcrumbs %}
     <div class="breadcrumbs">
-      <a href="../../../">
+      <a href="{% url 'admin:index' %}">
         {% trans "Home" %}
       </a>
-       &rsaquo;
-       <a href="../../">
-         {{ app_label|capfirst }}
+      &rsaquo;
+      <a href="{% url 'admin:app_list' app_label=opts.app_label %}">
+        {{ opts.app_config.verbose_name }}
+      </a>
+      &rsaquo;
+      <a href="{% url opts|admin_urlname:'changelist' %}">
+        {{ opts.verbose_name_plural|capfirst }}
       </a>
       &rsaquo;
       	{% trans 'Reorder' %}
     </div>
 {% endblock %}
+{% endif %}
 
 {% block content_title %}
 	<h1>
@@ -48,23 +87,25 @@
 	{% endif %}
 {% endblock %}
 
+{% block coltype %}flex{% endblock %}
+
 {% block content %}
 	<div id="content-main">
 		{% block object-tools %}
-	        <ul class="object-tools">
-	            <li>
-                    <a href="../">
-                    {% blocktrans with opts.verbose_name_plural|capfirst as model %}Return to {{ model }}{% endblocktrans %}
-                    </a>
-                </li>
-	        </ul>
-	    {% endblock %}
+      <ul class="object-tools">
+        <li>
+          <a id="return-to-changelist" href="{% url opts|admin_urlname:'changelist' %}">
+          {% blocktrans with opts.verbose_name_plural|capfirst as model %}Return to {{ model }}{% endblocktrans %}
+          </a>
+        </li>
+      </ul>
+    {% endblock %}
 		{% if objects %}
 		<div id="sortable">
 			{% if group_expression %}
-                 {% render_nested_sortable_objects objects group_expression %}
+        {% include "adminsortable/shared/nested_objects.html" %}
 			{% else %}
-			     {% render_sortable_objects objects %}
+        {% include "adminsortable/shared/objects.html" %}
 			{% endif %}
 		</div>
 		{% endif %}
diff --git a/adminsortable/templates/adminsortable/change_list_with_sort_link.html b/adminsortable/templates/adminsortable/change_list_with_sort_link.html
index c78550a..bb76115 100644
--- a/adminsortable/templates/adminsortable/change_list_with_sort_link.html
+++ b/adminsortable/templates/adminsortable/change_list_with_sort_link.html
@@ -1,15 +1,34 @@
 {% extends change_list_template_extends %}
 {% load i18n %}
 
+{% block extrahead %}
+    {{ block.super }}
+    <script type="text/javascript">
+    (function($) {
+        $(document).ready(function($) {
+            var url = window.location.href;
+            var urlParts = url.split('?');
+
+            if (urlParts.length === 2 && urlParts[1].substr(0, 2) !== 'q=') {
+                $('a#change-order').attr('href', './sort/?' + urlParts[1]);
+            }
+        });
+    })(django.jQuery);
+    </script>
+{% endblock %}
+
+
 {% block object-tools-items %}
     {% for sorting_filter in sorting_filters %}
     <li>
         <a href="./sort/?sort_filter={{ forloop.counter0 }}">{% trans 'Change Order of' %} {{ sorting_filter }}</a>
     </li>
     {% empty %}
+    {% if is_sortable %}
     <li>
-        <a href="./sort/">{% trans 'Change Order' %}</a>
+        <a id="change-order" href="./sort/">{% trans 'Change Order' %}</a>
     </li>
+    {% endif %}
     {% endfor %}
     {{ block.super }}
 {% endblock %}
diff --git a/adminsortable/templates/adminsortable/csrf/jquery.django-csrf.html b/adminsortable/templates/adminsortable/csrf/jquery.django-csrf.html
new file mode 100644
index 0000000..315a022
--- /dev/null
+++ b/adminsortable/templates/adminsortable/csrf/jquery.django-csrf.html
@@ -0,0 +1,34 @@
+<script>
+    // using jQuery
+    function getCookie(name) {
+        var cookieValue = null;
+        if (document.cookie && document.cookie !== '') {
+            var cookies = document.cookie.split(';');
+            for (var i = 0; i < cookies.length; i++) {
+                var cookie = django.jQuery.trim(cookies[i]);
+                // Does this cookie string begin with the name we want?
+                if (cookie.substring(0, name.length + 1) === (name + '=')) {
+                    cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
+                    break;
+                }
+            }
+        }
+        return cookieValue;
+    }
+
+    var csrftoken = '{{ csrf_token }}' || getCookie('{{ csrf_cookie_name }}');
+
+    function csrfSafeMethod(method) {
+        // these HTTP methods do not require CSRF protection
+        return (/^(GET|HEAD|OPTIONS|TRACE)$/.test(method));
+    }
+
+    django.jQuery.ajaxSetup({
+        crossDomain: false, // obviates need for sameOrigin test
+        beforeSend: function(xhr, settings) {
+            if (!csrfSafeMethod(settings.type)) {
+                xhr.setRequestHeader("{{ csrf_header_name }}", csrftoken);
+            }
+        }
+    });
+</script>
diff --git a/adminsortable/static/adminsortable/js/admin.sortable.stacked.inlines.js b/adminsortable/templates/adminsortable/edit_inline/admin.sortable.stacked.inlines.html
similarity index 84%
rename from adminsortable/static/adminsortable/js/admin.sortable.stacked.inlines.js
rename to adminsortable/templates/adminsortable/edit_inline/admin.sortable.stacked.inlines.html
index 4afbea3..eb100d7 100644
--- a/adminsortable/static/adminsortable/js/admin.sortable.stacked.inlines.js
+++ b/adminsortable/templates/adminsortable/edit_inline/admin.sortable.stacked.inlines.html
@@ -1,70 +1,78 @@
-(function($){
-
-    $(function() {
-        var sorting_urls = $(':hidden[name="admin_sorting_url"]');
-        if (sorting_urls.length > 0)
-        {
-            var sortable_inline_groups = sorting_urls.closest('.inline-group')
-            var sortable_inline_rows = sortable_inline_groups.find('.inline-related');
-
-            sortable_inline_groups.addClass('sortable')
-            sortable_inline_rows.addClass('sortable');
-
-            sortable_inline_groups.sortable({
-                axis : 'y',
-                containment : 'parent',
-                create: function(event, ui) {
-                    $('.inline-related :checkbox').unbind();
-                },
-                tolerance : 'pointer',
-                items : '.inline-related',
-                stop : function(event, ui)
-                {
-                    if ($('.inline-deletelink').length > 0) {
-                        $(ui.sender).sortable('cancel');
-                        alert($('#localized_save_before_reorder_message').val());
-                        return false;
-                    }
-
-                    var indexes = [];
-                    ui.item.parent().children('.inline-related').each(function(i)
-                    {
-                        var index_value = $(this).find(':hidden[name$="-id"]').val();
-                        if (index_value !== "" && index_value !== undefined) {
-                            indexes.push(index_value);
-                        }
-                    });
-
-                    $.ajax({
-                        url: ui.item.parent().find(':hidden[name="admin_sorting_url"]').val(),
-                        type: 'POST',
-                        data: { indexes : indexes.join(',') },
-                        success: function() {
-                            var fieldsets = ui.item.find('fieldset'),
-                                highlightedSelector = fieldsets.filter('.collapsed').length === fieldsets.length ? 'h3' : '.form-row',
-                                icons = ui.item.parent().find(highlightedSelector).find('.fa');
-
-                            // set icons based on position
-                            icons.removeClass('fa-sort-desc fa-sort-asc fa-sort');
-                            icons.each(function(index, element) {
-                                var icon = $(element);
-                                if (index === 0) {
-                                    icon.addClass('fa fa-sort-desc');
-                                }
-                                else if (index == icons.length - 1) {
-                                    icon.addClass('fa fa-sort-asc');
-                                }
-                                else  {
-                                    icon.addClass('fa fa-sort');
-                                }
-                            });
-
-                            ui.item.find(highlightedSelector).effect('highlight', {}, 1000);
-                        }
-                    });
-                }
-            });
-        }
-    });
-
-})(django.jQuery);
+<script>
+  (function($){
+
+    $(function() {
+        var sorting_urls = $(':hidden[name="admin_sorting_url"]');
+        if (sorting_urls.length > 0)
+        {
+            var sortable_inline_groups = sorting_urls.closest('.inline-group')
+            var sortable_inline_rows = sortable_inline_groups.find('.inline-related');
+
+            sortable_inline_groups.addClass('sortable')
+            sortable_inline_rows.addClass('sortable');
+
+            sortable_inline_groups.sortable({
+                axis : 'y',
+                containment : 'parent',
+                create: function(event, ui) {
+                    $('.inline-related :checkbox').unbind();
+                },
+                tolerance : 'pointer',
+                items : '.inline-related',
+                stop : function(event, ui)
+                {
+                    if ($('.inline-deletelink').length > 0) {
+                        $(ui.sender).sortable('cancel');
+                        alert($('#localized_save_before_reorder_message').val());
+                        return false;
+                    }
+
+                    var indexes = [];
+                    ui.item.parent().children('.inline-related').each(function(i)
+                    {
+                        var index_value = $(this).find(':hidden[name$="-id"]').val();
+                        if (index_value !== "" && index_value !== undefined) {
+                            indexes.push(index_value);
+                        }
+                    });
+
+                    $.ajax({
+                        url: ui.item.parent().find(':hidden[name="admin_sorting_url"]').val(),
+                        type: 'POST',
+                        data: { indexes : indexes.join(','), csrfmiddlewaretoken: window.csrftoken },
+                        success: function() {
+                            var fieldsets = ui.item.find('fieldset'),
+                                highlightedSelector = fieldsets.filter('.collapsed').length === fieldsets.length ? 'h3' : '.form-row',
+                                icons = ui.item.parent().find('h3 > .fa');
+
+                            // set icons based on position
+                            icons.removeClass('fa-sort-desc fa-sort-asc fa-sort');
+                            icons.each(function(index, element) {
+                                var icon = $(element);
+                                if (index === 0) {
+                                    icon.addClass('fa fa-sort-desc');
+                                }
+                                else if (index == icons.length - 1) {
+                                    icon.addClass('fa fa-sort-asc');
+                                }
+                                else  {
+                                    icon.addClass('fa fa-sort');
+                                }
+                            });
+
+                            ui.item.find(highlightedSelector).effect('highlight', {}, 1000);
+
+                            {% if after_sorting_js_callback_name %}
+                            {# if a callback is defined in a custom template, execute it #}
+                            window['{{ after_sorting_js_callback_name }}']();
+                            {% endif %}
+                        }
+                    });
+                }
+            });
+        }
+    });
+
+    })(django.jQuery);
+
+</script>
diff --git a/adminsortable/static/adminsortable/js/admin.sortable.tabular.inlines.js b/adminsortable/templates/adminsortable/edit_inline/admin.sortable.tabular.inlines.html
similarity index 81%
rename from adminsortable/static/adminsortable/js/admin.sortable.tabular.inlines.js
rename to adminsortable/templates/adminsortable/edit_inline/admin.sortable.tabular.inlines.html
index debd654..973cea0 100644
--- a/adminsortable/static/adminsortable/js/admin.sortable.tabular.inlines.js
+++ b/adminsortable/templates/adminsortable/edit_inline/admin.sortable.tabular.inlines.html
@@ -1,69 +1,77 @@
-(function($){
-
-    $(function() {
-        var sorting_urls = $(':hidden[name="admin_sorting_url"]');
-        if (sorting_urls.length)
-        {
-            var sortable_inline_group = sorting_urls.closest('.inline-group')
-            var tabular_inline_rows = sortable_inline_group.find('.tabular table tbody tr');
-
-            tabular_inline_rows.addClass('sortable');
-
-            sortable_inline_group.find('.tabular.inline-related').sortable({
-                axis : 'y',
-                containment : 'parent',
-                create: function(event, ui) {
-                    $('td.delete :checkbox').unbind();
-                },
-                tolerance : 'pointer',
-                items : 'tr:not(.add-row)',
-                stop : function(event, ui) {
-                    if ($('.inline-deletelink').length > 0) {
-                        $(ui.sender).sortable('cancel');
-                        alert($('#localized_save_before_reorder_message').val());
-                        return false;
-                    }
-
-                    var indexes = [];
-                    ui.item.parent().children('tr').each(function(i)
-                    {
-                        var index_value = $(this).find('.original :hidden:first').val();
-                        if (index_value !== '' && index_value !== undefined) {
-                            indexes.push(index_value);
-                        }
-                    });
-
-                    $.ajax({
-                        url: ui.item.parent().find(':hidden[name="admin_sorting_url"]').val(),
-                        type: 'POST',
-                        data: { indexes : indexes.join(',') },
-                        success: function() {
-                            // set icons based on position
-                            var icons = ui.item.parent().find('.fa');
-                            icons.removeClass('fa-sort-desc fa-sort-asc fa-sort');
-                            icons.each(function(index, element) {
-                                var icon = $(element);
-                                if (index === 0) {
-                                    icon.addClass('fa fa-sort-desc');
-                                }
-                                else if (index == icons.length - 1) {
-                                    icon.addClass('fa fa-sort-asc');
-                                }
-                                else  {
-                                    icon.addClass('fa fa-sort');
-                                }
-                            });
-
-                            // highlight sorted row, then re-stripe table
-                            ui.item.effect('highlight', {}, 1000);
-                            tabular_inline_rows.removeClass('row1 row2');
-                            $('.tabular table tbody tr:odd').addClass('row2');
-                            $('.tabular table tbody tr:even').addClass('row1');
-                        }
-                    });
-                }
-            });
-        }
-    });
-
-})(django.jQuery);
+<script>
+  (function($){
+
+    $(function() {
+        var sorting_urls = $(':hidden[name="admin_sorting_url"]');
+        if (sorting_urls.length)
+        {
+            var sortable_inline_group = sorting_urls.closest('.inline-group');
+            var tabular_inline_rows = sortable_inline_group.find('.tabular table tbody tr');
+
+            tabular_inline_rows.addClass('sortable');
+
+            sortable_inline_group.find('.tabular.inline-related tbody').sortable({
+                axis : 'y',
+                containment : 'parent',
+                create: function(event, ui) {
+                    $('td.delete :checkbox').unbind();
+                },
+                tolerance : 'pointer',
+                items : 'tr:not(.add-row)',
+                stop : function(event, ui) {
+                    if ($('.inline-deletelink').length > 0) {
+                        $(ui.sender).sortable('cancel');
+                        alert($('#localized_save_before_reorder_message').val());
+                        return false;
+                    }
+
+                    var indexes = [];
+                    ui.item.parent().children('tr').each(function(i)
+                    {
+                        var index_value = $(this).find('.original :input:first').val();
+                        if (index_value !== '' && index_value !== undefined) {
+                            indexes.push(index_value);
+                        }
+                    });
+
+                    $.ajax({
+                        url: ui.item.parent().find(':hidden[name="admin_sorting_url"]').val(),
+                        type: 'POST',
+                        data: { indexes : indexes.join(','), csrfmiddlewaretoken: window.csrftoken },
+                        success: function() {
+                            // set icons based on position
+                            var icons = ui.item.parent().find('a > .fa');
+                            icons.removeClass('fa-sort-desc fa-sort-asc fa-sort');
+                            icons.each(function(index, element) {
+                                var icon = $(element);
+                                if (index === 0) {
+                                    icon.addClass('fa fa-sort-desc');
+                                }
+                                else if (index == icons.length - 1) {
+                                    icon.addClass('fa fa-sort-asc');
+                                }
+                                else  {
+                                    icon.addClass('fa fa-sort');
+                                }
+                            });
+
+                            // highlight sorted row, then re-stripe table
+                            ui.item.effect('highlight', {}, 1000);
+                            tabular_inline_rows.removeClass('row1 row2');
+                            $('.tabular table tbody tr:odd').addClass('row2');
+                            $('.tabular table tbody tr:even').addClass('row1');
+
+                            {% if after_sorting_js_callback_name %}
+                            {# if a callback is defined in a custom template, execute it #}
+                            window['{{ after_sorting_js_callback_name }}']();
+                            {% endif %}
+                        }
+                    });
+                }
+            });
+        }
+    });
+
+    })(django.jQuery);
+
+</script>
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 c133fe6..0000000
--- a/adminsortable/templates/adminsortable/edit_inline/stacked-1.5.x.html
+++ /dev/null
@@ -1,86 +0,0 @@
-{% load i18n admin_modify adminsortable_tags %}
-{% load static from staticfiles %}
-<div class="inline-group" id="{{ inline_admin_formset.formset.prefix }}-group">
-  <h2>{{ 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 %}</h2>
-{{ inline_admin_formset.formset.management_form }}
-{{ inline_admin_formset.formset.non_form_errors }}
-
-{% for inline_admin_form in inline_admin_formset %}<div class="inline-related{% if inline_admin_form.original %} has_original{% endif %}{% if forloop.last %} empty-form last-related{% endif %}" id="{{ inline_admin_formset.formset.prefix }}-{% if not forloop.last %}{{ forloop.counter0 }}{% else %}empty{% endif %}">
-  <h3><b>{{ inline_admin_formset.opts.verbose_name|title }}:</b>&nbsp;<span class="inline_label">{% if inline_admin_form.original %}{{ inline_admin_form.original }}{% else %}#{{ forloop.counter }}{% endif %}</span>
-    {% if inline_admin_form.show_url %}<a href="../../../r/{{ inline_admin_form.original_content_type_id }}/{{ inline_admin_form.original.id }}/">{% trans "View on site" %}</a>{% endif %}
-    {% if inline_admin_formset.formset.can_delete and inline_admin_form.original %}<span class="delete">{{ inline_admin_form.deletion_field.field }} {{ inline_admin_form.deletion_field.label_tag }}</span>{% endif %}
-  </h3>
-  {% 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" with inline_admin_form_forloop=forloop.parentloop %}
-  {% 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 %}
-  <input type="hidden" name="admin_sorting_url" value="{% url 'admin:admin_do_sorting' inline_admin_form.original.model_type_id %}" />
-  {% endif %}
-</div>{% endfor %}
-</div>
-
-<script type="text/javascript">
-(function($) {
-    $(document).ready(function() {
-        var rows = "#{{ inline_admin_formset.formset.prefix }}-group .inline-related";
-        var updateInlineLabel = function(row) {
-            $(rows).find(".inline_label").each(function(i) {
-                var count = i + 1;
-                $(this).html($(this).html().replace(/(#\d+)/g, "#" + count));
-            });
-        }
-        var reinitDateTimeShortCuts = function() {
-            // Reinitialize the calendar and clock widgets by force, yuck.
-            if (typeof DateTimeShortcuts != "undefined") {
-                $(".datetimeshortcuts").remove();
-                DateTimeShortcuts.init();
-            }
-        }
-        var updateSelectFilter = function() {
-            // If any SelectFilter widgets were added, instantiate a new instance.
-            if (typeof SelectFilter != "undefined"){
-                $(".selectfilter").each(function(index, value){
-                  var namearr = value.name.split('-');
-                  SelectFilter.init(value.id, namearr[namearr.length-1], false, "{% static 'admin/' %}");
-                });
-                $(".selectfilterstacked").each(function(index, value){
-                  var namearr = value.name.split('-');
-                  SelectFilter.init(value.id, namearr[namearr.length-1], true, "{% static 'admin/' %}");
-                });
-            }
-        }
-        var initPrepopulatedFields = function(row) {
-            row.find('.prepopulated_field').each(function() {
-                var field = $(this);
-                var input = field.find('input, select, textarea');
-                var dependency_list = input.data('dependency_list') || [];
-                var dependencies = [];
-                $.each(dependency_list, function(i, field_name) {
-                  dependencies.push('#' + row.find(field_name).find('input, select, textarea').attr('id'));
-                });
-                if (dependencies.length) {
-                    input.prepopulate(dependencies, input.attr('maxlength'));
-                }
-            });
-        }
-        $(rows).formset({
-            prefix: "{{ inline_admin_formset.formset.prefix }}",
-            addText: "{% blocktrans with inline_admin_formset.opts.verbose_name|title as verbose_name %}Add another {{ verbose_name }}{% endblocktrans %}",
-            formCssClass: "dynamic-{{ inline_admin_formset.formset.prefix }}",
-            deleteCssClass: "inline-deletelink",
-            deleteText: "{% trans "Remove" %}",
-            emptyCssClass: "empty-form",
-            removed: updateInlineLabel,
-            added: (function(row) {
-                initPrepopulatedFields(row);
-                reinitDateTimeShortCuts();
-                updateSelectFilter();
-                updateInlineLabel(row);
-            })
-        });
-    });
-})(django.jQuery);
-</script>
diff --git a/adminsortable/templates/adminsortable/edit_inline/stacked.html b/adminsortable/templates/adminsortable/edit_inline/stacked.html
index a6e96f5..00b3561 100644
--- a/adminsortable/templates/adminsortable/edit_inline/stacked.html
+++ b/adminsortable/templates/adminsortable/edit_inline/stacked.html
@@ -1,35 +1,34 @@
-{% load i18n admin_urls admin_static django_template_additions %}
-{% get_django_version as django_version %}
-<div class="inline-group" id="{{ inline_admin_formset.formset.prefix }}-group">
-  <h2>{{ 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 %}</h2>
+{% load i18n admin_urls static %}
+<div class="js-inline-admin-formset inline-group"
+     id="{{ inline_admin_formset.formset.prefix }}-group"
+     data-inline-type="stacked"
+     data-inline-formset="{{ inline_admin_formset.inline_formset_data }}">
+<fieldset class="module {{ inline_admin_formset.classes }}">
+  <h2>{{ inline_admin_formset.opts.verbose_name_plural|capfirst }}</h2>
 {{ inline_admin_formset.formset.management_form }}
 {{ inline_admin_formset.formset.non_form_errors }}
 
-{% for inline_admin_form in inline_admin_formset %}<div class="inline-related {% if django_version.major >= 1 and django_version.minor >= 9 %}flat-admin{% endif %} {% if forloop.last %} empty-form last-related{% endif %} {% if inline_admin_form.original %} has_original{% endif %}" id="{{ inline_admin_formset.formset.prefix }}-{% if not forloop.last %}{{ forloop.counter0 }}{% else %}empty{% endif %}">
-  <h3><b>{{ inline_admin_formset.opts.verbose_name|title }}:</b>&nbsp;<span class="inline_label">{% if inline_admin_form.original %}{{ inline_admin_form.original }}{% if inline_admin_form.model_admin.show_change_link and inline_admin_form.model_admin.has_registered_model %} <a href="{% url inline_admin_form.model_admin.opts|admin_urlname:'change' inline_admin_form.original.pk|admin_urlquote %}" class="inlinechangelink">{% trans "Change" %}</a>{% endif %}
-  {% else %}#{{ forloop.counter }}{% endif %}</span>
-    {% if inline_admin_form.show_url %}<a href="{% url 'admin:view_on_site' inline_admin_form.original_content_type_id inline_admin_form.original.pk %}">{% trans "View on site" %}</a>{% endif %}
+{% for inline_admin_form in inline_admin_formset %}<div class="inline-related{% if inline_admin_form.original or inline_admin_form.show_url %} has_original{% endif %}{% if forloop.last %} empty-form last-related{% endif %}" id="{{ inline_admin_formset.formset.prefix }}-{% if not forloop.last %}{{ forloop.counter0 }}{% else %}empty{% endif %}">
+  <h3>
+    {% if inline_admin_form.original %}
+      {% with initial_forms_count=inline_admin_formset.formset.management_form.INITIAL_FORMS.value %}
+      <i class="fa fa-{% if forloop.first %}sort-desc{% elif forloop.counter == initial_forms_count %}sort-asc{% else %}sort{% endif %}"></i>
+      {% endwith %}
+    {% endif %}
+    <b>{{ inline_admin_formset.opts.verbose_name|capfirst }}:</b>&nbsp;<span class="inline_label">{% if inline_admin_form.original %}{{ inline_admin_form.original }}{% if inline_admin_form.model_admin.show_change_link and inline_admin_form.model_admin.has_registered_model %} <a href="{% url inline_admin_form.model_admin.opts|admin_urlname:'change' inline_admin_form.original.pk|admin_urlquote %}" class="inlinechangelink">{% trans "Change" %}</a>{% endif %}
+{% else %}#{{ forloop.counter }}{% endif %}</span>
+      {% if inline_admin_form.show_url %}<a href="{{ inline_admin_form.absolute_url }}">{% trans "View on site" %}</a>{% endif %}
     {% if inline_admin_formset.formset.can_delete and inline_admin_form.original %}<span class="delete">{{ inline_admin_form.deletion_field.field }} {{ inline_admin_form.deletion_field.label_tag }}</span>{% endif %}
   </h3>
   {% if inline_admin_form.form.non_field_errors %}{{ inline_admin_form.form.non_field_errors }}{% endif %}
   {% for fieldset in inline_admin_form %}
-    {% include "adminsortable/shared/fieldset.html" with inline_admin_form_forloop=forloop.parentloop initial_forms_count=inline_admin_formset.formset.management_form.initial.INITIAL_FORMS %}
+    {% include "admin/includes/fieldset.html" %}
   {% endfor %}
-  {% if inline_admin_form.original %}
-  <input type="hidden" name="admin_sorting_url" value="{% url 'admin:admin_do_sorting' inline_admin_form.original.model_type_id %}" />
-  {% endif %}
   {% if inline_admin_form.needs_explicit_pk_field %}{{ inline_admin_form.pk_field.field }}{% endif %}
   {{ inline_admin_form.fk_field.field }}
+  {% if inline_admin_form.original %}
+  <input type="hidden" name="admin_sorting_url" value="{% url opts|admin_urlname:'do_sorting' inline_admin_form.original.model_type_id %}" />
+  {% endif %}
 </div>{% endfor %}
+</fieldset>
 </div>
-
-<script type="text/javascript">
-(function($) {
-  $("#{{ inline_admin_formset.formset.prefix }}-group .inline-related").stackedFormset({
-    prefix: '{{ inline_admin_formset.formset.prefix }}',
-    adminStaticPrefix: '{% static "admin/" %}',
-    deleteText: "{% trans "Remove" %}",
-    addText: "{% blocktrans with verbose_name=inline_admin_formset.opts.verbose_name|title %}Add another {{ verbose_name }}{% endblocktrans %}"
-  });
-})(django.jQuery);
-</script>
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 30f1772..0000000
--- a/adminsortable/templates/adminsortable/edit_inline/tabular-1.5.x.html
+++ /dev/null
@@ -1,134 +0,0 @@
-{% load i18n admin_modify adminsortable_tags %}
-{% load static from staticfiles %}
-<div class="inline-group" id="{{ inline_admin_formset.formset.prefix }}-group">
-  <div class="tabular inline-related {% if forloop.last %}last-related{% endif %}">
-{{ inline_admin_formset.formset.management_form }}
-<fieldset class="module">
-   <h2>{{ inline_admin_formset.opts.verbose_name_plural|capfirst }} {% if inline_admin_formset.formset.initial_form_count > 1 %} - {% trans "drag and drop to change order" %}{% endif %}</h2>
-   {{ inline_admin_formset.formset.non_form_errors }}
-   <table>
-     <thead><tr>
-     {% for field in inline_admin_formset.fields %}
-       {% if not field.widget.is_hidden %}
-         <th{% if forloop.first %} colspan="2"{% endif %}{% if field.required %} class="required"{% endif %}>{{ field.label|capfirst }}</th>
-       {% endif %}
-     {% endfor %}
-     {% if inline_admin_formset.formset.can_delete %}<th>{% trans "Delete?" %}</th>{% endif %}
-     </tr></thead>
-
-     <tbody>
-     {% for inline_admin_form in inline_admin_formset %}
-        {% if inline_admin_form.form.non_field_errors %}
-        <tr><td colspan="{{ inline_admin_form|cell_count }}">{{ inline_admin_form.form.non_field_errors }}</td></tr>
-        {% endif %}
-        <tr class="{% cycle "row1" "row2" %} {% if inline_admin_form.original or inline_admin_form.show_url %}has_original{% endif %}{% if forloop.last %} empty-form{% endif %}"
-             id="{{ inline_admin_formset.formset.prefix }}-{% if not forloop.last %}{{ forloop.counter0 }}{% else %}empty{% endif %}">
-        <td class="original">
-
-          {% if inline_admin_form.original or inline_admin_form.show_url %}<p>
-          {% if inline_admin_form.original %} {{ inline_admin_form.original }}{% endif %}
-          {% if inline_admin_form.show_url %}<a href="../../../r/{{ inline_admin_form.original_content_type_id }}/{{ inline_admin_form.original.id }}/">{% trans "View on site" %}</a>{% endif %}
-            </p>{% endif %}
-          {% if inline_admin_form.has_auto_field %}{{ inline_admin_form.pk_field.field }}{% endif %}
-          {{ inline_admin_form.fk_field.field }}
-          {% spaceless %}
-          {% for fieldset in inline_admin_form %}
-            {% for line in fieldset %}
-              {% for field in line %}
-                {% if field.is_hidden %} {{ field.field }} {% endif %}
-              {% endfor %}
-            {% endfor %}
-          {% endfor %}
-          {% endspaceless %}
-          {% if inline_admin_form.original %}
-          <input type="hidden" name="admin_sorting_url" value="{% url 'admin:admin_do_sorting' inline_admin_form.original.model_type_id %}" />
-          {% endif %}
-        </td>
-        {% for fieldset in inline_admin_form %}
-          {% for line in fieldset %}
-            {% for field in line %}
-              <td class="{{ field.field.name }}">
-                  {% if inline_admin_form.original and forloop.parentloop.counter == 1  %}<i class="fa fa-arrows-v"></i>{% endif %}
-              {% if field.is_readonly %}
-                  <p>{{ field.contents }}</p>
-              {% else %}
-                  {{ field.field.errors.as_ul }}
-                  {{ field.field }}
-              {% endif %}
-              </td>
-            {% endfor %}
-          {% endfor %}
-        {% endfor %}
-        {% if inline_admin_formset.formset.can_delete %}
-          <td class="delete">{% if inline_admin_form.original %}{{ inline_admin_form.deletion_field.field }}{% endif %}</td>
-        {% endif %}
-        </tr>
-     {% endfor %}
-     </tbody>
-   </table>
-</fieldset>
-  </div>
-</div>
-
-<script type="text/javascript">
-(function($) {
-    $(document).ready(function($) {
-        var rows = "#{{ inline_admin_formset.formset.prefix }}-group .tabular.inline-related tbody tr";
-        var alternatingRows = function(row) {
-            $(rows).not(".add-row").removeClass("row1 row2")
-                .filter(":even").addClass("row1").end()
-                .filter(rows + ":odd").addClass("row2");
-        }
-        var reinitDateTimeShortCuts = function() {
-            // Reinitialize the calendar and clock widgets by force
-            if (typeof DateTimeShortcuts != "undefined") {
-                $(".datetimeshortcuts").remove();
-                DateTimeShortcuts.init();
-            }
-        }
-        var updateSelectFilter = function() {
-            // If any SelectFilter widgets are a part of the new form,
-            // instantiate a new SelectFilter instance for it.
-            if (typeof SelectFilter != "undefined"){
-                $(".selectfilter").each(function(index, value){
-                  var namearr = value.name.split('-');
-                  SelectFilter.init(value.id, namearr[namearr.length-1], false, "{% static 'admin/' %}");
-                });
-                $(".selectfilterstacked").each(function(index, value){
-                  var namearr = value.name.split('-');
-                  SelectFilter.init(value.id, namearr[namearr.length-1], true, "{% static 'admin/' %}");
-                });
-            }
-        }
-        var initPrepopulatedFields = function(row) {
-            row.find('.prepopulated_field').each(function() {
-                var field = $(this);
-                var input = field.find('input, select, textarea');
-                var dependency_list = input.data('dependency_list') || [];
-                var dependencies = [];
-                $.each(dependency_list, function(i, field_name) {
-                  dependencies.push('#' + row.find(field_name).find('input, select, textarea').attr('id'));
-                });
-                if (dependencies.length) {
-                    input.prepopulate(dependencies, input.attr('maxlength'));
-                }
-            });
-        }
-        $(rows).formset({
-            prefix: "{{ inline_admin_formset.formset.prefix }}",
-            addText: "{% blocktrans with inline_admin_formset.opts.verbose_name|title as verbose_name %}Add another {{ verbose_name }}{% endblocktrans %}",
-            formCssClass: "dynamic-{{ inline_admin_formset.formset.prefix }}",
-            deleteCssClass: "inline-deletelink",
-            deleteText: "{% trans "Remove" %}",
-            emptyCssClass: "empty-form",
-            removed: alternatingRows,
-            added: (function(row) {
-                initPrepopulatedFields(row);
-                reinitDateTimeShortCuts();
-                updateSelectFilter();
-                alternatingRows(row);
-            })
-        });
-    });
-})(django.jQuery);
-</script>
diff --git a/adminsortable/templates/adminsortable/edit_inline/tabular.html b/adminsortable/templates/adminsortable/edit_inline/tabular.html
index fc69ace..9abc9cb 100644
--- a/adminsortable/templates/adminsortable/edit_inline/tabular.html
+++ b/adminsortable/templates/adminsortable/edit_inline/tabular.html
@@ -1,17 +1,19 @@
-{% load i18n admin_urls admin_static admin_modify django_template_additions %}{% load cycle from future %}
-{% get_django_version as django_version %}
-<div class="inline-group {% if django_version.major >= 1 and django_version.minor >= 9 %}flat-admin{% endif %}" id="{{ inline_admin_formset.formset.prefix }}-group">
+{% load i18n admin_urls static admin_modify django_template_additions %}
+<div class="js-inline-admin-formset inline-group" id="{{ inline_admin_formset.formset.prefix }}-group"
+     data-inline-type="tabular"
+     data-inline-formset="{{ inline_admin_formset.inline_formset_data }}">
   <div class="tabular inline-related {% if forloop.last %}last-related{% endif %}">
 {{ inline_admin_formset.formset.management_form }}
-<fieldset class="module">
-   <h2>{{ inline_admin_formset.opts.verbose_name_plural|capfirst }} {% if inline_admin_formset.formset.initial_form_count > 1 %} - {% trans "drag and drop to change order" %}{% endif %}</h2>
+<fieldset class="module {{ inline_admin_formset.classes }}">
+   <h2>{{ inline_admin_formset.opts.verbose_name_plural|capfirst }}</h2>
    {{ inline_admin_formset.formset.non_form_errors }}
    <table>
      <thead><tr>
+       <th class="original"></th>
      {% for field in inline_admin_formset.fields %}
        {% if not field.widget.is_hidden %}
-         <th{% if forloop.first %} colspan="2"{% endif %}{% if field.required %} class="required"{% endif %}>{{ field.label|capfirst }}
-         {% if field.help_text %}&nbsp;<img src="{% static "admin/img/icon-unknown.gif" %}" class="help help-tooltip" width="10" height="10" alt="({{ field.help_text|striptags }})" title="{{ field.help_text|striptags }}" />{% endif %}
+         <th{% if field.required %} class="required"{% endif %}>{{ field.label|capfirst }}
+         {% if field.help_text %}&nbsp;<img src="{% static "admin/img/icon-unknown.svg" %}" class="help help-tooltip" width="10" height="10" alt="({{ field.help_text|striptags }})" title="{{ field.help_text|striptags }}" />{% endif %}
          </th>
        {% endif %}
      {% endfor %}
@@ -26,17 +28,15 @@
         <tr class="form-row {% cycle "row1" "row2" %} {% if inline_admin_form.original or inline_admin_form.show_url %}has_original{% endif %}{% if forloop.last %} empty-form{% endif %}"
              id="{{ inline_admin_formset.formset.prefix }}-{% if not forloop.last %}{{ forloop.counter0 }}{% else %}empty{% endif %}">
         <td class="original">
-            {% with initial_forms=inline_admin_form.formset.management_form.initial.INITIAL_FORMS %}
-            {% if forloop.counter <= initial_forms %}
-            <i class="fa fa-{% if forloop.first %}sort-desc{% elif forloop.counter == initial_forms %}sort-asc{% else %}sort{% endif %}"></i>
-            {% endif %}
-            {% endwith %}
           {% if inline_admin_form.original or inline_admin_form.show_url %}<p>
+            {% with initial_forms_count=inline_admin_form.formset.management_form.INITIAL_FORMS.value %}
+            <i class="fa fa-{% if forloop.first %}sort-desc{% elif forloop.counter == initial_forms_count %}sort-asc{% else %}sort{% endif %}"></i>
+            {% endwith %}
           {% if inline_admin_form.original %}
-              {{ inline_admin_form.original }}
-              {% if inline_admin_form.model_admin.show_change_link and inline_admin_form.model_admin.has_registered_model %}<a href="{% url inline_admin_form.model_admin.opts|admin_urlname:'change' inline_admin_form.original.pk|admin_urlquote %}" class="inlinechangelink">{% trans "Change" %}</a>{% endif %}
+          {{ inline_admin_form.original }}
+          {% if inline_admin_form.model_admin.show_change_link and inline_admin_form.model_admin.has_registered_model %}<a href="{% url inline_admin_form.model_admin.opts|admin_urlname:'change' inline_admin_form.original.pk|admin_urlquote %}" class="inlinechangelink">{% trans "Change" %}</a>{% endif %}
           {% endif %}
-          {% if inline_admin_form.show_url %}<a href="{% url 'admin:view_on_site' inline_admin_form.original_content_type_id inline_admin_form.original.pk %}">{% trans "View on site" %}</a>{% endif %}
+          {% if inline_admin_form.show_url %}<a href="{{ inline_admin_form.absolute_url }}">{% trans "View on site" %}</a>{% endif %}
             </p>{% endif %}
           {% if inline_admin_form.needs_explicit_pk_field %}{{ inline_admin_form.pk_field.field }}{% endif %}
           {{ inline_admin_form.fk_field.field }}
@@ -44,26 +44,28 @@
           {% for fieldset in inline_admin_form %}
             {% for line in fieldset %}
               {% for field in line %}
-                {% if field.is_hidden %} {{ field.field }} {% endif %}
+                {% if field.field.is_hidden %} {{ field.field }} {% endif %}
               {% endfor %}
             {% endfor %}
           {% endfor %}
           {% endspaceless %}
           {% if inline_admin_form.original %}
-          <input type="hidden" name="admin_sorting_url" value="{% url 'admin:admin_do_sorting' inline_admin_form.original.model_type_id %}" />
+          <input type="hidden" name="admin_sorting_url" value="{% url opts|admin_urlname:'do_sorting' inline_admin_form.original.model_type_id %}" />
           {% endif %}
         </td>
         {% for fieldset in inline_admin_form %}
           {% for line in fieldset %}
             {% for field in line %}
+              {% if not field.field.is_hidden %}
               <td{% if field.field.name %} class="field-{{ field.field.name }}"{% endif %}>
               {% if field.is_readonly %}
-                  <p>{{ field.contents|linebreaksbr }}</p>
+                  <p>{{ field.contents }}</p>
               {% else %}
                   {{ field.field.errors.as_ul }}
                   {{ field.field }}
               {% endif %}
               </td>
+              {% endif %}
             {% endfor %}
           {% endfor %}
         {% endfor %}
@@ -77,15 +79,3 @@
 </fieldset>
   </div>
 </div>
-
-<script type="text/javascript">
-
-(function($) {
-  $("#{{ inline_admin_formset.formset.prefix }}-group .tabular.inline-related tbody tr").tabularFormset({
-    prefix: "{{ inline_admin_formset.formset.prefix }}",
-    adminStaticPrefix: '{% static "admin/" %}',
-    addText: "{% blocktrans with inline_admin_formset.opts.verbose_name|title as verbose_name %}Add another {{ verbose_name }}{% endblocktrans %}",
-    deleteText: "{% trans 'Remove' %}"
-  });
-})(django.jQuery);
-</script>
diff --git a/adminsortable/templates/adminsortable/shared/fieldset.html b/adminsortable/templates/adminsortable/shared/fieldset.html
deleted file mode 100644
index 89659ae..0000000
--- a/adminsortable/templates/adminsortable/shared/fieldset.html
+++ /dev/null
@@ -1,35 +0,0 @@
-{# overrides admin/includes/fieldset.html: https://github.com/django/django/blob/master/django/contrib/admin/templates/admin/includes/fieldset.html #}
-<fieldset class="module aligned {{ fieldset.classes }}">
-    {% if fieldset.name %}<h2>{{ fieldset.name }}</h2>{% endif %}
-    {% if fieldset.description %}
-        <div class="description">{{ fieldset.description|safe }}</div>
-    {% endif %}
-    {% for line in fieldset %}
-        <div class="form-row{% if line.fields|length_is:'1' and line.errors %} errors{% endif %}{% if not line.has_visible_field %} hidden{% endif %}{% for field in line %}{% if field.field.name %} field-{{ field.field.name }}{% endif %}{% endfor %}">
-            {% if line.fields|length_is:'1' %}{{ line.errors }}{% endif %}
-            {% for field in line %}
-                <div{% if not line.fields|length_is:'1' %} class="field-box{% if field.field.name %} field-{{ field.field.name }}{% endif %}{% if not field.is_readonly and field.errors %} errors{% endif %}{% if field.field.is_hidden %} hidden{% endif %}"{% elif field.is_checkbox %} class="checkbox-row"{% endif %}>
-                    {% if not line.fields|length_is:'1' and not field.is_readonly %}{{ field.errors }}{% endif %}
-                    {% if forloop.first  %}
-                        {% if inline_admin_form_forloop.counter <= initial_forms_count %}
-                        <i class="fa fa-{% if inline_admin_form_forloop.first %}sort-desc{% elif inline_admin_form_forloop.counter == initial_forms_count %}sort-asc{% else %}sort{% endif %}"></i>
-                        {% endif %}
-                    {% endif %}
-                    {% if field.is_checkbox %}
-                        {{ field.field }}{{ field.label_tag }}
-                    {% else %}
-                        {{ field.label_tag }}
-                        {% if field.is_readonly %}
-                            <p>{{ field.contents }}</p>
-                        {% else %}
-                            {{ field.field }}
-                        {% endif %}
-                    {% endif %}
-                    {% if field.field.help_text %}
-                        <p class="help">{{ field.field.help_text|safe }}</p>
-                    {% endif %}
-                </div>
-            {% endfor %}
-        </div>
-    {% endfor %}
-</fieldset>
diff --git a/adminsortable/templates/adminsortable/shared/list_items.html b/adminsortable/templates/adminsortable/shared/list_items.html
index 29df030..21f9005 100644
--- a/adminsortable/templates/adminsortable/shared/list_items.html
+++ b/adminsortable/templates/adminsortable/shared/list_items.html
@@ -1,9 +1,8 @@
-{% load adminsortable_tags %}
 {% with list_objects_length=list_objects|length %}
     {% for object in list_objects %}
         <li>
             {% if list_objects_length > 1 %}
-                {% render_object_rep object forloop %}
+                {% include "adminsortable/shared/object_rep.html" %}
             {% else %}
                 {{ object }}
             {% endif %}
diff --git a/adminsortable/templates/adminsortable/shared/nested_objects.html b/adminsortable/templates/adminsortable/shared/nested_objects.html
index 8ef74d9..cf0483b 100644
--- a/adminsortable/templates/adminsortable/shared/nested_objects.html
+++ b/adminsortable/templates/adminsortable/shared/nested_objects.html
@@ -1,4 +1,4 @@
-{% load django_template_additions adminsortable_tags %}
+{% load django_template_additions %}
 {% dynamic_regroup objects by group_expression as regrouped_objects %}
 {% if regrouped_objects %}
     <ul {% if sortable_by_class_is_sortable %}class="sortable"{% endif %}>
@@ -6,7 +6,7 @@
             {% with object=regrouped_object.grouper %}
                 {% if object %}
                     <li class="parent">{% if sortable_by_class_is_sortable %}
-                            {% render_object_rep object forloop %}
+                            {% include "adminsortable/shared/object_rep.html" %}
                         {% else %}
                             {{ object }}
                         {% endif %}
@@ -14,7 +14,7 @@
                         {% if regrouped_object.list %}
                             {% with regrouped_object_list_length=regrouped_object.list|length %}
                             <ul {% if regrouped_object_list_length > 1 %}class="sortable"{% endif %}>
-                                {% render_list_items regrouped_object.list %}
+                                {% include "adminsortable/shared/list_items.html" with list_objects=regrouped_object.list %}
                             </ul>
                             {% endwith %}
                         {% endif %}
diff --git a/adminsortable/templates/adminsortable/shared/object_rep.html b/adminsortable/templates/adminsortable/shared/object_rep.html
index ffe9edf..960ba9c 100644
--- a/adminsortable/templates/adminsortable/shared/object_rep.html
+++ b/adminsortable/templates/adminsortable/shared/object_rep.html
@@ -1,6 +1,7 @@
-{% load adminsortable_tags %}
+{% load admin_urls l10n %}
 
 <form>
-    <input name="pk" type="hidden" value="{{ object.pk }}" />
+    <input name="pk" type="hidden" value="{{ object.pk|unlocalize }}" />
+    <a href="{% url opts|admin_urlname:'do_sorting' object.model_type_id|unlocalize %}{% if filters %}?{{ filters }}{% endif %}" class="admin_sorting_url"><i class="fa fa-{% if forloop.first %}sort-desc{% elif forloop.last %}sort-asc{% else %}sort{% endif %}"></i> {{ object }}</a>
+    {% csrf_token %}
 </form>
-<a href="{% url 'admin:admin_do_sorting' object.model_type_id %}" class="admin_sorting_url"><i class="fa fa-{% if forloop.first %}sort-desc{% elif forloop.last %}sort-asc{% else %}sort{% endif %}"></i> {{ object }}</a>
diff --git a/adminsortable/templates/adminsortable/shared/objects.html b/adminsortable/templates/adminsortable/shared/objects.html
index 8f0af7c..52e403d 100644
--- a/adminsortable/templates/adminsortable/shared/objects.html
+++ b/adminsortable/templates/adminsortable/shared/objects.html
@@ -1,7 +1,5 @@
-{% load adminsortable_tags %}
-
 {% if objects %}
 	<ul class="sortable single">
-		{% render_list_items objects %}
+		{% include "adminsortable/shared/list_items.html" with list_objects=objects %}
 	</ul>
 {% endif %}
\ No newline at end of file
diff --git a/adminsortable/templatetags/adminsortable_tags.py b/adminsortable/templatetags/adminsortable_tags.py
deleted file mode 100644
index bbed9a0..0000000
--- a/adminsortable/templatetags/adminsortable_tags.py
+++ /dev/null
@@ -1,35 +0,0 @@
-from django import template
-
-register = template.Library()
-
-
-@register.simple_tag(takes_context=True)
-def render_sortable_objects(context, objects,
-        sortable_objects_template='adminsortable/shared/objects.html'):
-    context.update({'objects': objects})
-    tmpl = template.loader.get_template(sortable_objects_template)
-    return tmpl.render(context)
-
-
-@register.simple_tag(takes_context=True)
-def render_nested_sortable_objects(context, objects, group_expression,
-        sortable_nested_objects_template='adminsortable/shared/nested_objects.html'):
-    context.update({'objects': objects, 'group_expression': group_expression})
-    tmpl = template.loader.get_template(sortable_nested_objects_template)
-    return tmpl.render(context)
-
-
-@register.simple_tag(takes_context=True)
-def render_list_items(context, list_objects,
-        sortable_list_items_template='adminsortable/shared/list_items.html'):
-    context.update({'list_objects': list_objects})
-    tmpl = template.loader.get_template(sortable_list_items_template)
-    return tmpl.render(context)
-
-
-@register.simple_tag(takes_context=True)
-def render_object_rep(context, obj, forloop,
-        sortable_object_rep_template='adminsortable/shared/object_rep.html'):
-    context.update({'object': obj, 'forloop': forloop})
-    tmpl = template.loader.get_template(sortable_object_rep_template)
-    return tmpl.render(context)
diff --git a/adminsortable/templatetags/django_template_additions.py b/adminsortable/templatetags/django_template_additions.py
index c05f349..0729bb3 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]
@@ -80,7 +76,7 @@ def dynamic_regroup(parser, token):
     return DynamicRegroupNode(target, parser, expression, var_name)
 
 
-@register.assignment_tag
+@register.simple_tag
 def get_django_version():
     version = django.VERSION
     return {'major': version[0], 'minor': version[1]}
diff --git a/debian/changelog b/debian/changelog
index 2252fb0..f905f72 100644
--- a/debian/changelog
+++ b/debian/changelog
@@ -1,4 +1,4 @@
-python-django-adminsortable (2.0.10-4) UNRELEASED; urgency=low
+python-django-adminsortable (2.3.0+git20220312.1.b9c1f3e-1) UNRELEASED; urgency=low
 
   [ Debian Janitor ]
   * Bump debhelper from deprecated 9 to 12.
@@ -12,8 +12,9 @@ python-django-adminsortable (2.0.10-4) UNRELEASED; urgency=low
   [ Debian Janitor ]
   * Bump debhelper from old 12 to 13.
   * Set upstream metadata fields: Repository-Browse.
+  * New upstream snapshot.
 
- -- Debian Janitor <janitor@jelmer.uk>  Sat, 18 Jul 2020 20:46:16 -0000
+ -- Debian Janitor <janitor@jelmer.uk>  Thu, 19 Jan 2023 21:34:02 -0000
 
 python-django-adminsortable (2.0.10-3) unstable; urgency=medium
 
diff --git a/django_admin_sortable.egg-info/PKG-INFO b/django_admin_sortable.egg-info/PKG-INFO
index 84cebe5..636125d 100644
--- a/django_admin_sortable.egg-info/PKG-INFO
+++ b/django_admin_sortable.egg-info/PKG-INFO
@@ -1,19 +1,796 @@
-Metadata-Version: 1.1
+Metadata-Version: 2.1
 Name: django-admin-sortable
-Version: 2.0.10
+Version: 2.3
 Summary: Drag and drop sorting for models and inline models in Django admin.
 Home-page: https://github.com/iambrandontaylor/django-admin-sortable
 Author: Brandon Taylor
 Author-email: alsoicode@gmail.com
 License: APL
-Description: UNKNOWN
-Platform: UNKNOWN
 Classifier: Development Status :: 5 - Production/Stable
 Classifier: Environment :: Web Environment
 Classifier: Framework :: Django
 Classifier: Intended Audience :: Developers
 Classifier: License :: OSI Approved :: Apache Software License
 Classifier: Operating System :: OS Independent
-Classifier: Programming Language :: Python :: 2
 Classifier: Programming Language :: Python :: 3
 Classifier: Topic :: Utilities
+License-File: AUTHORS
+
+Django Admin Sortable
+=====================
+
+|PyPI version| |Python versions| |Build Status|
+
+This project makes it easy to add drag-and-drop ordering to any model in
+Django admin. Inlines for a sortable model may also be made sortable,
+enabling individual items or groups of items to be sortable.
+
+If you find Django Admin Sortable to be helpful, consider `buying me a
+coffee <https://www.buymeacoffee.com/NY9TUAEwF>`__!
+
+Sorting model instances with a sortable parent:
+
+.. figure:: http://res.cloudinary.com/alsoicode/image/upload/v1451237555/django-admin-sortable/sortable-models.jpg
+   :alt: sortable-models
+
+   sortable-models
+
+Sorting inlines:
+
+.. figure:: http://res.cloudinary.com/alsoicode/image/upload/v1451237555/django-admin-sortable/sortable-inlines.jpg
+   :alt: sortable-inlines
+
+   sortable-inlines
+
+Supported Django Versions
+-------------------------
+
+For Django 4 use the latest version
+
+For Django 3 use 2.2.4
+
+For Django 1.8.x < 3.0, use 2.1.8.
+
+For Django 1.5.x to 1.7.x, use version 2.0.18.
+
+Other notes of interest regarding versions
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+django-admin-sortable 1.5.2 introduced backward-incompatible changes for
+Django 1.4.x
+
+django-admin-sortable 1.6.6 introduced a backward-incompatible change
+for the ``sorting_filters`` attribute. Please convert your attributes to
+the new tuple-based format if you haven’t already.
+
+django-admin-sortable 1.7.1 and higher are compatible with Python 3.
+
+django-admin-sortable 2.1.6 has a bug. Please don’t use it :)
+
+Installation
+------------
+
+1. ``$ pip install django-admin-sortable``
+
+–or–
+
+Download django-admin-sortable from
+`source <https://github.com/iambrandontaylor/django-admin-sortable/archive/master.zip>`__
+
+1. Unzip the directory and cd into the uncompressed project directory
+
+2.
+
+   -  Optional: Enable your virtualenv
+
+3. Run ``$ python setup.py install`` or add ``adminsortable`` to your
+   PYTHONPATH.
+
+Configuration
+-------------
+
+1. Add ``adminsortable`` to your ``INSTALLED_APPS``.
+2. Ensure ``django.template.context_processors.static`` is in your
+   ``TEMPLATES["OPTIONS"]["context_processors"]``.
+
+   -  (In older versions of Django, ensure
+      ``django.core.context_processors.static`` is in
+      ``TEMPLATE_CONTEXT_PROCESSORS`` instead.)
+
+3. Ensure that ``CSRF_COOKIE_HTTPONLY`` has not been set to ``True``, as
+   django-admin-sortable is currently incompatible with that setting.
+
+Static Media
+~~~~~~~~~~~~
+
+Preferred: Use the `staticfiles
+app <https://docs.djangoproject.com/en/3.0/howto/static-files/>`__
+
+Alternate: Copy the ``adminsortable`` folder from the ``static`` folder
+to the location you serve static files from.
+
+Testing
+~~~~~~~
+
+Have a look at the included sample_project to see working examples. The
+login credentials for admin are: admin/admin
+
+When a model is sortable, a tool-area link will be added that says
+“Change Order”. Click this link, and you will be taken to the custom
+view where you can drag-and-drop the records into order.
+
+Inlines may be drag-and-dropped into any order directly from the change
+form.
+
+Usage
+-----
+
+Models
+~~~~~~
+
+To add “sortability” to a model, you need to inherit ``SortableMixin``
+and at minimum, define:
+
+-  The field which should be used for ``Meta.ordering``, which must
+   resolve to one of the integer fields defined in Django’s ORM:
+
+-  ``PositiveIntegerField``
+
+-  ``IntegerField``
+
+-  ``PositiveSmallIntegerField``
+
+-  ``SmallIntegerField``
+
+-  ``BigIntegerField``
+
+-  ``Meta.ordering`` **must only contain one value**, otherwise, your
+   objects will not be sorted correctly.
+
+-  **IMPORTANT**: You must name the field you use for ordering something
+   other than “order_field” as this name is reserved by the
+   ``SortableMixin`` class.
+
+-  It is recommended that you set ``editable=False`` and
+   ``db_index=True`` on the field defined in ``Meta.ordering`` for a
+   seamless Django admin experience and faster lookups on the objects.
+
+Sample Model:
+
+.. code:: python
+
+   # models.py
+   from adminsortable.models import SortableMixin
+
+   class MySortableClass(SortableMixin):
+       title = models.CharField(max_length=50)
+
+       class Meta:
+           verbose_name = 'My Sortable Class'
+           verbose_name_plural = 'My Sortable Classes'
+           ordering = ['the_order']
+
+
+       # define the field the model should be ordered by
+       the_order = models.PositiveIntegerField(default=0, editable=False, db_index=True)
+
+       def __unicode__(self):
+           return self.title
+
+Support for models that don’t use an ``AutoField`` for their primary key
+are also supported in version 2.0.20 or higher.
+
+Common Use Case
+^^^^^^^^^^^^^^^
+
+A common use case is to have child objects that are sortable relative to
+a parent. If your parent object is also sortable, here’s how you would
+set up your models and admin options:
+
+.. code:: python
+
+   # models.py
+   from adminsortable.fields import SortableForeignKey
+
+   class Category(SortableMixin):
+       class Meta:
+           ordering = ['category_order']
+           verbose_name_plural = 'Categories'
+
+       title = models.CharField(max_length=50)
+
+       # ordering field
+       category_order = models.PositiveIntegerField(default=0, editable=False, db_index=True)
+
+   class Project(SortableMixin):
+       class Meta:
+           ordering = ['project_order']
+
+       category = SortableForeignKey(Category)
+       title = models.CharField(max_length=50)
+
+       # ordering field
+       project_order = models.PositiveIntegerField(default=0, editable=False, db_index=True)
+
+       def __unicode__(self):
+           return self.title
+
+   # admin.py
+   from adminsortable.admin import SortableAdmin
+
+   from your_app.models import Category, Project
+
+   admin.site.register(Category, SortableAdmin)
+   admin.site.register(Project, SortableAdmin)
+
+Sometimes you might have a parent model that is not sortable, but has
+child models that are. In that case define your models and admin options
+as such:
+
+.. code:: python
+
+   from adminsortable.fields import SortableForeignKey
+
+   # models.py
+   class Category(models.Model):
+       class Meta:
+           verbose_name_plural = 'Categories'
+
+       title = models.CharField(max_length=50)
+       ...
+
+   class Project(SortableMixin):
+       class Meta:
+           ordering = ['project_order']
+
+       category = SortableForeignKey(Category)
+       title = models.CharField(max_length=50)
+
+       # ordering field
+       project_order = models.PositiveIntegerField(default=0, editable=False, db_index=True)
+
+       def __unicode__(self):
+           return self.title
+
+   # admin
+   from adminsortable.admin import NonSortableParentAdmin, SortableStackedInline
+
+   from your_app.models import Category, Project
+
+   class ProjectInline(SortableStackedInline):
+       model = Project
+       extra = 1
+
+   class CategoryAdmin(NonSortableParentAdmin):
+       inlines = [ProjectInline]
+
+   admin.site.register(Category, CategoryAdmin)
+
+The ``NonSortableParentAdmin`` class is necessary to wire up the
+additional URL patterns and JavaScript that Django Admin Sortable needs
+to make your models sortable. The child model does not have to be an
+inline model, it can be wired directly to Django admin and the objects
+will be grouped by the non-sortable foreign key when sorting.
+
+Backwards Compatibility
+~~~~~~~~~~~~~~~~~~~~~~~
+
+If you previously used Django Admin Sortable, **DON’T PANIC** -
+everything will still work exactly as before **without any changes to
+your code**. Going forward, it is recommended that you use the new
+``SortableMixin`` on your models, as pre-2.0 compatibility might not be
+a permanent thing.
+
+Please note however that the ``Sortable`` class still contains the
+hard-coded ``order`` field, and meta inheritance requirements:
+
+.. code:: python
+
+   # legacy model definition
+
+   from adminsortable.models import Sortable
+
+   class Project(Sortable):
+       class Meta(Sortable.Meta):
+           pass
+       title = models.CharField(max_length=50)
+
+       def __unicode__(self):
+           return self.title
+
+Model Instance Methods
+^^^^^^^^^^^^^^^^^^^^^^
+
+Each instance of a sortable model has two convenience methods to get the
+next or previous instance:
+
+.. code:: python
+
+       .get_next()
+       .get_previous()
+
+By default, these methods will respect their order in relation to a
+``SortableForeignKey`` field, if present. Meaning, that given the
+following data:
+
+::
+
+   | Parent Model 1 |               |
+   |                | Child Model 1 |
+   |                | Child Model 2 |
+   | Parent Model 2 |               |
+   |                | Child Model 3 |
+   |                | Child Model 4 |
+   |                | Child Model 5 |
+
+“Child Model 2” ``get_next()`` would return ``None`` “Child Model 3”
+``get_previous`` would return ``None``
+
+If you wish to override this behavior, pass in:
+``filter_on_sortable_fk=False``:
+
+.. code:: python
+
+       your_instance.get_next(filter_on_sortable_fk=False)
+
+You may also pass in additional ORM “filer_args” as a list, or
+“filter_kwargs” as a dictionary, should you need to:
+
+.. code:: python
+
+       your_instance.get_next(
+           filter_args=[Q(field1=True) | Q(field2=True)],
+           filter_kwargs={'title__icontains': 'blue'}
+       )
+
+Deprecation Warning
+^^^^^^^^^^^^^^^^^^^
+
+Previously “filter_kwargs” was named “extra_filters”. With the addition
+of “filter_args”, “extra_filters” was renamed for consistency.
+
+Adding Sorting to an existing model
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Django 1.5.x to 1.6.x
+^^^^^^^^^^^^^^^^^^^^^
+
+If you’re adding Sorting to an existing model, it is recommended that
+you use `django-south <http://south.areacode.com/>`__ to create a schema
+migration to add the “order” field to your model. You will also need to
+create a data migration in order to add the appropriate values for the
+“order” column.
+
+Example assuming a model named “Category”:
+
+.. code:: python
+
+   def forwards(self, orm):
+       for index, category in enumerate(orm.Category.objects.all()):
+           category.order = index + 1
+           category.save()
+
+See: `this
+link <http://south.readthedocs.org/en/latest/tutorial/part3.html>`__ for
+more information on South Data Migrations.
+
+Django 1.7.x or higher
+^^^^^^^^^^^^^^^^^^^^^^
+
+Since schema migrations are built into Django 1.7, you don’t have to use
+south, but the process of adding and running migrations is nearly
+identical. Take a look at the
+`Migrations <https://docs.djangoproject.com/en/1.7/topics/migrations/>`__
+documentation to get started.
+
+Django Admin Integration
+~~~~~~~~~~~~~~~~~~~~~~~~
+
+To enable sorting in the admin, you need to inherit from
+``SortableAdmin``:
+
+.. code:: python
+
+   from django.contrib import admin
+   from myapp.models import MySortableClass
+   from adminsortable.admin import SortableAdmin
+
+   class MySortableAdminClass(SortableAdmin):
+       """Any admin options you need go here"""
+
+   admin.site.register(MySortableClass, MySortableAdminClass)
+
+To enable sorting on TabularInline models, you need to inherit from
+SortableTabularInline:
+
+.. code:: python
+
+   from adminsortable.admin import SortableTabularInline
+
+   class MySortableTabularInline(SortableTabularInline):
+       """Your inline options go here"""
+
+To enable sorting on StackedInline models, you need to inherit from
+SortableStackedInline:
+
+.. code:: python
+
+   from adminsortable.admin import SortableStackedInline
+
+   class MySortableStackedInline(SortableStackedInline):
+      """Your inline options go here"""
+
+There are also generic equivalents that you can inherit from:
+
+.. code:: python
+
+   from adminsortable.admin import (SortableGenericTabularInline,
+       SortableGenericStackedInline)
+       """Your generic inline options go here"""
+
+If your parent model is *not* sortable, but has child inlines that are,
+your parent model needs to inherit from ``NonSortableParentAdmin``:
+
+.. code:: python
+
+   from adminsortable.admin import (NonSortableParentAdmin,
+       SortableTabularInline)
+
+   class ChildTabularInline(SortableTabularInline):
+       model = YourModel
+
+   class ParentAdmin(NonSortableParentAdmin):
+       inlines = [ChildTabularInline]
+
+Overriding ``queryset()``
+^^^^^^^^^^^^^^^^^^^^^^^^^
+
+django-admin-sortable supports custom queryset overrides on admin models
+and inline models in Django admin!
+
+If you’re providing an override of a SortableAdmin or Sortable inline
+model, you don’t need to do anything extra. django-admin-sortable will
+automatically honor your queryset.
+
+Have a look at the WidgetAdmin class in the sample project for an
+example of an admin class with a custom ``queryset()`` override.
+
+Overriding ``queryset()`` for an inline model
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+This is a special case, which requires a few lines of extra code to
+properly determine the sortability of your model. Example:
+
+.. code:: python
+
+   # add this import to your admin.py
+   from adminsortable.utils import get_is_sortable
+
+
+   class ComponentInline(SortableStackedInline):
+       model = Component
+
+       def queryset(self, request):
+           qs = super(ComponentInline, self).queryset(request).filter(
+               title__icontains='foo')
+
+           # You'll need to add these lines to determine if your model
+           # is sortable once we hit the change_form() for the parent model.
+
+           if get_is_sortable(qs):
+               self.model.is_sortable = True
+           else:
+               self.model.is_sortable = False
+           return qs
+
+If you override the queryset of an inline, the number of objects present
+may change, and adminsortable won’t be able to automatically determine
+if the inline model is sortable from here, which is why we have to set
+the ``is_sortable`` property of the model in this method.
+
+Sorting subsets of objects
+^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+It is also possible to sort a subset of objects in your model by adding
+a ``sorting_filters`` tuple. This works exactly the same as
+``.filter()`` on a QuerySet, and is applied *after* ``get_queryset()``
+on the admin class, allowing you to override the queryset as you would
+normally in admin but apply additional filters for sorting. The text
+“Change Order of” will appear before each filter in the Change List
+template, and the filter groups are displayed from left to right in the
+order listed. If no ``sorting_filters`` are specified, the text “Change
+Order” will be displayed for the link.
+
+Self-Referential SortableForeignKey
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+You can specify a self-referential SortableForeignKey field, however the
+admin interface will currently show a model that is a grandchild at the
+same level as a child. I’m working to resolve this issue.
+
+Important!
+''''''''''
+
+django-admin-sortable 1.6.6 introduced a backwards-incompatible change
+for ``sorting_filters``. Previously this attribute was defined as a
+dictionary, so you’ll need to change your values over to the new
+tuple-based format.
+
+An example of sorting subsets would be a “Board of Directors”. In this
+use case, you have a list of “People” objects. Some of these people are
+on the Board of Directors and some not, and you need to sort them
+independently.
+
+.. code:: python
+
+   class Person(Sortable):
+       class Meta(Sortable.Meta):
+           verbose_name_plural = 'People'
+
+       first_name = models.CharField(max_length=50)
+       last_name = models.CharField(max_length=50)
+       is_board_member = models.BooleanField('Board Member', default=False)
+
+       sorting_filters = (
+           ('Board Members', {'is_board_member': True}),
+           ('Non-Board Members', {'is_board_member': False}),
+       )
+
+       def __unicode__(self):
+           return '{} {}'.format(self.first_name, self.last_name)
+
+Extending custom templates
+^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+By default, adminsortable’s change form and change list views inherit
+from Django admin’s standard templates. Sometimes you need to have a
+custom change form or change list, but also need adminsortable’s CSS and
+JavaScript for inline models that are sortable for example.
+
+SortableAdmin has two attributes you can override for this use case:
+
+.. code:: python
+
+   change_form_template_extends
+   change_list_template_extends
+
+These attributes have default values of:
+
+.. code:: python
+
+   change_form_template_extends = 'admin/change_form.html'
+   change_list_template_extends = 'admin/change_list.html'
+
+If you need to extend the inline change form templates, you’ll need to
+select the right one, depending on your version of Django. For 1.10.x or
+below, you’ll need to extend one of the following:
+
+::
+
+   templates/adminsortable/edit_inline/stacked-1.10.x.html
+   templates/adminsortable/edit_inline/tabular-inline-1.10.x.html
+
+otherwise, extend:
+
+::
+
+   templates/adminsortable/edit_inline/stacked.html
+   templates/adminsortable/edit_inline/tabular.html
+
+A Special Note About Stacked Inlines…
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+The height of a stacked inline model can dynamically increase, which can
+make them difficult to sort. If you anticipate the height of a stacked
+inline is going to be very tall, I would suggest using
+SortableTabularInline instead.
+
+Custom JS callbacks after sorting is complete
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+If you need to define a custom event or other callback to be executed
+after sorting is completed, you’ll need to:
+
+1. Create a custom template for to add your JavaScript
+2. Populate the ``after_sorting_js_callback_name`` on your model admin
+
+An example of this can be found in the “samples” application in the
+source. Here’s a model admin for a model called “Project”:
+
+.. code:: python
+
+   class ProjectAdmin(SortableAdmin):
+       inlines = [
+           CreditInline, NoteInline, GenericNoteInline,
+           NonSortableCreditInline, NonSortableNoteInline
+       ]
+       list_display = ['__str__', 'category']
+
+       after_sorting_js_callback_name = 'afterSortCallback'  # do not include () - just function name
+       sortable_change_list_template = 'adminsortable/custom_change_list.html'
+       sortable_change_form_template = "adminsortable/custom_change_form.html"
+
+This example is going to add a custom callback on the parent model, and
+it’s inlines. Here is the JavaScript added to the custom change list:
+
+.. code:: html+django
+
+   {% extends 'adminsortable/change_list.html' %}
+
+   {% block extrahead %}
+     {{ block.super }}
+
+     <script>
+       django.jQuery(document).on('order:changed', function(event) {
+         console.log(event.message);
+         // your code here
+       });
+
+       window['{{ after_sorting_js_callback_name }}'] = function() {
+         django.jQuery(document).trigger({ type: 'order:changed', message: 'Order changed', time: new Date() });
+       };
+     </script>
+   {% endblock %}
+
+and the custom change form, for the inline models:
+
+.. code:: html+django
+
+   {% extends "adminsortable/change_form.html" %}
+
+   {% block extrahead %}
+     {{ block.super }}
+
+     <script>
+       django.jQuery(document).on('order:changed', function(event) {
+         console.log(event.message);
+         // your code here
+       });
+
+       window['{{ after_sorting_js_callback_name }}'] = function() {
+         django.jQuery(document).trigger({ type: 'order:changed', message: 'Order changed', time: new Date() });
+       };
+     </script>
+   {% endblock %}
+
+Ideally, you’d pull in a shared piece of code for your callback to keep
+your code DRY.
+
+Django-CMS integration
+~~~~~~~~~~~~~~~~~~~~~~
+
+Django-CMS plugins use their own change form, and thus won’t
+automatically include the necessary JavaScript for django-admin-sortable
+to work. Fortunately, this is easy to resolve, as the ``CMSPlugin``
+class allows a change form template to be specified:
+
+.. code:: python
+
+   # example plugin
+   from cms.plugin_base import CMSPluginBase
+
+   class CMSCarouselPlugin(CMSPluginBase):
+       admin_preview = False
+       change_form_template = 'cms/sortable-stacked-inline-change-form.html'
+       inlines = [SlideInline]
+       model = Carousel
+       name = _('Carousel')
+       render_template = 'carousels/carousel.html'
+
+       def render(self, context, instance, placeholder):
+           context.update({
+               'carousel': instance,
+               'placeholder': placeholder
+           })
+           return context
+
+   plugin_pool.register_plugin(CMSCarouselPlugin)
+
+The contents of ``sortable-stacked-inline-change-form.html`` at a
+minimum need to extend the extrahead block with:
+
+.. code:: html+django
+
+   {% extends "admin/cms/page/plugin_change_form.html" %}
+   {% load static from staticfiles %}
+
+   {% block extrahead %}
+       {{ block.super }}
+       <script src="{% static 'adminsortable/js/jquery-ui-django-admin.min.js' %}"></script>
+       <script src="{% static 'adminsortable/js/jquery.ui.touch-punch.min.js' %}"></script>
+       <script src="{% static 'adminsortable/js/jquery.django-csrf.js' %}"></script>
+       <script src="{% static 'adminsortable/js/admin.sortable.stacked.inlines.js' %}"></script>
+
+       <link rel="stylesheet" type="text/css" href="{% static 'adminsortable/css/admin.sortable.inline.css' %}" />
+   {% endblock extrahead %}
+
+Sorting within Django-CMS is really only feasible for inline models of a
+plugin as Django-CMS already includes sorting for plugin instances. For
+tabular inlines, just substitute:
+
+.. code:: html+django
+
+   <script src="{% static 'adminsortable/js/admin.sortable.stacked.inlines.js' %}"></script>
+
+with:
+
+.. code:: html+django
+
+   <script src="{% static 'adminsortable/js/admin.sortable.tabular.inlines.js' %}"></script>
+
+Notes
+~~~~~
+
+From ``django-cms 3.x`` the path of change_form.html has changed.
+Replace the follwing line:
+
+.. code:: html+django
+
+   {% extends "admin/cms/page/plugin_change_form.html" %}
+
+with
+
+.. code:: html+django
+
+   {% extends "admin/cms/page/plugin/change_form.html" %}
+
+From ``django-admin-sortable 2.0.13`` the ``jquery.django-csrf.js`` was
+removed and you have to include the snippet-template. Change the
+following line:
+
+.. code:: html+django
+
+   <script type="text/javascript" src="{% static 'adminsortable/js/jquery.django-csrf.js' %}"></script>
+
+to
+
+.. code:: html+django
+
+   {% include 'adminsortable/csrf/jquery.django-csrf.html' with csrf_cookie_name='csrftoken' %}
+
+Please note, if you change the ``CSRF_COOKIE_NAME`` you have to adjust
+``csrf_cookie_name='YOUR_CSRF_COOKIE_NAME'``
+
+Rationale
+~~~~~~~~~
+
+Other projects have added drag-and-drop ordering to the ChangeList view,
+however this introduces a couple of problems…
+
+-  The ChangeList view supports pagination, which makes drag-and-drop
+   ordering across pages impossible.
+-  The ChangeList view by default, does not order records based on a
+   foreign key, nor distinguish between rows that are associated with a
+   foreign key. This makes ordering the records grouped by a foreign key
+   impossible.
+-  The ChangeList supports in-line editing, and adding drag-and-drop
+   ordering on top of that just seemed a little much in my opinion.
+
+Status
+~~~~~~
+
+django-admin-sortable is currently used in production.
+
+What’s new in 2.3.0?
+~~~~~~~~~~~~~~~~~~~~
+
+-  Django 4 compatibility
+
+Future
+~~~~~~
+
+-  Better template support for foreign keys that are self referential.
+   If someone would like to take on rendering recursive sortables, that
+   would be super.
+
+License
+~~~~~~~
+
+django-admin-sortable is released under the Apache Public License v2.
+
+.. |PyPI version| image:: https://img.shields.io/pypi/v/django-admin-sortable.svg
+   :target: https://pypi.python.org/pypi/django-admin-sortable
+.. |Python versions| image:: https://img.shields.io/pypi/pyversions/django-admin-sortable.svg
+   :target: https://pypi.python.org/pypi/django-admin-sortable
+.. |Build Status| image:: https://travis-ci.org/alsoicode/django-admin-sortable.svg?branch=master
+   :target: https://travis-ci.org/alsoicode/django-admin-sortable
diff --git a/django_admin_sortable.egg-info/SOURCES.txt b/django_admin_sortable.egg-info/SOURCES.txt
index a9493cd..4dd9499 100644
--- a/django_admin_sortable.egg-info/SOURCES.txt
+++ b/django_admin_sortable.egg-info/SOURCES.txt
@@ -1,3 +1,4 @@
+AUTHORS
 MANIFEST.in
 README.rst
 setup.cfg
@@ -12,6 +13,11 @@ adminsortable/locale/de/LC_MESSAGES/django.po
 adminsortable/locale/en/LC_MESSAGES/django.po
 adminsortable/locale/es/LC_MESSAGES/django.mo
 adminsortable/locale/es/LC_MESSAGES/django.po
+adminsortable/locale/hr/LC_MESSAGES/django.mo
+adminsortable/locale/hr/LC_MESSAGES/django.po
+adminsortable/locale/lv/LC_MESSAGES/django.po
+adminsortable/locale/nb/LC_MESSAGES/django.mo
+adminsortable/locale/nb/LC_MESSAGES/django.po
 adminsortable/locale/nl/LC_MESSAGES/django.mo
 adminsortable/locale/nl/LC_MESSAGES/django.po
 adminsortable/locale/pl/LC_MESSAGES/django.mo
@@ -20,32 +26,30 @@ adminsortable/locale/pt_BR/LC_MESSAGES/django.mo
 adminsortable/locale/pt_BR/LC_MESSAGES/django.po
 adminsortable/locale/ru/LC_MESSAGES/django.mo
 adminsortable/locale/ru/LC_MESSAGES/django.po
+adminsortable/locale/uk/LC_MESSAGES/django.mo
+adminsortable/locale/uk/LC_MESSAGES/django.po
 adminsortable/static/adminsortable/css/admin.sortable.css
 adminsortable/static/adminsortable/css/admin.sortable.inline.css
-adminsortable/static/adminsortable/js/admin.sortable.js
-adminsortable/static/adminsortable/js/admin.sortable.stacked.inlines.js
-adminsortable/static/adminsortable/js/admin.sortable.tabular.inlines.js
 adminsortable/static/adminsortable/js/jquery-ui-django-admin.min.js
-adminsortable/static/adminsortable/js/jquery.django-csrf.js
+adminsortable/static/adminsortable/js/jquery.ui.touch-punch.min.js
+adminsortable/templates/adminsortable/admin.sortable.html
 adminsortable/templates/adminsortable/change_form.html
 adminsortable/templates/adminsortable/change_list.html
 adminsortable/templates/adminsortable/change_list_with_sort_link.html
-adminsortable/templates/adminsortable/edit_inline/stacked-1.5.x.html
+adminsortable/templates/adminsortable/csrf/jquery.django-csrf.html
+adminsortable/templates/adminsortable/edit_inline/admin.sortable.stacked.inlines.html
+adminsortable/templates/adminsortable/edit_inline/admin.sortable.tabular.inlines.html
 adminsortable/templates/adminsortable/edit_inline/stacked.html
-adminsortable/templates/adminsortable/edit_inline/tabular-1.5.x.html
 adminsortable/templates/adminsortable/edit_inline/tabular.html
-adminsortable/templates/adminsortable/shared/fieldset.html
 adminsortable/templates/adminsortable/shared/list_items.html
 adminsortable/templates/adminsortable/shared/nested_objects.html
 adminsortable/templates/adminsortable/shared/object_rep.html
 adminsortable/templates/adminsortable/shared/objects.html
 adminsortable/templatetags/__init__.py
-adminsortable/templatetags/adminsortable_tags.py
 adminsortable/templatetags/django_template_additions.py
 django_admin_sortable.egg-info/PKG-INFO
 django_admin_sortable.egg-info/SOURCES.txt
 django_admin_sortable.egg-info/dependency_links.txt
 django_admin_sortable.egg-info/not-zip-safe
-django_admin_sortable.egg-info/pbr.json
 django_admin_sortable.egg-info/requires.txt
 django_admin_sortable.egg-info/top_level.txt
\ No newline at end of file
diff --git a/django_admin_sortable.egg-info/pbr.json b/django_admin_sortable.egg-info/pbr.json
deleted file mode 100644
index 06ee193..0000000
--- a/django_admin_sortable.egg-info/pbr.json
+++ /dev/null
@@ -1 +0,0 @@
-{"is_release": false, "git_version": "5823af8"}
\ No newline at end of file
diff --git a/setup.cfg b/setup.cfg
index 6bc2ff3..2ad61a8 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -1,5 +1,4 @@
 [egg_info]
 tag_date = 0
 tag_build = 
-tag_svn_revision = 0
 
diff --git a/setup.py b/setup.py
index 2f34301..b1f7bf1 100644
--- a/setup.py
+++ b/setup.py
@@ -1,9 +1,7 @@
 from setuptools import setup, find_packages
 
-try:
-    README = open('README').read()
-except:
-    README = None
+with open('README.rst', encoding='utf8') as readme_file:
+    README = readme_file.read()
 
 setup(
     author='Brandon Taylor',
@@ -14,7 +12,6 @@ setup(
                  'Intended Audience :: Developers',
                  'License :: OSI Approved :: Apache Software License',
                  'Operating System :: OS Independent',
-                 'Programming Language :: Python :: 2',
                  'Programming Language :: Python :: 3',
                  'Topic :: Utilities'],
     description='Drag and drop sorting for models and inline models in Django admin.',

Debdiff

[The following lists of changes regard files as different if they have different names, permissions or owners.]

Files in second set of .debs but not in first

-rw-r--r--  root/root   /usr/lib/python3/dist-packages/adminsortable/locale/hr/LC_MESSAGES/django.mo
-rw-r--r--  root/root   /usr/lib/python3/dist-packages/adminsortable/locale/hr/LC_MESSAGES/django.po
-rw-r--r--  root/root   /usr/lib/python3/dist-packages/adminsortable/locale/lv/LC_MESSAGES/django.po
-rw-r--r--  root/root   /usr/lib/python3/dist-packages/adminsortable/locale/nb/LC_MESSAGES/django.mo
-rw-r--r--  root/root   /usr/lib/python3/dist-packages/adminsortable/locale/nb/LC_MESSAGES/django.po
-rw-r--r--  root/root   /usr/lib/python3/dist-packages/adminsortable/locale/uk/LC_MESSAGES/django.mo
-rw-r--r--  root/root   /usr/lib/python3/dist-packages/adminsortable/locale/uk/LC_MESSAGES/django.po
-rw-r--r--  root/root   /usr/lib/python3/dist-packages/adminsortable/static/adminsortable/js/jquery.ui.touch-punch.min.js
-rw-r--r--  root/root   /usr/lib/python3/dist-packages/adminsortable/templates/adminsortable/admin.sortable.html
-rw-r--r--  root/root   /usr/lib/python3/dist-packages/adminsortable/templates/adminsortable/csrf/jquery.django-csrf.html
-rw-r--r--  root/root   /usr/lib/python3/dist-packages/adminsortable/templates/adminsortable/edit_inline/admin.sortable.stacked.inlines.html
-rw-r--r--  root/root   /usr/lib/python3/dist-packages/adminsortable/templates/adminsortable/edit_inline/admin.sortable.tabular.inlines.html
-rw-r--r--  root/root   /usr/lib/python3/dist-packages/django_admin_sortable-2.3.egg-info/PKG-INFO
-rw-r--r--  root/root   /usr/lib/python3/dist-packages/django_admin_sortable-2.3.egg-info/dependency_links.txt
-rw-r--r--  root/root   /usr/lib/python3/dist-packages/django_admin_sortable-2.3.egg-info/not-zip-safe
-rw-r--r--  root/root   /usr/lib/python3/dist-packages/django_admin_sortable-2.3.egg-info/requires.txt
-rw-r--r--  root/root   /usr/lib/python3/dist-packages/django_admin_sortable-2.3.egg-info/top_level.txt

Files in first set of .debs but not in second

-rw-r--r--  root/root   /usr/lib/python3/dist-packages/adminsortable/static/adminsortable/js/admin.sortable.js
-rw-r--r--  root/root   /usr/lib/python3/dist-packages/adminsortable/static/adminsortable/js/admin.sortable.stacked.inlines.js
-rw-r--r--  root/root   /usr/lib/python3/dist-packages/adminsortable/static/adminsortable/js/admin.sortable.tabular.inlines.js
-rw-r--r--  root/root   /usr/lib/python3/dist-packages/adminsortable/static/adminsortable/js/jquery.django-csrf.js
-rw-r--r--  root/root   /usr/lib/python3/dist-packages/adminsortable/templates/adminsortable/edit_inline/stacked-1.5.x.html
-rw-r--r--  root/root   /usr/lib/python3/dist-packages/adminsortable/templates/adminsortable/edit_inline/tabular-1.5.x.html
-rw-r--r--  root/root   /usr/lib/python3/dist-packages/adminsortable/templates/adminsortable/shared/fieldset.html
-rw-r--r--  root/root   /usr/lib/python3/dist-packages/adminsortable/templatetags/adminsortable_tags.py
-rw-r--r--  root/root   /usr/lib/python3/dist-packages/django_admin_sortable-2.0.10.egg-info/PKG-INFO
-rw-r--r--  root/root   /usr/lib/python3/dist-packages/django_admin_sortable-2.0.10.egg-info/dependency_links.txt
-rw-r--r--  root/root   /usr/lib/python3/dist-packages/django_admin_sortable-2.0.10.egg-info/not-zip-safe
-rw-r--r--  root/root   /usr/lib/python3/dist-packages/django_admin_sortable-2.0.10.egg-info/pbr.json
-rw-r--r--  root/root   /usr/lib/python3/dist-packages/django_admin_sortable-2.0.10.egg-info/requires.txt
-rw-r--r--  root/root   /usr/lib/python3/dist-packages/django_admin_sortable-2.0.10.egg-info/top_level.txt

No differences were encountered in the control files

More details

Full run details