Compare commits

..

No commits in common. "master" and "v2.2.3" have entirely different histories.

21 changed files with 300 additions and 262 deletions

View file

@ -1,52 +0,0 @@
---
name: Tests
on:
push:
branches:
- develop
- master
pull_request:
jobs:
tests:
name: Python ${{ matrix.python-version }}, Django ${{ matrix.django-version }}
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
python-version:
- "3.6"
- "3.7"
- "3.8"
- "3.9"
django-version:
- "2.2.17" # first version to support Python 3.9
- "3.1.3" # first version to support Python 3.9
- "3.2.0"
include:
- python-version: "3.8"
django-version: "4.0.0"
- python-version: "3.9"
django-version: "4.0.0"
- python-version: "3.10"
django-version: "3.2.9" # first version to support Python 3.10
- python-version: "3.10"
django-version: "4.0.0"
steps:
- uses: actions/checkout@v2
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v2
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
run: |
python -m pip install --upgrade pip wheel setuptools
python -m pip install --upgrade "django~=${{ matrix.django-version}}"
- name: Run tests
run: python manage.py test
working-directory: sample_project

3
.gitignore vendored
View file

@ -11,8 +11,5 @@ atlassian-*
.codeintel .codeintel
__pycache__ __pycache__
.venv/ .venv/
.coverage
.tox/
htmlcov/
build build
.vscode/* .vscode/*

43
.travis.yml Normal file
View file

@ -0,0 +1,43 @@
language: python
python:
- "2.7"
- "3.4"
- "3.5"
- "3.6"
env:
- DJANGO_VERSION=1.7.7 SAMPLE_PROJECT=sample_project
- DJANGO_VERSION=1.8.7 SAMPLE_PROJECT=sample_project
- DJANGO_VERSION=1.9 SAMPLE_PROJECT=sample_project
- DJANGO_VERSION=1.10 SAMPLE_PROJECT=sample_project
- DJANGO_VERSION=3.0.0 SAMPLE_PROJECT=sample_project
branches:
only:
- develop
matrix:
exclude:
-
python: "2.7"
env: DJANGO_VERSION=3.0.0 SAMPLE_PROJECT=sample_project
-
python: "3.5"
env: DJANGO_VERSION=1.7.7 SAMPLE_PROJECT=sample_project
-
python: "3.6"
env: DJANGO_VERSION=1.6.7 SAMPLE_PROJECT=sample_project
-
python: "3.6"
env: DJANGO_VERSION=1.7.7 SAMPLE_PROJECT=sample_project
install:
- pip install django==$DJANGO_VERSION
script:
- cd $SAMPLE_PROJECT
- python manage.py test

View file

@ -1,6 +1,3 @@
## Looking for maintainers
If you're interested in helping maintain and expand this library, please reach out to me. Due to other responsibilities and circumstances, I have basically no time to address issues for this codebase. I would greatly appreciate the help!
# Django Admin Sortable # Django Admin Sortable
[![PyPI version](https://img.shields.io/pypi/v/django-admin-sortable.svg)](https://pypi.python.org/pypi/django-admin-sortable) [![PyPI version](https://img.shields.io/pypi/v/django-admin-sortable.svg)](https://pypi.python.org/pypi/django-admin-sortable)
@ -22,9 +19,7 @@ Sorting inlines:
![sortable-inlines](http://res.cloudinary.com/alsoicode/image/upload/v1451237555/django-admin-sortable/sortable-inlines.jpg) ![sortable-inlines](http://res.cloudinary.com/alsoicode/image/upload/v1451237555/django-admin-sortable/sortable-inlines.jpg)
## Supported Django Versions ## Supported Django Versions
For Django 4 use the latest version For Django 3 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.8.x < 3.0, use 2.1.8.
@ -63,7 +58,7 @@ django-admin-sortable is currently incompatible with that setting.
### Static Media ### Static Media
Preferred: Preferred:
Use the [staticfiles app](https://docs.djangoproject.com/en/3.0/howto/static-files/) Use the [staticfiles app](https://docs.djangoproject.com/en/1.6/ref/contrib/staticfiles/)
Alternate: Alternate:
Copy the `adminsortable` folder from the `static` folder to the Copy the `adminsortable` folder from the `static` folder to the
@ -87,14 +82,14 @@ Inlines may be drag-and-dropped into any order directly from the change form.
To add "sortability" to a model, you need to inherit `SortableMixin` and at minimum, define: 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: - The field which should be used for `Meta.ordering`, which must resolve to one of the integer fields defined in Django's ORM:
- `PositiveIntegerField` - `PositiveIntegerField`
- `IntegerField` - `IntegerField`
- `PositiveSmallIntegerField` - `PositiveSmallIntegerField`
- `SmallIntegerField` - `SmallIntegerField`
- `BigIntegerField` - `BigIntegerField`
- ⚠️ `Meta.ordering` **must only contain one value**, otherwise, your objects will not be sorted correctly. - `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. - **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. - 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: Sample Model:
@ -160,8 +155,6 @@ admin.site.register(Category, SortableAdmin)
admin.site.register(Project, SortableAdmin) admin.site.register(Project, SortableAdmin)
``` ```
#### Sortable Model With Non-Sortable Parent
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: 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:
```python ```python
@ -205,60 +198,6 @@ 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. 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.
#### Sortable Many-to-Many Model
It is also possible to make many-to-many relations sortable, but it requires an explicit many-to-many model.
`models.py`:
```python
from django.db import models
from adminsortable.models import SortableMixin
from adminsortable.fields import SortableForeignKey
class Image(models.Model):
...
class Gallery(models.Model):
class Meta:
verbose_name_plural = 'Galleries'
...
images = models.ManyToManyField(
Image,
through_fields=('gallery', 'image'),
through='GalleryImageRelation',
verbose_name=_('Images')
)
class GalleryImageRelation(SortableMixin):
"""Many to many relation that allows users to sort images in galleries"""
class Meta:
ordering = ['image_order']
gallery = models.ForeignKey(Gallery, verbose_name=_("Gallery"))
image = SortableForeignKey(Image, verbose_name=_("Image"))
image_order = models.PositiveIntegerField(default=0, editable=False, db_index=True)
```
`admin.py`:
```python
from django.contrib import admin
from adminsortable.admin import (SortableAdmin, SortableTabularInline)
from .models import (Image, Gallery, GalleryImageRelation)
class GalleryImageRelationInlineAdmin(SortableTabularInline):
model = GalleryImageRelation
extra = 1
class GalleryAdmin(NonSortableParentAdmin):
inlines = (GalleryImageRelationInlineAdmin,)
admin.site.register(Image, admin.ModelAdmin)
admin.site.register(Gallery, GalleryAdmin)
```
Any non-editable space in each rendered inline will let you drag and drop them into order.
### Backwards Compatibility ### 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. 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.
@ -444,7 +383,7 @@ It is also possible to sort a subset of objects in your model by adding a `sorti
#### Self-Referential SortableForeignKey #### 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. 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! ##### 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. 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. 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.
@ -670,8 +609,8 @@ ordering on top of that just seemed a little much in my opinion.
### Status ### Status
django-admin-sortable is currently used in production. django-admin-sortable is currently used in production.
### What's new in 2.3.0? ### What's new in 2.2.3?
- Django 4 compatibility - Updated inline sortable templates to fix FontAwesome icon visibility and be compatible with Django 2 & 3.
### Future ### 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. - Better template support for foreign keys that are self referential. If someone would like to take on rendering recursive sortables, that would be super.

View file

@ -27,9 +27,7 @@ Sorting inlines:
Supported Django Versions Supported Django Versions
------------------------- -------------------------
For Django 4 use the latest version For Django 3 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.8.x < 3.0, use 2.1.8.
@ -86,7 +84,7 @@ Static Media
~~~~~~~~~~~~ ~~~~~~~~~~~~
Preferred: Use the `staticfiles Preferred: Use the `staticfiles
app <https://docs.djangoproject.com/en/3.0/howto/static-files/>`__ app <https://docs.djangoproject.com/en/1.6/ref/contrib/staticfiles/>`__
Alternate: Copy the ``adminsortable`` folder from the ``static`` folder Alternate: Copy the ``adminsortable`` folder from the ``static`` folder
to the location you serve static files from. to the location you serve static files from.
@ -753,10 +751,10 @@ Status
django-admin-sortable is currently used in production. django-admin-sortable is currently used in production.
Whats new in 2.3.0? Whats new in 2.2.3?
~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~
- Django 4 compatibility - Updated inline sortable templates to fix FontAwesome icon visibility and be compatible with Django 2 & 3.
Future Future
~~~~~~ ~~~~~~

View file

@ -1,4 +1,4 @@
VERSION = (2, 3, 0) VERSION = (2, 2, 3)
DEV_N = None DEV_N = None

View file

@ -1,7 +1,9 @@
import json import json
from urllib.parse import urlencode
from django import VERSION
from django.conf import settings from django.conf import settings
from django.conf.urls import url
from django.contrib.admin import ModelAdmin, TabularInline, StackedInline from django.contrib.admin import ModelAdmin, TabularInline, StackedInline
from django.contrib.admin.options import InlineModelAdmin from django.contrib.admin.options import InlineModelAdmin
from django.contrib.admin.views.main import IGNORED_PARAMS, PAGE_VAR from django.contrib.admin.views.main import IGNORED_PARAMS, PAGE_VAR
@ -9,13 +11,10 @@ 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.db import transaction from django.http import HttpResponse
from django.http import JsonResponse
from django.shortcuts import render from django.shortcuts import render
from django.template.defaultfilters import capfirst from django.template.defaultfilters import capfirst
from django.urls import re_path
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
@ -114,13 +113,13 @@ class SortableAdmin(SortableAdminBase, ModelAdmin):
info = self.model._meta.app_label, self.model._meta.model_name info = self.model._meta.app_label, self.model._meta.model_name
# this ajax view changes the order of instances of the model type # this ajax view changes the order of instances of the model type
admin_do_sorting_url = re_path( admin_do_sorting_url = url(
r'^sort/do-sorting/(?P<model_type_id>\d+)/$', r'^sort/do-sorting/(?P<model_type_id>\d+)/$',
self.admin_site.admin_view(self.do_sorting_view), self.admin_site.admin_view(self.do_sorting_view),
name='%s_%s_do_sorting' % info) name='%s_%s_do_sorting' % info)
# this view displays the sortable objects # this view displays the sortable objects
admin_sort_url = re_path( admin_sort_url = url(
r'^sort/$', r'^sort/$',
self.admin_site.admin_view(self.sort_view), self.admin_site.admin_view(self.sort_view),
name='%s_%s_sort' % info) name='%s_%s_sort' % info)
@ -161,7 +160,8 @@ class SortableAdmin(SortableAdminBase, ModelAdmin):
opts = self.model._meta opts = self.model._meta
jquery_lib_path = 'admin/js/vendor/jquery/jquery.js' jquery_lib_path = 'admin/js/jquery.js' if VERSION < (1, 9) \
else 'admin/js/vendor/jquery/jquery.js'
# Determine if we need to regroup objects relative to a # Determine if we need to regroup objects relative to a
# foreign key specified on the model class that is extending Sortable. # foreign key specified on the model class that is extending Sortable.
@ -176,15 +176,23 @@ class SortableAdmin(SortableAdminBase, ModelAdmin):
for field in self.model._meta.fields: for field in self.model._meta.fields:
if isinstance(field, SortableForeignKey): if isinstance(field, SortableForeignKey):
sortable_by_fk = field.remote_field.model try:
sortable_by_fk = field.remote_field.model
except AttributeError:
# Django < 1.9
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_class_is_sortable = sortable_by_fk.objects.count() >= 2
isinstance(sortable_by_fk, SortableMixin) and \
sortable_by_fk.objects.count() >= 2
if sortable_by_property: if sortable_by_property:
sortable_by_class = self.model.sortable_by # backwards compatibility for < 1.1.1, where sortable_by was a
sortable_by_expression = sortable_by_class.__name__.lower() # 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_display_name = sortable_by_class._meta \ sortable_by_class_display_name = sortable_by_class._meta \
.verbose_name_plural .verbose_name_plural
@ -221,9 +229,10 @@ class SortableAdmin(SortableAdminBase, ModelAdmin):
except AttributeError: except AttributeError:
verbose_name_plural = opts.verbose_name_plural verbose_name_plural = opts.verbose_name_plural
context = self.admin_site.each_context(request) if VERSION <= (1, 7):
context = {}
filters = urlencode(self.get_querystring_filters(request)) else:
context = self.admin_site.each_context(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(
@ -235,7 +244,6 @@ 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'),
@ -281,57 +289,47 @@ class SortableAdmin(SortableAdminBase, ModelAdmin):
response = {'objects_sorted': False} response = {'objects_sorted': False}
if request.META.get('HTTP_X_REQUESTED_WITH') == 'XMLHttpRequest': if request.is_ajax():
klass = ContentType.objects.get(id=model_type_id).model_class() try:
klass = ContentType.objects.get(id=model_type_id).model_class()
indexes = [str(idx) for idx in request.POST.get('indexes', []).split(',')] 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)])
# 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] 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 = objects_dict[objects_list[-1]] start_object = max(objects_dict.values(),
key=lambda x: getattr(x, order_field_name))
else: else:
step = 1 step = 1
start_object = objects_dict[objects_list[0]] start_object = min(objects_dict.values(),
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)
objects_to_update.append(obj) # only update the object's order field
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()
return JsonResponse(response) return HttpResponse(json.dumps(response, ensure_ascii=False),
content_type='application/json')
class NonSortableParentAdmin(SortableAdmin): class NonSortableParentAdmin(SortableAdmin):
@ -360,19 +358,31 @@ class SortableInlineBase(SortableAdminBase, InlineModelAdmin):
class SortableTabularInline(TabularInline, SortableInlineBase): class SortableTabularInline(TabularInline, SortableInlineBase):
"""Custom template that enables sorting for tabular inlines""" """Custom template that enables sorting for tabular inlines"""
template = 'adminsortable/edit_inline/tabular.html' if VERSION >= (2, 0):
template = 'adminsortable/edit_inline/tabular-1.10.x.html'
else:
template = 'adminsortable/edit_inline/tabular.html'
class SortableStackedInline(StackedInline, SortableInlineBase): class SortableStackedInline(StackedInline, SortableInlineBase):
"""Custom template that enables sorting for stacked inlines""" """Custom template that enables sorting for stacked inlines"""
template = 'adminsortable/edit_inline/stacked.html' if VERSION >= (2, 0):
template = 'adminsortable/edit_inline/stacked-1.10.x.html'
else:
template = 'adminsortable/edit_inline/stacked.html'
class SortableGenericTabularInline(GenericTabularInline, SortableInlineBase): class SortableGenericTabularInline(GenericTabularInline, SortableInlineBase):
"""Custom template that enables sorting for tabular inlines""" """Custom template that enables sorting for tabular inlines"""
template = 'adminsortable/edit_inline/tabular.html' if VERSION >= (2, 0):
template = 'adminsortable/edit_inline/tabular-1.10.x.html'
else:
template = 'adminsortable/edit_inline/tabular.html'
class SortableGenericStackedInline(GenericStackedInline, SortableInlineBase): class SortableGenericStackedInline(GenericStackedInline, SortableInlineBase):
"""Custom template that enables sorting for stacked inlines""" """Custom template that enables sorting for stacked inlines"""
template = 'adminsortable/edit_inline/stacked.html' if VERSION >= (2, 0):
template = 'adminsortable/edit_inline/stacked-1.10.x.html'
else:
template = 'adminsortable/edit_inline/stacked.html'

View file

@ -89,7 +89,7 @@ class SortableMixin(models.Model):
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
needs_default = (self._state.adding if VERSION >= (1, 8) else not self.pk) needs_default = (self._state.adding if VERSION >= (1, 8) else not self.pk)
if not getattr(self, self.order_field_name) and needs_default: if needs_default:
try: try:
current_max = self.__class__.objects.aggregate( current_max = self.__class__.objects.aggregate(
models.Max(self.order_field_name))[self.order_field_name + '__max'] or 0 models.Max(self.order_field_name))[self.order_field_name + '__max'] or 0

View file

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

View file

@ -0,0 +1,34 @@
{% 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 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 "admin/includes/fieldset.html" %}
{% endfor %}
{% 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>

View file

@ -1,34 +1,41 @@
{% load i18n admin_urls static %} {% load i18n admin_urls static django_template_additions %}
<div class="js-inline-admin-formset inline-group" {% get_django_version as django_version %}
id="{{ inline_admin_formset.formset.prefix }}-group" <div class="inline-group" id="{{ inline_admin_formset.formset.prefix }}-group">
data-inline-type="stacked" <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>
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.management_form }}
{{ inline_admin_formset.formset.non_form_errors }} {{ inline_admin_formset.formset.non_form_errors }}
{% 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 %}"> {% 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> <h3>
{% if inline_admin_form.original %} {% if inline_admin_form.original %}
{% with initial_forms_count=inline_admin_formset.formset.management_form.INITIAL_FORMS.value %} {% 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> <i class="fa fa-{% if forloop.first %}sort-desc{% elif forloop.counter == initial_forms_count %}sort-asc{% else %}sort{% endif %}"></i>
{% endwith %} {% endwith %}
{% endif %} {% 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 %} <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> {% 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_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_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 %} {% 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> </h3>
{% if inline_admin_form.form.non_field_errors %}{{ inline_admin_form.form.non_field_errors }}{% endif %} {% if inline_admin_form.form.non_field_errors %}{{ inline_admin_form.form.non_field_errors }}{% endif %}
{% for fieldset in inline_admin_form %} {% for fieldset in inline_admin_form %}
{% include "admin/includes/fieldset.html" %} {% include "admin/includes/fieldset.html" %}
{% endfor %} {% endfor %}
{% 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 %} {% 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 %}" /> <input type="hidden" name="admin_sorting_url" value="{% url opts|admin_urlname:'do_sorting' inline_admin_form.original.model_type_id %}" />
{% endif %} {% endif %}
{% if inline_admin_form.needs_explicit_pk_field %}{{ inline_admin_form.pk_field.field }}{% endif %}
{{ inline_admin_form.fk_field.field }}
</div>{% endfor %} </div>{% endfor %}
</fieldset>
</div> </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>

View file

@ -0,0 +1,81 @@
{% 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 {{ 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 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 %}
{% 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="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">
{% 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 %}
{% 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 }}
{% spaceless %}
{% for fieldset in inline_admin_form %}
{% for line in fieldset %}
{% for field in line %}
{% 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 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 }}</p>
{% else %}
{{ field.field.errors.as_ul }}
{{ field.field }}
{% endif %}
</td>
{% endif %}
{% 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>

View file

@ -1,19 +1,17 @@
{% load i18n admin_urls static admin_modify django_template_additions %} {% load i18n admin_urls static admin_modify django_template_additions %}{% load cycle from future %}
<div class="js-inline-admin-formset inline-group" id="{{ inline_admin_formset.formset.prefix }}-group" {% get_django_version as django_version %}
data-inline-type="tabular" <div class="inline-group {% if django_version.major >= 1 and django_version.minor >= 9 %}flat-admin{% endif %}" id="{{ inline_admin_formset.formset.prefix }}-group">
data-inline-formset="{{ inline_admin_formset.inline_formset_data }}">
<div class="tabular inline-related {% if forloop.last %}last-related{% endif %}"> <div class="tabular inline-related {% if forloop.last %}last-related{% endif %}">
{{ inline_admin_formset.formset.management_form }} {{ inline_admin_formset.formset.management_form }}
<fieldset class="module {{ inline_admin_formset.classes }}"> <fieldset class="module">
<h2>{{ inline_admin_formset.opts.verbose_name_plural|capfirst }}</h2> <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 }} {{ inline_admin_formset.formset.non_form_errors }}
<table> <table>
<thead><tr> <thead><tr>
<th class="original"></th>
{% for field in inline_admin_formset.fields %} {% for field in inline_admin_formset.fields %}
{% if not field.widget.is_hidden %} {% if not field.widget.is_hidden %}
<th{% if field.required %} class="required"{% endif %}>{{ field.label|capfirst }} <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.svg" %}" class="help help-tooltip" width="10" height="10" alt="({{ field.help_text|striptags }})" title="{{ field.help_text|striptags }}" />{% endif %} {% if field.help_text %}&nbsp;<img src="{% if django_version.major >= 1 and django_version.minor >= 9 %}{% static "admin/img/icon-unknown.svg" %}{% else %}{% static "admin/img/icon-unknown.gif" %}{% endif %}" class="help help-tooltip" width="10" height="10" alt="({{ field.help_text|striptags }})" title="{{ field.help_text|striptags }}" />{% endif %}
</th> </th>
{% endif %} {% endif %}
{% endfor %} {% endfor %}
@ -33,10 +31,10 @@
<i class="fa fa-{% if forloop.first %}sort-desc{% elif forloop.counter == initial_forms_count %}sort-asc{% else %}sort{% endif %}"></i> <i class="fa fa-{% if forloop.first %}sort-desc{% elif forloop.counter == initial_forms_count %}sort-asc{% else %}sort{% endif %}"></i>
{% endwith %} {% endwith %}
{% if inline_admin_form.original %} {% if inline_admin_form.original %}
{{ 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 %} {% 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 %} {% endif %}
{% if inline_admin_form.show_url %}<a href="{{ inline_admin_form.absolute_url }}">{% trans "View on site" %}</a>{% 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 %}
</p>{% endif %} </p>{% endif %}
{% if inline_admin_form.needs_explicit_pk_field %}{{ inline_admin_form.pk_field.field }}{% endif %} {% if inline_admin_form.needs_explicit_pk_field %}{{ inline_admin_form.pk_field.field }}{% endif %}
{{ inline_admin_form.fk_field.field }} {{ inline_admin_form.fk_field.field }}
@ -44,7 +42,7 @@
{% for fieldset in inline_admin_form %} {% for fieldset in inline_admin_form %}
{% for line in fieldset %} {% for line in fieldset %}
{% for field in line %} {% for field in line %}
{% if field.field.is_hidden %} {{ field.field }} {% endif %} {% if field.is_hidden %} {{ field.field }} {% endif %}
{% endfor %} {% endfor %}
{% endfor %} {% endfor %}
{% endfor %} {% endfor %}
@ -56,16 +54,14 @@
{% for fieldset in inline_admin_form %} {% for fieldset in inline_admin_form %}
{% for line in fieldset %} {% for line in fieldset %}
{% for field in line %} {% for field in line %}
{% if not field.field.is_hidden %}
<td{% if field.field.name %} class="field-{{ field.field.name }}"{% endif %}> <td{% if field.field.name %} class="field-{{ field.field.name }}"{% endif %}>
{% if field.is_readonly %} {% if field.is_readonly %}
<p>{{ field.contents }}</p> <p>{{ field.contents|linebreaksbr }}</p>
{% else %} {% else %}
{{ field.field.errors.as_ul }} {{ field.field.errors.as_ul }}
{{ field.field }} {{ field.field }}
{% endif %} {% endif %}
</td> </td>
{% endif %}
{% endfor %} {% endfor %}
{% endfor %} {% endfor %}
{% endfor %} {% endfor %}
@ -79,3 +75,15 @@
</fieldset> </fieldset>
</div> </div>
</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>

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 %}{% 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> <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>
{% csrf_token %} {% csrf_token %}
</form> </form>

View file

@ -3,8 +3,6 @@ Quickstart
To get started using ``django-admin-sortable`` simply install it using ``pip``:: To get started using ``django-admin-sortable`` simply install it using ``pip``::
.. code-block:: bash
$ pip install django-admin-sortable $ pip install django-admin-sortable
Add ``adminsortable`` to your project's ``INSTALLED_APPS`` setting. Add ``adminsortable`` to your project's ``INSTALLED_APPS`` setting.
@ -13,8 +11,6 @@ Ensure ``django.core.context_processors.static`` is in your ``TEMPLATE_CONTEXT_P
Define your model, inheriting from ``adminsortable.Sortable``:: Define your model, inheriting from ``adminsortable.Sortable``::
.. code-block:: python
# models.py # models.py
from adminsortable.models import Sortable from adminsortable.models import Sortable
@ -29,8 +25,6 @@ Define your model, inheriting from ``adminsortable.Sortable``::
Wire up your sortable model to Django admin:: Wire up your sortable model to Django admin::
.. code-block:: python
# admin.py # admin.py
from adminsortable.admin import SortableAdmin from adminsortable.admin import SortableAdmin
from .models import MySortableClass from .models import MySortableClass

View file

@ -9,6 +9,4 @@ Inlines may be drag-and-dropped into any order directly from the change form.
Unit and functional tests may be found in the ``app/tests.py`` file and run via: Unit and functional tests may be found in the ``app/tests.py`` file and run via:
.. code-block:: bash
$ python manage.py test app $ python manage.py test app

View file

@ -4,9 +4,7 @@ Using Django Admin Sortable
Models Models
------ ------
To add sorting to a model, your model needs to inherit from ``SortableMixin`` and at minimum, define an inner ``Meta.ordering`` value To add sorting to a model, your model needs to inherit from ``Sortable`` and have an inner ``Meta`` class that inherits from ``Sortable.Meta``::
.. code-block:: python
# models.py # models.py
from adminsortable.models import Sortable from adminsortable.models import Sortable
@ -24,7 +22,7 @@ It is also possible to order objects relative to another object that is a Foreig
.. note:: A small caveat here is that ``Category`` must also either inherit from ``Sortable`` or include an ``order`` property which is a ``PositiveSmallInteger`` field. This is due to the way Django admin instantiates classes. .. note:: A small caveat here is that ``Category`` must also either inherit from ``Sortable`` or include an ``order`` property which is a ``PositiveSmallInteger`` field. This is due to the way Django admin instantiates classes.
.. code-block:: python ::
# models.py # models.py
from adminsortable.fields import SortableForeignKey from adminsortable.fields import SortableForeignKey
@ -55,8 +53,6 @@ If you're adding Sorting to an existing model, it is recommended that you use `d
Example assuming a model named "Category":: Example assuming a model named "Category"::
.. code-block:: python
def forwards(self, orm): def forwards(self, orm):
for index, category in enumerate(orm.Category.objects.all()): for index, category in enumerate(orm.Category.objects.all()):
category.order = index + 1 category.order = index + 1
@ -69,8 +65,6 @@ Django Admin
To enable sorting in the admin, you need to inherit from ``SortableAdmin``:: To enable sorting in the admin, you need to inherit from ``SortableAdmin``::
.. code-block:: python
from django.contrib import admin from django.contrib import admin
from myapp.models import MySortableClass from myapp.models import MySortableClass
from adminsortable.admin import SortableAdmin from adminsortable.admin import SortableAdmin
@ -82,8 +76,6 @@ To enable sorting in the admin, you need to inherit from ``SortableAdmin``::
To enable sorting on TabularInline models, you need to inherit from SortableTabularInline:: To enable sorting on TabularInline models, you need to inherit from SortableTabularInline::
.. code-block:: python
from adminsortable.admin import SortableTabularInline from adminsortable.admin import SortableTabularInline
class MySortableTabularInline(SortableTabularInline): class MySortableTabularInline(SortableTabularInline):
@ -91,8 +83,6 @@ To enable sorting on TabularInline models, you need to inherit from SortableTabu
To enable sorting on StackedInline models, you need to inherit from SortableStackedInline:: To enable sorting on StackedInline models, you need to inherit from SortableStackedInline::
.. code-block:: python
from adminsortable.admin import SortableStackedInline from adminsortable.admin import SortableStackedInline
class MySortableStackedInline(SortableStackedInline): class MySortableStackedInline(SortableStackedInline):
@ -100,8 +90,6 @@ To enable sorting on StackedInline models, you need to inherit from SortableStac
There are also generic equivalents that you can inherit from:: There are also generic equivalents that you can inherit from::
.. code-block:: python
from adminsortable.admin import (SortableGenericTabularInline, from adminsortable.admin import (SortableGenericTabularInline,
SortableGenericStackedInline) SortableGenericStackedInline)
"""Your generic inline options go here""" """Your generic inline options go here"""
@ -120,8 +108,6 @@ 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:: This is a special case, which requires a few lines of extra code to properly determine the sortability of your model. Example::
.. code-block:: python
# add this import to your admin.py # add this import to your admin.py
from adminsortable.utils import get_is_sortable from adminsortable.utils import get_is_sortable
@ -151,8 +137,6 @@ It is also possible to sort a subset of objects in your model by adding a ``sort
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:: 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-block:: python
class Person(Sortable): class Person(Sortable):
class Meta(Sortable.Meta): class Meta(Sortable.Meta):
verbose_name_plural = 'People' verbose_name_plural = 'People'
@ -186,8 +170,6 @@ By default, adminsortable's change form and change list views inherit from Djang
These attributes have default values of:: These attributes have default values of::
.. code-block:: python
change_form_template_extends = 'admin/change_form.html' change_form_template_extends = 'admin/change_form.html'
change_list_template_extends = 'admin/change_list.html' change_list_template_extends = 'admin/change_list.html'

View file

@ -188,5 +188,3 @@ AUTH_PASSWORD_VALIDATORS = [
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
}, },
] ]
DEFAULT_AUTO_FIELD = 'django.db.models.AutoField'

View file

@ -82,11 +82,6 @@ class SortableTestCase(TestCase):
self.assertTrue(get_is_sortable(Category.objects.all()), self.assertTrue(get_is_sortable(Category.objects.all()),
'Category has more than one record. It should be sortable.') 'Category has more than one record. It should be sortable.')
def test_doesnt_overwrite_preexisting_order_field_value(self):
self.create_category()
category = Category.objects.create(title='Category 2', order=5)
self.assertEqual(category.order, 5)
def test_save_order_incremented(self): def test_save_order_incremented(self):
category1 = self.create_category() category1 = self.create_category()
self.assertEqual(category1.order, 1, 'Category 1 order should be 1.') self.assertEqual(category1.order, 1, 'Category 1 order should be 1.')

View file

@ -1,7 +1,9 @@
from setuptools import setup, find_packages from setuptools import setup, find_packages
with open('README.rst', encoding='utf8') as readme_file: try:
README = readme_file.read() README = open('README.rst').read()
except:
README = None
setup( setup(
author='Brandon Taylor', author='Brandon Taylor',
@ -12,6 +14,7 @@ setup(
'Intended Audience :: Developers', 'Intended Audience :: Developers',
'License :: OSI Approved :: Apache Software License', 'License :: OSI Approved :: Apache Software License',
'Operating System :: OS Independent', 'Operating System :: OS Independent',
'Programming Language :: Python :: 2',
'Programming Language :: Python :: 3', 'Programming Language :: Python :: 3',
'Topic :: Utilities'], 'Topic :: Utilities'],
description='Drag and drop sorting for models and inline models in Django admin.', description='Drag and drop sorting for models and inline models in Django admin.',
@ -19,6 +22,7 @@ setup(
install_requires=['django'], install_requires=['django'],
license='APL', license='APL',
long_description=README, long_description=README,
long_description_content_type='text/markdown',
name='django-admin-sortable', name='django-admin-sortable',
packages=find_packages(exclude=['sample_project']), packages=find_packages(exclude=['sample_project']),
url='https://github.com/iambrandontaylor/django-admin-sortable', url='https://github.com/iambrandontaylor/django-admin-sortable',

12
tox.ini
View file

@ -1,19 +1,21 @@
[tox] [tox]
envlist = django{2.2,3.1,3.2}-{py36,py37,py38,py39},coverage envlist = django{1.8,1.9,1.10,1.11,2}-{py27,py34,py35,py36},coverage
[testenv] [testenv]
deps = deps =
coverage coverage
django2.2: Django>=2.2 django1.8: Django>=1.8,<1.9
django3.1: Django>=3.1 django1.9: Django>=1.9,<1.10
django3.2: Django>=3.2 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 samples coverage run -p sample_project/manage.py test app
[testenv:coverage] [testenv:coverage]
deps = coverage deps = coverage