Fixed #16311 -- Added a RelatedOnlyFieldListFilter class in admin.filters.

This commit is contained in:
Stanislas Guerra 2014-05-21 12:32:19 +02:00 committed by Tim Graham
parent bf5382c6e5
commit 98e8da3709
7 changed files with 81 additions and 7 deletions

View File

@ -242,6 +242,7 @@ answer newbie questions, and generally made Django that much better:
Owen Griffiths Owen Griffiths
Espen Grindhaug <http://grindhaug.org/> Espen Grindhaug <http://grindhaug.org/>
Mike Grouchy <http://mikegrouchy.com/> Mike Grouchy <http://mikegrouchy.com/>
Stanislas Guerra <stan@slashdev.me>
Janos Guljas Janos Guljas
Thomas Güttler <hv@tbz-pariv.de> Thomas Güttler <hv@tbz-pariv.de>
Horst Gutmann <zerok@zerokspot.com> Horst Gutmann <zerok@zerokspot.com>

View File

@ -6,7 +6,8 @@ from django.contrib.admin.options import (HORIZONTAL, VERTICAL,
ModelAdmin, StackedInline, TabularInline) ModelAdmin, StackedInline, TabularInline)
from django.contrib.admin.filters import (ListFilter, SimpleListFilter, from django.contrib.admin.filters import (ListFilter, SimpleListFilter,
FieldListFilter, BooleanFieldListFilter, RelatedFieldListFilter, FieldListFilter, BooleanFieldListFilter, RelatedFieldListFilter,
ChoicesFieldListFilter, DateFieldListFilter, AllValuesFieldListFilter) ChoicesFieldListFilter, DateFieldListFilter, AllValuesFieldListFilter,
RelatedOnlyFieldListFilter)
from django.contrib.admin.sites import AdminSite, site from django.contrib.admin.sites import AdminSite, site
from django.utils.module_loading import autodiscover_modules from django.utils.module_loading import autodiscover_modules
@ -15,7 +16,7 @@ __all__ = [
"StackedInline", "TabularInline", "AdminSite", "site", "ListFilter", "StackedInline", "TabularInline", "AdminSite", "site", "ListFilter",
"SimpleListFilter", "FieldListFilter", "BooleanFieldListFilter", "SimpleListFilter", "FieldListFilter", "BooleanFieldListFilter",
"RelatedFieldListFilter", "ChoicesFieldListFilter", "DateFieldListFilter", "RelatedFieldListFilter", "ChoicesFieldListFilter", "DateFieldListFilter",
"AllValuesFieldListFilter", "autodiscover", "AllValuesFieldListFilter", "RelatedOnlyFieldListFilter", "autodiscover",
] ]

View File

@ -170,7 +170,7 @@ class RelatedFieldListFilter(FieldListFilter):
self.lookup_kwarg_isnull = '%s__isnull' % field_path self.lookup_kwarg_isnull = '%s__isnull' % field_path
self.lookup_val = request.GET.get(self.lookup_kwarg) self.lookup_val = request.GET.get(self.lookup_kwarg)
self.lookup_val_isnull = request.GET.get(self.lookup_kwarg_isnull) self.lookup_val_isnull = request.GET.get(self.lookup_kwarg_isnull)
self.lookup_choices = field.get_choices(include_blank=False) self.lookup_choices = self.field_choices(field, request, model_admin)
super(RelatedFieldListFilter, self).__init__( super(RelatedFieldListFilter, self).__init__(
field, request, params, model, model_admin, field_path) field, request, params, model, model_admin, field_path)
if hasattr(field, 'verbose_name'): if hasattr(field, 'verbose_name'):
@ -191,6 +191,9 @@ class RelatedFieldListFilter(FieldListFilter):
def expected_parameters(self): def expected_parameters(self):
return [self.lookup_kwarg, self.lookup_kwarg_isnull] return [self.lookup_kwarg, self.lookup_kwarg_isnull]
def field_choices(self, field, request, model_admin):
return field.get_choices(include_blank=False)
def choices(self, cl): def choices(self, cl):
from django.contrib.admin.views.main import EMPTY_CHANGELIST_VALUE from django.contrib.admin.views.main import EMPTY_CHANGELIST_VALUE
yield { yield {
@ -410,3 +413,9 @@ class AllValuesFieldListFilter(FieldListFilter):
} }
FieldListFilter.register(lambda f: True, AllValuesFieldListFilter) FieldListFilter.register(lambda f: True, AllValuesFieldListFilter)
class RelatedOnlyFieldListFilter(RelatedFieldListFilter):
def field_choices(self, field, request, model_admin):
limit_choices_to = {'pk__in': set(model_admin.get_queryset(request).values_list(field.name, flat=True))}
return field.get_choices(include_blank=False, limit_choices_to=limit_choices_to)

