Fixed #25790 -- Allowed disable column sorting in the admin changelist.

Thanks Ramiro Morales for completing the patch.
This commit is contained in:
Alexander Gaevsky 2016-02-09 02:35:03 +02:00 committed by Tim Graham
parent 7d96f0c49a
commit ef2512b2ff
8 changed files with 129 additions and 8 deletions

View File

@ -111,6 +111,7 @@ class BaseModelAdmin(metaclass=forms.MediaDefiningClass):
formfield_overrides = {}
readonly_fields = ()
ordering = None
sortable_by = None
view_on_site = True
show_full_result_count = True
checks_class = BaseModelAdminChecks
@ -353,6 +354,10 @@ class BaseModelAdmin(metaclass=forms.MediaDefiningClass):
qs = qs.order_by(*ordering)
return qs
def get_sortable_by(self, request):
"""Hook for specifying which fields can be sorted in the changelist."""
return self.sortable_by if self.sortable_by is not None else self.get_list_display(request)
def lookup_allowed(self, lookup, value):
from django.contrib.admin.filters import SimpleListFilter
@ -688,6 +693,7 @@ class ModelAdmin(BaseModelAdmin):
# Add the action checkboxes if any actions are available.
if self.get_actions(request):
list_display = ['action_checkbox'] + list(list_display)
sortable_by = self.get_sortable_by(request)
ChangeList = self.get_changelist(request)
return ChangeList(
request,
@ -702,6 +708,7 @@ class ModelAdmin(BaseModelAdmin):
self.list_max_show_all,
self.list_editable,
self,
sortable_by,
)
def get_object(self, request, object_id, from_field=None):

View File

@ -100,6 +100,7 @@ def result_headers(cl):
model_admin=cl.model_admin,
return_attr=True
)
is_field_sortable = cl.sortable_by is None or field_name in cl.sortable_by
if attr:
field_name = _coerce_field_name(field_name, i)
# Potentially not sortable
@ -115,13 +116,16 @@ def result_headers(cl):
admin_order_field = getattr(attr, "admin_order_field", None)
if not admin_order_field:
# Not sortable
yield {
"text": text,
"class_attrib": format_html(' class="column-{}"', field_name),
"sortable": False,
}
continue
is_field_sortable = False
if not is_field_sortable:
# Not sortable
yield {
'text': text,
'class_attrib': format_html(' class="column-{}"', field_name),
'sortable': False,
}
continue
# OK, it is sortable if we got this far
th_classes = ['sortable', 'column-{}'.format(field_name)]

View File

@ -35,7 +35,7 @@ IGNORED_PARAMS = (
class ChangeList:
def __init__(self, request, model, list_display, list_display_links,
list_filter, date_hierarchy, search_fields, list_select_related,
list_per_page, list_max_show_all, list_editable, model_admin):
list_per_page, list_max_show_all, list_editable, model_admin, sortable_by):
self.model = model
self.opts = model._meta
self.lookup_opts = self.opts
@ -50,6 +50,7 @@ class ChangeList:
self.list_max_show_all = list_max_show_all
self.model_admin = model_admin
self.preserved_filters = model_admin.get_preserved_filters(request)
self.sortable_by = sortable_by
# Get search parameters from the query string.
try:

View File

@ -1296,6 +1296,22 @@ subclass::
a full count on the table which can be expensive if the table contains a
large number of rows.
.. attribute:: ModelAdmin.sortable_by
.. versionadded:: 2.1
By default, the change list page allows sorting by all model fields (and
callables that have the ``admin_order_field`` property) specified in
:attr:`list_display`.
If you want to disable sorting for some columns, set ``sortable_by`` to
a collection (e.g. ``list``, ``tuple``, or ``set``) of the subset of
:attr:`list_display` that you want to be sortable. An empty collection
disables sorting for all columns.
If you need to specify this list dynamically, implement a
:meth:`~ModelAdmin.get_sortable_by` method instead.
.. attribute:: ModelAdmin.view_on_site
Set ``view_on_site`` to control whether or not to display the "View on site" link.
@ -1564,6 +1580,24 @@ templates used by the :class:`ModelAdmin` views:
to return the same kind of sequence type as for the
:attr:`~ModelAdmin.search_fields` attribute.
.. method:: ModelAdmin.get_sortable_by(request)
.. versionadded:: 2.1
The ``get_sortable_by()`` method is passed the ``HttpRequest`` and is
expected to return a collection (e.g. ``list``, ``tuple``, or ``set``) of
field names that will be sortable in the change list page.
Its default implementation returns :attr:`sortable_by` if it's set,
otherwise it defers to :meth:`get_list_display`.
For example, to prevent one or more columns from being sortable::
class PersonAdmin(admin.ModelAdmin):
def get_sortable_by(self, request):
return {*self.get_list_display(request)} - {'rank'}
.. method:: ModelAdmin.get_inline_instances(request, obj=None)
The ``get_inline_instances`` method is given the ``HttpRequest`` and the

