summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorFederico Jaramillo Martínez <federicojaramillom@gmail.com>2020-08-27 22:23:42 +0200
committerMariusz Felisiak <felisiak.mariusz@gmail.com>2020-08-31 10:40:21 +0200
commitc4e5384e739a1f79fe9f7d78431e3b48fcb09f48 (patch)
treea4cf13a442ee33923d90ec35835ca42e355bfaef
parent2986ec031d311891af8c59c845ce3b5e46d17a76 (diff)
[3.1.x] Fixed #31952 -- Fixed EmptyFieldListFilter crash with reverse relationships.
Thanks dacotagh for the report. Backport of 179d9dc0c2265176f9f7062a1d98dc44d896f91f from master
-rw-r--r--django/db/models/fields/reverse_related.py1
-rw-r--r--docs/releases/3.1.1.txt3
-rw-r--r--tests/admin_filters/models.py4
-rw-r--r--tests/admin_filters/tests.py49
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('/')