mirror of
https://github.com/jazzband/django-admin-sortable.git
synced 2026-03-16 22:10:30 +00:00
Compare commits
52 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b9c1f3e0c4 | ||
|
|
15766e79d8 | ||
|
|
844b51456f | ||
|
|
b0c25c303c | ||
|
|
c44edfe40a | ||
|
|
3ae816d64d | ||
|
|
aaaa92ba37 | ||
|
|
508e9f35d6 | ||
|
|
c0874bfd7e | ||
|
|
5aeaa27f5b | ||
|
|
f9a896f469 | ||
|
|
5671331b22 | ||
|
|
51f850f04b | ||
|
|
b56ded8f0f | ||
|
|
a6ea566ee8 | ||
|
|
b4dad1896b | ||
|
|
fd50e61037 | ||
|
|
b69c482dec | ||
|
|
757deb302d | ||
|
|
58e03a5ebf | ||
|
|
5e9e0b3564 | ||
|
|
7ed44fa84b | ||
|
|
616dcdfd7a | ||
|
|
eb3f18b09e | ||
|
|
3209998569 | ||
|
|
2e5f7addf4 | ||
|
|
795dc26275 | ||
|
|
7180833e6d | ||
|
|
e90dc03b2c | ||
|
|
de6eef8213 | ||
|
|
45b54a3402 | ||
|
|
3082950549 | ||
|
|
7deb73c806 | ||
|
|
01591305a3 | ||
|
|
3443c300d0 | ||
|
|
9e352ec474 | ||
|
|
387e9b8f2f | ||
|
|
f93cac291b | ||
|
|
21cab41381 | ||
|
|
899f92f53a | ||
|
|
1b700e0b25 | ||
|
|
a6fb5d0d36 | ||
|
|
00eb3fdb30 | ||
|
|
247078c7e0 | ||
|
|
2bb6a677fe | ||
|
|
3263eb0e34 | ||
|
|
d81790a387 | ||
|
|
493db1e75a | ||
|
|
b545106cbd | ||
|
|
77eedcc8b6 | ||
|
|
e2bb925a99 | ||
|
|
b286fcc96e |
21 changed files with 262 additions and 300 deletions
52
.github/workflows/tests.yml
vendored
Normal file
52
.github/workflows/tests.yml
vendored
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
---
|
||||
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
3
.gitignore
vendored
|
|
@ -11,5 +11,8 @@ atlassian-*
|
|||
.codeintel
|
||||
__pycache__
|
||||
.venv/
|
||||
.coverage
|
||||
.tox/
|
||||
htmlcov/
|
||||
build
|
||||
.vscode/*
|
||||
|
|
|
|||
43
.travis.yml
43
.travis.yml
|
|
@ -1,43 +0,0 @@
|
|||
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
|
||||
85
README.md
85
README.md
|
|
@ -1,3 +1,6 @@
|
|||
## 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
|
||||
|
||||
[](https://pypi.python.org/pypi/django-admin-sortable)
|
||||
|
|
@ -19,7 +22,9 @@ Sorting inlines:
|
|||

|
||||
|
||||
## Supported Django Versions
|
||||
For Django 3 use the latest version
|
||||
For Django 4 use the latest version
|
||||
|
||||
For Django 3 use 2.2.4
|
||||
|
||||
For Django 1.8.x < 3.0, use 2.1.8.
|
||||
|
||||
|
|
@ -58,7 +63,7 @@ django-admin-sortable is currently incompatible with that setting.
|
|||
|
||||
### Static Media
|
||||
Preferred:
|
||||
Use the [staticfiles app](https://docs.djangoproject.com/en/1.6/ref/contrib/staticfiles/)
|
||||
Use the [staticfiles app](https://docs.djangoproject.com/en/3.0/howto/static-files/)
|
||||
|
||||
Alternate:
|
||||
Copy the `adminsortable` folder from the `static` folder to the
|
||||
|
|
@ -82,14 +87,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:
|
||||
|
||||
- The field which should be used for `Meta.ordering`, which must resolve to one of the integer fields defined in Django's ORM:
|
||||
- `PositiveIntegerField`
|
||||
- `IntegerField`
|
||||
- `PositiveSmallIntegerField`
|
||||
- `SmallIntegerField`
|
||||
- `BigIntegerField`
|
||||
- `PositiveIntegerField`
|
||||
- `IntegerField`
|
||||
- `PositiveSmallIntegerField`
|
||||
- `SmallIntegerField`
|
||||
- `BigIntegerField`
|
||||
|
||||
- `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.
|
||||
- ⚠️ `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.
|
||||
- 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:
|
||||
|
|
@ -155,6 +160,8 @@ admin.site.register(Category, 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:
|
||||
|
||||
```python
|
||||
|
|
@ -198,6 +205,60 @@ 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.
|
||||
|
||||
#### 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
|
||||
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.
|
||||
|
|
@ -383,7 +444,7 @@ It is also possible to sort a subset of objects in your model by adding a `sorti
|
|||
#### 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.
|
||||
|
||||
##### 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.
|
||||
|
||||
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.
|
||||
|
|
@ -609,8 +670,8 @@ ordering on top of that just seemed a little much in my opinion.
|
|||
### Status
|
||||
django-admin-sortable is currently used in production.
|
||||
|
||||
### What's new in 2.2.3?
|
||||
- Updated inline sortable templates to fix FontAwesome icon visibility and be compatible with Django 2 & 3.
|
||||
### What's new in 2.3.0?
|
||||
- Django 4 compatibility
|
||||
|
||||
### 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.
|
||||
|
|
|
|||
10
README.rst
10
README.rst
|
|
@ -27,7 +27,9 @@ Sorting inlines:
|
|||
Supported Django Versions
|
||||
-------------------------
|
||||
|
||||
For Django 3 use the latest version
|
||||
For Django 4 use the latest version
|
||||
|
||||
For Django 3 use 2.2.4
|
||||
|
||||
For Django 1.8.x < 3.0, use 2.1.8.
|
||||
|
||||
|
|
@ -84,7 +86,7 @@ Static Media
|
|||
~~~~~~~~~~~~
|
||||
|
||||
Preferred: Use the `staticfiles
|
||||
app <https://docs.djangoproject.com/en/1.6/ref/contrib/staticfiles/>`__
|
||||
app <https://docs.djangoproject.com/en/3.0/howto/static-files/>`__
|
||||
|
||||
Alternate: Copy the ``adminsortable`` folder from the ``static`` folder
|
||||
to the location you serve static files from.
|
||||
|
|
@ -751,10 +753,10 @@ Status
|
|||
|
||||
django-admin-sortable is currently used in production.
|
||||
|
||||
What’s new in 2.2.3?
|
||||
What’s new in 2.3.0?
|
||||
~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
- Updated inline sortable templates to fix FontAwesome icon visibility and be compatible with Django 2 & 3.
|
||||
- Django 4 compatibility
|
||||
|
||||
Future
|
||||
~~~~~~
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
VERSION = (2, 2, 3)
|
||||
VERSION = (2, 3, 0)
|
||||
DEV_N = None
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1,9 +1,7 @@
|
|||
import json
|
||||
|
||||
from django import VERSION
|
||||
from urllib.parse import urlencode
|
||||
|
||||
from django.conf import settings
|
||||
from django.conf.urls import url
|
||||
from django.contrib.admin import ModelAdmin, TabularInline, StackedInline
|
||||
from django.contrib.admin.options import InlineModelAdmin
|
||||
from django.contrib.admin.views.main import IGNORED_PARAMS, PAGE_VAR
|
||||
|
|
@ -11,10 +9,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 JsonResponse
|
||||
from django.shortcuts import render
|
||||
from django.template.defaultfilters import capfirst
|
||||
from django.urls import re_path
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.utils.translation import gettext as _
|
||||
from django.views.decorators.http import require_POST
|
||||
|
||||
from adminsortable.fields import SortableForeignKey
|
||||
|
|
@ -113,13 +114,13 @@ class SortableAdmin(SortableAdminBase, ModelAdmin):
|
|||
info = self.model._meta.app_label, self.model._meta.model_name
|
||||
|
||||
# this ajax view changes the order of instances of the model type
|
||||
admin_do_sorting_url = url(
|
||||
admin_do_sorting_url = re_path(
|
||||
r'^sort/do-sorting/(?P<model_type_id>\d+)/$',
|
||||
self.admin_site.admin_view(self.do_sorting_view),
|
||||
name='%s_%s_do_sorting' % info)
|
||||
|
||||
# this view displays the sortable objects
|
||||
admin_sort_url = url(
|
||||
admin_sort_url = re_path(
|
||||
r'^sort/$',
|
||||
self.admin_site.admin_view(self.sort_view),
|
||||
name='%s_%s_sort' % info)
|
||||
|
|
@ -160,8 +161,7 @@ class SortableAdmin(SortableAdminBase, ModelAdmin):
|
|||
|
||||
opts = self.model._meta
|
||||
|
||||
jquery_lib_path = 'admin/js/jquery.js' if VERSION < (1, 9) \
|
||||
else 'admin/js/vendor/jquery/jquery.js'
|
||||
jquery_lib_path = 'admin/js/vendor/jquery/jquery.js'
|
||||
|
||||
# Determine if we need to regroup objects relative to a
|
||||
# foreign key specified on the model class that is extending Sortable.
|
||||
|
|
@ -176,23 +176,15 @@ class SortableAdmin(SortableAdminBase, ModelAdmin):
|
|||
|
||||
for field in self.model._meta.fields:
|
||||
if isinstance(field, SortableForeignKey):
|
||||
try:
|
||||
sortable_by_fk = field.remote_field.model
|
||||
except AttributeError:
|
||||
# Django < 1.9
|
||||
sortable_by_fk = field.rel.to
|
||||
sortable_by_fk = field.remote_field.model
|
||||
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
|
||||
# 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 = self.model.sortable_by
|
||||
sortable_by_expression = sortable_by_class.__name__.lower()
|
||||
|
||||
sortable_by_class_display_name = sortable_by_class._meta \
|
||||
.verbose_name_plural
|
||||
|
|
@ -229,10 +221,9 @@ class SortableAdmin(SortableAdminBase, ModelAdmin):
|
|||
except AttributeError:
|
||||
verbose_name_plural = opts.verbose_name_plural
|
||||
|
||||
if VERSION <= (1, 7):
|
||||
context = {}
|
||||
else:
|
||||
context = self.admin_site.each_context(request)
|
||||
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(
|
||||
|
|
@ -244,6 +235,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'),
|
||||
|
|
@ -289,47 +281,57 @@ class SortableAdmin(SortableAdminBase, ModelAdmin):
|
|||
|
||||
response = {'objects_sorted': False}
|
||||
|
||||
if request.is_ajax():
|
||||
try:
|
||||
klass = ContentType.objects.get(id=model_type_id).model_class()
|
||||
if request.META.get('HTTP_X_REQUESTED_WITH') == 'XMLHttpRequest':
|
||||
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}
|
||||
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]
|
||||
|
||||
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))
|
||||
|
||||
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()
|
||||
|
||||
return HttpResponse(json.dumps(response, ensure_ascii=False),
|
||||
content_type='application/json')
|
||||
return JsonResponse(response)
|
||||
|
||||
|
||||
class NonSortableParentAdmin(SortableAdmin):
|
||||
|
|
@ -358,31 +360,19 @@ class SortableInlineBase(SortableAdminBase, InlineModelAdmin):
|
|||
|
||||
class SortableTabularInline(TabularInline, SortableInlineBase):
|
||||
"""Custom template that enables sorting for tabular inlines"""
|
||||
if VERSION >= (2, 0):
|
||||
template = 'adminsortable/edit_inline/tabular-1.10.x.html'
|
||||
else:
|
||||
template = 'adminsortable/edit_inline/tabular.html'
|
||||
template = 'adminsortable/edit_inline/tabular.html'
|
||||
|
||||
|
||||
class SortableStackedInline(StackedInline, SortableInlineBase):
|
||||
"""Custom template that enables sorting for stacked inlines"""
|
||||
if VERSION >= (2, 0):
|
||||
template = 'adminsortable/edit_inline/stacked-1.10.x.html'
|
||||
else:
|
||||
template = 'adminsortable/edit_inline/stacked.html'
|
||||
template = 'adminsortable/edit_inline/stacked.html'
|
||||
|
||||
|
||||
class SortableGenericTabularInline(GenericTabularInline, SortableInlineBase):
|
||||
"""Custom template that enables sorting for tabular inlines"""
|
||||
if VERSION >= (2, 0):
|
||||
template = 'adminsortable/edit_inline/tabular-1.10.x.html'
|
||||
else:
|
||||
template = 'adminsortable/edit_inline/tabular.html'
|
||||
template = 'adminsortable/edit_inline/tabular.html'
|
||||
|
||||
|
||||
class SortableGenericStackedInline(GenericStackedInline, SortableInlineBase):
|
||||
"""Custom template that enables sorting for stacked inlines"""
|
||||
if VERSION >= (2, 0):
|
||||
template = 'adminsortable/edit_inline/stacked-1.10.x.html'
|
||||
else:
|
||||
template = 'adminsortable/edit_inline/stacked.html'
|
||||
template = 'adminsortable/edit_inline/stacked.html'
|
||||
|
|
|
|||
|
|
@ -89,7 +89,7 @@ class SortableMixin(models.Model):
|
|||
|
||||
def save(self, *args, **kwargs):
|
||||
needs_default = (self._state.adding if VERSION >= (1, 8) else not self.pk)
|
||||
if needs_default:
|
||||
if not getattr(self, self.order_field_name) and needs_default:
|
||||
try:
|
||||
current_max = self.__class__.objects.aggregate(
|
||||
models.Max(self.order_field_name))[self.order_field_name + '__max'] or 0
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,34 +0,0 @@
|
|||
{% 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> <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>
|
||||
|
|
@ -1,41 +1,34 @@
|
|||
{% load i18n admin_urls static django_template_additions %}
|
||||
{% get_django_version as django_version %}
|
||||
<div class="inline-group" id="{{ inline_admin_formset.formset.prefix }}-group">
|
||||
<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>
|
||||
{% 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 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 %}">
|
||||
{% 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|title }}:</b> <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="{% url 'admin:view_on_site' inline_admin_form.original_content_type_id inline_admin_form.original.pk %}">{% trans "View on site" %}</a>{% endif %}
|
||||
<b>{{ inline_admin_formset.opts.verbose_name|capfirst }}:</b> <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 %}
|
||||
{% if inline_admin_form.needs_explicit_pk_field %}{{ inline_admin_form.pk_field.field }}{% endif %}
|
||||
{{ inline_admin_form.fk_field.field }}
|
||||
</div>{% endfor %}
|
||||
</fieldset>
|
||||
</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>
|
||||
|
|
|
|||
|
|
@ -1,81 +0,0 @@
|
|||
{% 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 %} <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>
|
||||
|
|
@ -1,17 +1,19 @@
|
|||
{% load i18n admin_urls static admin_modify django_template_additions %}{% load cycle from future %}
|
||||
{% get_django_version as django_version %}
|
||||
<div class="inline-group {% if django_version.major >= 1 and django_version.minor >= 9 %}flat-admin{% endif %}" id="{{ inline_admin_formset.formset.prefix }}-group">
|
||||
{% 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">
|
||||
<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>
|
||||
<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 forloop.first %} colspan="2"{% endif %}{% if field.required %} class="required"{% endif %}>{{ field.label|capfirst }}
|
||||
{% if field.help_text %} <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{% if field.required %} class="required"{% endif %}>{{ field.label|capfirst }}
|
||||
{% if field.help_text %} <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 %}
|
||||
|
|
@ -31,10 +33,10 @@
|
|||
<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 %}
|
||||
{{ 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="{% 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_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 }}
|
||||
|
|
@ -42,7 +44,7 @@
|
|||
{% for fieldset in inline_admin_form %}
|
||||
{% for line in fieldset %}
|
||||
{% for field in line %}
|
||||
{% if field.is_hidden %} {{ field.field }} {% endif %}
|
||||
{% if field.field.is_hidden %} {{ field.field }} {% endif %}
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
|
|
@ -54,14 +56,16 @@
|
|||
{% 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|linebreaksbr }}</p>
|
||||
<p>{{ field.contents }}</p>
|
||||
{% else %}
|
||||
{{ field.field.errors.as_ul }}
|
||||
{{ field.field }}
|
||||
{% endif %}
|
||||
</td>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
|
|
@ -75,15 +79,3 @@
|
|||
</fieldset>
|
||||
</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>
|
||||
|
|
|
|||
|
|
@ -2,6 +2,6 @@
|
|||
|
||||
<form>
|
||||
<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 %}
|
||||
</form>
|
||||
|
|
|
|||
|
|
@ -3,6 +3,8 @@ Quickstart
|
|||
|
||||
To get started using ``django-admin-sortable`` simply install it using ``pip``::
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
$ pip install django-admin-sortable
|
||||
|
||||
Add ``adminsortable`` to your project's ``INSTALLED_APPS`` setting.
|
||||
|
|
@ -11,6 +13,8 @@ Ensure ``django.core.context_processors.static`` is in your ``TEMPLATE_CONTEXT_P
|
|||
|
||||
Define your model, inheriting from ``adminsortable.Sortable``::
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
# models.py
|
||||
from adminsortable.models import Sortable
|
||||
|
||||
|
|
@ -25,6 +29,8 @@ Define your model, inheriting from ``adminsortable.Sortable``::
|
|||
|
||||
Wire up your sortable model to Django admin::
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
# admin.py
|
||||
from adminsortable.admin import SortableAdmin
|
||||
from .models import MySortableClass
|
||||
|
|
|
|||
|
|
@ -9,4 +9,6 @@ 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:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
$ python manage.py test app
|
||||
|
|
|
|||
|
|
@ -4,7 +4,9 @@ Using Django Admin Sortable
|
|||
Models
|
||||
------
|
||||
|
||||
To add sorting to a model, your model needs to inherit from ``Sortable`` and have an inner ``Meta`` class that inherits from ``Sortable.Meta``::
|
||||
To add sorting to a model, your model needs to inherit from ``SortableMixin`` and at minimum, define an inner ``Meta.ordering`` value
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
# models.py
|
||||
from adminsortable.models import Sortable
|
||||
|
|
@ -22,7 +24,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.
|
||||
|
||||
::
|
||||
.. code-block:: python
|
||||
|
||||
# models.py
|
||||
from adminsortable.fields import SortableForeignKey
|
||||
|
|
@ -53,6 +55,8 @@ If you're adding Sorting to an existing model, it is recommended that you use `d
|
|||
|
||||
Example assuming a model named "Category"::
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
def forwards(self, orm):
|
||||
for index, category in enumerate(orm.Category.objects.all()):
|
||||
category.order = index + 1
|
||||
|
|
@ -65,6 +69,8 @@ Django Admin
|
|||
|
||||
To enable sorting in the admin, you need to inherit from ``SortableAdmin``::
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from django.contrib import admin
|
||||
from myapp.models import MySortableClass
|
||||
from adminsortable.admin import SortableAdmin
|
||||
|
|
@ -76,6 +82,8 @@ To enable sorting in the admin, you need to inherit from ``SortableAdmin``::
|
|||
|
||||
To enable sorting on TabularInline models, you need to inherit from SortableTabularInline::
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from adminsortable.admin import SortableTabularInline
|
||||
|
||||
class MySortableTabularInline(SortableTabularInline):
|
||||
|
|
@ -83,6 +91,8 @@ To enable sorting on TabularInline models, you need to inherit from SortableTabu
|
|||
|
||||
To enable sorting on StackedInline models, you need to inherit from SortableStackedInline::
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from adminsortable.admin import SortableStackedInline
|
||||
|
||||
class MySortableStackedInline(SortableStackedInline):
|
||||
|
|
@ -90,6 +100,8 @@ To enable sorting on StackedInline models, you need to inherit from SortableStac
|
|||
|
||||
There are also generic equivalents that you can inherit from::
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from adminsortable.admin import (SortableGenericTabularInline,
|
||||
SortableGenericStackedInline)
|
||||
"""Your generic inline options go here"""
|
||||
|
|
@ -108,6 +120,8 @@ 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::
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
# add this import to your admin.py
|
||||
from adminsortable.utils import get_is_sortable
|
||||
|
||||
|
|
@ -137,6 +151,8 @@ 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::
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
class Person(Sortable):
|
||||
class Meta(Sortable.Meta):
|
||||
verbose_name_plural = 'People'
|
||||
|
|
@ -170,6 +186,8 @@ By default, adminsortable's change form and change list views inherit from Djang
|
|||
|
||||
These attributes have default values of::
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
change_form_template_extends = 'admin/change_form.html'
|
||||
change_list_template_extends = 'admin/change_list.html'
|
||||
|
||||
|
|
|
|||
|
|
@ -188,3 +188,5 @@ AUTH_PASSWORD_VALIDATORS = [
|
|||
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
|
||||
},
|
||||
]
|
||||
|
||||
DEFAULT_AUTO_FIELD = 'django.db.models.AutoField'
|
||||
|
|
@ -82,6 +82,11 @@ class SortableTestCase(TestCase):
|
|||
self.assertTrue(get_is_sortable(Category.objects.all()),
|
||||
'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):
|
||||
category1 = self.create_category()
|
||||
self.assertEqual(category1.order, 1, 'Category 1 order should be 1.')
|
||||
|
|
|
|||
8
setup.py
8
setup.py
|
|
@ -1,9 +1,7 @@
|
|||
from setuptools import setup, find_packages
|
||||
|
||||
try:
|
||||
README = open('README.rst').read()
|
||||
except:
|
||||
README = None
|
||||
with open('README.rst', encoding='utf8') as readme_file:
|
||||
README = readme_file.read()
|
||||
|
||||
setup(
|
||||
author='Brandon Taylor',
|
||||
|
|
@ -14,7 +12,6 @@ setup(
|
|||
'Intended Audience :: Developers',
|
||||
'License :: OSI Approved :: Apache Software License',
|
||||
'Operating System :: OS Independent',
|
||||
'Programming Language :: Python :: 2',
|
||||
'Programming Language :: Python :: 3',
|
||||
'Topic :: Utilities'],
|
||||
description='Drag and drop sorting for models and inline models in Django admin.',
|
||||
|
|
@ -22,7 +19,6 @@ setup(
|
|||
install_requires=['django'],
|
||||
license='APL',
|
||||
long_description=README,
|
||||
long_description_content_type='text/markdown',
|
||||
name='django-admin-sortable',
|
||||
packages=find_packages(exclude=['sample_project']),
|
||||
url='https://github.com/iambrandontaylor/django-admin-sortable',
|
||||
|
|
|
|||
12
tox.ini
12
tox.ini
|
|
@ -1,21 +1,19 @@
|
|||
[tox]
|
||||
envlist = django{1.8,1.9,1.10,1.11,2}-{py27,py34,py35,py36},coverage
|
||||
envlist = django{2.2,3.1,3.2}-{py36,py37,py38,py39},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.1: Django>=3.1
|
||||
django3.2: Django>=3.2
|
||||
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
|
||||
|
|
|
|||
Loading…
Reference in a new issue