commit
7deb73c806
12
.travis.yml
12
.travis.yml
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
11
tox.ini
|
|
@ -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
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue