diff --git a/django/contrib/admin/options.py b/django/contrib/admin/options.py index 70b2ccba21..5a5682a6cd 100644 --- a/django/contrib/admin/options.py +++ b/django/contrib/admin/options.py @@ -371,16 +371,20 @@ class BaseModelAdmin(metaclass=forms.MediaDefiningClass): if len(relation_parts) <= 1: # Either a local field filter, or no fields at all. return True - clean_lookup = LOOKUP_SEP.join(relation_parts) - valid_lookups = [self.date_hierarchy] + valid_lookups = {self.date_hierarchy} for filter_item in self.list_filter: if isinstance(filter_item, type) and issubclass(filter_item, SimpleListFilter): - valid_lookups.append(filter_item.parameter_name) + valid_lookups.add(filter_item.parameter_name) elif isinstance(filter_item, (list, tuple)): - valid_lookups.append(filter_item[0]) + valid_lookups.add(filter_item[0]) else: - valid_lookups.append(filter_item) - return clean_lookup in valid_lookups + valid_lookups.add(filter_item) + + # Is it a valid relational lookup? + return not { + LOOKUP_SEP.join(relation_parts), + LOOKUP_SEP.join(relation_parts + [part]) + }.isdisjoint(valid_lookups) def to_field_allowed(self, request, to_field): """ diff --git a/tests/modeladmin/tests.py b/tests/modeladmin/tests.py index b85764a290..4862664dbc 100644 --- a/tests/modeladmin/tests.py +++ b/tests/modeladmin/tests.py @@ -8,8 +8,10 @@ from django.contrib.admin.options import ( from django.contrib.admin.sites import AdminSite from django.contrib.admin.widgets import AdminDateWidget, AdminRadioSelect from django.contrib.auth.models import User +from django.db import models from django.forms.widgets import Select from django.test import SimpleTestCase, TestCase +from django.test.utils import isolate_apps from .models import Band, Concert @@ -90,6 +92,33 @@ class ModelAdminTests(TestCase): ma = BandAdmin(Band, self.site) self.assertTrue(ma.lookup_allowed('name__nonexistent', 'test_value')) + @isolate_apps('modeladmin') + def test_lookup_allowed_onetoone(self): + class Department(models.Model): + code = models.CharField(max_length=4, unique=True) + + class Employee(models.Model): + department = models.ForeignKey(Department, models.CASCADE, to_field="code") + + class EmployeeProfile(models.Model): + employee = models.OneToOneField(Employee, models.CASCADE) + + class EmployeeInfo(models.Model): + employee = models.OneToOneField(Employee, models.CASCADE) + description = models.CharField(max_length=100) + + class EmployeeProfileAdmin(ModelAdmin): + list_filter = [ + 'employee__employeeinfo__description', + 'employee__department__code', + ] + + ma = EmployeeProfileAdmin(EmployeeProfile, self.site) + # Reverse OneToOneField + self.assertIs(ma.lookup_allowed('employee__employeeinfo__description', 'test_value'), True) + # OneToOneField and ForeignKey + self.assertIs(ma.lookup_allowed('employee__department__code', 'test_value'), True) + def test_field_arguments(self): # If fields is specified, fieldsets_add and fieldsets_change should # just stick the fields into a formsets structure and return it.