diff --git a/django/contrib/admin/filters.py b/django/contrib/admin/filters.py index f986bca713..d6f260bb61 100644 --- a/django/contrib/admin/filters.py +++ b/django/contrib/admin/filters.py @@ -269,25 +269,43 @@ FieldListFilter.register( class ChoicesFieldListFilter(FieldListFilter): def __init__(self, field, request, params, model, model_admin, field_path): self.lookup_kwarg = '%s__exact' % field_path + self.lookup_kwarg_isnull = '%s__isnull' % field_path self.lookup_val = request.GET.get(self.lookup_kwarg) + self.lookup_val_isnull = request.GET.get(self.lookup_kwarg_isnull) super(ChoicesFieldListFilter, self).__init__( field, request, params, model, model_admin, field_path) def expected_parameters(self): - return [self.lookup_kwarg] + return [self.lookup_kwarg, self.lookup_kwarg_isnull] def choices(self, changelist): yield { 'selected': self.lookup_val is None, - 'query_string': changelist.get_query_string({}, [self.lookup_kwarg]), + 'query_string': changelist.get_query_string( + {}, [self.lookup_kwarg, self.lookup_kwarg_isnull] + ), 'display': _('All') } + none_title = '' for lookup, title in self.field.flatchoices: + if lookup is None: + none_title = title + continue yield { 'selected': smart_text(lookup) == self.lookup_val, - 'query_string': changelist.get_query_string({self.lookup_kwarg: lookup}), + 'query_string': changelist.get_query_string( + {self.lookup_kwarg: lookup}, [self.lookup_kwarg_isnull] + ), 'display': title, } + if none_title: + yield { + 'selected': bool(self.lookup_val_isnull), + 'query_string': changelist.get_query_string({ + self.lookup_kwarg_isnull: 'True', + }, [self.lookup_kwarg]), + 'display': none_title, + } FieldListFilter.register(lambda f: bool(f.choices), ChoicesFieldListFilter) diff --git a/tests/admin_filters/models.py b/tests/admin_filters/models.py index db37958882..8860201d35 100644 --- a/tests/admin_filters/models.py +++ b/tests/admin_filters/models.py @@ -75,5 +75,12 @@ class Bookmark(models.Model): url = models.URLField() tags = GenericRelation(TaggedItem) + CHOICES = [ + ('a', 'A'), + (None, 'None'), + ('', '-'), + ] + none_or_null = models.CharField(max_length=20, choices=CHOICES, blank=True, null=True) + def __str__(self): return self.url diff --git a/tests/admin_filters/tests.py b/tests/admin_filters/tests.py index 3d2f7ab9c6..ba6934f4ed 100644 --- a/tests/admin_filters/tests.py +++ b/tests/admin_filters/tests.py @@ -302,6 +302,22 @@ class ListFiltersTests(TestCase): modeladmin.list_max_show_all, modeladmin.list_editable, modeladmin, ) + def test_choicesfieldlistfilter_has_none_choice(self): + """ + The last choice is for the None value. + """ + class BookmarkChoicesAdmin(ModelAdmin): + list_display = ['none_or_null'] + list_filter = ['none_or_null'] + + modeladmin = BookmarkChoicesAdmin(Bookmark, site) + request = self.request_factory.get('/', {}) + changelist = self.get_changelist(request, Bookmark, modeladmin) + filterspec = changelist.get_filters(request)[0][0] + choices = list(filterspec.choices(changelist)) + self.assertEqual(choices[-1]['display'], 'None') + self.assertEqual(choices[-1]['query_string'], '?none_or_null__isnull=True') + def test_datefieldlistfilter(self): modeladmin = BookAdmin(Book, site)