diff --git a/django/contrib/admin/views/main.py b/django/contrib/admin/views/main.py index 761ed3a7544..fa7b2a60365 100644 --- a/django/contrib/admin/views/main.py +++ b/django/contrib/admin/views/main.py @@ -286,8 +286,11 @@ class ChangeList: order_field = self.get_ordering_field(field_name) if not order_field: continue # No 'admin_order_field', skip it + if hasattr(order_field, 'as_sql'): + # order_field is an expression. + ordering.append(order_field.desc() if pfx == '-' else order_field.asc()) # reverse order if order_field has already "-" as prefix - if order_field.startswith('-') and pfx == "-": + elif order_field.startswith('-') and pfx == '-': ordering.append(order_field[1:]) else: ordering.append(pfx + order_field) diff --git a/docs/ref/contrib/admin/index.txt b/docs/ref/contrib/admin/index.txt index a917498aed1..760cce69be8 100644 --- a/docs/ref/contrib/admin/index.txt +++ b/docs/ref/contrib/admin/index.txt @@ -745,6 +745,24 @@ subclass:: author_first_name.admin_order_field = 'author__first_name' + :doc:`Query expressions ` may be used in + ``admin_order_field``. For example:: + + from django.db.models import Value + from django.db.models.functions import Concat + + class Person(models.Model): + first_name = models.CharField(max_length=50) + last_name = models.CharField(max_length=50) + + def full_name(self): + return self.first_name + ' ' + self.last_name + full_name.admin_order_field = Concat('first_name', Value(' '), 'last_name') + + .. versionadded:: 2.1 + + Support for expressions in ``admin_order_field`` was added. + * Elements of ``list_display`` can also be properties. Please note however, that due to the way properties work in Python, setting ``short_description`` on a property is only possible when using the diff --git a/docs/releases/2.1.txt b/docs/releases/2.1.txt index 33d6982443e..414da311093 100644 --- a/docs/releases/2.1.txt +++ b/docs/releases/2.1.txt @@ -47,6 +47,9 @@ Minor features :meth:`.ModelAdmin.get_sortable_by` method allow limiting the columns that can be sorted in the change list page. +* The ``admin_order_field`` attribute for elements in + :attr:`.ModelAdmin.list_display` may now be a query expression. + :mod:`django.contrib.admindocs` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/tests/admin_views/admin.py b/tests/admin_views/admin.py index 04e40c2e0d0..ceae4c11a27 100644 --- a/tests/admin_views/admin.py +++ b/tests/admin_views/admin.py @@ -1,3 +1,4 @@ +import datetime import os import tempfile from io import StringIO @@ -94,6 +95,7 @@ class ArticleAdmin(admin.ModelAdmin): list_display = ( 'content', 'date', callable_year, 'model_year', 'modeladmin_year', 'model_year_reversed', 'section', lambda obj: obj.title, + 'order_by_expression', ) list_editable = ('section',) list_filter = ('date', 'section') @@ -110,6 +112,12 @@ class ArticleAdmin(admin.ModelAdmin): }) ) + def order_by_expression(self, obj): + return obj.model_year + # This ordering isn't particularly useful but shows that expressions can + # be used for admin_order_field. + order_by_expression.admin_order_field = models.F('date') + datetime.timedelta(days=3) + def changelist_view(self, request): return super().changelist_view(request, extra_context={'extra_var': 'Hello!'}) diff --git a/tests/admin_views/tests.py b/tests/admin_views/tests.py index 54a0590d210..8cdaa2c9a8e 100644 --- a/tests/admin_views/tests.py +++ b/tests/admin_views/tests.py @@ -349,6 +349,32 @@ class AdminViewBasicTest(AdminViewBasicTestCase): "Results of sorting on callable are out of order." ) + def test_change_list_sorting_callable_query_expression(self): + """ + Query expressions may be used for admin_order_field. (column 9 is + order_by_expression in ArticleAdmin). + """ + response = self.client.get(reverse('admin:admin_views_article_changelist'), {'o': '9'}) + self.assertContentBefore( + response, 'Oldest content', 'Middle content', + 'Results of sorting on callable are out of order.' + ) + self.assertContentBefore( + response, 'Middle content', 'Newest content', + 'Results of sorting on callable are out of order.' + ) + + def test_change_list_sorting_callable_query_expression_reverse(self): + response = self.client.get(reverse('admin:admin_views_article_changelist'), {'o': '-9'}) + self.assertContentBefore( + response, 'Middle content', 'Oldest content', + 'Results of sorting on callable are out of order.' + ) + self.assertContentBefore( + response, 'Newest content', 'Middle content', + 'Results of sorting on callable are out of order.' + ) + def test_change_list_sorting_model(self): """ Ensure we can sort on a list_display field that is a Model method