From 9cefdfc43f0bae696b56fa5a0bf22346f85affff Mon Sep 17 00:00:00 2001 From: Tom Carrick Date: Thu, 16 Nov 2023 09:11:27 +0100 Subject: [PATCH] Refs #10743 -- Enabled ordering for lookups in ModelAdmin.list_display. Co-authored-by: Natalia <124304+nessita@users.noreply.github.com> Co-authored-by: Nina Menezes --- .../contrib/admin/templatetags/admin_list.py | 3 +- tests/admin_changelist/admin.py | 5 +- tests/admin_changelist/tests.py | 56 +++++++++++++++++++ 3 files changed, 62 insertions(+), 2 deletions(-) diff --git a/django/contrib/admin/templatetags/admin_list.py b/django/contrib/admin/templatetags/admin_list.py index 0c32290b6ca1..fdf6e63f5fb9 100644 --- a/django/contrib/admin/templatetags/admin_list.py +++ b/django/contrib/admin/templatetags/admin_list.py @@ -18,6 +18,7 @@ ) from django.core.exceptions import ObjectDoesNotExist from django.db import models +from django.db.models.constants import LOOKUP_SEP from django.template import Library from django.template.loader import get_template from django.templatetags.static import static @@ -112,7 +113,7 @@ def result_headers(cl): # Set ordering for attr that is a property, if defined. if isinstance(attr, property) and hasattr(attr, "fget"): admin_order_field = getattr(attr.fget, "admin_order_field", None) - if not admin_order_field: + if not admin_order_field and LOOKUP_SEP not in field_name: is_field_sortable = False if not is_field_sortable: diff --git a/tests/admin_changelist/admin.py b/tests/admin_changelist/admin.py index 3e6009b0c5c4..349ef7d465b6 100644 --- a/tests/admin_changelist/admin.py +++ b/tests/admin_changelist/admin.py @@ -3,7 +3,7 @@ from django.contrib.auth.models import User from django.core.paginator import Paginator -from .models import Band, Child, Event, Parent, ProxyUser, Swallow +from .models import Band, Child, Event, GrandChild, Parent, ProxyUser, Swallow site = admin.AdminSite(name="admin") @@ -57,6 +57,9 @@ class GrandChildAdmin(admin.ModelAdmin): list_display = ["name", "parent__name", "parent__parent__name"] +site.register(GrandChild, GrandChildAdmin) + + class CustomPaginationAdmin(ChildAdmin): paginator = CustomPaginator diff --git a/tests/admin_changelist/tests.py b/tests/admin_changelist/tests.py index 72fac8cd61d6..4f267635f15f 100644 --- a/tests/admin_changelist/tests.py +++ b/tests/admin_changelist/tests.py @@ -2073,3 +2073,59 @@ def test_collapse_filter_with_unescaped_title(self): By.CSS_SELECTOR, "[data-filter-title='It\\'s OK']" ).get_attribute("open") ) + + def test_list_display_ordering(self): + from selenium.webdriver.common.by import By + + parent_a = Parent.objects.create(name="Parent A") + child_l = Child.objects.create(name="Child L", parent=None) + child_m = Child.objects.create(name="Child M", parent=parent_a) + GrandChild.objects.create(name="Grandchild X", parent=child_m) + GrandChild.objects.create(name="Grandchild Y", parent=child_l) + GrandChild.objects.create(name="Grandchild Z", parent=None) + + self.admin_login(username="super", password="secret") + changelist_url = reverse("admin:admin_changelist_grandchild_changelist") + self.selenium.get(self.live_server_url + changelist_url) + + def find_result_row_texts(): + table = self.selenium.find_element(By.ID, "result_list") + # Drop header from the result list + return [row.text for row in table.find_elements(By.TAG_NAME, "tr")][1:] + + def expected_from_queryset(qs): + return [ + " ".join("-" if i is None else i for i in item) + for item in qs.values_list( + "name", "parent__name", "parent__parent__name" + ) + ] + + cases = [ + # Order ascending by `name`. + ("th.sortable.column-name", ("name",)), + # Order descending by `name`. + ("th.sortable.column-name", ("-name",)), + # Order ascending by `parent__name`. + ("th.sortable.column-parent__name", ("parent__name", "-name")), + # Order descending by `parent__name`. + ("th.sortable.column-parent__name", ("-parent__name", "-name")), + # Order ascending by `parent__parent__name`. + ( + "th.sortable.column-parent__parent__name", + ("parent__parent__name", "-parent__name", "-name"), + ), + # Order descending by `parent__parent__name`. + ( + "th.sortable.column-parent__parent__name", + ("-parent__parent__name", "-parent__name", "-name"), + ), + ] + for css_selector, ordering in cases: + with self.subTest(ordering=ordering): + # self.selenium.get(self.live_server_url + changelist_url) + self.selenium.find_element(By.CSS_SELECTOR, css_selector).click() + expected = expected_from_queryset( + GrandChild.objects.all().order_by(*ordering) + ) + self.assertEqual(find_result_row_texts(), expected)