diff --git a/.gitignore b/.gitignore
index 811e497..79bda0e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,4 @@
+.idea
*.log
*.pot
*.pyc
diff --git a/adminsortable2/__init__.py b/adminsortable2/__init__.py
index a62e53d..7e49527 100644
--- a/adminsortable2/__init__.py
+++ b/adminsortable2/__init__.py
@@ -1 +1 @@
-__version__ = '0.7.8'
+__version__ = '1.0'
diff --git a/adminsortable2/admin.py b/adminsortable2/admin.py
index 25540cc..bb4afa2 100644
--- a/adminsortable2/admin.py
+++ b/adminsortable2/admin.py
@@ -4,40 +4,28 @@
from types import MethodType
from django import forms
-from django.contrib.admin.views.main import ORDER_VAR
-# Remove check when support for python < 3 is dropped.
-import sys
-if sys.version_info[0] >= 3:
- from django.utils.translation import gettext_lazy as _
-else:
- from django.utils.translation import ugettext_lazy as _
-from django.utils.safestring import mark_safe
-from django.urls import path
from django.contrib import admin, messages
+from django.contrib.admin.views.main import ORDER_VAR
+from django.contrib.contenttypes.admin import GenericStackedInline, GenericTabularInline
+from django.contrib.contenttypes.forms import BaseGenericInlineFormSet
+from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import ImproperlyConfigured
from django.core.paginator import EmptyPage
from django.core.serializers.json import DjangoJSONEncoder
-try:
- from django.urls import reverse
-except ImportError: # Django<1.11
- from django.core.urlresolvers import reverse
from django.db import router, transaction
from django.db.models.aggregates import Max
from django.db.models.expressions import F
from django.db.models.functions import Coalesce
from django.db.models.signals import post_save, pre_save
-from django.forms.models import BaseInlineFormSet
from django.forms import widgets
+from django.forms.models import BaseInlineFormSet
from django.http import (
HttpResponse, HttpResponseBadRequest,
- HttpResponseNotAllowed, HttpResponseForbidden)
-from django.contrib.contenttypes.models import ContentType
-from django.contrib.contenttypes.forms import (
- BaseGenericInlineFormSet,
-)
-from django.contrib.contenttypes.admin import (
- GenericStackedInline, GenericTabularInline
+ HttpResponseNotAllowed, HttpResponseForbidden
)
+from django.utils.safestring import mark_safe
+from django.utils.translation import gettext_lazy as _
+from django.urls import path, reverse
__all__ = ['SortableAdminMixin', 'SortableInlineAdminMixin']
@@ -55,8 +43,9 @@ def _get_default_ordering(model, model_admin):
# then try with the model ordering
none, prefix, field_name = model._meta.ordering[0].rpartition('-')
except (AttributeError, IndexError):
- msg = "Model {0}.{1} requires a list or tuple 'ordering' in its Meta class"
- raise ImproperlyConfigured(msg.format(model.__module__, model.__name__))
+ raise ImproperlyConfigured(
+ f"Model {model.__module__}.{model.__name__} requires a list or tuple 'ordering' in its Meta class"
+ )
else:
return prefix, field_name
@@ -66,14 +55,16 @@ class MovePageActionForm(admin.helpers.ActionForm):
required=False,
initial=1,
widget=widgets.NumberInput(attrs={'id': 'changelist-form-step'}),
- label=False)
+ label=False
+ )
page = forms.IntegerField(
required=False,
widget=widgets.NumberInput(attrs={'id': 'changelist-form-page'}),
- label=False)
+ label=False
+ )
-class SortableAdminBase(object):
+class SortableAdminBase:
@property
def media(self):
css = {'all': ['adminsortable2/css/sortable.css']}
@@ -120,28 +111,37 @@ def __init__(self, model, admin_site):
# Insert the magic field into the same position as the first occurrence
# of the default_order_field, or, if not present, at the start
try:
- self.default_order_field_index = self.list_display.index(self.default_order_field)
+ self.default_order_field_index = self.list_display.index(
+ self.default_order_field
+ )
except ValueError:
self.default_order_field_index = 0
self.list_display.insert(self.default_order_field_index, '_reorder')
# Remove *all* occurrences of the field from `list_display`
if self.list_display and self.default_order_field in self.list_display:
- self.list_display = [f for f in self.list_display if f != self.default_order_field]
+ self.list_display = [
+ f for f in self.list_display if f != self.default_order_field
+ ]
# Remove *all* occurrences of the field from `list_display_links`
if self.list_display_links and self.default_order_field in self.list_display_links:
- self.list_display_links = [f for f in self.list_display_links if f != self.default_order_field]
+ self.list_display_links = [
+ f for f in self.list_display_links if
+ f != self.default_order_field
+ ]
# Remove *all* occurrences of the field from `ordering`
if self.ordering and self.default_order_field in self.ordering:
- self.ordering = [f for f in self.ordering if f != self.default_order_field]
+ self.ordering = [
+ f for f in self.ordering if f != self.default_order_field
+ ]
rev_field = '-' + self.default_order_field
if self.ordering and rev_field in self.ordering:
self.ordering = [f for f in self.ordering if f != rev_field]
def _get_update_url_name(self):
- return '%s_%s_sortable_update' % (self.model._meta.app_label, self.model._meta.model_name)
+ return f'{self.model._meta.app_label}_{self.model._meta.model_name}_sortable_update'
def get_urls(self):
my_urls = [
@@ -176,14 +176,15 @@ def get_changelist(self, request, **kwargs):
first_order_direction, first_order_field_index = self._get_first_ordering(request)
if first_order_field_index == self.default_order_field_index:
self.enable_sorting = True
- self.order_by = "{}{}".format(first_order_direction, self.default_order_field)
+ self.order_by = f"{first_order_direction}{self.default_order_field}"
else:
self.enable_sorting = False
return super().get_changelist(request, **kwargs)
def _get_first_ordering(self, request):
"""
- Must be consistent with `django.contrib.admin.views.main.ChangeList.get_ordering`.
+ Must be consistent with
+ `django.contrib.admin.views.main.ChangeList.get_ordering`.
"""
order_var = request.GET.get(ORDER_VAR)
if order_var is None:
@@ -216,19 +217,22 @@ def media(self):
def _add_reorder_method(self):
"""
- Adds a bound method, named '_reorder' to the current instance of this class, with attributes
- allow_tags, short_description and admin_order_field.
- This can only be done using a function, since it is not possible to add dynamic attributes
- to bound methods.
+ Adds a bound method, named '_reorder' to the current instance of
+ this class, with attributes allow_tags, short_description and
+ admin_order_field.
+ This can only be done using a function, since it is not possible
+ to add dynamic attributes to bound methods.
"""
def func(this, item):
html = ''
if this.enable_sorting:
- html = '
'.format(getattr(item, this.default_order_field), item.pk)
+ html = '' \
+ '
'.format(getattr(item, this.default_order_field), item.pk)
return mark_safe(html)
setattr(func, 'allow_tags', True)
- # if the field used for ordering has a verbose name use it, otherwise default to "Sort"
+ # if the field used for ordering has a verbose name use it,
+ # otherwise default to "Sort"
for order_field in self.model._meta.fields:
if order_field.name == self.default_order_field:
short_description = getattr(order_field, 'verbose_name', None)
@@ -250,11 +254,17 @@ def update_order(self, request):
startorder = int(request.POST.get('startorder'))
endorder = int(request.POST.get('endorder', 0))
moved_items = list(self._move_item(request, startorder, endorder))
- return HttpResponse(json.dumps(moved_items, cls=DjangoJSONEncoder), content_type='application/json;charset=UTF-8')
+ return HttpResponse(
+ json.dumps(moved_items, cls=DjangoJSONEncoder),
+ content_type='application/json;charset=UTF-8'
+ )
def save_model(self, request, obj, form, change):
if not change:
- setattr(obj, self.default_order_field, self.get_max_order(request, obj) + 1)
+ setattr(
+ obj, self.default_order_field,
+ self.get_max_order(request, obj) + 1
+ )
super().save_model(request, obj, form, change)
def move_to_exact_page(self, request, queryset):
@@ -281,21 +291,21 @@ def _move_item(self, request, startorder, endorder):
extra_model_filters = self.get_extra_model_filters(request)
return self.move_item(startorder, endorder, extra_model_filters)
- def move_item(self, startorder, endorder, extra_model_filters = None):
+ def move_item(self, startorder, endorder, extra_model_filters=None):
model = self.model
rank_field = self.default_order_field
- if endorder < startorder: # Drag up
+ if endorder < startorder: # Drag up
move_filter = {
- '{0}__gte'.format(rank_field): endorder,
- '{0}__lte'.format(rank_field): startorder - 1,
+ f'{rank_field}__gte': endorder,
+ f'{rank_field}__lte': startorder - 1,
}
move_delta = +1
- order_by = '-{0}'.format(rank_field)
- elif endorder > startorder: # Drag down
+ order_by = f'-{rank_field}'
+ elif endorder > startorder: # Drag down
move_filter = {
- '{0}__gte'.format(rank_field): startorder + 1,
- '{0}__lte'.format(rank_field): endorder,
+ f'{rank_field}__gte': startorder + 1,
+ f'{rank_field}__lte': endorder,
}
move_delta = -1
order_by = rank_field
@@ -311,17 +321,23 @@ def move_item(self, startorder, endorder, extra_model_filters = None):
try:
obj = model.objects.get(**obj_filters)
except model.MultipleObjectsReturned:
- msg = "Detected non-unique values in field '{0}' used for sorting this model.\nConsider to run \n"\
- " python manage.py reorder {1}\n"\
- "to adjust this inconsistency."
+
# noinspection PyProtectedMember
- raise model.MultipleObjectsReturned(msg.format(rank_field, model._meta.label))
+ raise model.MultipleObjectsReturned(
+ "Detected non-unique values in field '{rank_field}' used for sorting this model.\n"
+ "Consider to run \n python manage.py reorder {model._meta.label}\n"
+ "to adjust this inconsistency."
+ )
move_qs = model.objects.filter(**move_filter).order_by(order_by)
move_objs = list(move_qs)
for instance in move_objs:
- setattr(instance, rank_field, getattr(instance, rank_field) + move_delta)
- # Do not run `instance.save()`, because it will be updated later in bulk by `move_qs.update`.
+ setattr(
+ instance, rank_field,
+ getattr(instance, rank_field) + move_delta
+ )
+ # Do not run `instance.save()`, because it will be updated
+ # later in bulk by `move_qs.update`.
pre_save.send(
model,
instance=instance,
@@ -343,16 +359,22 @@ def move_item(self, startorder, endorder, extra_model_filters = None):
setattr(obj, rank_field, endorder)
obj.save(update_fields=[rank_field])
- return [{'pk': instance.pk, 'order': getattr(instance, rank_field)} for instance in chain(move_objs, [obj])]
+ return [{
+ 'pk': instance.pk,
+ 'order': getattr(instance, rank_field)
+ } for instance in chain(move_objs, [obj])]
- def get_extra_model_filters(self, request):
+ @staticmethod
+ def get_extra_model_filters(request):
"""
Returns additional fields to filter sortable objects
"""
return {}
def get_max_order(self, request, obj=None):
- return self.model.objects.aggregate(max_order=Coalesce(Max(self.default_order_field), 0))['max_order']
+ return self.model.objects.aggregate(
+ max_order=Coalesce(Max(self.default_order_field), 0)
+ )['max_order']
def _bulk_move(self, request, queryset, method):
if not self.enable_sorting:
@@ -378,7 +400,7 @@ def _bulk_move(self, request, queryset, method):
raise Exception('Invalid method')
if target_page_number == current_page_number:
- # If you want the selected items to be moved to the start of the current page, then just do not return here.
+ # If you want the selected items to be moved to the start of the current page, then just do not return here
return
try:
@@ -390,13 +412,19 @@ def _bulk_move(self, request, queryset, method):
queryset_size = queryset.count()
page_size = page.end_index() - page.start_index() + 1
if queryset_size > page_size:
- msg = _("The target page size is {}. It is too small for {} items.").format(page_size, queryset_size)
+ msg = _(f"The target page size is {page_size}. It is too small for {queryset_size} items.")
self.message_user(request, msg, level=messages.ERROR)
return
- endorders_start = getattr(objects[page.start_index() - 1], self.default_order_field)
+ endorders_start = getattr(
+ objects[page.start_index() - 1], self.default_order_field
+ )
endorders_step = -1 if self.order_by.startswith('-') else 1
- endorders = range(endorders_start, endorders_start + endorders_step * queryset_size, endorders_step)
+ endorders = range(
+ endorders_start,
+ endorders_start + endorders_step * queryset_size,
+ endorders_step
+ )
if page.number > current_page_number: # Move forward (like drag down)
queryset = queryset.reverse()
@@ -418,26 +446,28 @@ def get_update_url(self, request):
"""
Returns a callback URL used for updating items via AJAX drag-n-drop
"""
- return reverse('{}:{}'.format(self.admin_site.name, self._get_update_url_name()))
+ return reverse(f'{self.admin_site.name}:{self._get_update_url_name()}')
class PolymorphicSortableAdminMixin(SortableAdminMixin):
"""
- If the admin class is used for a polymorphic model, hence inherits from
- ``PolymorphicParentModelAdmin`` rather than ``admin.ModelAdmin``, then
- additionally inherit from ``PolymorphicSortableAdminMixin`` rather than
- ``SortableAdminMixin``.
+ If the admin class is used for a polymorphic model, hence inherits from ``PolymorphicParentModelAdmin``
+ rather than ``admin.ModelAdmin``, then additionally inherit from ``PolymorphicSortableAdminMixin``
+ rather than ``SortableAdminMixin``.
"""
def get_max_order(self, request, obj=None):
- return self.base_model.objects.aggregate(max_order=Coalesce(Max(self.default_order_field), 0))['max_order']
+ return self.base_model.objects.aggregate(
+ max_order=Coalesce(Max(self.default_order_field), 0)
+ )['max_order']
-class CustomInlineFormSetMixin():
+class CustomInlineFormSetMixin:
def __init__(self, *args, **kwargs):
self.default_order_direction, self.default_order_field = _get_default_ordering(self.model, self)
if self.default_order_field not in self.form.base_fields:
- self.form.base_fields[self.default_order_field] = self.model._meta.get_field(self.default_order_field).formfield()
+ self.form.base_fields[self.default_order_field] = \
+ self.model._meta.get_field(self.default_order_field).formfield()
self.form.base_fields[self.default_order_field].is_hidden = True
self.form.base_fields[self.default_order_field].required = False
@@ -446,15 +476,20 @@ def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
def get_max_order(self):
- query_set = self.model.objects.filter(**{self.fk.get_attname(): self.instance.pk})
- return query_set.aggregate(max_order=Coalesce(Max(self.default_order_field), 0))['max_order']
+ query_set = self.model.objects.filter(
+ **{self.fk.get_attname(): self.instance.pk}
+ )
+ return query_set.aggregate(
+ max_order=Coalesce(Max(self.default_order_field), 0)
+ )['max_order']
def save_new(self, form, commit=True):
"""
- New objects do not have a valid value in their ordering field. On object save, add an order
- bigger than all other order fields for the current parent_model.
- Strange behaviour when field has a default, this might be evaluated on new object and the value
- will be not None, but the default value.
+ New objects do not have a valid value in their ordering field.
+ On object save, add an order bigger than all other order fields
+ for the current parent_model.
+ Strange behaviour when field has a default, this might be evaluated
+ on new object and the value will be not None, but the default value.
"""
obj = super().save_new(form, commit=False)
@@ -464,14 +499,17 @@ def save_new(self, form, commit=True):
setattr(obj, self.default_order_field, max_order + 1)
if commit:
obj.save()
- # form.save_m2m() can be called via the formset later on if commit=False
+ # form.save_m2m() can be called via the formset later on
+ # if commit=False
if commit and hasattr(form, 'save_m2m'):
form.save_m2m()
return obj
+
class CustomInlineFormSet(CustomInlineFormSetMixin, BaseInlineFormSet):
pass
+
class SortableInlineAdminMixin(SortableAdminBase):
formset = CustomInlineFormSet
@@ -530,14 +568,27 @@ def template(self):
return 'adminsortable2/stacked.html'
elif self.is_tabular:
return 'adminsortable2/tabular.html'
- raise ImproperlyConfigured('Class {0}.{1} must also derive from admin.TabularInline or admin.StackedInline'.format(self.__module__, self.__class__))
+ raise ImproperlyConfigured(
+ f'Class {self.__module__}.{self.__class__} must also derive from admin.TabularInline or '
+ f'admin.StackedInline'
+ )
class CustomGenericInlineFormSet(CustomInlineFormSetMixin, BaseGenericInlineFormSet):
def get_max_order(self):
- query_set = self.model.objects.filter(**{self.ct_fk_field.name: self.instance.pk,
- self.ct_field.name: ContentType.objects.get_for_model(self.instance, for_concrete_model=self.for_concrete_model)})
- return query_set.aggregate(max_order=Coalesce(Max(self.default_order_field), 0))['max_order']
+ query_set = self.model.objects.filter(
+ **{
+ self.ct_fk_field.name: self.instance.pk,
+ self.ct_field.name: ContentType.objects.get_for_model(
+ self.instance,
+ for_concrete_model=self.for_concrete_model
+ )
+ }
+ )
+ return query_set.aggregate(
+ max_order=Coalesce(Max(self.default_order_field), 0)
+ )['max_order']
+
class SortableGenericInlineAdminMixin(SortableInlineAdminMixin):
formset = CustomGenericInlineFormSet
diff --git a/adminsortable2/management/commands/reorder.py b/adminsortable2/management/commands/reorder.py
index 050d645..291f40c 100644
--- a/adminsortable2/management/commands/reorder.py
+++ b/adminsortable2/management/commands/reorder.py
@@ -18,7 +18,7 @@ def handle(self, *args, **options):
raise CommandError('Unable to load model "%s"' % modelname)
if not hasattr(Model._meta, 'ordering') or len(Model._meta.ordering) == 0:
- raise CommandError('Model "{0}" does not define field "ordering" in its Meta class'.format(modelname))
+ raise CommandError(f'Model "{modelname}" does not define field "ordering" in its Meta class')
orderfield = Model._meta.ordering[0]
if orderfield[0] == '-':
@@ -28,4 +28,4 @@ def handle(self, *args, **options):
setattr(obj, orderfield, order)
obj.save()
- self.stdout.write('Successfully reordered model "{0}"'.format(modelname))
+ self.stdout.write(f'Successfully reordered model "{modelname}"')
diff --git a/docs/source/changelog.rst b/docs/source/changelog.rst
index 588643e..f3d3fa7 100644
--- a/docs/source/changelog.rst
+++ b/docs/source/changelog.rst
@@ -5,7 +5,7 @@ Release history
===============
-0.8
+1.0
---
* Drop support for Python-2.7, 3.4 and 3.5.
* Drop support for Django-1.10, 1.11, 2.0 and 2.1.