Fixed #25790 -- Allowed disable column sorting in the admin changelist.
Thanks Ramiro Morales for completing the patch.
This commit is contained in:
parent
7d96f0c49a
commit
ef2512b2ff
|
@ -111,6 +111,7 @@ class BaseModelAdmin(metaclass=forms.MediaDefiningClass):
|
||||||
formfield_overrides = {}
|
formfield_overrides = {}
|
||||||
readonly_fields = ()
|
readonly_fields = ()
|
||||||
ordering = None
|
ordering = None
|
||||||
|
sortable_by = None
|
||||||
view_on_site = True
|
view_on_site = True
|
||||||
show_full_result_count = True
|
show_full_result_count = True
|
||||||
checks_class = BaseModelAdminChecks
|
checks_class = BaseModelAdminChecks
|
||||||
|
@ -353,6 +354,10 @@ class BaseModelAdmin(metaclass=forms.MediaDefiningClass):
|
||||||
qs = qs.order_by(*ordering)
|
qs = qs.order_by(*ordering)
|
||||||
return qs
|
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):
|
def lookup_allowed(self, lookup, value):
|
||||||
from django.contrib.admin.filters import SimpleListFilter
|
from django.contrib.admin.filters import SimpleListFilter
|
||||||
|
|
||||||
|
@ -688,6 +693,7 @@ class ModelAdmin(BaseModelAdmin):
|
||||||
# Add the action checkboxes if any actions are available.
|
# Add the action checkboxes if any actions are available.
|
||||||
if self.get_actions(request):
|
if self.get_actions(request):
|
||||||
list_display = ['action_checkbox'] + list(list_display)
|
list_display = ['action_checkbox'] + list(list_display)
|
||||||
|
sortable_by = self.get_sortable_by(request)
|
||||||
ChangeList = self.get_changelist(request)
|
ChangeList = self.get_changelist(request)
|
||||||
return ChangeList(
|
return ChangeList(
|
||||||
request,
|
request,
|
||||||
|
@ -702,6 +708,7 @@ class ModelAdmin(BaseModelAdmin):
|
||||||
self.list_max_show_all,
|
self.list_max_show_all,
|
||||||
self.list_editable,
|
self.list_editable,
|
||||||
self,
|
self,
|
||||||
|
sortable_by,
|
||||||
)
|
)
|
||||||
|
|
||||||
def get_object(self, request, object_id, from_field=None):
|
def get_object(self, request, object_id, from_field=None):
|
||||||
|
|
|
@ -100,6 +100,7 @@ def result_headers(cl):
|
||||||
model_admin=cl.model_admin,
|
model_admin=cl.model_admin,
|
||||||
return_attr=True
|
return_attr=True
|
||||||
)
|
)
|
||||||
|
is_field_sortable = cl.sortable_by is None or field_name in cl.sortable_by
|
||||||
if attr:
|
if attr:
|
||||||
field_name = _coerce_field_name(field_name, i)
|
field_name = _coerce_field_name(field_name, i)
|
||||||
# Potentially not sortable
|
# Potentially not sortable
|
||||||
|
@ -115,11 +116,14 @@ def result_headers(cl):
|
||||||
|
|
||||||
admin_order_field = getattr(attr, "admin_order_field", None)
|
admin_order_field = getattr(attr, "admin_order_field", None)
|
||||||
if not admin_order_field:
|
if not admin_order_field:
|
||||||
|
is_field_sortable = False
|
||||||
|
|
||||||
|
if not is_field_sortable:
|
||||||
# Not sortable
|
# Not sortable
|
||||||
yield {
|
yield {
|
||||||
"text": text,
|
'text': text,
|
||||||
"class_attrib": format_html(' class="column-{}"', field_name),
|
'class_attrib': format_html(' class="column-{}"', field_name),
|
||||||
"sortable": False,
|
'sortable': False,
|
||||||
}
|
}
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
|
|
@ -35,7 +35,7 @@ IGNORED_PARAMS = (
|
||||||
class ChangeList:
|
class ChangeList:
|
||||||
def __init__(self, request, model, list_display, list_display_links,
|
def __init__(self, request, model, list_display, list_display_links,
|
||||||
list_filter, date_hierarchy, search_fields, list_select_related,
|
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.model = model
|
||||||
self.opts = model._meta
|
self.opts = model._meta
|
||||||
self.lookup_opts = self.opts
|
self.lookup_opts = self.opts
|
||||||
|
@ -50,6 +50,7 @@ class ChangeList:
|
||||||
self.list_max_show_all = list_max_show_all
|
self.list_max_show_all = list_max_show_all
|
||||||
self.model_admin = model_admin
|
self.model_admin = model_admin
|
||||||
self.preserved_filters = model_admin.get_preserved_filters(request)
|
self.preserved_filters = model_admin.get_preserved_filters(request)
|
||||||
|
self.sortable_by = sortable_by
|
||||||
|
|
||||||
# Get search parameters from the query string.
|
# Get search parameters from the query string.
|
||||||
try:
|
try:
|
||||||
|
|
|
@ -1296,6 +1296,22 @@ subclass::
|
||||||
a full count on the table which can be expensive if the table contains a
|
a full count on the table which can be expensive if the table contains a
|
||||||
large number of rows.
|
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
|
.. attribute:: ModelAdmin.view_on_site
|
||||||
|
|
||||||
Set ``view_on_site`` to control whether or not to display the "View on site" link.
|
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
|
to return the same kind of sequence type as for the
|
||||||
:attr:`~ModelAdmin.search_fields` attribute.
|
: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)
|
.. method:: ModelAdmin.get_inline_instances(request, obj=None)
|
||||||
|
|
||||||
The ``get_inline_instances`` method is given the ``HttpRequest`` and the
|
The ``get_inline_instances`` method is given the ``HttpRequest`` and the
|
||||||
|
|
|
@ -43,6 +43,10 @@ Minor features
|
||||||
* You can now :ref:`override the the default admin site
|
* You can now :ref:`override the the default admin site
|
||||||
<overriding-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`
|
:mod:`django.contrib.admindocs`
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
|
|
@ -1069,3 +1069,43 @@ site2.register(Person, save_as_continue=False)
|
||||||
site7 = admin.AdminSite(name="admin7")
|
site7 = admin.AdminSite(name="admin7")
|
||||||
site7.register(Article, ArticleAdmin2)
|
site7.register(Article, ArticleAdmin2)
|
||||||
site7.register(Section)
|
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)
|
||||||
|
|
|
@ -131,6 +131,7 @@ class AdminViewBasicTestCase(TestCase):
|
||||||
cls.chap4 = Chapter.objects.create(title='Chapter 2', content='[ insert contents here ]', book=cls.b2)
|
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.cx1 = ChapterXtra1.objects.create(chap=cls.chap1, xtra='ChapterXtra1 1')
|
||||||
cls.cx2 = ChapterXtra1.objects.create(chap=cls.chap3, xtra='ChapterXtra1 2')
|
cls.cx2 = ChapterXtra1.objects.create(chap=cls.chap3, xtra='ChapterXtra1 2')
|
||||||
|
Actor.objects.create(name='Palin', age=27)
|
||||||
|
|
||||||
# Post data for edit inline
|
# Post data for edit inline
|
||||||
cls.inline_post_data = {
|
cls.inline_post_data = {
|
||||||
|
@ -930,6 +931,35 @@ class AdminViewBasicTest(AdminViewBasicTestCase):
|
||||||
self.assertContains(response, 'question__expires__month=10')
|
self.assertContains(response, 'question__expires__month=10')
|
||||||
self.assertContains(response, 'question__expires__year=2016')
|
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=[{
|
@override_settings(TEMPLATES=[{
|
||||||
'BACKEND': 'django.template.backends.django.DjangoTemplates',
|
'BACKEND': 'django.template.backends.django.DjangoTemplates',
|
||||||
|
|
|
@ -12,6 +12,7 @@ urlpatterns = [
|
||||||
url(r'^test_admin/admin3/', (admin.site.get_urls(), 'admin', 'admin3'), {'form_url': 'pony'}),
|
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/admin4/', customadmin.simple_site.urls),
|
||||||
url(r'^test_admin/admin5/', admin.site2.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),
|
url(r'^test_admin/admin7/', admin.site7.urls),
|
||||||
# All admin views accept `extra_context` to allow adding it like this:
|
# 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': {}}),
|
url(r'^test_admin/admin8/', (admin.site.get_urls(), 'admin', 'admin-extra-context'), {'extra_context': {}}),
|
||||||
|
|
Loading…
Reference in New Issue