From 818d8fc8cd90f9b42d351c6991a7f3527377eaa9 Mon Sep 17 00:00:00 2001 From: "utkarsh.arya@zomato.com" Date: Sat, 15 Nov 2025 22:59:07 +0000 Subject: [PATCH] Fix admin filters to respect related model Meta.ordering RelatedFieldListFilter and RelatedOnlyFieldListFilter now fall back to the related model's Meta.ordering when ModelAdmin ordering is not set. This ensures consistent ordering in admin filters and resolves issues where ordering was previously ignored. --- django/contrib/admin/filters.py | 10 ++++- tests/admin_filters/models.py | 17 +++++++ tests/admin_filters/tests.py | 79 ++++++++++++++++++++++++++++++++- 3 files changed, 104 insertions(+), 2 deletions(-) diff --git a/django/contrib/admin/filters.py b/django/contrib/admin/filters.py index d65e01d5e2..0b9bce1cf3 100644 --- a/django/contrib/admin/filters.py +++ b/django/contrib/admin/filters.py @@ -198,6 +198,8 @@ class RelatedFieldListFilter(FieldListFilter): related_admin = model_admin.admin_site._registry.get(field.remote_field.model) if related_admin is not None: ordering = related_admin.get_ordering(request) + if not ordering: + ordering = field.remote_field.model._meta.ordering return field.get_choices(include_blank=False, ordering=ordering) def choices(self, changelist): @@ -419,4 +421,10 @@ FieldListFilter.register(lambda f: True, AllValuesFieldListFilter) class RelatedOnlyFieldListFilter(RelatedFieldListFilter): def field_choices(self, field, request, model_admin): pk_qs = model_admin.get_queryset(request).distinct().values_list('%s__pk' % self.field_path, flat=True) - return field.get_choices(include_blank=False, limit_choices_to={'pk__in': pk_qs}) + ordering = () + related_admin = model_admin.admin_site._registry.get(field.remote_field.model) + if related_admin is not None: + ordering = related_admin.get_ordering(request) + if not ordering: + ordering = field.remote_field.model._meta.ordering + return field.get_choices(include_blank=False, limit_choices_to={'pk__in': pk_qs}, ordering=ordering) diff --git a/tests/admin_filters/models.py b/tests/admin_filters/models.py index ae78282d34..080f4734fb 100644 --- a/tests/admin_filters/models.py +++ b/tests/admin_filters/models.py @@ -77,3 +77,20 @@ class Bookmark(models.Model): def __str__(self): return self.url + + +class ImplicitlyOrderedBook(models.Model): + title = models.CharField(max_length=50) + author = models.ForeignKey( + User, + models.SET_NULL, + verbose_name="Verbose Author", + related_name='implicit_books_authored', + blank=True, null=True, + ) + + class Meta: + ordering = ('title',) + + def __str__(self): + return self.title diff --git a/tests/admin_filters/tests.py b/tests/admin_filters/tests.py index 4ff7d012e5..327a9f1079 100644 --- a/tests/admin_filters/tests.py +++ b/tests/admin_filters/tests.py @@ -12,7 +12,7 @@ from django.contrib.auth.models import User from django.core.exceptions import ImproperlyConfigured from django.test import RequestFactory, TestCase, override_settings -from .models import Book, Bookmark, Department, Employee, TaggedItem +from .models import Book, Bookmark, Department, Employee, ImplicitlyOrderedBook, TaggedItem def select_by(dictlist, key, value): @@ -1253,3 +1253,80 @@ class ListFiltersTests(TestCase): changelist = modeladmin.get_changelist_instance(request) changelist.get_results(request) self.assertEqual(changelist.full_result_count, 4) + + def test_relatedfieldlistfilter_foreignkey_ordering_fallback_to_model_meta(self): + """ + RelatedFieldListFilter ordering falls back to the related model's + Meta.ordering when there's no ModelAdmin registered for it. + """ + # Create ImplicitlyOrderedBook instances with different titles + # to test that ordering uses Meta.ordering + ImplicitlyOrderedBook.objects.create(title='Zulu Book', author=self.alfred) + ImplicitlyOrderedBook.objects.create(title='Alpha Book', author=self.bob) + ImplicitlyOrderedBook.objects.create(title='Beta Book', author=self.lisa) + + class ImplicitlyOrderedBookAdmin(ModelAdmin): + list_filter = ('author',) + + modeladmin = ImplicitlyOrderedBookAdmin(ImplicitlyOrderedBook, site) + + request = self.request_factory.get('/') + request.user = self.alfred + changelist = modeladmin.get_changelist_instance(request) + filterspec = changelist.get_filters(request)[0][0] + + # User model doesn't have Meta.ordering, so should be ordered by pk + # which is the default behavior + expected = [(self.alfred.pk, 'alfred'), (self.bob.pk, 'bob'), (self.lisa.pk, 'lisa')] + self.assertEqual(filterspec.lookup_choices, expected) + + def test_relatedonlyfieldlistfilter_foreignkey_ordering_with_model_admin(self): + """ + RelatedOnlyFieldListFilter ordering respects ModelAdmin.ordering. + """ + class EmployeeAdminWithOrdering(ModelAdmin): + ordering = ('name',) + + class BookAdmin(ModelAdmin): + list_filter = (('employee', RelatedOnlyFieldListFilter),) + + site.register(Employee, EmployeeAdminWithOrdering) + self.addCleanup(lambda: site.unregister(Employee)) + + self.djangonaut_book.employee = self.john + self.djangonaut_book.save() + self.bio_book.employee = self.jack + self.bio_book.save() + + modeladmin = BookAdmin(Book, site) + + request = self.request_factory.get('/') + request.user = self.alfred + changelist = modeladmin.get_changelist_instance(request) + filterspec = changelist.get_filters(request)[0][0] + expected = [(self.jack.pk, 'Jack Red'), (self.john.pk, 'John Blue')] + self.assertEqual(filterspec.lookup_choices, expected) + + def test_relatedonlyfieldlistfilter_foreignkey_ordering_fallback_to_model_meta(self): + """ + RelatedOnlyFieldListFilter ordering falls back to the related model's + Meta.ordering when there's no ModelAdmin registered for it. + """ + # Create ImplicitlyOrderedBook instances + book1 = ImplicitlyOrderedBook.objects.create(title='Zulu Book', author=self.alfred) + book2 = ImplicitlyOrderedBook.objects.create(title='Alpha Book', author=self.bob) + book3 = ImplicitlyOrderedBook.objects.create(title='Beta Book', author=self.lisa) + + class ImplicitlyOrderedBookAdmin(ModelAdmin): + list_filter = (('author', RelatedOnlyFieldListFilter),) + + modeladmin = ImplicitlyOrderedBookAdmin(ImplicitlyOrderedBook, site) + + request = self.request_factory.get('/') + request.user = self.alfred + changelist = modeladmin.get_changelist_instance(request) + filterspec = changelist.get_filters(request)[0][0] + + # User model doesn't have Meta.ordering, so should be ordered by pk + expected = [(self.alfred.pk, 'alfred'), (self.bob.pk, 'bob'), (self.lisa.pk, 'lisa')] + self.assertEqual(filterspec.lookup_choices, expected)