diff --git a/django/contrib/admin/filterspecs.py b/django/contrib/admin/filterspecs.py index 965b32bb08..05f6b1ad5d 100644 --- a/django/contrib/admin/filterspecs.py +++ b/django/contrib/admin/filterspecs.py @@ -74,9 +74,12 @@ class RelatedFilterSpec(FilterSpec): self.lookup_title = other_model._meta.verbose_name else: self.lookup_title = f.verbose_name # use field name - rel_name = other_model._meta.pk.name + if hasattr(f, 'rel'): + rel_name = f.rel.get_related_field().name + else: + rel_name = other_model._meta.pk.name self.lookup_kwarg = '%s__%s__exact' % (self.field_path, rel_name) - self.lookup_kwarg_isnull = '%s__isnull' % (self.field_path) + self.lookup_kwarg_isnull = '%s__isnull' % self.field_path self.lookup_val = request.GET.get(self.lookup_kwarg, None) self.lookup_val_isnull = request.GET.get( self.lookup_kwarg_isnull, None) @@ -176,7 +179,7 @@ FilterSpec.register(lambda f: bool(f.choices), ChoicesFilterSpec) class DateFieldFilterSpec(FilterSpec): def __init__(self, f, request, params, model, model_admin, - field_path=None): + field_path=None): super(DateFieldFilterSpec, self).__init__(f, request, params, model, model_admin, field_path=field_path) diff --git a/django/contrib/admin/options.py b/django/contrib/admin/options.py index 43c5503f99..3d06050c54 100644 --- a/django/contrib/admin/options.py +++ b/django/contrib/admin/options.py @@ -225,18 +225,18 @@ class BaseModelAdmin(object): # if foo has been specificially included in the lookup list; so # drop __id if it is the last part. However, first we need to find # the pk attribute name. - pk_attr_name = None + rel_name = None for part in parts[:-1]: field, _, _, _ = model._meta.get_field_by_name(part) if hasattr(field, 'rel'): model = field.rel.to - pk_attr_name = model._meta.pk.name + rel_name = field.rel.get_related_field().name elif isinstance(field, RelatedObject): model = field.model - pk_attr_name = model._meta.pk.name + rel_name = model._meta.pk.name else: - pk_attr_name = None - if pk_attr_name and len(parts) > 1 and parts[-1] == pk_attr_name: + rel_name = None + if rel_name and len(parts) > 1 and parts[-1] == rel_name: parts.pop() try: diff --git a/tests/regressiontests/admin_filterspecs/models.py b/tests/regressiontests/admin_filterspecs/models.py index 5b284c7799..55239d940d 100644 --- a/tests/regressiontests/admin_filterspecs/models.py +++ b/tests/regressiontests/admin_filterspecs/models.py @@ -21,3 +21,18 @@ class BoolTest(models.Model): default=NO, choices=YES_NO_CHOICES ) + + +class Department(models.Model): + code = models.CharField(max_length=4, unique=True) + description = models.CharField(max_length=50, blank=True, null=True) + + def __unicode__(self): + return self.description + +class Employee(models.Model): + department = models.ForeignKey(Department, to_field="code") + name = models.CharField(max_length=100) + + def __unicode__(self): + return self.name diff --git a/tests/regressiontests/admin_filterspecs/tests.py b/tests/regressiontests/admin_filterspecs/tests.py index 8b9e734313..c73e53e428 100644 --- a/tests/regressiontests/admin_filterspecs/tests.py +++ b/tests/regressiontests/admin_filterspecs/tests.py @@ -6,7 +6,7 @@ from django.contrib import admin from django.contrib.admin.views.main import ChangeList from django.utils.encoding import force_unicode -from models import Book, BoolTest +from models import Book, BoolTest, Employee, Department def select_by(dictlist, key, value): return [x for x in dictlist if x[key] == value][0] @@ -32,7 +32,6 @@ class FilterSpecsTests(TestCase): self.request_factory = RequestFactory() - def get_changelist(self, request, model, modeladmin): return ChangeList(request, model, modeladmin.list_display, modeladmin.list_display_links, modeladmin.list_filter, modeladmin.date_hierarchy, modeladmin.search_fields, @@ -200,6 +199,67 @@ class FilterSpecsTests(TestCase): self.assertEqual(choice['selected'], True) self.assertEqual(choice['query_string'], '?completed__exact=1') + def test_fk_with_to_field(self): + """ + Ensure that a filter on a FK respects the FK's to_field attribute. + Refs #17972. + """ + modeladmin = EmployeeAdmin(Employee, admin.site) + + dev = Department.objects.create(code='DEV', description='Development') + design = Department.objects.create(code='DSN', description='Design') + john = Employee.objects.create(name='John Blue', department=dev) + jack = Employee.objects.create(name='Jack Red', department=design) + + request = self.request_factory.get('/', {}) + changelist = self.get_changelist(request, Employee, modeladmin) + + # Make sure the correct queryset is returned + queryset = changelist.get_query_set() + self.assertEqual(list(queryset), [jack, john]) + + filterspec = changelist.get_filters(request)[0][-1] + self.assertEqual(force_unicode(filterspec.title()), u'department') + choices = list(filterspec.choices(changelist)) + + self.assertEqual(choices[0]['display'], u'All') + self.assertEqual(choices[0]['selected'], True) + self.assertEqual(choices[0]['query_string'], '?') + + self.assertEqual(choices[1]['display'], u'Development') + self.assertEqual(choices[1]['selected'], False) + self.assertEqual(choices[1]['query_string'], '?department__code__exact=DEV') + + self.assertEqual(choices[2]['display'], u'Design') + self.assertEqual(choices[2]['selected'], False) + self.assertEqual(choices[2]['query_string'], '?department__code__exact=DSN') + + # Filter by Department=='Development' -------------------------------- + + request = self.request_factory.get('/', {'department__code__exact': 'DEV'}) + changelist = self.get_changelist(request, Employee, modeladmin) + + # Make sure the correct queryset is returned + queryset = changelist.get_query_set() + self.assertEqual(list(queryset), [john]) + + filterspec = changelist.get_filters(request)[0][-1] + self.assertEqual(force_unicode(filterspec.title()), u'department') + choices = list(filterspec.choices(changelist)) + + self.assertEqual(choices[0]['display'], u'All') + self.assertEqual(choices[0]['selected'], False) + self.assertEqual(choices[0]['query_string'], '?') + + self.assertEqual(choices[1]['display'], u'Development') + self.assertEqual(choices[1]['selected'], True) + self.assertEqual(choices[1]['query_string'], '?department__code__exact=DEV') + + self.assertEqual(choices[2]['display'], u'Design') + self.assertEqual(choices[2]['selected'], False) + self.assertEqual(choices[2]['query_string'], '?department__code__exact=DSN') + + class CustomUserAdmin(UserAdmin): list_filter = ('books_authored', 'books_contributed') @@ -209,3 +269,7 @@ class BookAdmin(admin.ModelAdmin): class BoolTestAdmin(admin.ModelAdmin): list_filter = ('completed',) + +class EmployeeAdmin(admin.ModelAdmin): + list_display = ['name', 'department'] + list_filter = ['department'] \ No newline at end of file