Ticket #11868: 11868_luke3.diff

File 11868_luke3.diff, 15.3 KB (added by lukeplant, 4 years ago)

Fixed some existing tests, and bugs with handling columns invalid for sorting

  • django/contrib/admin/media/css/base.css

    diff -r 2297d9993a41 django/contrib/admin/media/css/base.css
    a b  
    326326    background: url(../img/admin/arrow-down.gif) right .4em no-repeat;
    327327}
    328328
     329table thead th.sorted a span.text {
     330   display: block;
     331   float: left;
     332}
     333
     334table thead th.sorted a span.sortpos {
     335   display: block;
     336   float: right;
     337   font-size: .6em;
     338}
     339
     340table thead th.sorted a img {
     341   vertical-align: bottom;
     342   /* Make it look like a link */
     343   border-bottom: 1px solid #5b80b2;
     344}
     345
     346table thead th.sorted a span.clear {
     347   display: block;
     348   clear: both;
     349}
     350
     351#sorting-popup-div {
     352    position: absolute;
     353    background-color: white;
     354    border: 1px solid #ddd;
     355    z-index: 2000; /* more than filters on right */
     356    padding-right: 10px;
     357}
     358
    329359/* ORDERABLE TABLES */
    330360
    331361table.orderable tbody tr td:hover {
  • django/contrib/admin/templates/admin/change_list_results.html

    diff -r 2297d9993a41 django/contrib/admin/templates/admin/change_list_results.html
    a b  
     1{% load adminmedia %}
     2{% load i18n %}
    13{% if result_hidden_fields %}
    24<div class="hiddenfields">{# DIV for HTML validation #}
    35{% for item in result_hidden_fields %}{{ item }}{% endfor %}
     
    810<table id="result_list">
    911<thead>
    1012<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_primary_sort.png" alt="Primary sort field" />{% endif %}
     20      {{ header.sort_pos }}</span>
     21    {% endif %}
     22    <span class="clear"></span></a>
     23  {% endif %}
     24</th>{% endfor %}
    1525</tr>
    1626</thead>
    1727<tbody>
     
    2434</tbody>
    2535</table>
    2636</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    <li>{{ header.text|capfirst }}</li>
     45  {% endif %}
     46{% endfor %}
     47</ol>
     48<p><a href="{{ reset_order_url }}">{% trans "Reset sorting" %}</a></p>
     49</div>
     50<script type="text/javascript">
     51<!--
     52(function($) {
     53    $(document).ready(function() {
     54        var popup = $('#sorting-popup-div');
     55        /* These next lines seems necessary to prime the popup: */
     56        popup.offset({left:-1000, top:0});
     57        popup.show();
     58        var popupWidth = popup.width()
     59        popup.hide();
     60
     61        $('#primary-sort-icon').toggle(function(ev) {
     62                                          ev.preventDefault();
     63                                          var img = $(this);
     64                                          var pos = img.offset();
     65                                          pos.top += img.height();
     66                                          if (pos.left + popupWidth >
     67                                              $(window).width()) {
     68                                              pos.left -= popupWidth;
     69                                          }
     70                                          popup.show();
     71                                          popup.offset(pos);
     72                                      },
     73                                      function(ev) {
     74                                          ev.preventDefault();
     75                                          popup.hide();
     76                                      });
     77    });
     78})(django.jQuery);
     79//-->
     80</script>
     81
    2782{% endif %}
  • django/contrib/admin/templatetags/admin_list.py

    diff -r 2297d9993a41 django/contrib/admin/templatetags/admin_list.py
    a b  
    9393            if field_name == 'action_checkbox':
    9494                yield {
    9595                    "text": header,
     96                    "sort_pos": 0,
    9697                    "class_attrib": mark_safe(' class="action-checkbox-column"')
    9798                }
    9899                continue
     
    100101            # It is a non-field, but perhaps one that is sortable
    101102            admin_order_field = getattr(attr, "admin_order_field", None)
    102103            if not admin_order_field:
    103                 yield {"text": header}
     104                yield {"text": header, "sort_pos": 0 }
    104105                continue
    105106
    106107            # So this _is_ a sortable non-field.  Go to the yield
     
    110111
    111112        th_classes = []
    112113        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()]
     114        ordering_fields = cl.get_ordering_fields()
     115        sort_pos = 0
     116        if field_name in ordering_fields or admin_order_field in ordering_fields:
     117            if not field_name in ordering_fields:
     118                field_name = admin_order_field
     119            order_type = ordering_fields.get(field_name).lower()
     120            sort_pos = ordering_fields.keys().index(field_name) + 1
     121            th_classes.append('sorted %sending' % order_type)
     122            new_order_type = {'asc': 'desc', 'desc': 'asc'}[order_type]
     123
     124        # build new ordering param
     125        o_list = []
     126        make_qs_param = lambda t, n: ('-' if t == 'desc' else '') + str(n)
     127
     128        for f in ordering_fields.keys():
     129            try:
     130                n = cl.list_display.index(f)
     131            except ValueError:
     132                continue
     133
     134            if f == field_name:
     135                # We want clicking on this header to bring the ordering to the
     136                # front
     137                o_list.insert(0, make_qs_param(new_order_type, n))
     138            else:
     139                o_list.append(make_qs_param(ordering_fields.get(f), n))
     140
     141        if field_name not in ordering_fields:
     142            n = cl.list_display.index(field_name)
     143            o_list.insert(0, make_qs_param(new_order_type, n))
     144
     145        o_list = '.'.join(o_list)
    116146
    117147        yield {
    118148            "text": header,
    119149            "sortable": True,
    120             "url": cl.get_query_string({ORDER_VAR: i, ORDER_TYPE_VAR: new_order_type}),
     150            "sort_pos": sort_pos,
     151            "url": cl.get_query_string({ORDER_VAR: o_list}),
    121152            "class_attrib": mark_safe(th_classes and ' class="%s"' % ' '.join(th_classes) or '')
    122153        }
    123154
     
    231262    return {'cl': cl,
    232263            'result_hidden_fields': list(result_hidden_fields(cl)),
    233264            'result_headers': list(result_headers(cl)),
     265            'reset_order_url': cl.get_query_string(remove=[ORDER_VAR]),
    234266            'results': list(results(cl))}
    235267
    236268@register.inclusion_tag('admin/date_hierarchy.html')
  • django/contrib/admin/views/main.py

    diff -r 2297d9993a41 django/contrib/admin/views/main.py
    a b  
    33from django.core.exceptions import SuspiciousOperation
    44from django.core.paginator import InvalidPage
    55from django.db import models
     6from django.utils.datastructures import SortedDict
    67from django.utils.encoding import force_unicode, smart_str
    78from django.utils.translation import ugettext, ugettext_lazy
    89from django.utils.http import urlencode
     
    7576            self.list_editable = ()
    7677        else:
    7778            self.list_editable = list_editable
    78         self.order_field, self.order_type = self.get_ordering()
     79        self.ordering = self.get_ordering()
    7980        self.query = request.GET.get(SEARCH_VAR, '')
    8081        self.query_set = self.get_query_set(request)
    8182        self.get_results(request)
     
    171172        # manually-specified ordering from the query string.
    172173        ordering = self.model_admin.ordering or lookup_opts.ordering or ['-' + lookup_opts.pk.name]
    173174
    174         if ordering[0].startswith('-'):
    175             order_field, order_type = ordering[0][1:], 'desc'
    176         else:
    177             order_field, order_type = ordering[0], 'asc'
    178175        if ORDER_VAR in params:
    179             try:
    180                 field_name = self.list_display[int(params[ORDER_VAR])]
     176            # Clear ordering and used params
     177            ordering = []
     178            order_params = params[ORDER_VAR].split('.')
     179            for p in order_params:
    181180                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.
     181                    none, pfx, idx = p.rpartition('-')
     182                    field_name = self.list_display[int(idx)]
    186183                    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
     184                        f = lookup_opts.get_field(field_name)
     185                    except models.FieldDoesNotExist:
     186                        # See whether field_name is a name of a non-field
     187                        # that allows sorting.
     188                        try:
     189                            if callable(field_name):
     190                                attr = field_name
     191                            elif hasattr(self.model_admin, field_name):
     192                                attr = getattr(self.model_admin, field_name)
     193                            else:
     194                                attr = getattr(self.model, field_name)
     195                            field_name = attr.admin_order_field
     196                        except AttributeError:
     197                            continue # No 'admin_order_field', skip it
     198                    else:
     199                        field_name = f.name
     200
     201                    ordering.append(pfx + field_name)
     202
     203                except (IndexError, ValueError):
     204                    continue # Invalid ordering specified, skip it.
     205
     206        return ordering
     207
     208    def get_ordering_fields(self):
     209        # Returns a SortedDict of ordering fields and asc/desc
     210        ordering_fields = SortedDict()
     211        for o in self.ordering:
     212            none, t, f = o.rpartition('-')
     213            ordering_fields[f] = 'desc' if t == '-' else 'asc'
     214        return ordering_fields
    203215
    204216    def get_lookup_params(self, use_distinct=False):
    205217        lookup_params = self.params.copy() # a dictionary of the query string
     
    290302                            break
    291303
    292304        # Set ordering.
    293         if self.order_field:
    294             qs = qs.order_by('%s%s' % ((self.order_type == 'desc' and '-' or ''), self.order_field))
     305        if self.ordering:
     306            qs = qs.order_by(*self.ordering)
    295307
    296308        # Apply keyword searches.
    297309        def construct_search(field_name):
  • docs/ref/contrib/admin/index.txt

    diff -r 2297d9993a41 docs/ref/contrib/admin/index.txt
    a b  
    696696    If this isn't provided, the Django admin will use the model's default
    697697    ordering.
    698698
    699     .. admonition:: Note
    700 
    701         Django will only honor the first element in the list/tuple; any others
    702         will be ignored.
     699    .. versionchanged:: 1.4
     700
     701    Django honors all elements in the list/tuple; before 1.4, any others
     702    were ignored.
    703703
    704704.. attribute:: ModelAdmin.paginator
    705705
  • docs/releases/1.4.txt

    diff -r 2297d9993a41 docs/releases/1.4.txt
    a b  
    4646known as "FilterSpec" which was used internally. For more details, see the
    4747documentation for :attr:`~django.contrib.admin.ModelAdmin.list_filter`.
    4848
     49Multiple sort in admin interface
     50~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
     51
     52The admin change list now supports sorting on multiple columns. It respects all
     53elements of the :attr:`~django.contrib.admin.ModelAdmin.ordering` attribute, and
     54sorting on multiple columns by clicking on headers is designed to work similarly
     55to how desktop GUIs do it.
     56
    4957Tools for cryptographic signing
    5058~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    5159
  • tests/regressiontests/admin_views/tests.py

    diff -r 2297d9993a41 tests/regressiontests/admin_views/tests.py
    a b  
    204204        Ensure we can sort on a list_display field that is a callable
    205205        (column 2 is callable_year in ArticleAdmin)
    206206        """
    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})
    208208        self.assertEqual(response.status_code, 200)
    209209        self.assertTrue(
    210210            response.content.index('Oldest content') < response.content.index('Middle content') and
     
    217217        Ensure we can sort on a list_display field that is a Model method
    218218        (colunn 3 is 'model_year' in ArticleAdmin)
    219219        """
    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'})
    221221        self.assertEqual(response.status_code, 200)
    222222        self.assertTrue(
    223223            response.content.index('Newest content') < response.content.index('Middle content') and
     
    230230        Ensure we can sort on a list_display field that is a ModelAdmin method
    231231        (colunn 4 is 'modeladmin_year' in ArticleAdmin)
    232232        """
    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'})
    234234        self.assertEqual(response.status_code, 200)
    235235        self.assertTrue(
    236236            response.content.index('Oldest content') < response.content.index('Middle content') and
     
    19561956            'action' : 'external_mail',
    19571957            'index': 0,
    19581958        }
    1959         url = '/test_admin/admin/admin_views/externalsubscriber/?ot=asc&o=1'
     1959        url = '/test_admin/admin/admin_views/externalsubscriber/?o=1'
    19601960        response = self.client.post(url, action_data)
    19611961        self.assertRedirects(response, url)
    19621962
Back to Top