Compare commits

..

81 commits

Author SHA1 Message Date
Brandon Taylor
b9c1f3e0c4
Merge pull request #252 from gafderks/gafderks-patch-1
Specify UTF8 encoding for readme
2022-03-12 19:03:22 -05:00
Geert Derks
15766e79d8
Specify UTF8 encoding for readme 2022-01-18 00:13:42 +01:00
Brandon Taylor
844b51456f - Version bump to 2.3.0
- Updated readme
2021-12-21 20:37:48 -05:00
Brandon Taylor
b0c25c303c
Merge pull request #250 from michael-k/gh-actions-python3.10-django4.0
Switch from Travis CI to GitHub Actions and run tests against Python 3.10 and Django 4.0
2021-12-16 20:58:56 -05:00
Michael Käufl
c44edfe40a
Run tests against Python 3.10 and Django 4.0 2021-12-16 12:57:57 +01:00
Michael Käufl
3ae816d64d
Switch from Travis CI to GitHub Actions 2021-12-16 12:56:14 +01:00
Brandon Taylor
aaaa92ba37
Merge pull request #248 from mjr/master
Support Django 4.0
2021-12-13 15:44:00 -05:00
Manaia Junior
508e9f35d6
Replace deprecated django.conf.urls with django.urls 2021-12-13 17:30:10 -03:00
Brandon Taylor
c0874bfd7e
Merge pull request #245 from sh-cho/patch-1
Update REAMDME
2021-07-27 08:25:36 -04:00
Seonghyeon Cho
5aeaa27f5b
Add warning emojis 2021-07-26 23:31:24 +09:00
Seonghyeon Cho
f9a896f469
Fix space for nested list in markdown 2021-07-26 23:29:27 +09:00
Brandon Taylor
5671331b22
Merge pull request #244 from galuszkak/feature/update
chore: cleanup and add support for Python 3.9
2021-07-18 08:56:32 -04:00
Kamil Gałuszka
51f850f04b chore: cleanup and add support for Python 3.9 2021-07-17 01:11:19 +02:00
Brandon Taylor
b56ded8f0f
Update README.md 2021-04-08 09:12:16 -04:00
Brandon Taylor
a6ea566ee8
Merge pull request #238 from genisysram/master
Adding code to run on Powersystem
2020-12-09 08:50:15 -05:00
genisysram
b4dad1896b
added ppc64le architecture 2020-12-03 20:24:14 +05:30
genisysram
fd50e61037
added code to run on power system 2020-11-20 21:45:23 +05:30
genisysram
b69c482dec
Update .travis.yml 2020-11-09 10:39:31 +05:30
Brandon Taylor
757deb302d
Merge pull request #233 from alsoicode/develop
Develop
2020-07-24 10:11:41 -04:00
Brandon Taylor
58e03a5ebf
Merge pull request #232 from blag/better-docs
Better docs
2020-07-24 10:11:12 -04:00
blag
5e9e0b3564
Add section on many-to-many relations 2020-07-22 01:55:34 -07:00
blag
7ed44fa84b
Add section title to sortable model with non-sortable parent 2020-07-22 01:55:34 -07:00
blag
616dcdfd7a
Remove incorrect long_description_content_type argument 2020-07-22 01:55:33 -07:00
blag
eb3f18b09e
Use a context manager to read README.rst 2020-07-22 01:55:33 -07:00
blag
3209998569
Add code-block segments to documentation 2020-07-22 01:55:33 -07:00
blag
2e5f7addf4
Update usage docs 2020-07-22 01:55:33 -07:00
Brandon Taylor
795dc26275
Merge pull request #230 from alsoicode/develop
Develop
2020-07-21 20:30:11 -04:00
Brandon Taylor
7180833e6d
Merge pull request #228 from blag/update-gitignore
Add a few more generated things to .gitignore
2020-07-21 20:29:46 -04:00
Brandon Taylor
e90dc03b2c
Merge pull request #229 from blag/fix-saving-with-specified-order-value
Fix saving with specified order value
2020-07-21 20:29:34 -04:00
blag
de6eef8213
Add test for default order field value check 2020-05-26 17:35:19 -07:00
blag
45b54a3402
Don't apply a default value if the value is already specified 2020-05-26 17:27:50 -07:00
blag
3082950549
Add a few more generated things to .gitignore 2020-05-26 15:10:48 -07:00
Brandon Taylor
7deb73c806
Merge pull request #227 from blag/develop
Fixups and improvements
2020-05-26 16:30:18 -04:00
blag
01591305a3
Update tox with updated Django and Python versions (except for Django 2.2 and Python 3.5) 2020-05-26 13:16:30 -07:00
blag
3443c300d0
Remove Python 3.6+ only code 2020-05-26 12:54:28 -07:00
blag
9e352ec474
Add Django 2.2 to the build matrix 2020-05-26 12:22:30 -07:00
blag
387e9b8f2f
Relax Django version to 3.0 (to use the latest 3.0.x) 2020-05-26 12:22:09 -07:00
blag
f93cac291b
Remove Python 3.4 from the build matrix 2020-05-26 12:20:18 -07:00
blag
21cab41381
Use Python 3's urllib.parse.urlencode instead of django.utils.six 2020-05-26 12:15:12 -07:00
blag
899f92f53a
Return an HTTP 400 if the queryset size has changed since the page was loaded 2020-05-25 18:45:10 -07:00
blag
1b700e0b25
Use Python 3.6's ordered dictionaries 2020-05-25 18:45:10 -07:00
blag
a6fb5d0d36
Lock filtered objects before updating them in bulk 2020-05-25 18:45:10 -07:00
blag
00eb3fdb30
Pass the queryset filters from the request into the object_rep.html template 2020-05-25 18:45:09 -07:00
blag
247078c7e0
Tweak CSS to restrict elements that have cursor: move 2020-05-25 18:44:32 -07:00
blag
2bb6a677fe
Check that the parent model is a SortableMixin before enabling sorting them 2020-05-19 18:37:55 -07:00
Brandon Taylor
3263eb0e34
Merge pull request #225 from alsoicode/develop
Develop
2020-03-01 12:27:04 -05:00
Brandon Taylor
d81790a387 Removed all Python 2.x testing 2020-03-01 12:25:20 -05:00
Brandon Taylor
493db1e75a Downgraded Django to latest available in Python 2.7 2020-03-01 12:23:22 -05:00
Brandon Taylor
b545106cbd Excluded Django 3 from environments lower than Python 3.6 2020-03-01 12:16:45 -05:00
Brandon Taylor
77eedcc8b6 Updated 2.x version of Django to latest in series available to Travis 2020-03-01 12:08:10 -05:00
Brandon Taylor
e2bb925a99 Updated Travis config to exclude outdated versions of Django 2020-03-01 12:05:01 -05:00
Brandon Taylor
b286fcc96e Updated links to static files documentation. 2020-02-29 20:36:21 -05:00
Brandon Taylor
db162bf890 Fix inline admin templates to display FontAwesome icons and be Django 2 & 3 compatible.
Version bump to 2.2.3.
Updated READMEs.
2020-01-14 20:05:25 -05:00
Brandon Taylor
bee77a28db Version bump to 2.2.2
Updated READMEs
2020-01-14 18:55:54 -05:00
Brandon Taylor
e0cdbba528
Merge pull request #223 from alsoicode/bugfix/font-awesome-icons-for-inlines
FontAwesome Icons in Inline templates
2020-01-14 18:53:48 -05:00
Brandon Taylor
723ddda6c2 FontAwesome Icons in Inline templates
- Fixed VERSION check to enable correct templates if VERSION > 2
2020-01-14 18:53:07 -05:00
Brandon Taylor
257e22fb8a Version bump to 2.2.1
Updated READMEs
2020-01-14 18:31:00 -05:00
Brandon Taylor
f1ea519c90
Merge pull request #222 from alsoicode/bugfix/sortable-inline-tabular-selector
Bugfix/sortable inline tabular selector
2020-01-14 18:28:44 -05:00
Brandon Taylor
8311881f2f Fixed overzealous selector for sortable tabular inlines. 2020-01-14 18:26:49 -05:00
Brandon Taylor
4054d51b17 Dropped Django 1.6.7 from Travis 2019-12-28 09:16:25 -05:00
Brandon Taylor
c9f486c955 Updated Travis to include Python 3.6 and Django 3 2019-12-28 08:45:49 -05:00
Brandon Taylor
99bf2fb5f2 Version bump to 2.2.0
Updated README.
Updated models to be compliant with Django 3.
2019-12-07 17:36:54 -05:00
Brandon Taylor
45478baa7b
Merge pull request #218 from cjtapper/master
Remove deprecated template tag libraries.
2019-12-07 17:22:30 -05:00
Chris Tapper
01fc6a18b6 Remove deprecated template tag libraries.
`{% load staticfiles %}` and `{% load adminstatic %}` were
[deprecated in Django 2.1](https://docs.djangoproject.com/en/2.2/releases/2.1/#features-deprecated-in-2-1),
and [removed in Django 3.0](https://docs.djangoproject.com/en/dev/releases/3.0/#features-removed-in-3-0).

Instead, `{% load static %}` should be used.
2019-10-27 18:51:55 +01:00
Brandon Taylor
5cb0f1ebf8 Fix Font Awesome Icon selection scoping
- Narrowed scoping of selector so it doesn't interfere with custom widgets after drop
- Version bump to 2.0.18
- Updated README
2019-08-28 07:53:50 -04:00
Brandon Taylor
d22d4d174c Version bump to 2.1.17.
Updated README
2019-07-20 11:36:54 -04:00
Brandon Taylor
71bd1be135
Merge pull request #214 from SerhiyRomanov/ukrainian_translation
Fixed empty django.mo file for Ukrainian translation (sorry for this)
2019-07-17 16:44:48 -04:00
Serhiy Romanov
9def367234 Fixed empty django.mo file for Ukrainian translation (sorry for this) 2019-07-17 22:45:14 +03:00
Brandon Taylor
e069cc39c0 Version bump to 2.1.6
Updated readme with credit for Ukrainian translations.
2019-06-22 20:42:35 -04:00
Brandon Taylor
4b96c03f01
Merge pull request #212 from SerhiyRomanov/ukrainian_translation
Add Ukrainian translation
2019-06-22 20:38:26 -04:00
Serhiy Romanov
f839d4d9fa Add Ukrainian translation 2019-06-22 00:04:10 +03:00
Brandon Taylor
14436afb6f Meged changes
Updated readme
Proper version bump to 2.1.15
2019-06-03 14:29:17 -04:00
Brandon Taylor
8fc4956adc Removed old .rst for README.
Updated README.md
Version bump to 2.1.11
Updated setup.py to support markdown for long description
2019-06-03 14:26:01 -04:00
Brandon Taylor
bfb1969e50 Removed unused imports 2019-06-03 14:13:21 -04:00
Brandon Taylor
97003cd5cc Added matching for PAGE_VAR 2019-04-17 11:24:43 -04:00
Brandon Taylor
debe9db327 Added matching for PAGE_VAR 2019-04-17 11:22:21 -04:00
Brandon Taylor
eff9872799 Version bump to 2.1.14
Updated readme
2019-02-23 07:59:00 -05:00
Brandon Taylor
08ee36ee3e
Merge pull request #209 from stephrdev/improve-querystring-filter
Improve querystring filtering.
2019-02-22 07:55:56 -05:00
Stephan Jaekel
cdce5e453b Improve querystring filtering.
This changed excludes more querystring parameters and relies on Django's
knowledge of what keys should be excluded. It basically does the same
what the ChangeList class would do [1].

[1] https://github.com/django/django/blob/master/django/contrib/admin/views/main.py#L91-L102
2019-02-22 11:14:07 +01:00
Brandon Taylor
e2bee04990 Fix sorting for raw_id objects
- Added common function to get querystring filters
- Excluded querystring parameters used for raw_id fields
- Version bump to 2.1.13
- Updated readme
2019-02-21 09:56:08 -05:00
Brandon Taylor
9b143ca58e Applied migrations 2019-02-15 09:08:39 -05:00
29 changed files with 424 additions and 325 deletions

52
.github/workflows/tests.yml vendored Normal file
View 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

5
.gitignore vendored
View file

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

View file

@ -1,39 +0,0 @@
language: python
python:
- "2.7"
- "3.4"
- "3.5"
env:
- DJANGO_VERSION=1.6.7 SAMPLE_PROJECT=sample_project
- 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
branches:
only:
- develop
matrix:
exclude:
-
python: "3.4"
env: DJANGO_VERSION=1.6.7 SAMPLE_PROJECT=sample_project
-
python: "3.5"
env: DJANGO_VERSION=1.6.7 SAMPLE_PROJECT=sample_project
-
python: "3.5"
env: DJANGO_VERSION=1.7.7 SAMPLE_PROJECT=sample_project
install:
- pip install django==$DJANGO_VERSION
script:
- cd $SAMPLE_PROJECT
- python manage.py test

4
.vscode/settings.json vendored Normal file
View file

@ -0,0 +1,4 @@
{
"python.pythonPath": "/Users/btaylor/virtualenvs/django-admin-sortable/bin/python",
"python.linting.pylintPath": "/Users/btaylor/virtualenvs/django-admin-sortable/bin/pylint"
}

View file

@ -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
[![PyPI version](https://img.shields.io/pypi/v/django-admin-sortable.svg)](https://pypi.python.org/pypi/django-admin-sortable)
@ -19,9 +22,14 @@ Sorting inlines:
![sortable-inlines](http://res.cloudinary.com/alsoicode/image/upload/v1451237555/django-admin-sortable/sortable-inlines.jpg)
## Supported Django Versions
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.
For Django 1.5.x to 1.7.x, use version 2.0.18.
For Django 1.8.x or higher, use the latest version.
### Other notes of interest regarding versions
django-admin-sortable 1.5.2 introduced backward-incompatible changes for Django 1.4.x
@ -55,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
@ -79,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:
@ -152,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
@ -195,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.
@ -252,7 +316,7 @@ You may also pass in additional ORM "filer_args" as a list, or "filter_kwargs" a
```
#### Deprecation Warning
Previously "filter_kwargs" was named "extra_filters". With the addition of "filter_args", "extra_filters" was renamed for consistency. "extra_filters" will be removed in the next version of django-admin-sortable.
Previously "filter_kwargs" was named "extra_filters". With the addition of "filter_args", "extra_filters" was renamed for consistency.
### Adding Sorting to an existing model
@ -380,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.
@ -606,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.1.12?
- Fixed multiple list filter issue that was causing incorrect sortable objects to be displayed
### 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.

View file

@ -1,9 +1,7 @@
Django Admin Sortable
=====================
`PyPI version <https://pypi.python.org/pypi/django-admin-sortable>`__
`Python versions <https://pypi.python.org/pypi/django-admin-sortable>`__
`Build Status <https://travis-ci.org/alsoicode/django-admin-sortable>`__
|PyPI version| |Python versions| |Build Status|
This project makes it easy to add drag-and-drop ordering to any model in
Django admin. Inlines for a sortable model may also be made sortable,
@ -29,9 +27,13 @@ Sorting inlines:
Supported Django Versions
-------------------------
For Django 1.5.x to 1.7.x, use version 2.0.18.
For Django 4 use the latest version
For Django 1.8.x or higher, 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.5.x to 1.7.x, use version 2.0.18.
Other notes of interest regarding versions
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@ -58,6 +60,7 @@ Download django-admin-sortable from
`source <https://github.com/iambrandontaylor/django-admin-sortable/archive/master.zip>`__
1. Unzip the directory and cd into the uncompressed project directory
2.
- Optional: Enable your virtualenv
@ -83,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.
@ -112,17 +115,24 @@ and at minimum, define:
- The field which should be used for ``Meta.ordering``, which must
resolve to one of the integer fields defined in Djangos ORM:
- ``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.
- 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.
@ -320,8 +330,6 @@ Deprecation Warning
Previously “filter_kwargs” was named “extra_filters”. With the addition
of “filter_args”, “extra_filters” was renamed for consistency.
“extra_filters” will be removed in the next version of
django-admin-sortable.
Adding Sorting to an existing model
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@ -745,10 +753,10 @@ Status
django-admin-sortable is currently used in production.
Whats new in 2.1.12?
~~~~~~~~~~~~~~~~~~~~~
Whats new in 2.3.0?
~~~~~~~~~~~~~~~~~~~~
- Fixed multiple list filter issue that was causing incorrect sortable objects to be displayed
- Django 4 compatibility
Future
~~~~~~
@ -761,3 +769,10 @@ License
~~~~~~~
django-admin-sortable is released under the Apache Public License v2.
.. |PyPI version| image:: https://img.shields.io/pypi/v/django-admin-sortable.svg
:target: https://pypi.python.org/pypi/django-admin-sortable
.. |Python versions| image:: https://img.shields.io/pypi/pyversions/django-admin-sortable.svg
:target: https://pypi.python.org/pypi/django-admin-sortable
.. |Build Status| image:: https://travis-ci.org/alsoicode/django-admin-sortable.svg?branch=master
:target: https://travis-ci.org/alsoicode/django-admin-sortable

View file

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

View file

@ -1,19 +1,21 @@
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
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, Http404
from django.shortcuts import render, get_object_or_404
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
@ -34,6 +36,15 @@ class SortableAdminBase(object):
after_sorting_js_callback_name = None
def get_querystring_filters(self, request):
filters = {}
for k, v in request.GET.items():
if k not in IGNORED_PARAMS and k != PAGE_VAR:
filters[k] = v
return filters
def changelist_view(self, request, extra_context=None):
"""
If the model that inherits Sortable has more than one object,
@ -42,10 +53,7 @@ class SortableAdminBase(object):
"""
# apply any filters via the querystring
filters = {}
for k, v in request.GET.items():
filters.update({ k: v })
filters = self.get_querystring_filters(request)
# Check if the filtered queryset contains more than 1 item
# to enable sort link
@ -106,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)
@ -131,7 +139,8 @@ class SortableAdmin(SortableAdminBase, ModelAdmin):
# get sort group index from querystring if present
sort_filter_index = request.GET.get('sort_filter')
filters = {}
# apply any filters via the querystring
filters = self.get_querystring_filters(request)
if sort_filter_index:
try:
@ -139,10 +148,6 @@ class SortableAdmin(SortableAdminBase, ModelAdmin):
except (IndexError, ValueError):
pass
# apply any filters via the querystring
for k, v in request.GET.items():
filters.update({ k: v })
# Apply any sort filters to create a subset of sortable objects
return self.get_queryset(request).filter(**filters)
@ -156,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.
@ -172,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
@ -225,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(
@ -240,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'),
@ -285,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):
@ -354,31 +360,19 @@ class SortableInlineBase(SortableAdminBase, InlineModelAdmin):
class SortableTabularInline(TabularInline, SortableInlineBase):
"""Custom template that enables sorting for tabular inlines"""
if VERSION >= (1, 10):
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 >= (1, 10):
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 >= (1, 10):
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 >= (1, 10):
template = 'adminsortable/edit_inline/stacked-1.10.x.html'
else:
template = 'adminsortable/edit_inline/stacked.html'
template = 'adminsortable/edit_inline/stacked.html'

Binary file not shown.

View file

@ -0,0 +1,110 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2019-06-21 23:37+0300\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"Language: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=4; plural=(n % 1 == 0 && n % 10 == 1 && n % 100 != "
"11 ? 0 : n % 1 == 0 && n % 10 >= 2 && n % 10 <= 4 && (n % 100 < 12 || n % "
"100 > 14) ? 1 : n % 1 == 0 && (n % 10 ==0 || (n % 10 >=5 && n % 10 <=9) || "
"(n % 100 >=11 && n % 100 <=14 )) ? 2: 3);\n"
#: templates/adminsortable/change_form.html:33
msgid ""
"There are unsaved changes on this page. Please save your changes before "
"reordering."
msgstr "На цій сторінці є незбережені зміни. Будь ласка, збережіть ваші зміни перед зміною порядку."
#: templates/adminsortable/change_list.html:53
#, python-format
msgid "Drag and drop %(model)s to change display order"
msgstr "Перетягніть %(model)s, щоб змінити порядок відображення"
#: templates/adminsortable/change_list.html:53
msgid "Django site admin"
msgstr "Django адміністрування"
#: templates/adminsortable/change_list.html:59
msgid "Home"
msgstr "Домівка"
#: templates/adminsortable/change_list.html:70
msgid "Reorder"
msgstr "Зміна порядку"
#: templates/adminsortable/change_list.html:78
#, python-format
msgid "Drag and drop %(sort_type)s %(model)s to change their order."
msgstr "Перетягніть %(sort_type)s %(model)s, щоб змінити їх порядок"
#: templates/adminsortable/change_list.html:80
#, python-format
msgid "Drag and drop %(model)s to change their order."
msgstr "Перетягніть %(model)s, щоб змінити їх порядок"
#: templates/adminsortable/change_list.html:85
#, python-format
msgid ""
"You may also drag and drop %(sortable_by_class_display_name)s to change "
"their order."
msgstr "Ви також можете перетягнути %(sortable_by_class_display_name)s, щоб змінити їх порядок"
#: templates/adminsortable/change_list.html:98
#, python-format
msgid "Return to %(model)s"
msgstr "Повернутись до %(model)s"
#: templates/adminsortable/change_list_with_sort_link.html:24
msgid "Change Order of"
msgstr "Змінити порядок"
#: templates/adminsortable/change_list_with_sort_link.html:29
msgid "Change Order"
msgstr "Змінити порядок"
#: templates/adminsortable/edit_inline/stacked-1.10.x.html:12
#: templates/adminsortable/edit_inline/stacked.html:15
#: templates/adminsortable/edit_inline/tabular-1.10.x.html:34
#: templates/adminsortable/edit_inline/tabular.html:35
msgid "Change"
msgstr "Змінити"
#: templates/adminsortable/edit_inline/stacked-1.10.x.html:14
#: templates/adminsortable/edit_inline/stacked.html:17
#: templates/adminsortable/edit_inline/tabular-1.10.x.html:36
#: templates/adminsortable/edit_inline/tabular.html:37
msgid "View on site"
msgstr "Дивитися на сайті"
#: templates/adminsortable/edit_inline/stacked.html:4
#: templates/adminsortable/edit_inline/tabular.html:7
msgid "drag and drop to change order"
msgstr "перетягніть, щоб змінити порядок"
#: templates/adminsortable/edit_inline/stacked.html:37
#: templates/adminsortable/edit_inline/tabular.html:86
msgid "Remove"
msgstr "Видалити"
#: templates/adminsortable/edit_inline/stacked.html:38
#: templates/adminsortable/edit_inline/tabular.html:85
#, python-format
msgid "Add another %(verbose_name)s"
msgstr "Додати ще %(verbose_name)s"
#: templates/adminsortable/edit_inline/tabular-1.10.x.html:20
#: templates/adminsortable/edit_inline/tabular.html:18
msgid "Delete?"
msgstr "Видалити?"

View file

@ -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

View file

@ -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;
}

View file

@ -1,6 +1,6 @@
{% extends change_form_template_extends %}
{% load i18n admin_modify %}
{% load static from staticfiles %}
{% load static %}
{% block extrahead %}
{{ block.super }}

View file

@ -10,7 +10,7 @@
tabular_inline_rows.addClass('sortable');
sortable_inline_group.find('.tabular.inline-related').sortable({
sortable_inline_group.find('.tabular.inline-related tbody').sortable({
axis : 'y',
containment : 'parent',
create: function(event, ui) {
@ -40,7 +40,7 @@
data: { indexes : indexes.join(','), csrfmiddlewaretoken: window.csrftoken },
success: function() {
// set icons based on position
var icons = ui.item.parent().find('.fa');
var icons = ui.item.parent().find('a > .fa');
icons.removeClass('fa-sort-desc fa-sort-asc fa-sort');
icons.each(function(index, element) {
var icon = $(element);

View file

@ -1,28 +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><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,41 +1,34 @@
{% load i18n admin_urls admin_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>&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="{% 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>&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 %}
{% 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>

View file

@ -1,78 +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 %}&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>
{% 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,17 +1,19 @@
{% load i18n admin_urls admin_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 %}&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{% 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 %}
@ -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>

View file

@ -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>

View file

@ -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

View file

@ -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

View file

@ -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'

View file

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

View file

@ -52,6 +52,12 @@ class WidgetAdmin(SortableAdmin):
admin.site.register(Widget, WidgetAdmin)
class CreditAdmin(SortableAdmin):
raw_id_fields = ('project',)
admin.site.register(Credit, CreditAdmin)
class CreditInline(SortableTabularInline):
model = Credit
extra = 1

View file

@ -3,13 +3,11 @@ import uuid
from django.contrib.contenttypes.fields import GenericForeignKey
from django.contrib.contenttypes.models import ContentType
from django.db import models
from django.utils.encoding import python_2_unicode_compatible
from adminsortable.fields import SortableForeignKey
from adminsortable.models import Sortable, SortableMixin
@python_2_unicode_compatible
class SimpleModel(models.Model):
class Meta:
abstract = True
@ -30,7 +28,6 @@ class Category(SimpleModel, SortableMixin):
# A model with an override of its queryset for admin
@python_2_unicode_compatible
class Widget(SimpleModel, SortableMixin):
class Meta:
ordering = ['order']
@ -60,7 +57,6 @@ class Project(SimpleModel, SortableMixin):
# Registered as a tabular inline on `Project`
@python_2_unicode_compatible
class Credit(SortableMixin):
class Meta:
ordering = ['order']
@ -76,7 +72,6 @@ class Credit(SortableMixin):
# Registered as a stacked inline on `Project`
@python_2_unicode_compatible
class Note(SortableMixin):
class Meta:
ordering = ['order']
@ -91,7 +86,6 @@ class Note(SortableMixin):
# Registered as a tabular inline on `Project` which can't be sorted
@python_2_unicode_compatible
class NonSortableCredit(models.Model):
project = models.ForeignKey(Project, on_delete=models.CASCADE)
first_name = models.CharField(max_length=30, help_text="Given name")
@ -102,7 +96,6 @@ class NonSortableCredit(models.Model):
# Registered as a stacked inline on `Project` which can't be sorted
@python_2_unicode_compatible
class NonSortableNote(models.Model):
project = models.ForeignKey(Project, on_delete=models.CASCADE)
text = models.CharField(max_length=100)
@ -112,7 +105,6 @@ class NonSortableNote(models.Model):
# A generic bound model
@python_2_unicode_compatible
class GenericNote(SimpleModel, SortableMixin):
content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE,
verbose_name=u"Content type", related_name="generic_notes")
@ -130,7 +122,6 @@ class GenericNote(SimpleModel, SortableMixin):
# An model registered as an inline that has a custom queryset
@python_2_unicode_compatible
class Component(SimpleModel, SortableMixin):
class Meta:
ordering = ['order']
@ -143,7 +134,6 @@ class Component(SimpleModel, SortableMixin):
return self.title
@python_2_unicode_compatible
class Person(SortableMixin):
class Meta:
ordering = ['order']
@ -168,7 +158,6 @@ class Person(SortableMixin):
return '{0} {1}'.format(self.first_name, self.last_name)
@python_2_unicode_compatible
class NonSortableCategory(SimpleModel):
class Meta(SimpleModel.Meta):
verbose_name = 'Non-Sortable Category'
@ -178,7 +167,6 @@ class NonSortableCategory(SimpleModel):
return self.title
@python_2_unicode_compatible
class SortableCategoryWidget(SimpleModel, SortableMixin):
class Meta:
ordering = ['order']
@ -194,7 +182,6 @@ class SortableCategoryWidget(SimpleModel, SortableMixin):
return self.title
@python_2_unicode_compatible
class SortableNonInlineCategory(SimpleModel, SortableMixin):
"""Example of a model that is sortable, but has a SortableForeignKey
that is *not* sortable, and is also not defined as an inline of the
@ -214,7 +201,6 @@ class SortableNonInlineCategory(SimpleModel, SortableMixin):
return self.title
@python_2_unicode_compatible
class CustomWidget(SortableMixin, SimpleModel):
# custom field for ordering
@ -230,7 +216,6 @@ class CustomWidget(SortableMixin, SimpleModel):
return self.title
@python_2_unicode_compatible
class CustomWidgetComponent(SortableMixin, SimpleModel):
custom_widget = models.ForeignKey(CustomWidget, on_delete=models.CASCADE)
@ -248,7 +233,6 @@ class CustomWidgetComponent(SortableMixin, SimpleModel):
return self.title
@python_2_unicode_compatible
class BackwardCompatibleWidget(Sortable, SimpleModel):
class Meta(Sortable.Meta):
@ -259,7 +243,6 @@ class BackwardCompatibleWidget(Sortable, SimpleModel):
return self.title
@python_2_unicode_compatible
class TestNonAutoFieldModel(SortableMixin):
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
order = models.PositiveIntegerField(editable=False, db_index=True)

View file

@ -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.')

View file

@ -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.',

12
tox.ini
View file

@ -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