Permalink
Browse files

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
  • Loading branch information...
1 parent 78b3797 commit 5434ce231d75004bdbe5cf2b7b24ce67a2a6e737 @spookylukey spookylukey committed Jun 2, 2011
@@ -326,6 +326,34 @@ table thead th.descending a {
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 */
table.orderable tbody tr td:hover {
@@ -91,6 +91,14 @@ table thead th.descending a {
background-position: left;
}
+table thead th.sorted a span.text {
+ float: right;
+}
+
+table thead th.sorted a span.sortpos {
+ float: left;
+}
+
/* dashboard styles */
.dashboard .module table td a {
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
@@ -1,3 +1,5 @@
+{% load adminmedia %}
+{% load i18n %}
{% if result_hidden_fields %}
<div class="hiddenfields">{# DIV for HTML validation #}
{% for item in result_hidden_fields %}{{ item }}{% endfor %}
@@ -8,10 +10,18 @@
<table id="result_list">
<thead>
<tr>
-{% for header in result_headers %}<th scope="col"{{ header.class_attrib }}>
-{% if header.sortable %}<a href="{{ header.url }}">{% endif %}
-{{ header.text|capfirst }}
-{% if header.sortable %}</a>{% endif %}</th>{% endfor %}
+{% for header in result_headers %}
+<th scope="col" {{ header.class_attrib }}>
+ {% if header.sortable %}<a href="{{ header.url }}">{% endif %}
+ <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="" />&nbsp;{% endif %}
+ {{ header.sort_pos }}</span>
+ {% endif %}
+ <span class="clear"></span></a>
+ {% endif %}
+</th>{% endfor %}
</tr>
</thead>
<tbody>
@@ -24,4 +34,53 @@
</tbody>
</table>
</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 %}
@@ -7,6 +7,7 @@
from django.core.exceptions import ObjectDoesNotExist
from django.db import models
from django.utils import formats
+from django.utils.datastructures import SortedDict
from django.utils.html import escape, conditional_escape
from django.utils.safestring import mark_safe
from django.utils.text import capfirst
@@ -81,43 +82,90 @@ def result_headers(cl):
"""
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):
- 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,
return_attr = True
)
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 field_name == 'action_checkbox':
+ if info['field_name'] == 'action_checkbox':
yield {
- "text": header,
+ "text": info['text'],
"class_attrib": mark_safe(' class="action-checkbox-column"')
}
continue
- # It is a non-field, but perhaps one that is sortable
- admin_order_field = getattr(attr, "admin_order_field", None)
- if not admin_order_field:
- yield {"text": header}
+ if not info['admin_order_field']:
+ # Not sortable
+ yield {"text": info['text']}
continue
- # So this _is_ a sortable non-field. Go to the yield
- # after the else clause.
- else:
- admin_order_field = None
-
+ # OK, it is sortable if we got this far
th_classes = []
+ order_type = ''
new_order_type = 'asc'
- if field_name == cl.order_field or admin_order_field == cl.order_field:
- th_classes.append('sorted %sending' % cl.order_type.lower())
- new_order_type = {'asc': 'desc', 'desc': 'asc'}[cl.order_type.lower()]
+ sort_pos = 0
+ # Is it currently being sorted on?
+ 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 {
- "text": header,
+ "text": info['text'],
"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 '')
}
@@ -228,9 +276,14 @@ def result_list(cl):
"""
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,
'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))}
@register.inclusion_tag('admin/date_hierarchy.html')
@@ -3,6 +3,7 @@
from django.core.exceptions import SuspiciousOperation
from django.core.paginator import InvalidPage
from django.db import models
+from django.utils.datastructures import SortedDict
from django.utils.encoding import force_unicode, smart_str
from django.utils.translation import ugettext, ugettext_lazy
from django.utils.http import urlencode
@@ -75,7 +76,7 @@ def __init__(self, request, model, list_display, list_display_links,
self.list_editable = ()
else:
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_set = self.get_query_set(request)
self.get_results(request)
@@ -166,40 +167,54 @@ def get_results(self, request):
def get_ordering(self):
lookup_opts, params = self.lookup_opts, self.params
# For ordering, first check the "ordering" parameter in the admin
- # options, then check the object's default ordering. If neither of
- # those exist, order descending by ID by default. Finally, look for
- # manually-specified ordering from the query string.
- ordering = self.model_admin.ordering or lookup_opts.ordering or ['-' + lookup_opts.pk.name]
+ # options, then check the object's default ordering. Finally, a
+ # manually-specified ordering from the query string overrides anything.
+ ordering = []
+ 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:
- try:
- field_name = self.list_display[int(params[ORDER_VAR])]
+ # Clear ordering and used params
+ ordering = []
+ order_params = params[ORDER_VAR].split('.')
+ for p in order_params:
try:
- f = lookup_opts.get_field(field_name)
- except models.FieldDoesNotExist:
- # See whether field_name is a name of a non-field
- # that allows sorting.
+ none, pfx, idx = p.rpartition('-')
+ field_name = self.list_display[int(idx)]
try:
- if callable(field_name):
- attr = field_name
- elif hasattr(self.model_admin, field_name):
- attr = getattr(self.model_admin, field_name)
- else:
- attr = getattr(self.model, field_name)
- order_field = attr.admin_order_field
- except AttributeError:
- pass
- else:
- order_field = f.name
- except (IndexError, ValueError):
- pass # Invalid ordering specified. Just use the default.
- if ORDER_TYPE_VAR in params and params[ORDER_TYPE_VAR] in ('asc', 'desc'):
- order_type = params[ORDER_TYPE_VAR]
- return order_field, order_type
+ f = lookup_opts.get_field(field_name)
+ except models.FieldDoesNotExist:
+ # See whether field_name is a name of a non-field
+ # that allows sorting.
+ try:
+ if callable(field_name):
+ attr = field_name
+ elif hasattr(self.model_admin, field_name):
+ attr = getattr(self.model_admin, field_name)
+ else:
+ attr = getattr(self.model, field_name)
+ field_name = attr.admin_order_field
+ except AttributeError:
+ continue # No 'admin_order_field', skip it
+ else:
+ 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):
lookup_params = self.params.copy() # a dictionary of the query string
@@ -290,8 +305,8 @@ def get_query_set(self, request):
break
# Set ordering.
- if self.order_field:
- qs = qs.order_by('%s%s' % ((self.order_type == 'desc' and '-' or ''), self.order_field))
+ if self.ordering:
+ qs = qs.order_by(*self.ordering)
# Apply keyword searches.
def construct_search(field_name):
@@ -696,10 +696,10 @@ subclass::
If this isn't provided, the Django admin will use the model's default
ordering.
- .. admonition:: Note
+ .. versionchanged:: 1.4
- Django will only honor the first element in the list/tuple; any others
- will be ignored.
+ Django honors all elements in the list/tuple; before 1.4, only the first
+ was respected.
.. attribute:: ModelAdmin.paginator
Oops, something went wrong.

0 comments on commit 5434ce2

Please sign in to comment.