Fixed #16311 -- Added a RelatedOnlyFieldListFilter class in admin.filters.
This commit is contained in:
parent
bf5382c6e5
commit
98e8da3709
1
AUTHORS
1
AUTHORS
|
@ -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>
|
||||||
|
|
|
@ -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",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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`
|
||||||
^^^^^^^^^^^^^^^^^^^^^^^^^^
|
^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
Loading…
Reference in New Issue