View File

@ -726,7 +726,7 @@ class Field(RegisterLookupMixin):
def get_validator_unique_lookup_type(self): def get_validator_unique_lookup_type(self):
return '%s__exact' % self.name return '%s__exact' % self.name
def get_choices(self, include_blank=True, blank_choice=BLANK_CHOICE_DASH): def get_choices(self, include_blank=True, blank_choice=BLANK_CHOICE_DASH, limit_choices_to=None):
"""Returns choices with a default blank choices included, for use """Returns choices with a default blank choices included, for use
as SelectField choices for this field.""" as SelectField choices for this field."""
blank_defined = False blank_defined = False
@ -743,15 +743,16 @@ class Field(RegisterLookupMixin):
if self.choices: if self.choices:
return first_choice + choices return first_choice + choices
rel_model = self.rel.to rel_model = self.rel.to
limit_choices_to = limit_choices_to or self.get_limit_choices_to()
if hasattr(self.rel, 'get_related_field'): if hasattr(self.rel, 'get_related_field'):
lst = [(getattr(x, self.rel.get_related_field().attname), lst = [(getattr(x, self.rel.get_related_field().attname),
smart_text(x)) smart_text(x))
for x in rel_model._default_manager.complex_filter( for x in rel_model._default_manager.complex_filter(
self.get_limit_choices_to())] limit_choices_to)]
else: else:
lst = [(x._get_pk_val(), smart_text(x)) lst = [(x._get_pk_val(), smart_text(x))
for x in rel_model._default_manager.complex_filter( for x in rel_model._default_manager.complex_filter(
self.get_limit_choices_to())] limit_choices_to)]
return first_choice + lst return first_choice + lst
def get_choices_default(self): def get_choices_default(self):

View File

@ -880,6 +880,20 @@ subclass::
('is_staff', admin.BooleanFieldListFilter), ('is_staff', admin.BooleanFieldListFilter),
) )
.. versionadded:: 1.8
You can now limit the choices of a related model to the objects
involved in that relation using ``RelatedOnlyFieldListFilter``::
class BookAdmin(admin.ModelAdmin):
list_filter = (
('author', admin.RelatedOnlyFieldListFilter),
)
Assuming ``author`` is a ``ForeignKey`` to a ``User`` model, this will
limit the ``list_filter`` choices to the users who have written a book
instead of listing all users.
.. note:: .. note::
The ``FieldListFilter`` API is considered internal and might be The ``FieldListFilter`` API is considered internal and might be

View File

@ -39,6 +39,11 @@ Minor features
:attr:`~django.contrib.admin.InlineModelAdmin.show_change_link` that :attr:`~django.contrib.admin.InlineModelAdmin.show_change_link` that
supports showing a link to an inline object's change form. supports showing a link to an inline object's change form.
* Use the new ``django.contrib.admin.RelatedOnlyFieldListFilter`` in
:attr:`ModelAdmin.list_filter <django.contrib.admin.ModelAdmin.list_filter>`
to limit the ``list_filter`` choices to foreign objects which are attached to
those from the ``ModelAdmin``.
:mod:`django.contrib.auth` :mod:`django.contrib.auth`
^^^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^

View File

