Merge pull request #227 from blag/develop

Fixups and improvements
master
Brandon Taylor 2020-05-26 16:30:18 -04:00 committed by GitHub
commit 7deb73c806
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 59 additions and 35 deletions

View File

@ -1,12 +1,14 @@
language: python language: python
python: python:
- "3.4"
- "3.5" - "3.5"
- "3.6" - "3.6"
- "3.7"
- "3.8"
env: env:
- DJANGO_VERSION=3.0.0 SAMPLE_PROJECT=sample_project - DJANGO_VERSION=2.2 SAMPLE_PROJECT=sample_project
- DJANGO_VERSION=3.0 SAMPLE_PROJECT=sample_project
branches: branches:
only: only:
@ -14,13 +16,9 @@ branches:
matrix: matrix:
exclude: exclude:
-
python: "3.4"
env: DJANGO_VERSION=3.0.0 SAMPLE_PROJECT=sample_project
- -
python: "3.5" python: "3.5"
env: DJANGO_VERSION=3.0.0 SAMPLE_PROJECT=sample_project env: DJANGO_VERSION=3.0 SAMPLE_PROJECT=sample_project
install: install:
- pip install django==$DJANGO_VERSION - pip install django==$DJANGO_VERSION

View File

@ -1,4 +1,5 @@
import json import json
from urllib.parse import urlencode
from django import VERSION from django import VERSION
@ -11,10 +12,12 @@ from django.contrib.contenttypes.admin import (GenericStackedInline,
GenericTabularInline) GenericTabularInline)
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import PermissionDenied 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.shortcuts import render
from django.template.defaultfilters import capfirst from django.template.defaultfilters import capfirst
from django.utils.decorators import method_decorator from django.utils.decorators import method_decorator
from django.utils.translation import gettext as _
from django.views.decorators.http import require_POST from django.views.decorators.http import require_POST
from adminsortable.fields import SortableForeignKey from adminsortable.fields import SortableForeignKey
@ -182,7 +185,9 @@ class SortableAdmin(SortableAdminBase, ModelAdmin):
# Django < 1.9 # Django < 1.9
sortable_by_fk = field.rel.to sortable_by_fk = field.rel.to
sortable_by_field_name = field.name.lower() 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: if sortable_by_property:
# backwards compatibility for < 1.1.1, where sortable_by was a # backwards compatibility for < 1.1.1, where sortable_by was a
@ -234,6 +239,8 @@ class SortableAdmin(SortableAdminBase, ModelAdmin):
else: else:
context = self.admin_site.each_context(request) context = self.admin_site.each_context(request)
filters = urlencode(self.get_querystring_filters(request))
context.update({ context.update({
'title': u'Drag and drop {0} to change display order'.format( 'title': u'Drag and drop {0} to change display order'.format(
capfirst(verbose_name_plural)), capfirst(verbose_name_plural)),
@ -244,6 +251,7 @@ class SortableAdmin(SortableAdminBase, ModelAdmin):
'sortable_by_class': sortable_by_class, 'sortable_by_class': sortable_by_class,
'sortable_by_class_is_sortable': sortable_by_class_is_sortable, 'sortable_by_class_is_sortable': sortable_by_class_is_sortable,
'sortable_by_class_display_name': sortable_by_class_display_name, 'sortable_by_class_display_name': sortable_by_class_display_name,
'filters': filters,
'jquery_lib_path': jquery_lib_path, 'jquery_lib_path': jquery_lib_path,
'csrf_cookie_name': getattr(settings, 'CSRF_COOKIE_NAME', 'csrftoken'), 'csrf_cookie_name': getattr(settings, 'CSRF_COOKIE_NAME', 'csrftoken'),
'csrf_header_name': getattr(settings, 'CSRF_HEADER_NAME', 'X-CSRFToken'), 'csrf_header_name': getattr(settings, 'CSRF_HEADER_NAME', 'X-CSRFToken'),
@ -290,41 +298,62 @@ class SortableAdmin(SortableAdminBase, ModelAdmin):
response = {'objects_sorted': False} response = {'objects_sorted': False}
if request.is_ajax(): 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, indexes = [str(idx) for idx in request.POST.get('indexes', []).split(',')]
request.POST.get('indexes', []).split(',')))
objects_dict = dict([(str(obj.pk), obj) for obj in
klass.objects.filter(pk__in=indexes)])
# 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():
# 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({
'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] order_field_name = klass._meta.ordering[0]
if order_field_name.startswith('-'): if order_field_name.startswith('-'):
order_field_name = order_field_name[1:] order_field_name = order_field_name[1:]
step = -1 step = -1
start_object = max(objects_dict.values(), start_object = objects_dict[objects_list[-1]]
key=lambda x: getattr(x, order_field_name))
else: else:
step = 1 step = 1
start_object = min(objects_dict.values(), start_object = objects_dict[objects_list[0]]
key=lambda x: getattr(x, order_field_name))
start_index = getattr(start_object, order_field_name, start_index = getattr(start_object, order_field_name,
len(indexes)) len(indexes))
objects_to_update = []
for index in indexes: for index in indexes:
obj = objects_dict.get(index) obj = objects_dict.get(index)
# perform the update only if the order field has changed # perform the update only if the order field has changed
if getattr(obj, order_field_name) != start_index: if getattr(obj, order_field_name) != start_index:
setattr(obj, order_field_name, start_index) setattr(obj, order_field_name, start_index)
# only update the object's order field objects_to_update.append(obj)
obj.save(update_fields=(order_field_name,))
start_index += step start_index += step
qs.bulk_update(objects_to_update, [order_field_name])
response = {'objects_sorted': True} response = {'objects_sorted': True}
except (KeyError, IndexError, klass.DoesNotExist,
AttributeError, ValueError):
pass
self.after_sorting() self.after_sorting()

View File

@ -10,8 +10,8 @@
margin-left: 1em; margin-left: 1em;
} }
#sortable ul li, #sortable ul.sortable li,
#sortable ul li a #sortable ul.sortable li a
{ {
cursor: move; cursor: move;
} }

View File

@ -2,6 +2,6 @@
<form> <form>
<input name="pk" type="hidden" value="{{ object.pk|unlocalize }}" /> <input name="pk" type="hidden" value="{{ object.pk|unlocalize }}" />
<a href="{% url opts|admin_urlname:'do_sorting' object.model_type_id|unlocalize %}" class="admin_sorting_url"><i class="fa fa-{% if forloop.first %}sort-desc{% elif forloop.last %}sort-asc{% else %}sort{% endif %}"></i> {{ object }}</a> <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 %} {% csrf_token %}
</form> </form>

11
tox.ini
View File

@ -1,21 +1,18 @@
[tox] [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] [testenv]
deps = deps =
coverage coverage
django1.8: Django>=1.8,<1.9 django2.2: Django>=2.2
django1.9: Django>=1.9,<1.10 django3.0: Django>=3.0
django1.10: Django>=1.10,<1.11
django1.11: Django>=1.11a1,<1.12
django2.0: Django>=2.0
whitelist_externals = cd whitelist_externals = cd
setenv = setenv =
PYTHONPATH = {toxinidir}/sample_project PYTHONPATH = {toxinidir}/sample_project
PYTHONWARNINGS = module PYTHONWARNINGS = module
PYTHONDONTWRITEBYTECODE = 1 PYTHONDONTWRITEBYTECODE = 1
commands = commands =
coverage run -p sample_project/manage.py test app coverage run -p sample_project/manage.py test samples
[testenv:coverage] [testenv:coverage]
deps = coverage deps = coverage