Kaydet (Commit) 98e8da37 authored tarafından Stanislas Guerra's avatar Stanislas Guerra Kaydeden (comit) Tim Graham

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

üst bf5382c6
...@@ -242,6 +242,7 @@ answer newbie questions, and generally made Django that much better: ...@@ -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, ...@@ -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__ = [ ...@@ -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): ...@@ -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): ...@@ -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): ...@@ -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): ...@@ -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): ...@@ -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:: ...@@ -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 ...@@ -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 ...@@ -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): ...@@ -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): ...@@ -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): ...@@ -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): ...@@ -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)
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment