From d9330d5be2ee60b208dcab2616eb164ea2e6bf36 Mon Sep 17 00:00:00 2001 From: Juan Pedro Fisanotti Date: Sat, 23 Feb 2013 15:02:01 -0300 Subject: [PATCH] Fixed #6585 -- Admin relationship widgets: Respect ordering defined by target model's ModelAdmin. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Thanks Gary Wilson for the report and Juan Pedro Fisanotti, Carlos Matías de la Torre for the fix. --- AUTHORS | 2 ++ django/contrib/admin/options.py | 21 +++++++++++++++ tests/admin_ordering/models.py | 1 + tests/admin_ordering/tests.py | 48 +++++++++++++++++++++++++++++++++ 4 files changed, 72 insertions(+) diff --git a/AUTHORS b/AUTHORS index d59404e7718..a7adc5a9edd 100644 --- a/AUTHORS +++ b/AUTHORS @@ -165,6 +165,7 @@ answer newbie questions, and generally made Django that much better: Matt Dennenbaum deric@monowerks.com Max Derkachev + Carlos Matías de la Torre Rajesh Dhawan Sander Dijkhuis Jordan Dimov @@ -205,6 +206,7 @@ answer newbie questions, and generally made Django that much better: Stefane Fermgier J. Pablo Fernandez Maciej Fijalkowski + Juan Pedro Fisanotti Ben Firshman Matthew Flanagan Eric Floehr diff --git a/django/contrib/admin/options.py b/django/contrib/admin/options.py index 8f78aaf8dd4..cce92c6a95b 100644 --- a/django/contrib/admin/options.py +++ b/django/contrib/admin/options.py @@ -157,6 +157,19 @@ class BaseModelAdmin(six.with_metaclass(forms.MediaDefiningClass)): ) return db_field.formfield(**kwargs) + def get_field_queryset(self, db, db_field, request): + """ + If the ModelAdmin specifies ordering, the queryset should respect that + ordering. Otherwise don't specify the queryset, let the field decide + (returns None in that case). + """ + related_admin = self.admin_site._registry.get(db_field.rel.to, None) + if related_admin is not None: + ordering = related_admin.get_ordering(request) + if ordering is not None and ordering != (): + return db_field.rel.to._default_manager.using(db).order_by(*ordering).complex_filter(db_field.rel.limit_choices_to) + return None + def formfield_for_foreignkey(self, db_field, request=None, **kwargs): """ Get a form Field for a ForeignKey. @@ -171,6 +184,10 @@ class BaseModelAdmin(six.with_metaclass(forms.MediaDefiningClass)): }) kwargs['empty_label'] = db_field.blank and _('None') or None + queryset = self.get_field_queryset(db, db_field, request) + if queryset is not None: + kwargs['queryset'] = queryset + return db_field.formfield(**kwargs) def formfield_for_manytomany(self, db_field, request=None, **kwargs): @@ -190,6 +207,10 @@ class BaseModelAdmin(six.with_metaclass(forms.MediaDefiningClass)): elif db_field.name in (list(self.filter_vertical) + list(self.filter_horizontal)): kwargs['widget'] = widgets.FilteredSelectMultiple(db_field.verbose_name, (db_field.name in self.filter_vertical)) + queryset = self.get_field_queryset(db, db_field, request) + if queryset is not None: + kwargs['queryset'] = queryset + return db_field.formfield(**kwargs) def _declared_fieldsets(self): diff --git a/tests/admin_ordering/models.py b/tests/admin_ordering/models.py index 195b3c36946..3da52b1b00f 100644 --- a/tests/admin_ordering/models.py +++ b/tests/admin_ordering/models.py @@ -15,6 +15,7 @@ class Song(models.Model): band = models.ForeignKey(Band) name = models.CharField(max_length=100) duration = models.IntegerField() + other_interpreters = models.ManyToManyField(Band, related_name='covers') class Meta: ordering = ('name',) diff --git a/tests/admin_ordering/tests.py b/tests/admin_ordering/tests.py index faae834f930..10faa9533f2 100644 --- a/tests/admin_ordering/tests.py +++ b/tests/admin_ordering/tests.py @@ -1,6 +1,7 @@ from __future__ import absolute_import, unicode_literals from django.test import TestCase, RequestFactory +from django.contrib import admin from django.contrib.admin.options import ModelAdmin from django.contrib.auth.models import User @@ -104,3 +105,50 @@ class TestInlineModelAdminOrdering(TestCase): inline = SongInlineNewOrdering(self.b, None) names = [s.name for s in inline.queryset(request)] self.assertEqual(['Jaded', 'Pink', 'Dude (Looks Like a Lady)'], names) + + +class TestRelatedFieldsAdminOrdering(TestCase): + def setUp(self): + self.b1 = Band(name='Pink Floyd', bio='', rank=1) + self.b1.save() + self.b2 = Band(name='Foo Fighters', bio='', rank=5) + self.b2.save() + + # we need to register a custom ModelAdmin (instead of just using + # ModelAdmin) because the field creator tries to find the ModelAdmin + # for the related model + class SongAdmin(admin.ModelAdmin): + pass + admin.site.register(Song, SongAdmin) + + def check_ordering_of_field_choices(self, correct_ordering): + fk_field = admin.site._registry[Song].formfield_for_foreignkey(Song.band.field) + m2m_field = admin.site._registry[Song].formfield_for_manytomany(Song.other_interpreters.field) + + self.assertEqual(list(fk_field.queryset), correct_ordering) + self.assertEqual(list(m2m_field.queryset), correct_ordering) + + def test_no_admin_fallback_to_model_ordering(self): + # should be ordered by name (as defined by the model) + self.check_ordering_of_field_choices([self.b2, self.b1]) + + def test_admin_with_no_ordering_fallback_to_model_ordering(self): + class NoOrderingBandAdmin(admin.ModelAdmin): + pass + admin.site.register(Band, NoOrderingBandAdmin) + + # should be ordered by name (as defined by the model) + self.check_ordering_of_field_choices([self.b2, self.b1]) + + def test_admin_ordering_beats_model_ordering(self): + class StaticOrderingBandAdmin(admin.ModelAdmin): + ordering = ('rank', ) + admin.site.register(Band, StaticOrderingBandAdmin) + + # should be ordered by rank (defined by the ModelAdmin) + self.check_ordering_of_field_choices([self.b1, self.b2]) + + def tearDown(self): + admin.site.unregister(Song) + if Band in admin.site._registry: + admin.site.unregister(Band)