Fixed #11868 - Multiple sort in admin changelist.
Many thanks to bendavis78 for the initial patch, and for input from others. Also fixed #7309. If people were relying on the undocumented default ordering applied by the admin before, they will need to add 'ordering = ["-pk"]' to their ModelAdmin. git-svn-id: http://code.djangoproject.com/svn/django/trunk@16316 bcc190cf-cafb-0310-a4f2-bffc1f526a37
This commit is contained in:
parent
78b37975c9
commit
5434ce231d
|
@ -326,6 +326,34 @@ table thead th.descending a {
|
||||||
background: url(../img/admin/arrow-down.gif) right .4em no-repeat;
|
background: url(../img/admin/arrow-down.gif) right .4em no-repeat;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
table thead th.sorted a span.text {
|
||||||
|
display: block;
|
||||||
|
float: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
table thead th.sorted a span.sortpos {
|
||||||
|
display: block;
|
||||||
|
float: right;
|
||||||
|
font-size: .6em;
|
||||||
|
}
|
||||||
|
|
||||||
|
table thead th.sorted a img {
|
||||||
|
vertical-align: bottom;
|
||||||
|
}
|
||||||
|
|
||||||
|
table thead th.sorted a span.clear {
|
||||||
|
display: block;
|
||||||
|
clear: both;
|
||||||
|
}
|
||||||
|
|
||||||
|
#sorting-popup-div {
|
||||||
|
position: absolute;
|
||||||
|
background-color: white;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
z-index: 2000; /* more than filters on right */
|
||||||
|
padding-right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
/* ORDERABLE TABLES */
|
/* ORDERABLE TABLES */
|
||||||
|
|
||||||
table.orderable tbody tr td:hover {
|
table.orderable tbody tr td:hover {
|
||||||
|
|
|
@ -91,6 +91,14 @@ table thead th.descending a {
|
||||||
background-position: left;
|
background-position: left;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
table thead th.sorted a span.text {
|
||||||
|
float: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
table thead th.sorted a span.sortpos {
|
||||||
|
float: left;
|
||||||
|
}
|
||||||
|
|
||||||
/* dashboard styles */
|
/* dashboard styles */
|
||||||
|
|
||||||
.dashboard .module table td a {
|
.dashboard .module table td a {
|
||||||
|
|
Binary file not shown.
After Width: | Height: | Size: 402 B |
|
@ -1,3 +1,5 @@
|
||||||
|
{% load adminmedia %}
|
||||||
|
{% load i18n %}
|
||||||
{% if result_hidden_fields %}
|
{% if result_hidden_fields %}
|
||||||
<div class="hiddenfields">{# DIV for HTML validation #}
|
<div class="hiddenfields">{# DIV for HTML validation #}
|
||||||
{% for item in result_hidden_fields %}{{ item }}{% endfor %}
|
{% for item in result_hidden_fields %}{{ item }}{% endfor %}
|
||||||
|
@ -8,10 +10,18 @@
|
||||||
<table id="result_list">
|
<table id="result_list">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
{% for header in result_headers %}<th scope="col"{{ header.class_attrib }}>
|
{% for header in result_headers %}
|
||||||
{% if header.sortable %}<a href="{{ header.url }}">{% endif %}
|
<th scope="col" {{ header.class_attrib }}>
|
||||||
{{ header.text|capfirst }}
|
{% if header.sortable %}<a href="{{ header.url }}">{% endif %}
|
||||||
{% if header.sortable %}</a>{% endif %}</th>{% endfor %}
|
<span class="text">{{ header.text|capfirst }}</span>
|
||||||
|
{% if header.sortable %}
|
||||||
|
{% if header.sort_pos > 0 %}<span class="sortpos">
|
||||||
|
{% if header.sort_pos == 1 %}<img id="primary-sort-icon" src="{% admin_media_prefix %}img/admin/icon_cog.gif" alt="" /> {% endif %}
|
||||||
|
{{ header.sort_pos }}</span>
|
||||||
|
{% endif %}
|
||||||
|
<span class="clear"></span></a>
|
||||||
|
{% endif %}
|
||||||
|
</th>{% endfor %}
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
|
@ -24,4 +34,53 @@
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{# Sorting popup: #}
|
||||||
|
<div style="display: none;" id="sorting-popup-div">
|
||||||
|
<p>{% trans "Sorting by:" %}</p>
|
||||||
|
<ol>
|
||||||
|
{% for header in result_headers|dictsort:"sort_pos" %}
|
||||||
|
{% if header.sort_pos > 0 %}
|
||||||
|
{% if header.ascending %}
|
||||||
|
<li>{% blocktrans with fieldname=header.text %}{{ fieldname }} (ascending){% endblocktrans %}</li>
|
||||||
|
{% else %}
|
||||||
|
<li>{% blocktrans with fieldname=header.text %}{{ fieldname }} (descending){% endblocktrans %}</li>
|
||||||
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
</ol>
|
||||||
|
<p><a href="{{ reset_sorting_url }}">{% trans "Reset sorting" %}</a></p>
|
||||||
|
</div>
|
||||||
|
<script type="text/javascript">
|
||||||
|
<!--
|
||||||
|
(function($) {
|
||||||
|
$(document).ready(function() {
|
||||||
|
var popup = $('#sorting-popup-div');
|
||||||
|
/* These next lines seems necessary to prime the popup: */
|
||||||
|
popup.offset({left:-1000, top:0});
|
||||||
|
popup.show();
|
||||||
|
var popupWidth = popup.width();
|
||||||
|
popup.hide();
|
||||||
|
|
||||||
|
$('#primary-sort-icon').toggle(function(ev) {
|
||||||
|
ev.preventDefault();
|
||||||
|
var img = $(this);
|
||||||
|
var pos = img.offset();
|
||||||
|
pos.top += img.height();
|
||||||
|
if (pos.left + popupWidth >
|
||||||
|
$(window).width()) {
|
||||||
|
pos.left -= popupWidth;
|
||||||
|
}
|
||||||
|
popup.show();
|
||||||
|
popup.offset(pos);
|
||||||
|
},
|
||||||
|
function(ev) {
|
||||||
|
ev.preventDefault();
|
||||||
|
popup.hide();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
})(django.jQuery);
|
||||||
|
//-->
|
||||||
|
</script>
|
||||||
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
|
@ -7,6 +7,7 @@ from django.contrib.admin.views.main import (ALL_VAR, EMPTY_CHANGELIST_VALUE,
|
||||||
from django.core.exceptions import ObjectDoesNotExist
|
from django.core.exceptions import ObjectDoesNotExist
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.utils import formats
|
from django.utils import formats
|
||||||
|
from django.utils.datastructures import SortedDict
|
||||||
from django.utils.html import escape, conditional_escape
|
from django.utils.html import escape, conditional_escape
|
||||||
from django.utils.safestring import mark_safe
|
from django.utils.safestring import mark_safe
|
||||||
from django.utils.text import capfirst
|
from django.utils.text import capfirst
|
||||||
|
@ -81,43 +82,90 @@ def result_headers(cl):
|
||||||
"""
|
"""
|
||||||
Generates the list column headers.
|
Generates the list column headers.
|
||||||
"""
|
"""
|
||||||
lookup_opts = cl.lookup_opts
|
# We need to know the 'ordering field' that corresponds to each
|
||||||
|
# item in list_display, and we need other info, so do a pre-pass
|
||||||
|
# on list_display
|
||||||
|
list_display_info = SortedDict()
|
||||||
for i, field_name in enumerate(cl.list_display):
|
for i, field_name in enumerate(cl.list_display):
|
||||||
header, attr = label_for_field(field_name, cl.model,
|
admin_order_field = None
|
||||||
|
text, attr = label_for_field(field_name, cl.model,
|
||||||
model_admin = cl.model_admin,
|
model_admin = cl.model_admin,
|
||||||
return_attr = True
|
return_attr = True
|
||||||
)
|
)
|
||||||
if attr:
|
if attr:
|
||||||
|
admin_order_field = getattr(attr, "admin_order_field", None)
|
||||||
|
if admin_order_field is None:
|
||||||
|
ordering_field_name = field_name
|
||||||
|
else:
|
||||||
|
ordering_field_name = admin_order_field
|
||||||
|
list_display_info[ordering_field_name] = dict(text=text,
|
||||||
|
attr=attr,
|
||||||
|
index=i,
|
||||||
|
admin_order_field=admin_order_field,
|
||||||
|
field_name=field_name)
|
||||||
|
|
||||||
|
del admin_order_field, text, attr
|
||||||
|
|
||||||
|
ordering_fields = cl.get_ordering_fields()
|
||||||
|
|
||||||
|
for ordering_field_name, info in list_display_info.items():
|
||||||
|
if info['attr']:
|
||||||
|
# Potentially not sortable
|
||||||
|
|
||||||
# if the field is the action checkbox: no sorting and special class
|
# if the field is the action checkbox: no sorting and special class
|
||||||
if field_name == 'action_checkbox':
|
if info['field_name'] == 'action_checkbox':
|
||||||
yield {
|
yield {
|
||||||
"text": header,
|
"text": info['text'],
|
||||||
"class_attrib": mark_safe(' class="action-checkbox-column"')
|
"class_attrib": mark_safe(' class="action-checkbox-column"')
|
||||||
}
|
}
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# It is a non-field, but perhaps one that is sortable
|
if not info['admin_order_field']:
|
||||||
admin_order_field = getattr(attr, "admin_order_field", None)
|
# Not sortable
|
||||||
if not admin_order_field:
|
yield {"text": info['text']}
|
||||||
yield {"text": header}
|
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# So this _is_ a sortable non-field. Go to the yield
|
# OK, it is sortable if we got this far
|
||||||
# after the else clause.
|
|
||||||
else:
|
|
||||||
admin_order_field = None
|
|
||||||
|
|
||||||
th_classes = []
|
th_classes = []
|
||||||
|
order_type = ''
|
||||||
new_order_type = 'asc'
|
new_order_type = 'asc'
|
||||||
if field_name == cl.order_field or admin_order_field == cl.order_field:
|
sort_pos = 0
|
||||||
th_classes.append('sorted %sending' % cl.order_type.lower())
|
# Is it currently being sorted on?
|
||||||
new_order_type = {'asc': 'desc', 'desc': 'asc'}[cl.order_type.lower()]
|
if ordering_field_name in ordering_fields:
|
||||||
|
order_type = ordering_fields.get(ordering_field_name).lower()
|
||||||
|
sort_pos = ordering_fields.keys().index(ordering_field_name) + 1
|
||||||
|
th_classes.append('sorted %sending' % order_type)
|
||||||
|
new_order_type = {'asc': 'desc', 'desc': 'asc'}[order_type]
|
||||||
|
|
||||||
|
# build new ordering param
|
||||||
|
o_list = []
|
||||||
|
make_qs_param = lambda t, n: ('-' if t == 'desc' else '') + str(n)
|
||||||
|
|
||||||
|
for f, ot in ordering_fields.items():
|
||||||
|
try:
|
||||||
|
colnum = list_display_info[f]['index']
|
||||||
|
except KeyError:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if f == ordering_field_name:
|
||||||
|
# We want clicking on this header to bring the ordering to the
|
||||||
|
# front
|
||||||
|
o_list.insert(0, make_qs_param(new_order_type, colnum))
|
||||||
|
else:
|
||||||
|
o_list.append(make_qs_param(ot, colnum))
|
||||||
|
|
||||||
|
if ordering_field_name not in ordering_fields:
|
||||||
|
colnum = list_display_info[ordering_field_name]['index']
|
||||||
|
o_list.insert(0, make_qs_param(new_order_type, colnum))
|
||||||
|
|
||||||
|
o_list = '.'.join(o_list)
|
||||||
|
|
||||||
yield {
|
yield {
|
||||||
"text": header,
|
"text": info['text'],
|
||||||
"sortable": True,
|
"sortable": True,
|
||||||
"url": cl.get_query_string({ORDER_VAR: i, ORDER_TYPE_VAR: new_order_type}),
|
"ascending": order_type == "asc",
|
||||||
|
"sort_pos": sort_pos,
|
||||||
|
"url": cl.get_query_string({ORDER_VAR: o_list}),
|
||||||
"class_attrib": mark_safe(th_classes and ' class="%s"' % ' '.join(th_classes) or '')
|
"class_attrib": mark_safe(th_classes and ' class="%s"' % ' '.join(th_classes) or '')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -228,9 +276,14 @@ def result_list(cl):
|
||||||
"""
|
"""
|
||||||
Displays the headers and data list together
|
Displays the headers and data list together
|
||||||
"""
|
"""
|
||||||
|
headers = list(result_headers(cl))
|
||||||
|
for h in headers:
|
||||||
|
# Sorting in templates depends on sort_pos attribute
|
||||||
|
h.setdefault('sort_pos', 0)
|
||||||
return {'cl': cl,
|
return {'cl': cl,
|
||||||
'result_hidden_fields': list(result_hidden_fields(cl)),
|
'result_hidden_fields': list(result_hidden_fields(cl)),
|
||||||
'result_headers': list(result_headers(cl)),
|
'result_headers': headers,
|
||||||
|
'reset_sorting_url': cl.get_query_string(remove=[ORDER_VAR]),
|
||||||
'results': list(results(cl))}
|
'results': list(results(cl))}
|
||||||
|
|
||||||
@register.inclusion_tag('admin/date_hierarchy.html')
|
@register.inclusion_tag('admin/date_hierarchy.html')
|
||||||
|
|
|
@ -3,6 +3,7 @@ import operator
|
||||||
from django.core.exceptions import SuspiciousOperation
|
from django.core.exceptions import SuspiciousOperation
|
||||||
from django.core.paginator import InvalidPage
|
from django.core.paginator import InvalidPage
|
||||||
from django.db import models
|
from django.db import models
|
||||||
|
from django.utils.datastructures import SortedDict
|
||||||
from django.utils.encoding import force_unicode, smart_str
|
from django.utils.encoding import force_unicode, smart_str
|
||||||
from django.utils.translation import ugettext, ugettext_lazy
|
from django.utils.translation import ugettext, ugettext_lazy
|
||||||
from django.utils.http import urlencode
|
from django.utils.http import urlencode
|
||||||
|
@ -75,7 +76,7 @@ class ChangeList(object):
|
||||||
self.list_editable = ()
|
self.list_editable = ()
|
||||||
else:
|
else:
|
||||||
self.list_editable = list_editable
|
self.list_editable = list_editable
|
||||||
self.order_field, self.order_type = self.get_ordering()
|
self.ordering = self.get_ordering()
|
||||||
self.query = request.GET.get(SEARCH_VAR, '')
|
self.query = request.GET.get(SEARCH_VAR, '')
|
||||||
self.query_set = self.get_query_set(request)
|
self.query_set = self.get_query_set(request)
|
||||||
self.get_results(request)
|
self.get_results(request)
|
||||||
|
@ -166,40 +167,54 @@ class ChangeList(object):
|
||||||
def get_ordering(self):
|
def get_ordering(self):
|
||||||
lookup_opts, params = self.lookup_opts, self.params
|
lookup_opts, params = self.lookup_opts, self.params
|
||||||
# For ordering, first check the "ordering" parameter in the admin
|
# For ordering, first check the "ordering" parameter in the admin
|
||||||
# options, then check the object's default ordering. If neither of
|
# options, then check the object's default ordering. Finally, a
|
||||||
# those exist, order descending by ID by default. Finally, look for
|
# manually-specified ordering from the query string overrides anything.
|
||||||
# manually-specified ordering from the query string.
|
ordering = []
|
||||||
ordering = self.model_admin.ordering or lookup_opts.ordering or ['-' + lookup_opts.pk.name]
|
if self.model_admin.ordering:
|
||||||
|
ordering = self.model_admin.ordering
|
||||||
|
elif lookup_opts.ordering:
|
||||||
|
ordering = lookup_opts.ordering
|
||||||
|
|
||||||
if ordering[0].startswith('-'):
|
|
||||||
order_field, order_type = ordering[0][1:], 'desc'
|
|
||||||
else:
|
|
||||||
order_field, order_type = ordering[0], 'asc'
|
|
||||||
if ORDER_VAR in params:
|
if ORDER_VAR in params:
|
||||||
try:
|
# Clear ordering and used params
|
||||||
field_name = self.list_display[int(params[ORDER_VAR])]
|
ordering = []
|
||||||
|
order_params = params[ORDER_VAR].split('.')
|
||||||
|
for p in order_params:
|
||||||
try:
|
try:
|
||||||
f = lookup_opts.get_field(field_name)
|
none, pfx, idx = p.rpartition('-')
|
||||||
except models.FieldDoesNotExist:
|
field_name = self.list_display[int(idx)]
|
||||||
# See whether field_name is a name of a non-field
|
|
||||||
# that allows sorting.
|
|
||||||
try:
|
try:
|
||||||
if callable(field_name):
|
f = lookup_opts.get_field(field_name)
|
||||||
attr = field_name
|
except models.FieldDoesNotExist:
|
||||||
elif hasattr(self.model_admin, field_name):
|
# See whether field_name is a name of a non-field
|
||||||
attr = getattr(self.model_admin, field_name)
|
# that allows sorting.
|
||||||
else:
|
try:
|
||||||
attr = getattr(self.model, field_name)
|
if callable(field_name):
|
||||||
order_field = attr.admin_order_field
|
attr = field_name
|
||||||
except AttributeError:
|
elif hasattr(self.model_admin, field_name):
|
||||||
pass
|
attr = getattr(self.model_admin, field_name)
|
||||||
else:
|
else:
|
||||||
order_field = f.name
|
attr = getattr(self.model, field_name)
|
||||||
except (IndexError, ValueError):
|
field_name = attr.admin_order_field
|
||||||
pass # Invalid ordering specified. Just use the default.
|
except AttributeError:
|
||||||
if ORDER_TYPE_VAR in params and params[ORDER_TYPE_VAR] in ('asc', 'desc'):
|
continue # No 'admin_order_field', skip it
|
||||||
order_type = params[ORDER_TYPE_VAR]
|
else:
|
||||||
return order_field, order_type
|
field_name = f.name
|
||||||
|
|
||||||
|
ordering.append(pfx + field_name)
|
||||||
|
|
||||||
|
except (IndexError, ValueError):
|
||||||
|
continue # Invalid ordering specified, skip it.
|
||||||
|
|
||||||
|
return ordering
|
||||||
|
|
||||||
|
def get_ordering_fields(self):
|
||||||
|
# Returns a SortedDict of ordering fields and asc/desc
|
||||||
|
ordering_fields = SortedDict()
|
||||||
|
for o in self.ordering:
|
||||||
|
none, t, f = o.rpartition('-')
|
||||||
|
ordering_fields[f] = 'desc' if t == '-' else 'asc'
|
||||||
|
return ordering_fields
|
||||||
|
|
||||||
def get_lookup_params(self, use_distinct=False):
|
def get_lookup_params(self, use_distinct=False):
|
||||||
lookup_params = self.params.copy() # a dictionary of the query string
|
lookup_params = self.params.copy() # a dictionary of the query string
|
||||||
|
@ -290,8 +305,8 @@ class ChangeList(object):
|
||||||
break
|
break
|
||||||
|
|
||||||
# Set ordering.
|
# Set ordering.
|
||||||
if self.order_field:
|
if self.ordering:
|
||||||
qs = qs.order_by('%s%s' % ((self.order_type == 'desc' and '-' or ''), self.order_field))
|
qs = qs.order_by(*self.ordering)
|
||||||
|
|
||||||
# Apply keyword searches.
|
# Apply keyword searches.
|
||||||
def construct_search(field_name):
|
def construct_search(field_name):
|
||||||
|
|
|
@ -696,10 +696,10 @@ subclass::
|
||||||
If this isn't provided, the Django admin will use the model's default
|
If this isn't provided, the Django admin will use the model's default
|
||||||
ordering.
|
ordering.
|
||||||
|
|
||||||
.. admonition:: Note
|
.. versionchanged:: 1.4
|
||||||
|
|
||||||
Django will only honor the first element in the list/tuple; any others
|
Django honors all elements in the list/tuple; before 1.4, only the first
|
||||||
will be ignored.
|
was respected.
|
||||||
|
|
||||||
.. attribute:: ModelAdmin.paginator
|
.. attribute:: ModelAdmin.paginator
|
||||||
|
|
||||||
|
|
|
@ -46,6 +46,14 @@ not custom filters. This has been rectified with a simple API previously
|
||||||
known as "FilterSpec" which was used internally. For more details, see the
|
known as "FilterSpec" which was used internally. For more details, see the
|
||||||
documentation for :attr:`~django.contrib.admin.ModelAdmin.list_filter`.
|
documentation for :attr:`~django.contrib.admin.ModelAdmin.list_filter`.
|
||||||
|
|
||||||
|
Multiple sort in admin interface
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
The admin change list now supports sorting on multiple columns. It respects all
|
||||||
|
elements of the :attr:`~django.contrib.admin.ModelAdmin.ordering` attribute, and
|
||||||
|
sorting on multiple columns by clicking on headers is designed to work similarly
|
||||||
|
to how desktop GUIs do it.
|
||||||
|
|
||||||
Tools for cryptographic signing
|
Tools for cryptographic signing
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
|
|
@ -67,11 +67,11 @@ class CustomUserAdmin(UserAdmin):
|
||||||
|
|
||||||
class BookAdmin(ModelAdmin):
|
class BookAdmin(ModelAdmin):
|
||||||
list_filter = ('year', 'author', 'contributors', 'is_best_seller', 'date_registered', 'no')
|
list_filter = ('year', 'author', 'contributors', 'is_best_seller', 'date_registered', 'no')
|
||||||
order_by = '-id'
|
ordering = ('-id',)
|
||||||
|
|
||||||
class DecadeFilterBookAdmin(ModelAdmin):
|
class DecadeFilterBookAdmin(ModelAdmin):
|
||||||
list_filter = ('author', DecadeListFilterWithTitleAndParameter)
|
list_filter = ('author', DecadeListFilterWithTitleAndParameter)
|
||||||
order_by = '-id'
|
ordering = ('-id',)
|
||||||
|
|
||||||
class DecadeFilterBookAdminWithoutTitle(ModelAdmin):
|
class DecadeFilterBookAdminWithoutTitle(ModelAdmin):
|
||||||
list_filter = (DecadeListFilterWithoutTitle,)
|
list_filter = (DecadeListFilterWithoutTitle,)
|
||||||
|
|
|
@ -243,9 +243,6 @@ class Person(models.Model):
|
||||||
def __unicode__(self):
|
def __unicode__(self):
|
||||||
return self.name
|
return self.name
|
||||||
|
|
||||||
class Meta:
|
|
||||||
ordering = ["id"]
|
|
||||||
|
|
||||||
class BasePersonModelFormSet(BaseModelFormSet):
|
class BasePersonModelFormSet(BaseModelFormSet):
|
||||||
def clean(self):
|
def clean(self):
|
||||||
for person_dict in self.cleaned_data:
|
for person_dict in self.cleaned_data:
|
||||||
|
@ -259,13 +256,17 @@ class PersonAdmin(admin.ModelAdmin):
|
||||||
list_editable = ('gender', 'alive')
|
list_editable = ('gender', 'alive')
|
||||||
list_filter = ('gender',)
|
list_filter = ('gender',)
|
||||||
search_fields = ('^name',)
|
search_fields = ('^name',)
|
||||||
ordering = ["id"]
|
|
||||||
save_as = True
|
save_as = True
|
||||||
|
|
||||||
def get_changelist_formset(self, request, **kwargs):
|
def get_changelist_formset(self, request, **kwargs):
|
||||||
return super(PersonAdmin, self).get_changelist_formset(request,
|
return super(PersonAdmin, self).get_changelist_formset(request,
|
||||||
formset=BasePersonModelFormSet, **kwargs)
|
formset=BasePersonModelFormSet, **kwargs)
|
||||||
|
|
||||||
|
def queryset(self, request):
|
||||||
|
# Order by a field that isn't in list display, to be able to test
|
||||||
|
# whether ordering is preserved.
|
||||||
|
return super(PersonAdmin, self).queryset(request).order_by('age')
|
||||||
|
|
||||||
|
|
||||||
class Persona(models.Model):
|
class Persona(models.Model):
|
||||||
"""
|
"""
|
||||||
|
@ -357,6 +358,9 @@ class Media(models.Model):
|
||||||
class Podcast(Media):
|
class Podcast(Media):
|
||||||
release_date = models.DateField()
|
release_date = models.DateField()
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
ordering = ('release_date',) # overridden in PodcastAdmin
|
||||||
|
|
||||||
class PodcastAdmin(admin.ModelAdmin):
|
class PodcastAdmin(admin.ModelAdmin):
|
||||||
list_display = ('name', 'release_date')
|
list_display = ('name', 'release_date')
|
||||||
list_editable = ('release_date',)
|
list_editable = ('release_date',)
|
||||||
|
@ -795,6 +799,7 @@ class StoryAdmin(admin.ModelAdmin):
|
||||||
list_display_links = ('title',) # 'id' not in list_display_links
|
list_display_links = ('title',) # 'id' not in list_display_links
|
||||||
list_editable = ('content', )
|
list_editable = ('content', )
|
||||||
form = StoryForm
|
form = StoryForm
|
||||||
|
ordering = ["-pk"]
|
||||||
|
|
||||||
class OtherStory(models.Model):
|
class OtherStory(models.Model):
|
||||||
title = models.CharField(max_length=100)
|
title = models.CharField(max_length=100)
|
||||||
|
@ -804,6 +809,7 @@ class OtherStoryAdmin(admin.ModelAdmin):
|
||||||
list_display = ('id', 'title', 'content')
|
list_display = ('id', 'title', 'content')
|
||||||
list_display_links = ('title', 'id') # 'id' in list_display_links
|
list_display_links = ('title', 'id') # 'id' in list_display_links
|
||||||
list_editable = ('content', )
|
list_editable = ('content', )
|
||||||
|
ordering = ["-pk"]
|
||||||
|
|
||||||
admin.site.register(Article, ArticleAdmin)
|
admin.site.register(Article, ArticleAdmin)
|
||||||
admin.site.register(CustomArticle, CustomArticleAdmin)
|
admin.site.register(CustomArticle, CustomArticleAdmin)
|
||||||
|
|
|
@ -32,7 +32,7 @@ from django.utils import unittest
|
||||||
|
|
||||||
# local test models
|
# local test models
|
||||||
from models import (Article, BarAccount, CustomArticle, EmptyModel,
|
from models import (Article, BarAccount, CustomArticle, EmptyModel,
|
||||||
FooAccount, Gallery, GalleryAdmin, ModelWithStringPrimaryKey,
|
FooAccount, Gallery, PersonAdmin, ModelWithStringPrimaryKey,
|
||||||
Person, Persona, Picture, Podcast, Section, Subscriber, Vodcast,
|
Person, Persona, Picture, Podcast, Section, Subscriber, Vodcast,
|
||||||
Language, Collector, Widget, Grommet, DooHickey, FancyDoodad, Whatsit,
|
Language, Collector, Widget, Grommet, DooHickey, FancyDoodad, Whatsit,
|
||||||
Category, Post, Plot, FunkyTag, Chapter, Book, Promo, WorkHour, Employee,
|
Category, Post, Plot, FunkyTag, Chapter, Book, Promo, WorkHour, Employee,
|
||||||
|
@ -204,7 +204,7 @@ class AdminViewBasicTest(TestCase):
|
||||||
Ensure we can sort on a list_display field that is a callable
|
Ensure we can sort on a list_display field that is a callable
|
||||||
(column 2 is callable_year in ArticleAdmin)
|
(column 2 is callable_year in ArticleAdmin)
|
||||||
"""
|
"""
|
||||||
response = self.client.get('/test_admin/%s/admin_views/article/' % self.urlbit, {'ot': 'asc', 'o': 2})
|
response = self.client.get('/test_admin/%s/admin_views/article/' % self.urlbit, {'o': 2})
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
self.assertTrue(
|
self.assertTrue(
|
||||||
response.content.index('Oldest content') < response.content.index('Middle content') and
|
response.content.index('Oldest content') < response.content.index('Middle content') and
|
||||||
|
@ -217,7 +217,7 @@ class AdminViewBasicTest(TestCase):
|
||||||
Ensure we can sort on a list_display field that is a Model method
|
Ensure we can sort on a list_display field that is a Model method
|
||||||
(colunn 3 is 'model_year' in ArticleAdmin)
|
(colunn 3 is 'model_year' in ArticleAdmin)
|
||||||
"""
|
"""
|
||||||
response = self.client.get('/test_admin/%s/admin_views/article/' % self.urlbit, {'ot': 'dsc', 'o': 3})
|
response = self.client.get('/test_admin/%s/admin_views/article/' % self.urlbit, {'o': '-3'})
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
self.assertTrue(
|
self.assertTrue(
|
||||||
response.content.index('Newest content') < response.content.index('Middle content') and
|
response.content.index('Newest content') < response.content.index('Middle content') and
|
||||||
|
@ -230,7 +230,7 @@ class AdminViewBasicTest(TestCase):
|
||||||
Ensure we can sort on a list_display field that is a ModelAdmin method
|
Ensure we can sort on a list_display field that is a ModelAdmin method
|
||||||
(colunn 4 is 'modeladmin_year' in ArticleAdmin)
|
(colunn 4 is 'modeladmin_year' in ArticleAdmin)
|
||||||
"""
|
"""
|
||||||
response = self.client.get('/test_admin/%s/admin_views/article/' % self.urlbit, {'ot': 'asc', 'o': 4})
|
response = self.client.get('/test_admin/%s/admin_views/article/' % self.urlbit, {'o': '4'})
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
self.assertTrue(
|
self.assertTrue(
|
||||||
response.content.index('Oldest content') < response.content.index('Middle content') and
|
response.content.index('Oldest content') < response.content.index('Middle content') and
|
||||||
|
@ -238,6 +238,81 @@ class AdminViewBasicTest(TestCase):
|
||||||
"Results of sorting on ModelAdmin method are out of order."
|
"Results of sorting on ModelAdmin method are out of order."
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def testChangeListSortingMultiple(self):
|
||||||
|
p1 = Person.objects.create(name="Chris", gender=1, alive=True)
|
||||||
|
p2 = Person.objects.create(name="Chris", gender=2, alive=True)
|
||||||
|
p3 = Person.objects.create(name="Bob", gender=1, alive=True)
|
||||||
|
link = '<a href="%s/'
|
||||||
|
|
||||||
|
# Sort by name, gender
|
||||||
|
# This hard-codes the URL because it'll fail if it runs against the
|
||||||
|
# 'admin2' custom admin (which doesn't have the Person model).
|
||||||
|
response = self.client.get('/test_admin/admin/admin_views/person/', {'o': '1.2'})
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertTrue(
|
||||||
|
response.content.index(link % p3.id) < response.content.index(link % p1.id) and
|
||||||
|
response.content.index(link % p1.id) < response.content.index(link % p2.id)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Sort by gender descending, name
|
||||||
|
response = self.client.get('/test_admin/admin/admin_views/person/', {'o': '-2.1'})
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertTrue(
|
||||||
|
response.content.index(link % p2.id) < response.content.index(link % p3.id) and
|
||||||
|
response.content.index(link % p3.id) < response.content.index(link % p1.id)
|
||||||
|
)
|
||||||
|
|
||||||
|
def testChangeListSortingPreserveQuerySetOrdering(self):
|
||||||
|
# If no ordering on ModelAdmin, or query string, the underlying order of
|
||||||
|
# the queryset should not be changed.
|
||||||
|
|
||||||
|
p1 = Person.objects.create(name="Amy", gender=1, alive=True, age=80)
|
||||||
|
p2 = Person.objects.create(name="Bob", gender=1, alive=True, age=70)
|
||||||
|
p3 = Person.objects.create(name="Chris", gender=2, alive=False, age=60)
|
||||||
|
link = '<a href="%s/'
|
||||||
|
|
||||||
|
# This hard-codes the URL because it'll fail if it runs against the
|
||||||
|
# 'admin2' custom admin (which doesn't have the Person model).
|
||||||
|
response = self.client.get('/test_admin/admin/admin_views/person/', {})
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertTrue(
|
||||||
|
response.content.index(link % p3.id) < response.content.index(link % p2.id) and
|
||||||
|
response.content.index(link % p2.id) < response.content.index(link % p1.id)
|
||||||
|
)
|
||||||
|
|
||||||
|
def testChangeListSortingModelMeta(self):
|
||||||
|
# Test ordering on Model Meta is respected
|
||||||
|
|
||||||
|
l1 = Language.objects.create(iso='ur', name='Urdu')
|
||||||
|
l2 = Language.objects.create(iso='ar', name='Arabic')
|
||||||
|
link = '<a href="%s/'
|
||||||
|
|
||||||
|
response = self.client.get('/test_admin/admin/admin_views/language/', {})
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertTrue(
|
||||||
|
response.content.index(link % l2.pk) < response.content.index(link % l1.pk)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Test we can override with query string
|
||||||
|
response = self.client.get('/test_admin/admin/admin_views/language/', {'o':'-1'})
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertTrue(
|
||||||
|
response.content.index(link % l1.pk) < response.content.index(link % l2.pk)
|
||||||
|
)
|
||||||
|
|
||||||
|
def testChangeListSortingModelAdmin(self):
|
||||||
|
# Test ordering on Model Admin is respected, and overrides Model Meta
|
||||||
|
dt = datetime.datetime.now()
|
||||||
|
p1 = Podcast.objects.create(name="A", release_date=dt)
|
||||||
|
p2 = Podcast.objects.create(name="B", release_date=dt - datetime.timedelta(10))
|
||||||
|
|
||||||
|
link = '<a href="%s/'
|
||||||
|
response = self.client.get('/test_admin/admin/admin_views/podcast/', {})
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertTrue(
|
||||||
|
response.content.index(link % p1.pk) < response.content.index(link % p2.pk)
|
||||||
|
)
|
||||||
|
|
||||||
def testLimitedFilter(self):
|
def testLimitedFilter(self):
|
||||||
"""Ensure admin changelist filters do not contain objects excluded via limit_choices_to.
|
"""Ensure admin changelist filters do not contain objects excluded via limit_choices_to.
|
||||||
This also tests relation-spanning filters (e.g. 'color__value').
|
This also tests relation-spanning filters (e.g. 'color__value').
|
||||||
|
@ -1956,7 +2031,7 @@ class AdminActionsTest(TestCase):
|
||||||
'action' : 'external_mail',
|
'action' : 'external_mail',
|
||||||
'index': 0,
|
'index': 0,
|
||||||
}
|
}
|
||||||
url = '/test_admin/admin/admin_views/externalsubscriber/?ot=asc&o=1'
|
url = '/test_admin/admin/admin_views/externalsubscriber/?o=1'
|
||||||
response = self.client.post(url, action_data)
|
response = self.client.post(url, action_data)
|
||||||
self.assertRedirects(response, url)
|
self.assertRedirects(response, url)
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue