Fixed various bugs related to having multiple columns in admin list_display with the same sort field

Thanks to julien for the report

Refs #11868

git-svn-id: http://code.djangoproject.com/svn/django/trunk@16319 bcc190cf-cafb-0310-a4f2-bffc1f526a37
This commit is contained in:
Luke Plant 2011-06-03 11:54:29 +00:00
parent 22598d0044
commit cb996cce05
4 changed files with 118 additions and 52 deletions

View File

@ -85,7 +85,7 @@ def result_headers(cl):
# 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()
ordering_field_columns = cl.get_ordering_field_columns()
for i, field_name in enumerate(cl.list_display):
admin_order_field = None
text, attr = label_for_field(field_name, cl.model,
@ -93,36 +93,20 @@ def result_headers(cl):
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 info['field_name'] == 'action_checkbox':
if field_name == 'action_checkbox':
yield {
"text": info['text'],
"text": text,
"class_attrib": mark_safe(' class="action-checkbox-column"')
}
continue
if not info['admin_order_field']:
admin_order_field = getattr(attr, "admin_order_field", None)
if not admin_order_field:
# Not sortable
yield {"text": info['text']}
yield {"text": text}
continue
# OK, it is sortable if we got this far
@ -131,9 +115,9 @@ def result_headers(cl):
new_order_type = 'asc'
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
if i in ordering_field_columns:
order_type = ordering_field_columns.get(i).lower()
sort_pos = ordering_field_columns.keys().index(i) + 1
th_classes.append('sorted %sending' % order_type)
new_order_type = {'asc': 'desc', 'desc': 'asc'}[order_type]
@ -141,27 +125,21 @@ def result_headers(cl):
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:
for j, ot in ordering_field_columns.items():
if j == i: # Same column
# We want clicking on this header to bring the ordering to the
# front
o_list.insert(0, make_qs_param(new_order_type, colnum))
o_list.insert(0, make_qs_param(new_order_type, j))
else:
o_list.append(make_qs_param(ot, colnum))
o_list.append(make_qs_param(ot, j))
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))
if i not in ordering_field_columns:
o_list.insert(0, make_qs_param(new_order_type, i))
o_list = '.'.join(o_list)
yield {
"text": info['text'],
"text": text,
"sortable": True,
"ascending": order_type == "asc",
"sort_pos": sort_pos,

View File

@ -164,16 +164,20 @@ class ChangeList(object):
self.multi_page = multi_page
self.paginator = paginator
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. Finally, a
# manually-specified ordering from the query string overrides anything.
def _get_default_ordering(self):
ordering = []
if self.model_admin.ordering:
ordering = self.model_admin.ordering
elif lookup_opts.ordering:
ordering = lookup_opts.ordering
elif self.lookup_opts.ordering:
ordering = self.lookup_opts.ordering
return ordering
def get_ordering(self):
params = self.params
# For ordering, first check the "ordering" parameter in the admin
# options, then check the object's default ordering. Finally, a
# manually-specified ordering from the query string overrides anything.
ordering = self._get_default_ordering()
if ORDER_VAR in params:
# Clear ordering and used params
@ -184,7 +188,7 @@ class ChangeList(object):
none, pfx, idx = p.rpartition('-')
field_name = self.list_display[int(idx)]
try:
f = lookup_opts.get_field(field_name)
f = self.lookup_opts.get_field(field_name)
except models.FieldDoesNotExist:
# See whether field_name is a name of a non-field
# that allows sorting.
@ -208,12 +212,44 @@ class ChangeList(object):
return ordering
def get_ordering_fields(self):
# Returns a SortedDict of ordering fields and asc/desc
def get_ordering_field_columns(self):
# Returns a SortedDict of ordering field column numbers and asc/desc
# We must cope with more than one column having the same underlying sort
# field, so we base things on column numbers.
ordering = self._get_default_ordering()
ordering_fields = SortedDict()
for o in self.ordering:
none, t, f = o.rpartition('-')
ordering_fields[f] = 'desc' if t == '-' else 'asc'
if ORDER_VAR not in self.params:
# for ordering specified on ModelAdmin or model Meta, we don't know
# the right column numbers absolutely, because there might be more
# than one column associated with that ordering, so we guess.
for f in ordering:
if f.startswith('-'):
f = f[1:]
order_type = 'desc'
else:
order_type = 'asc'
i = None
try:
# Search for simply field name first
i = self.list_display.index(f)
except ValueError:
# No match, but their might be a match if we take into account
# 'admin_order_field'
for j, attr in enumerate(self.list_display):
if getattr(attr, 'admin_order_field', '') == f:
i = j
break
if i is not None:
ordering_fields[i] = order_type
else:
for p in self.params[ORDER_VAR].split('.'):
none, pfx, idx = p.rpartition('-')
try:
idx = int(idx)
except ValueError:
continue # skip it
ordering_fields[idx] = 'desc' if pfx == '-' else 'asc'
return ordering_fields
def get_lookup_params(self, use_distinct=False):

View File

@ -811,6 +811,20 @@ class OtherStoryAdmin(admin.ModelAdmin):
list_editable = ('content', )
ordering = ["-pk"]
class ComplexSortedPerson(models.Model):
name = models.CharField(max_length=100)
age = models.PositiveIntegerField()
is_employee = models.NullBooleanField()
class ComplexSortedPersonAdmin(admin.ModelAdmin):
list_display = ('name', 'age', 'is_employee', 'colored_name')
ordering = ('name',)
def colored_name(self, obj):
return '<span style="color: #%s;">%s</span>' % ('ff00ff', obj.name)
colored_name.allow_tags = True
colored_name.admin_order_field = 'name'
admin.site.register(Article, ArticleAdmin)
admin.site.register(CustomArticle, CustomArticleAdmin)
admin.site.register(Section, save_as=True, inlines=[ArticleInline])
@ -872,3 +886,4 @@ admin.site.register(Album, AlbumAdmin)
admin.site.register(Question)
admin.site.register(Answer)
admin.site.register(PrePopulatedPost, PrePopulatedPostAdmin)
admin.site.register(ComplexSortedPerson, ComplexSortedPersonAdmin)

View File

@ -37,7 +37,8 @@ from models import (Article, BarAccount, CustomArticle, EmptyModel,
Language, Collector, Widget, Grommet, DooHickey, FancyDoodad, Whatsit,
Category, Post, Plot, FunkyTag, Chapter, Book, Promo, WorkHour, Employee,
Question, Answer, Inquisition, Actor, FoodDelivery,
RowLevelChangePermissionModel, Paper, CoverLetter, Story, OtherStory)
RowLevelChangePermissionModel, Paper, CoverLetter, Story, OtherStory,
ComplexSortedPerson)
class AdminViewBasicTest(TestCase):
@ -313,6 +314,42 @@ class AdminViewBasicTest(TestCase):
response.content.index(link % p1.pk) < response.content.index(link % p2.pk)
)
def testMultipleSortSameField(self):
# Check that we get the columns we expect if we have two columns
# that correspond to the same ordering field
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)
)
p1 = ComplexSortedPerson.objects.create(name="Bob", age=10)
p2 = ComplexSortedPerson.objects.create(name="Amy", age=20)
link = '<a href="%s/'
response = self.client.get('/test_admin/admin/admin_views/complexsortedperson/', {})
self.assertEqual(response.status_code, 200)
# Should have 5 columns (including action checkbox col)
self.assertContains(response, '<th scope="col"', count=5)
self.assertContains(response, 'Name')
self.assertContains(response, 'Colored name')
# Check order
self.assertTrue(
response.content.index('Name') < response.content.index('Colored name')
)
# Check sorting - should be by name
self.assertTrue(
response.content.index(link % p2.id) < response.content.index(link % p1.id)
)
def testLimitedFilter(self):
"""Ensure admin changelist filters do not contain objects excluded via limit_choices_to.
This also tests relation-spanning filters (e.g. 'color__value').