Code

Ticket #11868: 11868_luke1.diff

File 11868_luke1.diff, 11.7 KB (added by lukeplant, 3 years ago)

Part way implementation of latest proposal

Line 
1diff -r b3394ccce096 django/contrib/admin/media/css/base.css
2--- a/django/contrib/admin/media/css/base.css   Tue May 31 21:29:35 2011 +0000
3+++ b/django/contrib/admin/media/css/base.css   Wed Jun 01 17:31:26 2011 +0100
4@@ -326,6 +326,36 @@
5     background: url(../img/admin/arrow-down.gif) right .4em no-repeat;
6 }
7 
8+table thead th.sorted a span.text {
9+   display: block;
10+   float: left;
11+}
12+
13+table thead th.sorted a span.sortpos {
14+   display: block;
15+   float: right;
16+   font-size: .6em;
17+}
18+
19+table thead th.sorted a img {
20+   vertical-align: bottom;
21+   /* Make it look like a link */
22+   border-bottom: 1px solid #5b80b2;
23+}
24+
25+table thead th.sorted a span.clear {
26+   display: block;
27+   clear: both;
28+}
29+
30+#sorting-popup-div {
31+    position: absolute;
32+    background-color: white;
33+    border: 1px solid #ddd;
34+    z-index: 2000; /* more than filters on right */
35+    padding-right: 10px;
36+}
37+
38 /* ORDERABLE TABLES */
39 
40 table.orderable tbody tr td:hover {
41diff -r b3394ccce096 django/contrib/admin/templates/admin/change_list_results.html
42--- a/django/contrib/admin/templates/admin/change_list_results.html     Tue May 31 21:29:35 2011 +0000
43+++ b/django/contrib/admin/templates/admin/change_list_results.html     Wed Jun 01 17:31:26 2011 +0100
44@@ -1,3 +1,4 @@
45+{% load adminmedia %}
46 {% if result_hidden_fields %}
47 <div class="hiddenfields">{# DIV for HTML validation #}
48 {% for item in result_hidden_fields %}{{ item }}{% endfor %}
49@@ -8,10 +9,18 @@
50 <table id="result_list">
51 <thead>
52 <tr>
53-{% for header in result_headers %}<th scope="col"{{ header.class_attrib }}>
54-{% if header.sortable %}<a href="{{ header.url }}">{% endif %}
55-{{ header.text|capfirst }}
56-{% if header.sortable %}</a>{% endif %}</th>{% endfor %}
57+{% for header in result_headers %}
58+<th scope="col" {{ header.class_attrib }}>
59+  {% if header.sortable %}<a href="{{ header.url }}">{% endif %}
60+  <span class="text">{{ header.text|capfirst }}</span>
61+  {% if header.sortable %}
62+    {% if header.sort_pos > 0 %}<span class="sortpos">
63+      {% 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 %}
64+      {{ header.sort_pos }}</span>
65+    {% endif %}
66+    <span class="clear"></span></a>
67+  {% endif %}
68+</th>{% endfor %}
69 </tr>
70 </thead>
71 <tbody>
72@@ -24,4 +33,49 @@
73 </tbody>
74 </table>
75 </div>
76+
77+{# Sorting popup: #}
78+<div style="display: none;" id="sorting-popup-div">
79+<p>Sorting by:</p>
80+<ol>
81+{% for header in result_headers|dictsort:"sort_pos" %}
82+  {% if header.sort_pos > 0 %}
83+    <li>{{ header.text|capfirst }}</li>
84+  {% endif %}
85+{% endfor %}
86+</ol>
87+<p><a href="{{ reset_order_url }}">Reset sorting</a></p>
88+</div>
89+<script type="text/javascript">
90+<!--
91+(function($) {
92+    $(document).ready(function() {
93+        var popup = $('#sorting-popup-div');
94+        /* These next lines seems necessary to prime the popup: */
95+        popup.offset({left:-1000, top:0});
96+        popup.show();
97+        var popupWidth = popup.width()
98+        popup.hide();
99+
100+        $('#primary-sort-icon').toggle(function(ev) {
101+                                          ev.preventDefault();
102+                                          var img = $(this);
103+                                          var pos = img.offset();
104+                                          pos.top += img.height();
105+                                          if (pos.left + popupWidth >
106+                                              $(window).width()) {
107+                                              pos.left -= popupWidth;
108+                                          }
109+                                          popup.show();
110+                                          popup.offset(pos);
111+                                      },
112+                                      function(ev) {
113+                                          ev.preventDefault();
114+                                          popup.hide();
115+                                      });
116+    });
117+})(django.jQuery);
118+//-->
119+</script>
120+
121 {% endif %}
122diff -r b3394ccce096 django/contrib/admin/templatetags/admin_list.py
123--- a/django/contrib/admin/templatetags/admin_list.py   Tue May 31 21:29:35 2011 +0000
124+++ b/django/contrib/admin/templatetags/admin_list.py   Wed Jun 01 17:31:26 2011 +0100
125@@ -93,6 +93,7 @@
126             if field_name == 'action_checkbox':
127                 yield {
128                     "text": header,
129+                    "sort_pos": 0,
130                     "class_attrib": mark_safe(' class="action-checkbox-column"')
131                 }
132                 continue
133@@ -100,7 +101,7 @@
134             # It is a non-field, but perhaps one that is sortable
135             admin_order_field = getattr(attr, "admin_order_field", None)
136             if not admin_order_field:
137-                yield {"text": header}
138+                yield {"text": header, "sort_pos": 0 }
139                 continue
140 
141             # So this _is_ a sortable non-field.  Go to the yield
142@@ -110,14 +111,44 @@
143 
144         th_classes = []
145         new_order_type = 'asc'
146-        if field_name == cl.order_field or admin_order_field == cl.order_field:
147-            th_classes.append('sorted %sending' % cl.order_type.lower())
148-            new_order_type = {'asc': 'desc', 'desc': 'asc'}[cl.order_type.lower()]
149+        ordering_fields = cl.get_ordering_fields()
150+        sort_pos = 0
151+        if field_name in ordering_fields or admin_order_field in ordering_fields:
152+            if not field_name in ordering_fields:
153+                field_name = admin_order_field
154+            order_type = ordering_fields.get(field_name).lower()
155+            sort_pos = ordering_fields.keys().index(field_name) + 1
156+            th_classes.append('sorted %sending' % order_type)
157+            new_order_type = {'asc': 'desc', 'desc': 'asc'}[order_type]
158+
159+        # build new ordering param
160+        o_list = []
161+        make_qs_param = lambda t, n: ('-' if t == 'desc' else '') + str(n)
162+
163+        for f in ordering_fields.keys():
164+            try:
165+                n = cl.list_display.index(f)
166+            except ValueError:
167+                continue
168+
169+            if f == field_name:
170+                # We want clicking on this header to bring the ordering to the
171+                # front
172+                o_list.insert(0, make_qs_param(new_order_type, n))
173+            else:
174+                o_list.append(make_qs_param(ordering_fields.get(f), n))
175+
176+        if field_name not in ordering_fields:
177+            n = cl.list_display.index(field_name)
178+            o_list.insert(0, make_qs_param(new_order_type, n))
179+
180+        o_list = '.'.join(o_list)
181 
182         yield {
183             "text": header,
184             "sortable": True,
185-            "url": cl.get_query_string({ORDER_VAR: i, ORDER_TYPE_VAR: new_order_type}),
186+            "sort_pos": sort_pos,
187+            "url": cl.get_query_string({ORDER_VAR: o_list}),
188             "class_attrib": mark_safe(th_classes and ' class="%s"' % ' '.join(th_classes) or '')
189         }
190 
191@@ -231,6 +262,7 @@
192     return {'cl': cl,
193             'result_hidden_fields': list(result_hidden_fields(cl)),
194             'result_headers': list(result_headers(cl)),
195+            'reset_order_url': cl.get_query_string(remove=[ORDER_VAR]),
196             'results': list(results(cl))}
197 
198 @register.inclusion_tag('admin/date_hierarchy.html')
199diff -r b3394ccce096 django/contrib/admin/views/main.py
200--- a/django/contrib/admin/views/main.py        Tue May 31 21:29:35 2011 +0000
201+++ b/django/contrib/admin/views/main.py        Wed Jun 01 17:31:26 2011 +0100
202@@ -3,6 +3,7 @@
203 from django.core.exceptions import SuspiciousOperation
204 from django.core.paginator import InvalidPage
205 from django.db import models
206+from django.utils.datastructures import SortedDict
207 from django.utils.encoding import force_unicode, smart_str
208 from django.utils.translation import ugettext, ugettext_lazy
209 from django.utils.http import urlencode
210@@ -75,7 +76,7 @@
211             self.list_editable = ()
212         else:
213             self.list_editable = list_editable
214-        self.order_field, self.order_type = self.get_ordering()
215+        self.ordering = self.get_ordering()
216         self.query = request.GET.get(SEARCH_VAR, '')
217         self.query_set = self.get_query_set(request)
218         self.get_results(request)
219@@ -171,35 +172,46 @@
220         # manually-specified ordering from the query string.
221         ordering = self.model_admin.ordering or lookup_opts.ordering or ['-' + lookup_opts.pk.name]
222 
223-        if ordering[0].startswith('-'):
224-            order_field, order_type = ordering[0][1:], 'desc'
225-        else:
226-            order_field, order_type = ordering[0], 'asc'
227         if ORDER_VAR in params:
228-            try:
229-                field_name = self.list_display[int(params[ORDER_VAR])]
230+            # Clear ordering and used params
231+            ordering = []
232+            order_params = params[ORDER_VAR].split('.')
233+            for p in order_params:
234                 try:
235-                    f = lookup_opts.get_field(field_name)
236-                except models.FieldDoesNotExist:
237-                    # See whether field_name is a name of a non-field
238-                    # that allows sorting.
239+                    none, pfx, idx = p.rpartition('-')
240+                    field_name = self.list_display[int(idx)]
241                     try:
242-                        if callable(field_name):
243-                            attr = field_name
244-                        elif hasattr(self.model_admin, field_name):
245-                            attr = getattr(self.model_admin, field_name)
246-                        else:
247-                            attr = getattr(self.model, field_name)
248-                        order_field = attr.admin_order_field
249-                    except AttributeError:
250-                        pass
251-                else:
252-                    order_field = f.name
253-            except (IndexError, ValueError):
254-                pass # Invalid ordering specified. Just use the default.
255-        if ORDER_TYPE_VAR in params and params[ORDER_TYPE_VAR] in ('asc', 'desc'):
256-            order_type = params[ORDER_TYPE_VAR]
257-        return order_field, order_type
258+                        f = lookup_opts.get_field(field_name)
259+                    except models.FieldDoesNotExist:
260+                        # See whether field_name is a name of a non-field
261+                        # that allows sorting.
262+                        try:
263+                            if callable(field_name):
264+                                attr = field_name
265+                            elif hasattr(self.model_admin, field_name):
266+                                attr = getattr(self.model_admin, field_name)
267+                            else:
268+                                attr = getattr(self.model, field_name)
269+                            field_name = attr.admin_order_field
270+                        except AttributeError:
271+                            pass
272+                    else:
273+                        field_name = f.name
274+
275+                    ordering.append(pfx + field_name)
276+
277+                except (IndexError, ValueError):
278+                    pass # Invalid ordering specified. Skip it.
279+
280+        return ordering
281+
282+    def get_ordering_fields(self):
283+        # Returns a SortedDict of ordering fields and asc/desc
284+        ordering_fields = SortedDict()
285+        for o in self.ordering:
286+            none, t, f = o.rpartition('-')
287+            ordering_fields[f] = 'desc' if t == '-' else 'asc'
288+        return ordering_fields
289 
290     def get_lookup_params(self, use_distinct=False):
291         lookup_params = self.params.copy() # a dictionary of the query string
292@@ -290,8 +302,8 @@
293                             break
294 
295         # Set ordering.
296-        if self.order_field:
297-            qs = qs.order_by('%s%s' % ((self.order_type == 'desc' and '-' or ''), self.order_field))
298+        if self.ordering:
299+            qs = qs.order_by(*self.ordering)
300 
301         # Apply keyword searches.
302         def construct_search(field_name):