From 179d9dc0c2265176f9f7062a1d98dc44d896f91f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Federico=20Jaramillo=20Marti=CC=81nez?= Date: Thu, 27 Aug 2020 22:23:42 +0200 Subject: [PATCH] Fixed #31952 -- Fixed EmptyFieldListFilter crash with reverse relationships. Thanks dacotagh for the report. --- django/db/models/fields/reverse_related.py | 1 + docs/releases/3.1.1.txt | 3 ++ tests/admin_filters/models.py | 4 ++ tests/admin_filters/tests.py | 49 +++++++++++++++++++++- 4 files changed, 55 insertions(+), 2 deletions(-) diff --git a/django/db/models/fields/reverse_related.py b/django/db/models/fields/reverse_related.py index 8f0c95962d..77c5d23d69 100644 --- a/django/db/models/fields/reverse_related.py +++ b/django/db/models/fields/reverse_related.py @@ -33,6 +33,7 @@ class ForeignObjectRel(FieldCacheMixin): # Reverse relations are always nullable (Django can't enforce that a # foreign key on the related model points to this model). null = True + empty_strings_allowed = False def __init__(self, field, to, related_name=None, related_query_name=None, limit_choices_to=None, parent_link=False, on_delete=None): diff --git a/docs/releases/3.1.1.txt b/docs/releases/3.1.1.txt index b1d598bf3c..932d6b467d 100644 --- a/docs/releases/3.1.1.txt +++ b/docs/releases/3.1.1.txt @@ -59,3 +59,6 @@ Bugfixes * Fixed a ``QuerySet.delete()`` crash on MySQL, following a performance regression in Django 3.1 on MariaDB 10.3.2+, when filtering against an aggregate function (:ticket:`31965`). + +* Fixed a ``django.contrib.admin.EmptyFieldListFilter`` crash when using on + reverse relations (:ticket:`31952`). diff --git a/tests/admin_filters/models.py b/tests/admin_filters/models.py index ae78282d34..cee3af5b90 100644 --- a/tests/admin_filters/models.py +++ b/tests/admin_filters/models.py @@ -38,6 +38,10 @@ class Book(models.Model): return self.title +class ImprovedBook(models.Model): + book = models.OneToOneField(Book, models.CASCADE) + + class Department(models.Model): code = models.CharField(max_length=4, unique=True) description = models.CharField(max_length=50, blank=True, null=True) diff --git a/tests/admin_filters/tests.py b/tests/admin_filters/tests.py index 75769085e0..16a7ce495c 100644 --- a/tests/admin_filters/tests.py +++ b/tests/admin_filters/tests.py @@ -12,7 +12,9 @@ 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, ImprovedBook, TaggedItem, +) def select_by(dictlist, key, value): @@ -252,11 +254,15 @@ class BookAdminWithEmptyFieldListFilter(ModelAdmin): list_filter = [ ('author', EmptyFieldListFilter), ('title', EmptyFieldListFilter), + ('improvedbook', EmptyFieldListFilter), ] class DepartmentAdminWithEmptyFieldListFilter(ModelAdmin): - list_filter = [('description', EmptyFieldListFilter)] + list_filter = [ + ('description', EmptyFieldListFilter), + ('employee', EmptyFieldListFilter), + ] class ListFiltersTests(TestCase): @@ -1432,6 +1438,45 @@ class ListFiltersTests(TestCase): queryset = changelist.get_queryset(request) self.assertCountEqual(queryset, expected_result) + def test_emptylistfieldfilter_reverse_relationships(self): + class UserAdminReverseRelationship(UserAdmin): + list_filter = ( + ('books_contributed', EmptyFieldListFilter), + ) + + ImprovedBook.objects.create(book=self.guitar_book) + no_employees = Department.objects.create(code='NONE', description=None) + + book_admin = BookAdminWithEmptyFieldListFilter(Book, site) + department_admin = DepartmentAdminWithEmptyFieldListFilter(Department, site) + user_admin = UserAdminReverseRelationship(User, site) + + tests = [ + # Reverse one-to-one relationship. + ( + book_admin, + {'improvedbook__isempty': '1'}, + [self.django_book, self.bio_book, self.djangonaut_book], + ), + (book_admin, {'improvedbook__isempty': '0'}, [self.guitar_book]), + # Reverse foreign key relationship. + (department_admin, {'employee__isempty': '1'}, [no_employees]), + (department_admin, {'employee__isempty': '0'}, [self.dev, self.design]), + # Reverse many-to-many relationship. + (user_admin, {'books_contributed__isempty': '1'}, [self.alfred]), + (user_admin, {'books_contributed__isempty': '0'}, [self.bob, self.lisa]), + ] + for modeladmin, query_string, expected_result in tests: + with self.subTest( + modeladmin=modeladmin.__class__.__name__, + query_string=query_string, + ): + request = self.request_factory.get('/', query_string) + request.user = self.alfred + changelist = modeladmin.get_changelist_instance(request) + queryset = changelist.get_queryset(request) + self.assertCountEqual(queryset, expected_result) + def test_emptylistfieldfilter_choices(self): modeladmin = BookAdminWithEmptyFieldListFilter(Book, site) request = self.request_factory.get('/')