Skip to content
This repository

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse code

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...
commit 5434ce231d75004bdbe5cf2b7b24ce67a2a6e737 1 parent 78b3797
Luke Plant authored June 02, 2011
28  django/contrib/admin/media/css/base.css
@@ -326,6 +326,34 @@ table thead th.descending a {
326 326
     background: url(../img/admin/arrow-down.gif) right .4em no-repeat;
327 327
 }
328 328
 
  329
+table thead th.sorted a span.text {
  330
+   display: block;
  331
+   float: left;
  332
+}
  333
+
  334
+table thead th.sorted a span.sortpos {
  335
+   display: block;
  336
+   float: right;
  337
+   font-size: .6em;
  338
+}
  339
+
  340
+table thead th.sorted a img {
  341
+   vertical-align: bottom;
  342
+}
  343
+
  344
+table thead th.sorted a span.clear {
  345
+   display: block;
  346
+   clear: both;
  347
+}
  348
+
  349
+#sorting-popup-div {
  350
+    position: absolute;
  351
+    background-color: white;
  352
+    border: 1px solid #ddd;
  353
+    z-index: 2000; /* more than filters on right */
  354
+    padding-right: 10px;
  355
+}
  356
+
329 357
 /* ORDERABLE TABLES */
330 358
 
331 359
 table.orderable tbody tr td:hover {
8  django/contrib/admin/media/css/rtl.css
@@ -91,6 +91,14 @@ table thead th.descending a {
91 91
     background-position: left;
92 92
 }
93 93
 
  94
+table thead th.sorted a span.text {
  95
+   float: right;
  96
+}
  97
+
  98
+table thead th.sorted a span.sortpos {
  99
+   float: left;
  100
+}
  101
+
94 102
 /* dashboard styles */
95 103
 
96 104
 .dashboard .module table td a {
BIN  django/contrib/admin/media/img/admin/icon_cog.gif
67  django/contrib/admin/templates/admin/change_list_results.html
... ...
@@ -1,3 +1,5 @@
  1
+{% load adminmedia %}
  2
+{% load i18n %}
1 3
 {% if result_hidden_fields %}
2 4
 <div class="hiddenfields">{# DIV for HTML validation #}
3 5
 {% for item in result_hidden_fields %}{{ item }}{% endfor %}
@@ -8,10 +10,18 @@
8 10
 <table id="result_list">
9 11
 <thead>
10 12
 <tr>
11  
-{% for header in result_headers %}<th scope="col"{{ header.class_attrib }}>
12  
-{% if header.sortable %}<a href="{{ header.url }}">{% endif %}
13  
-{{ header.text|capfirst }}
14  
-{% if header.sortable %}</a>{% endif %}</th>{% endfor %}
  13
+{% for header in result_headers %}
  14
+<th scope="col" {{ header.class_attrib }}>
  15
+  {% if header.sortable %}<a href="{{ header.url }}">{% endif %}
  16
+  <span class="text">{{ header.text|capfirst }}</span>
  17
+  {% if header.sortable %}
  18
+    {% if header.sort_pos > 0 %}<span class="sortpos">
  19
+      {% if header.sort_pos == 1 %}<img id="primary-sort-icon" src="{% admin_media_prefix %}img/admin/icon_cog.gif" alt="" />&nbsp;{% endif %}
  20
+      {{ header.sort_pos }}</span>
  21
+    {% endif %}
  22
+    <span class="clear"></span></a>
  23
+  {% endif %}
  24
+</th>{% endfor %}
15 25
 </tr>
16 26
 </thead>
17 27
 <tbody>
@@ -24,4 +34,53 @@
24 34
 </tbody>
25 35
 </table>
26 36
 </div>
  37
+
  38
+{# Sorting popup: #}
  39
+<div style="display: none;" id="sorting-popup-div">
  40
+<p>{% trans "Sorting by:" %}</p>
  41
+<ol>
  42
+{% for header in result_headers|dictsort:"sort_pos" %}
  43
+  {% if header.sort_pos > 0 %}
  44
+    {% if header.ascending %}
  45
+      <li>{% blocktrans with fieldname=header.text %}{{ fieldname }} (ascending){% endblocktrans %}</li>
  46
+    {% else %}
  47
+      <li>{% blocktrans with fieldname=header.text %}{{ fieldname }} (descending){% endblocktrans %}</li>
  48
+    {% endif %}
  49
+  {% endif %}
  50
+{% endfor %}
  51
+</ol>
  52
+<p><a href="{{ reset_sorting_url }}">{% trans "Reset sorting" %}</a></p>
  53
+</div>
  54
+<script type="text/javascript">
  55
+<!--
  56
+(function($) {
  57
+    $(document).ready(function() {
  58
+        var popup = $('#sorting-popup-div');
  59
+        /* These next lines seems necessary to prime the popup: */
  60
+        popup.offset({left:-1000, top:0});
  61
+        popup.show();
  62
+        var popupWidth = popup.width();
  63
+        popup.hide();
  64
+
  65
+        $('#primary-sort-icon').toggle(function(ev) {
  66
+                                          ev.preventDefault();
  67
+                                          var img = $(this);
  68
+                                          var pos = img.offset();
  69
+                                          pos.top += img.height();
  70
+                                          if (pos.left + popupWidth >
  71
+                                              $(window).width()) {
  72
+                                              pos.left -= popupWidth;
  73
+                                          }
  74
+                                          popup.show();
  75
+                                          popup.offset(pos);
  76
+                                      },
  77
+                                      function(ev) {
  78
+                                          ev.preventDefault();
  79
+                                          popup.hide();
  80
+                                      });
  81
+    });
  82
+})(django.jQuery);
  83
+//-->
  84
+</script>
  85
+
27 86
 {% endif %}
93  django/contrib/admin/templatetags/admin_list.py
@@ -7,6 +7,7 @@
7 7
 from django.core.exceptions import ObjectDoesNotExist
8 8
 from django.db import models
9 9
 from django.utils import formats
  10
+from django.utils.datastructures import SortedDict
10 11
 from django.utils.html import escape, conditional_escape
11 12
 from django.utils.safestring import mark_safe
12 13
 from django.utils.text import capfirst
@@ -81,43 +82,90 @@ def result_headers(cl):
81 82
     """
82 83
     Generates the list column headers.
83 84
     """
84  
-    lookup_opts = cl.lookup_opts
85  
-
  85
+    # We need to know the 'ordering field' that corresponds to each
  86
+    # item in list_display, and we need other info, so do a pre-pass
  87
+    # on list_display
  88
+    list_display_info = SortedDict()
86 89
     for i, field_name in enumerate(cl.list_display):
87  
-        header, attr = label_for_field(field_name, cl.model,
  90
+        admin_order_field = None
  91
+        text, attr = label_for_field(field_name, cl.model,
88 92
             model_admin = cl.model_admin,
89 93
             return_attr = True
90 94
         )
91 95
         if attr:
  96
+            admin_order_field = getattr(attr, "admin_order_field", None)
  97
+        if admin_order_field is None:
  98
+            ordering_field_name = field_name
  99
+        else:
  100
+            ordering_field_name = admin_order_field
  101
+        list_display_info[ordering_field_name] = dict(text=text,
  102
+                                                      attr=attr,
  103
+                                                      index=i,
  104
+                                                      admin_order_field=admin_order_field,
  105
+                                                      field_name=field_name)
  106
+
  107
+    del admin_order_field, text, attr
  108
+
  109
+    ordering_fields = cl.get_ordering_fields()
  110
+
  111
+    for ordering_field_name, info in list_display_info.items():
  112
+        if info['attr']:
  113
+            # Potentially not sortable
  114
+
92 115
             # if the field is the action checkbox: no sorting and special class
93  
-            if field_name == 'action_checkbox':
  116
+            if info['field_name'] == 'action_checkbox':
94 117
                 yield {
95  
-                    "text": header,
  118
+                    "text": info['text'],
96 119
                     "class_attrib": mark_safe(' class="action-checkbox-column"')
97 120
                 }
98 121
                 continue
99 122
 
100  
-            # It is a non-field, but perhaps one that is sortable
101  
-            admin_order_field = getattr(attr, "admin_order_field", None)
102  
-            if not admin_order_field:
103  
-                yield {"text": header}
  123
+            if not info['admin_order_field']:
  124
+                # Not sortable
  125
+                yield {"text": info['text']}
104 126
                 continue
105 127
 
106  
-            # So this _is_ a sortable non-field.  Go to the yield
107  
-            # after the else clause.
108  
-        else:
109  
-            admin_order_field = None
110  
-
  128
+        # OK, it is sortable if we got this far
111 129
         th_classes = []
  130
+        order_type = ''
112 131
         new_order_type = 'asc'
113  
-        if field_name == cl.order_field or admin_order_field == cl.order_field:
114  
-            th_classes.append('sorted %sending' % cl.order_type.lower())
115  
-            new_order_type = {'asc': 'desc', 'desc': 'asc'}[cl.order_type.lower()]
  132
+        sort_pos = 0
  133
+        # Is it currently being sorted on?
  134
+        if ordering_field_name in ordering_fields:
  135
+            order_type = ordering_fields.get(ordering_field_name).lower()
  136
+            sort_pos = ordering_fields.keys().index(ordering_field_name) + 1
  137
+            th_classes.append('sorted %sending' % order_type)
  138
+            new_order_type = {'asc': 'desc', 'desc': 'asc'}[order_type]
  139
+
  140
+        # build new ordering param
  141
+        o_list = []
  142
+        make_qs_param = lambda t, n: ('-' if t == 'desc' else '') + str(n)
  143
+
  144
+        for f, ot in ordering_fields.items():
  145
+            try:
  146
+                colnum = list_display_info[f]['index']
  147
+            except KeyError:
  148
+                continue
  149
+
  150
+            if f == ordering_field_name:
  151
+                # We want clicking on this header to bring the ordering to the
  152
+                # front
  153
+                o_list.insert(0, make_qs_param(new_order_type, colnum))
  154
+            else:
  155
+                o_list.append(make_qs_param(ot, colnum))
  156
+
  157
+        if ordering_field_name not in ordering_fields:
  158
+            colnum = list_display_info[ordering_field_name]['index']
  159
+            o_list.insert(0, make_qs_param(new_order_type, colnum))
  160
+
  161
+        o_list = '.'.join(o_list)
116 162
 
117 163
         yield {
118  
-            "text": header,
  164
+            "text": info['text'],
119 165
             "sortable": True,
120  
-            "url": cl.get_query_string({ORDER_VAR: i, ORDER_TYPE_VAR: new_order_type}),
  166
+            "ascending": order_type == "asc",
  167
+            "sort_pos": sort_pos,
  168
+            "url": cl.get_query_string({ORDER_VAR: o_list}),
121 169
             "class_attrib": mark_safe(th_classes and ' class="%s"' % ' '.join(th_classes) or '')
122 170
         }
123 171
 
@@ -228,9 +276,14 @@ def result_list(cl):
228 276
     """
229 277
     Displays the headers and data list together
230 278
     """
  279
+    headers = list(result_headers(cl))
  280
+    for h in headers:
  281
+        # Sorting in templates depends on sort_pos attribute
  282
+        h.setdefault('sort_pos', 0)
231 283
     return {'cl': cl,
232 284
             'result_hidden_fields': list(result_hidden_fields(cl)),
233  
-            'result_headers': list(result_headers(cl)),
  285
+            'result_headers': headers,
  286
+            'reset_sorting_url': cl.get_query_string(remove=[ORDER_VAR]),
234 287
             'results': list(results(cl))}
235 288
 
236 289
 @register.inclusion_tag('admin/date_hierarchy.html')
81  django/contrib/admin/views/main.py
@@ -3,6 +3,7 @@
3 3
 from django.core.exceptions import SuspiciousOperation
4 4
 from django.core.paginator import InvalidPage
5 5
 from django.db import models
  6
+from django.utils.datastructures import SortedDict
6 7
 from django.utils.encoding import force_unicode, smart_str
7 8
 from django.utils.translation import ugettext, ugettext_lazy
8 9
 from django.utils.http import urlencode
@@ -75,7 +76,7 @@ def __init__(self, request, model, list_display, list_display_links,
75 76
             self.list_editable = ()
76 77
         else:
77 78
             self.list_editable = list_editable
78  
-        self.order_field, self.order_type = self.get_ordering()
  79
+        self.ordering = self.get_ordering()
79 80
         self.query = request.GET.get(SEARCH_VAR, '')
80 81
         self.query_set = self.get_query_set(request)
81 82
         self.get_results(request)
@@ -166,40 +167,54 @@ def get_results(self, request):
166 167
     def get_ordering(self):
167 168
         lookup_opts, params = self.lookup_opts, self.params
168 169
         # For ordering, first check the "ordering" parameter in the admin
169  
-        # options, then check the object's default ordering. If neither of
170  
-        # those exist, order descending by ID by default. Finally, look for
171  
-        # manually-specified ordering from the query string.
172  
-        ordering = self.model_admin.ordering or lookup_opts.ordering or ['-' + lookup_opts.pk.name]
  170
+        # options, then check the object's default ordering. Finally, a
  171
+        # manually-specified ordering from the query string overrides anything.
  172
+        ordering = []
  173
+        if self.model_admin.ordering:
  174
+            ordering = self.model_admin.ordering
  175
+        elif lookup_opts.ordering:
  176
+            ordering = lookup_opts.ordering
173 177
 
174  
-        if ordering[0].startswith('-'):
175  
-            order_field, order_type = ordering[0][1:], 'desc'
176  
-        else:
177  
-            order_field, order_type = ordering[0], 'asc'
178 178
         if ORDER_VAR in params:
179  
-            try:
180  
-                field_name = self.list_display[int(params[ORDER_VAR])]
  179
+            # Clear ordering and used params
  180
+            ordering = []
  181
+            order_params = params[ORDER_VAR].split('.')
  182
+            for p in order_params:
181 183
                 try:
182  
-                    f = lookup_opts.get_field(field_name)
183  
-                except models.FieldDoesNotExist:
184  
-                    # See whether field_name is a name of a non-field
185  
-                    # that allows sorting.
  184
+                    none, pfx, idx = p.rpartition('-')
  185
+                    field_name = self.list_display[int(idx)]
186 186
                     try:
187  
-                        if callable(field_name):
188  
-                            attr = field_name
189  
-                        elif hasattr(self.model_admin, field_name):
190  
-                            attr = getattr(self.model_admin, field_name)
191  
-                        else:
192  
-                            attr = getattr(self.model, field_name)
193  
-                        order_field = attr.admin_order_field
194  
-                    except AttributeError:
195  
-                        pass
196  
-                else:
197  
-                    order_field = f.name
198  
-            except (IndexError, ValueError):
199  
-                pass # Invalid ordering specified. Just use the default.
200  
-        if ORDER_TYPE_VAR in params and params[ORDER_TYPE_VAR] in ('asc', 'desc'):
201  
-            order_type = params[ORDER_TYPE_VAR]
202  
-        return order_field, order_type
  187
+                        f = lookup_opts.get_field(field_name)
  188
+                    except models.FieldDoesNotExist:
  189
+                        # See whether field_name is a name of a non-field
  190
+                        # that allows sorting.
  191
+                        try:
  192
+                            if callable(field_name):
  193
+                                attr = field_name
  194
+                            elif hasattr(self.model_admin, field_name):
  195
+                                attr = getattr(self.model_admin, field_name)
  196
+                            else:
  197
+                                attr = getattr(self.model, field_name)
  198
+                            field_name = attr.admin_order_field
  199
+                        except AttributeError:
  200
+                            continue # No 'admin_order_field', skip it
  201
+                    else:
  202
+                        field_name = f.name
  203
+
  204
+                    ordering.append(pfx + field_name)
  205
+
  206
+                except (IndexError, ValueError):
  207
+                    continue # Invalid ordering specified, skip it.
  208
+
  209
+        return ordering
  210
+
  211
+    def get_ordering_fields(self):
  212
+        # Returns a SortedDict of ordering fields and asc/desc
  213
+        ordering_fields = SortedDict()
  214
+        for o in self.ordering:
  215
+            none, t, f = o.rpartition('-')
  216
+            ordering_fields[f] = 'desc' if t == '-' else 'asc'
  217
+        return ordering_fields
203 218
 
204 219
     def get_lookup_params(self, use_distinct=False):
205 220
         lookup_params = self.params.copy() # a dictionary of the query string
@@ -290,8 +305,8 @@ def get_query_set(self, request):
290 305
                             break
291 306
 
292 307
         # Set ordering.
293  
-        if self.order_field:
294  
-            qs = qs.order_by('%s%s' % ((self.order_type == 'desc' and '-' or ''), self.order_field))
  308
+        if self.ordering:
  309
+            qs = qs.order_by(*self.ordering)
295 310
 
296 311
         # Apply keyword searches.
297 312
         def construct_search(field_name):
6  docs/ref/contrib/admin/index.txt
@@ -696,10 +696,10 @@ subclass::
696 696
     If this isn't provided, the Django admin will use the model's default
697 697
     ordering.
698 698
 
699  
-    .. admonition:: Note
  699
+    .. versionchanged:: 1.4
700 700
 
701  
-        Django will only honor the first element in the list/tuple; any others
702  
-        will be ignored.
  701
+    Django honors all elements in the list/tuple; before 1.4, only the first
  702
+    was respected.
703 703
 
704 704
 .. attribute:: ModelAdmin.paginator
705 705
 
8  docs/releases/1.4.txt
@@ -46,6 +46,14 @@ not custom filters. This has been rectified with a simple API previously
46 46
 known as "FilterSpec" which was used internally. For more details, see the
47 47
 documentation for :attr:`~django.contrib.admin.ModelAdmin.list_filter`.
48 48
 
  49
+Multiple sort in admin interface
  50
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  51
+
  52
+The admin change list now supports sorting on multiple columns. It respects all
  53
+elements of the :attr:`~django.contrib.admin.ModelAdmin.ordering` attribute, and
  54
+sorting on multiple columns by clicking on headers is designed to work similarly
  55
+to how desktop GUIs do it.
  56
+
49 57
 Tools for cryptographic signing
50 58
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
51 59
 
4  tests/regressiontests/admin_filters/tests.py
@@ -67,11 +67,11 @@ class CustomUserAdmin(UserAdmin):
67 67
 
68 68
 class BookAdmin(ModelAdmin):
69 69
     list_filter = ('year', 'author', 'contributors', 'is_best_seller', 'date_registered', 'no')
70  
-    order_by = '-id'
  70
+    ordering = ('-id',)
71 71
 
72 72
 class DecadeFilterBookAdmin(ModelAdmin):
73 73
     list_filter = ('author', DecadeListFilterWithTitleAndParameter)
74  
-    order_by = '-id'
  74
+    ordering = ('-id',)
75 75
 
76 76
 class DecadeFilterBookAdminWithoutTitle(ModelAdmin):
77 77
     list_filter = (DecadeListFilterWithoutTitle,)
14  tests/regressiontests/admin_views/models.py
@@ -243,9 +243,6 @@ class Person(models.Model):
243 243
     def __unicode__(self):
244 244
         return self.name
245 245
 
246  
-    class Meta:
247  
-        ordering = ["id"]
248  
-
249 246
 class BasePersonModelFormSet(BaseModelFormSet):
250 247
     def clean(self):
251 248
         for person_dict in self.cleaned_data:
@@ -259,13 +256,17 @@ class PersonAdmin(admin.ModelAdmin):
259 256
     list_editable = ('gender', 'alive')
260 257
     list_filter = ('gender',)
261 258
     search_fields = ('^name',)
262  
-    ordering = ["id"]
263 259
     save_as = True
264 260
 
265 261
     def get_changelist_formset(self, request, **kwargs):
266 262
         return super(PersonAdmin, self).get_changelist_formset(request,
267 263
             formset=BasePersonModelFormSet, **kwargs)
268 264
 
  265
+    def queryset(self, request):
  266
+        # Order by a field that isn't in list display, to be able to test
  267
+        # whether ordering is preserved.
  268
+        return super(PersonAdmin, self).queryset(request).order_by('age')
  269
+
269 270
 
270 271
 class Persona(models.Model):
271 272
     """
@@ -357,6 +358,9 @@ class Media(models.Model):
357 358
 class Podcast(Media):
358 359
     release_date = models.DateField()
359 360
 
  361
+    class Meta:
  362
+        ordering = ('release_date',) # overridden in PodcastAdmin
  363
+
360 364
 class PodcastAdmin(admin.ModelAdmin):
361 365
     list_display = ('name', 'release_date')
362 366
     list_editable = ('release_date',)
@@ -795,6 +799,7 @@ class StoryAdmin(admin.ModelAdmin):
795 799
     list_display_links = ('title',) # 'id' not in list_display_links
796 800
     list_editable = ('content', )
797 801
     form = StoryForm
  802
+    ordering = ["-pk"]
798 803
 
799 804
 class OtherStory(models.Model):
800 805
     title = models.CharField(max_length=100)
@@ -804,6 +809,7 @@ class OtherStoryAdmin(admin.ModelAdmin):
804 809
     list_display = ('id', 'title', 'content')
805 810
     list_display_links = ('title', 'id') # 'id' in list_display_links
806 811
     list_editable = ('content', )
  812
+    ordering = ["-pk"]
807 813
 
808 814
 admin.site.register(Article, ArticleAdmin)
809 815
 admin.site.register(CustomArticle, CustomArticleAdmin)
85  tests/regressiontests/admin_views/tests.py
@@ -32,7 +32,7 @@
32 32
 
33 33
 # local test models
34 34
 from models import (Article, BarAccount, CustomArticle, EmptyModel,
35  
-    FooAccount, Gallery, GalleryAdmin, ModelWithStringPrimaryKey,
  35
+    FooAccount, Gallery, PersonAdmin, ModelWithStringPrimaryKey,
36 36
     Person, Persona, Picture, Podcast, Section, Subscriber, Vodcast,
37 37
     Language, Collector, Widget, Grommet, DooHickey, FancyDoodad, Whatsit,
38 38
     Category, Post, Plot, FunkyTag, Chapter, Book, Promo, WorkHour, Employee,
@@ -204,7 +204,7 @@ def testChangeListSortingCallable(self):
204 204
         Ensure we can sort on a list_display field that is a callable
205 205
         (column 2 is callable_year in ArticleAdmin)
206 206
         """
207  
-        response = self.client.get('/test_admin/%s/admin_views/article/' % self.urlbit, {'ot': 'asc', 'o': 2})
  207
+        response = self.client.get('/test_admin/%s/admin_views/article/' % self.urlbit, {'o': 2})
208 208
         self.assertEqual(response.status_code, 200)
209 209
         self.assertTrue(
210 210
             response.content.index('Oldest content') < response.content.index('Middle content') and
@@ -217,7 +217,7 @@ def testChangeListSortingModel(self):
217 217
         Ensure we can sort on a list_display field that is a Model method
218 218
         (colunn 3 is 'model_year' in ArticleAdmin)
219 219
         """
220  
-        response = self.client.get('/test_admin/%s/admin_views/article/' % self.urlbit, {'ot': 'dsc', 'o': 3})
  220
+        response = self.client.get('/test_admin/%s/admin_views/article/' % self.urlbit, {'o': '-3'})
221 221
         self.assertEqual(response.status_code, 200)
222 222
         self.assertTrue(
223 223
             response.content.index('Newest content') < response.content.index('Middle content') and
@@ -230,7 +230,7 @@ def testChangeListSortingModelAdmin(self):
230 230
         Ensure we can sort on a list_display field that is a ModelAdmin method
231 231
         (colunn 4 is 'modeladmin_year' in ArticleAdmin)
232 232
         """
233  
-        response = self.client.get('/test_admin/%s/admin_views/article/' % self.urlbit, {'ot': 'asc', 'o': 4})
  233
+        response = self.client.get('/test_admin/%s/admin_views/article/' % self.urlbit, {'o': '4'})
234 234
         self.assertEqual(response.status_code, 200)
235 235
         self.assertTrue(
236 236
             response.content.index('Oldest content') < response.content.index('Middle content') and
@@ -238,6 +238,81 @@ def testChangeListSortingModelAdmin(self):
238 238
             "Results of sorting on ModelAdmin method are out of order."
239 239
         )
240 240
 
  241
+    def testChangeListSortingMultiple(self):
  242
+        p1 = Person.objects.create(name="Chris", gender=1, alive=True)
  243
+        p2 = Person.objects.create(name="Chris", gender=2, alive=True)
  244
+        p3 = Person.objects.create(name="Bob", gender=1, alive=True)
  245
+        link = '<a href="%s/'
  246
+
  247
+        # Sort by name, gender
  248
+        # This hard-codes the URL because it'll fail if it runs against the
  249
+        # 'admin2' custom admin (which doesn't have the Person model).
  250
+        response = self.client.get('/test_admin/admin/admin_views/person/', {'o': '1.2'})
  251
+        self.assertEqual(response.status_code, 200)
  252
+        self.assertTrue(
  253
+            response.content.index(link % p3.id) < response.content.index(link % p1.id) and
  254
+            response.content.index(link % p1.id) < response.content.index(link % p2.id)
  255
+        )
  256
+
  257
+        # Sort by gender descending, name
  258
+        response = self.client.get('/test_admin/admin/admin_views/person/', {'o': '-2.1'})
  259
+        self.assertEqual(response.status_code, 200)
  260
+        self.assertTrue(
  261
+            response.content.index(link % p2.id) < response.content.index(link % p3.id) and
  262
+            response.content.index(link % p3.id) < response.content.index(link % p1.id)
  263
+        )
  264
+
  265
+    def testChangeListSortingPreserveQuerySetOrdering(self):
  266
+        # If no ordering on ModelAdmin, or query string, the underlying order of
  267
+        # the queryset should not be changed.
  268
+
  269
+        p1 = Person.objects.create(name="Amy", gender=1, alive=True, age=80)
  270
+        p2 = Person.objects.create(name="Bob", gender=1, alive=True, age=70)
  271
+        p3 = Person.objects.create(name="Chris", gender=2, alive=False, age=60)
  272
+        link = '<a href="%s/'
  273
+
  274
+        # This hard-codes the URL because it'll fail if it runs against the
  275
+        # 'admin2' custom admin (which doesn't have the Person model).
  276
+        response = self.client.get('/test_admin/admin/admin_views/person/', {})
  277
+        self.assertEqual(response.status_code, 200)
  278
+        self.assertTrue(
  279
+            response.content.index(link % p3.id) < response.content.index(link % p2.id) and
  280
+            response.content.index(link % p2.id) < response.content.index(link % p1.id)
  281
+        )
  282
+
  283
+    def testChangeListSortingModelMeta(self):
  284
+        # Test ordering on Model Meta is respected
  285
+
  286
+        l1 = Language.objects.create(iso='ur', name='Urdu')
  287
+        l2 = Language.objects.create(iso='ar', name='Arabic')
  288
+        link = '<a href="%s/'
  289
+
  290
+        response = self.client.get('/test_admin/admin/admin_views/language/', {})
  291
+        self.assertEqual(response.status_code, 200)
  292
+        self.assertTrue(
  293
+            response.content.index(link % l2.pk) < response.content.index(link % l1.pk)
  294
+        )
  295
+
  296
+        # Test we can override with query string
  297
+        response = self.client.get('/test_admin/admin/admin_views/language/', {'o':'-1'})
  298
+        self.assertEqual(response.status_code, 200)
  299
+        self.assertTrue(
  300
+            response.content.index(link % l1.pk) < response.content.index(link % l2.pk)
  301
+        )
  302
+
  303
+    def testChangeListSortingModelAdmin(self):
  304
+        # Test ordering on Model Admin is respected, and overrides Model Meta
  305
+        dt = datetime.datetime.now()
  306
+        p1 = Podcast.objects.create(name="A", release_date=dt)
  307
+        p2 = Podcast.objects.create(name="B", release_date=dt - datetime.timedelta(10))
  308
+
  309
+        link = '<a href="%s/'
  310
+        response = self.client.get('/test_admin/admin/admin_views/podcast/', {})
  311
+        self.assertEqual(response.status_code, 200)
  312
+        self.assertTrue(
  313
+            response.content.index(link % p1.pk) < response.content.index(link % p2.pk)
  314
+        )
  315
+
241 316
     def testLimitedFilter(self):
242 317
         """Ensure admin changelist filters do not contain objects excluded via limit_choices_to.
243 318
         This also tests relation-spanning filters (e.g. 'color__value').
@@ -1956,7 +2031,7 @@ def test_default_redirect(self):
1956 2031
             'action' : 'external_mail',
1957 2032
             'index': 0,
1958 2033
         }
1959  
-        url = '/test_admin/admin/admin_views/externalsubscriber/?ot=asc&o=1'
  2034
+        url = '/test_admin/admin/admin_views/externalsubscriber/?o=1'
1960 2035
         response = self.client.post(url, action_data)
1961 2036
         self.assertRedirects(response, url)
1962 2037
 

0 notes on commit 5434ce2

Please sign in to comment.
Something went wrong with that request. Please try again.