View File

@ -43,6 +43,10 @@ Minor features
* You can now :ref:`override the the default admin site
<overriding-default-admin-site>`.
* The new :attr:`.ModelAdmin.sortable_by` attribute and
:meth:`.ModelAdmin.get_sortable_by` method allow limiting the columns that
can be sorted in the change list page.
:mod:`django.contrib.admindocs`
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

View File

@ -1069,3 +1069,43 @@ site2.register(Person, save_as_continue=False)
site7 = admin.AdminSite(name="admin7")
site7.register(Article, ArticleAdmin2)
site7.register(Section)
# Used to test ModelAdmin.sortable_by and get_sortable_by().
class ArticleAdmin6(admin.ModelAdmin):
list_display = (
'content', 'date', callable_year, 'model_year', 'modeladmin_year',
'model_year_reversed', 'section',
)
sortable_by = ('date', callable_year)
def modeladmin_year(self, obj):
return obj.date.year
modeladmin_year.admin_order_field = 'date'
class ActorAdmin6(admin.ModelAdmin):
list_display = ('name', 'age')
sortable_by = ('name',)
def get_sortable_by(self, request):
return ('age',)
class ChapterAdmin6(admin.ModelAdmin):
list_display = ('title', 'book')
sortable_by = ()
class ColorAdmin6(admin.ModelAdmin):
list_display = ('value',)
def get_sortable_by(self, request):
return ()
site6 = admin.AdminSite(name='admin6')
site6.register(Article, ArticleAdmin6)
site6.register(Actor, ActorAdmin6)
site6.register(Chapter, ChapterAdmin6)
site6.register(Color, ColorAdmin6)

View File

@ -131,6 +131,7 @@ class AdminViewBasicTestCase(TestCase):
cls.chap4 = Chapter.objects.create(title='Chapter 2', content='[ insert contents here ]', book=cls.b2)
cls.cx1 = ChapterXtra1.objects.create(chap=cls.chap1, xtra='ChapterXtra1 1')
cls.cx2 = ChapterXtra1.objects.create(chap=cls.chap3, xtra='ChapterXtra1 2')
Actor.objects.create(name='Palin', age=27)
# Post data for edit inline
cls.inline_post_data = {
@ -930,6 +931,35 @@ class AdminViewBasicTest(AdminViewBasicTestCase):
self.assertContains(response, 'question__expires__month=10')
self.assertContains(response, 'question__expires__year=2016')
def test_sortable_by_columns_subset(self):
expected_sortable_fields = ('date', 'callable_year')
expected_not_sortable_fields = (
'content', 'model_year', 'modeladmin_year', 'model_year_reversed',
'section',
)
response = self.client.get(reverse('admin6:admin_views_article_changelist'))
for field_name in expected_sortable_fields:
self.assertContains(response, '<th scope="col" class="sortable column-%s">' % field_name)
for field_name in expected_not_sortable_fields:
self.assertContains(response, '<th scope="col" class="column-%s">' % field_name)
def test_get_sortable_by_columns_subset(self):
response = self.client.get(reverse('admin6:admin_views_actor_changelist'))
self.assertContains(response, '<th scope="col" class="sortable column-age">')
self.assertContains(response, '<th scope="col" class="column-name">')
def test_sortable_by_no_column(self):
expected_not_sortable_fields = ('title', 'book')
response = self.client.get(reverse('admin6:admin_views_chapter_changelist'))
for field_name in expected_not_sortable_fields:
self.assertContains(response, '<th scope="col" class="column-%s">' % field_name)
self.assertNotContains(response, '<th scope="col" class="sortable column')
def test_get_sortable_by_no_column(self):
response = self.client.get(reverse('admin6:admin_views_color_changelist'))
self.assertContains(response, '<th scope="col" class="column-value">')
self.assertNotContains(response, '<th scope="col" class="sortable column')
@override_settings(TEMPLATES=[{
'BACKEND': 'django.template.backends.django.DjangoTemplates',

View File

@ -12,6 +12,7 @@ urlpatterns = [
url(r'^test_admin/admin3/', (admin.site.get_urls(), 'admin', 'admin3'), {'form_url': 'pony'}),
url(r'^test_admin/admin4/', customadmin.simple_site.urls),
url(r'^test_admin/admin5/', admin.site2.urls),
url(r'^test_admin/admin6/', admin.site6.urls),
url(r'^test_admin/admin7/', admin.site7.urls),
# All admin views accept `extra_context` to allow adding it like this:
url(r'^test_admin/admin8/', (admin.site.get_urls(), 'admin', 'admin-extra-context'), {'extra_context': {}}),