@ -3,7 +3,7 @@ from __future__ import unicode_literals
import datetime import datetime
from django.contrib.admin import (site, ModelAdmin, SimpleListFilter, from django.contrib.admin import (site, ModelAdmin, SimpleListFilter,
BooleanFieldListFilter, AllValuesFieldListFilter) BooleanFieldListFilter, AllValuesFieldListFilter, RelatedOnlyFieldListFilter)
from django.contrib.admin.views.main import ChangeList from django.contrib.admin.views.main import ChangeList
from django.contrib.auth.admin import UserAdmin from django.contrib.auth.admin import UserAdmin
from django.contrib.auth.models import User from django.contrib.auth.models import User
@ -134,6 +134,15 @@ class BookAdminWithUnderscoreLookupAndTuple(BookAdmin):
list_filter = ('year', ('author__email', AllValuesFieldListFilter), 'contributors', 'is_best_seller', 'date_registered', 'no') list_filter = ('year', ('author__email', AllValuesFieldListFilter), 'contributors', 'is_best_seller', 'date_registered', 'no')
class BookAdminRelatedOnlyFilter(ModelAdmin):
list_filter = (
'year', 'is_best_seller', 'date_registered', 'no',
('author', RelatedOnlyFieldListFilter),
('contributors', RelatedOnlyFieldListFilter),
)
ordering = ('-id',)
class DecadeFilterBookAdmin(ModelAdmin): class DecadeFilterBookAdmin(ModelAdmin):
list_filter = ('author', DecadeListFilterWithTitleAndParameter) list_filter = ('author', DecadeListFilterWithTitleAndParameter)
ordering = ('-id',) ordering = ('-id',)
@ -359,6 +368,13 @@ class ListFiltersTests(TestCase):
def test_relatedfieldlistfilter_foreignkey(self): def test_relatedfieldlistfilter_foreignkey(self):
modeladmin = BookAdmin(Book, site) modeladmin = BookAdmin(Book, site)
request = self.request_factory.get('/')
changelist = self.get_changelist(request, Book, modeladmin)
# Make sure that all users are present in the author's list filter
filterspec = changelist.get_filters(request)[0][1]
self.assertEqual(filterspec.lookup_choices, [(1, 'alfred'), (2, 'bob'), (3, 'lisa')])
request = self.request_factory.get('/', {'author__isnull': 'True'}) request = self.request_factory.get('/', {'author__isnull': 'True'})
changelist = self.get_changelist(request, Book, modeladmin) changelist = self.get_changelist(request, Book, modeladmin)
@ -387,6 +403,13 @@ class ListFiltersTests(TestCase):
def test_relatedfieldlistfilter_manytomany(self): def test_relatedfieldlistfilter_manytomany(self):
modeladmin = BookAdmin(Book, site) modeladmin = BookAdmin(Book, site)
request = self.request_factory.get('/')
changelist = self.get_changelist(request, Book, modeladmin)
# Make sure that all users are present in the contrib's list filter
filterspec = changelist.get_filters(request)[0][2]
self.assertEqual(filterspec.lookup_choices, [(1, 'alfred'), (2, 'bob'), (3, 'lisa')])
request = self.request_factory.get('/', {'contributors__isnull': 'True'}) request = self.request_factory.get('/', {'contributors__isnull': 'True'})
changelist = self.get_changelist(request, Book, modeladmin) changelist = self.get_changelist(request, Book, modeladmin)
@ -464,6 +487,26 @@ class ListFiltersTests(TestCase):
self.assertEqual(choice['selected'], True) self.assertEqual(choice['selected'], True)
self.assertEqual(choice['query_string'], '?books_contributed__id__exact=%d' % self.django_book.pk) self.assertEqual(choice['query_string'], '?books_contributed__id__exact=%d' % self.django_book.pk)
def test_relatedonlyfieldlistfilter_foreignkey(self):
modeladmin = BookAdminRelatedOnlyFilter(Book, site)
request = self.request_factory.get('/')
changelist = self.get_changelist(request, Book, modeladmin)
# Make sure that only actual authors are present in author's list filter
filterspec = changelist.get_filters(request)[0][1]
self.assertEqual(filterspec.lookup_choices, [(1, 'alfred'), (2, 'bob')])
def test_relatedonlyfieldlistfilter_manytomany(self):
modeladmin = BookAdminRelatedOnlyFilter(Book, site)
request = self.request_factory.get('/')
changelist = self.get_changelist(request, Book, modeladmin)
# Make sure that only actual contributors are present in contrib's list filter
filterspec = changelist.get_filters(request)[0][2]
self.assertEqual(filterspec.lookup_choices, [(2, 'bob'), (3, 'lisa')])
def test_booleanfieldlistfilter(self): def test_booleanfieldlistfilter(self):
modeladmin = BookAdmin(Book, site) modeladmin = BookAdmin(Book, site)
self.verify_booleanfieldlistfilter(modeladmin) self.verify_booleanfieldlistfilter(modeladmin)