From 2bb6a677fefcaaf2fa09694abe174f8cf405b508 Mon Sep 17 00:00:00 2001 From: blag Date: Tue, 19 May 2020 18:37:55 -0700 Subject: [PATCH 01/12] Check that the parent model is a SortableMixin before enabling sorting them --- adminsortable/admin.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/adminsortable/admin.py b/adminsortable/admin.py index a3c3362..24f2eb5 100644 --- a/adminsortable/admin.py +++ b/adminsortable/admin.py @@ -182,7 +182,9 @@ class SortableAdmin(SortableAdminBase, ModelAdmin): # Django < 1.9 sortable_by_fk = field.rel.to sortable_by_field_name = field.name.lower() - sortable_by_class_is_sortable = sortable_by_fk.objects.count() >= 2 + 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 From 247078c7e049b3713b379a9f8fd66a4125db69ce Mon Sep 17 00:00:00 2001 From: blag Date: Sun, 24 May 2020 03:13:00 -0700 Subject: [PATCH 02/12] Tweak CSS to restrict elements that have cursor: move --- adminsortable/static/adminsortable/css/admin.sortable.css | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/adminsortable/static/adminsortable/css/admin.sortable.css b/adminsortable/static/adminsortable/css/admin.sortable.css index e1c857c..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; } From 00eb3fdb300852244242bce6005466697d07facd Mon Sep 17 00:00:00 2001 From: blag Date: Sun, 24 May 2020 02:46:19 -0700 Subject: [PATCH 03/12] Pass the queryset filters from the request into the object_rep.html template --- adminsortable/admin.py | 4 ++++ adminsortable/templates/adminsortable/shared/object_rep.html | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/adminsortable/admin.py b/adminsortable/admin.py index 24f2eb5..10bdeab 100644 --- a/adminsortable/admin.py +++ b/adminsortable/admin.py @@ -15,6 +15,7 @@ from django.http import HttpResponse from django.shortcuts import render from django.template.defaultfilters import capfirst from django.utils.decorators import method_decorator +from django.utils.six.moves.urllib.parse import urlencode from django.views.decorators.http import require_POST from adminsortable.fields import SortableForeignKey @@ -236,6 +237,8 @@ class SortableAdmin(SortableAdminBase, ModelAdmin): else: 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)), @@ -246,6 +249,7 @@ class SortableAdmin(SortableAdminBase, ModelAdmin): '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, + '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'), diff --git a/adminsortable/templates/adminsortable/shared/object_rep.html b/adminsortable/templates/adminsortable/shared/object_rep.html index 8b5383b..960ba9c 100644 --- a/adminsortable/templates/adminsortable/shared/object_rep.html +++ b/adminsortable/templates/adminsortable/shared/object_rep.html @@ -2,6 +2,6 @@
- {{ object }} + {{ object }} {% csrf_token %}
From a6fb5d0d36396514da7b2c43ce4fd6983ce049c2 Mon Sep 17 00:00:00 2001 From: blag Date: Sun, 24 May 2020 03:07:12 -0700 Subject: [PATCH 04/12] Lock filtered objects before updating them in bulk --- adminsortable/admin.py | 33 ++++++++++++++++++++------------- 1 file changed, 20 insertions(+), 13 deletions(-) diff --git a/adminsortable/admin.py b/adminsortable/admin.py index 10bdeab..256e818 100644 --- a/adminsortable/admin.py +++ b/adminsortable/admin.py @@ -11,11 +11,13 @@ from django.contrib.contenttypes.admin import (GenericStackedInline, GenericTabularInline) from django.contrib.contenttypes.models import ContentType from django.core.exceptions import PermissionDenied -from django.http import HttpResponse +from django.db import transaction +from django.http import HttpResponse, HttpResponseBadRequest from django.shortcuts import render from django.template.defaultfilters import capfirst from django.utils.decorators import method_decorator from django.utils.six.moves.urllib.parse import urlencode +from django.utils.translation import gettext as _ from django.views.decorators.http import require_POST from adminsortable.fields import SortableForeignKey @@ -296,14 +298,21 @@ class SortableAdmin(SortableAdminBase, ModelAdmin): response = {'objects_sorted': False} if request.is_ajax(): - try: - klass = ContentType.objects.get(id=model_type_id).model_class() + klass = ContentType.objects.get(id=model_type_id).model_class() - indexes = list(map(str, - request.POST.get('indexes', []).split(','))) - objects_dict = dict([(str(obj.pk), obj) for obj in - klass.objects.filter(pk__in=indexes)]) + 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} order_field_name = klass._meta.ordering[0] if order_field_name.startswith('-'): @@ -318,19 +327,17 @@ class SortableAdmin(SortableAdminBase, ModelAdmin): start_index = getattr(start_object, order_field_name, len(indexes)) - + objects_to_update = [] for index in indexes: obj = objects_dict.get(index) # 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) - # only update the object's order field - obj.save(update_fields=(order_field_name,)) + 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 self.after_sorting() From 1b700e0b251fd333a825c71e3c672db0fa33117e Mon Sep 17 00:00:00 2001 From: blag Date: Sun, 24 May 2020 15:54:34 -0700 Subject: [PATCH 05/12] Use Python 3.6's ordered dictionaries --- adminsortable/admin.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/adminsortable/admin.py b/adminsortable/admin.py index 256e818..cd855c1 100644 --- a/adminsortable/admin.py +++ b/adminsortable/admin.py @@ -313,17 +313,17 @@ class SortableAdmin(SortableAdminBase, ModelAdmin): with transaction.atomic(): objects_dict = {str(obj.pk): obj for obj in qs} + objects_list = list(objects_dict.keys()) 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)) From 899f92f53a0bf6ccce8b06b161dee690b7bf93d9 Mon Sep 17 00:00:00 2001 From: blag Date: Sun, 24 May 2020 15:58:44 -0700 Subject: [PATCH 06/12] Return an HTTP 400 if the queryset size has changed since the page was loaded --- adminsortable/admin.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/adminsortable/admin.py b/adminsortable/admin.py index cd855c1..50ef603 100644 --- a/adminsortable/admin.py +++ b/adminsortable/admin.py @@ -314,6 +314,15 @@ class SortableAdmin(SortableAdminBase, ModelAdmin): objects_dict = {str(obj.pk): obj for obj in qs} objects_list = list(objects_dict.keys()) + if len(indexes) != len(objects_dict): + return HttpResponseBadRequest( + json.dumps({ + 'objects_sorted': False, + 'reason': _("An object has been added or removed " + "since the last load. Please refresh " + "the page and try reordering again."), + }, ensure_ascii=False), + content_type='application/json') order_field_name = klass._meta.ordering[0] if order_field_name.startswith('-'): From 21cab41381466df28fb01fc5312f5a68a0cf8815 Mon Sep 17 00:00:00 2001 From: blag Date: Tue, 26 May 2020 12:15:12 -0700 Subject: [PATCH 07/12] Use Python 3's urllib.parse.urlencode instead of django.utils.six --- adminsortable/admin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/adminsortable/admin.py b/adminsortable/admin.py index 50ef603..9fca771 100644 --- a/adminsortable/admin.py +++ b/adminsortable/admin.py @@ -1,4 +1,5 @@ import json +from urllib.parse import urlencode from django import VERSION @@ -16,7 +17,6 @@ from django.http import HttpResponse, HttpResponseBadRequest from django.shortcuts import render from django.template.defaultfilters import capfirst from django.utils.decorators import method_decorator -from django.utils.six.moves.urllib.parse import urlencode from django.utils.translation import gettext as _ from django.views.decorators.http import require_POST From f93cac291bd0a308555982c6f94d0d7356d285f8 Mon Sep 17 00:00:00 2001 From: blag Date: Tue, 26 May 2020 12:20:18 -0700 Subject: [PATCH 08/12] Remove Python 3.4 from the build matrix --- .travis.yml | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/.travis.yml b/.travis.yml index cf3c863..49a88d0 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,9 +1,10 @@ language: python python: - - "3.4" - "3.5" - "3.6" + - "3.7" + - "3.8" env: - DJANGO_VERSION=3.0.0 SAMPLE_PROJECT=sample_project @@ -14,10 +15,6 @@ branches: matrix: exclude: - - - python: "3.4" - env: DJANGO_VERSION=3.0.0 SAMPLE_PROJECT=sample_project - - python: "3.5" env: DJANGO_VERSION=3.0.0 SAMPLE_PROJECT=sample_project From 387e9b8f2fbfb4505cc4d196bd8043ea6356d4dd Mon Sep 17 00:00:00 2001 From: blag Date: Tue, 26 May 2020 12:21:13 -0700 Subject: [PATCH 09/12] Relax Django version to 3.0 (to use the latest 3.0.x) --- .travis.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 49a88d0..6e2dc11 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,7 +7,7 @@ python: - "3.8" env: - - DJANGO_VERSION=3.0.0 SAMPLE_PROJECT=sample_project + - DJANGO_VERSION=3.0 SAMPLE_PROJECT=sample_project branches: only: @@ -17,7 +17,7 @@ matrix: exclude: - python: "3.5" - env: DJANGO_VERSION=3.0.0 SAMPLE_PROJECT=sample_project + env: DJANGO_VERSION=3.0 SAMPLE_PROJECT=sample_project install: - pip install django==$DJANGO_VERSION From 9e352ec474ff76941d8d601cd6d5a058f63e20d2 Mon Sep 17 00:00:00 2001 From: blag Date: Tue, 26 May 2020 12:22:30 -0700 Subject: [PATCH 10/12] Add Django 2.2 to the build matrix --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index 6e2dc11..fd4e063 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,6 +7,7 @@ python: - "3.8" env: + - DJANGO_VERSION=2.2 SAMPLE_PROJECT=sample_project - DJANGO_VERSION=3.0 SAMPLE_PROJECT=sample_project branches: From 3443c300d0a575b1c8d44b88085e11f39c6bb4df Mon Sep 17 00:00:00 2001 From: blag Date: Tue, 26 May 2020 12:54:28 -0700 Subject: [PATCH 11/12] Remove Python 3.6+ only code --- adminsortable/admin.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/adminsortable/admin.py b/adminsortable/admin.py index 9fca771..15e3eab 100644 --- a/adminsortable/admin.py +++ b/adminsortable/admin.py @@ -312,8 +312,15 @@ class SortableAdmin(SortableAdminBase, ModelAdmin): with transaction.atomic(): - objects_dict = {str(obj.pk): obj for obj in qs} - objects_list = list(objects_dict.keys()) + # Python 3.6+ only + # objects_dict = {str(obj.pk): obj for obj in qs} + # objects_list = list(objects_dict.keys()) + objects_dict = {} + objects_list = [] + for obj in qs: + key = str(obj.pk) + objects_dict[key] = obj + objects_list.append(key) if len(indexes) != len(objects_dict): return HttpResponseBadRequest( json.dumps({ From 01591305a31510376558d6d009de34fdce090a2f Mon Sep 17 00:00:00 2001 From: blag Date: Tue, 26 May 2020 13:05:12 -0700 Subject: [PATCH 12/12] Update tox with updated Django and Python versions (except for Django 2.2 and Python 3.5) --- tox.ini | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/tox.ini b/tox.ini index b67add1..efd978f 100644 --- a/tox.ini +++ b/tox.ini @@ -1,21 +1,18 @@ [tox] -envlist = django{1.8,1.9,1.10,1.11,2}-{py27,py34,py35,py36},coverage +envlist = django{2.2,3.0}-{py36,py37,py38},coverage [testenv] deps = coverage - django1.8: Django>=1.8,<1.9 - django1.9: Django>=1.9,<1.10 - django1.10: Django>=1.10,<1.11 - django1.11: Django>=1.11a1,<1.12 - django2.0: Django>=2.0 + django2.2: Django>=2.2 + django3.0: Django>=3.0 whitelist_externals = cd setenv = PYTHONPATH = {toxinidir}/sample_project PYTHONWARNINGS = module PYTHONDONTWRITEBYTECODE = 1 commands = - coverage run -p sample_project/manage.py test app + coverage run -p sample_project/manage.py test samples [testenv:coverage] deps = coverage