From 98e8da3709e400d549e87c7ff1450a2685c0adcf Mon Sep 17 00:00:00 2001 From: Stanislas Guerra Date: Wed, 21 May 2014 12:32:19 +0200 Subject: [PATCH] Fixed #16311 -- Added a RelatedOnlyFieldListFilter class in admin.filters. --- AUTHORS | 1 + django/contrib/admin/__init__.py | 5 ++-- django/contrib/admin/filters.py | 11 ++++++- django/db/models/fields/__init__.py | 7 +++-- docs/ref/contrib/admin/index.txt | 14 +++++++++ docs/releases/1.8.txt | 5 ++++ tests/admin_filters/tests.py | 45 ++++++++++++++++++++++++++++- 7 files changed, 81 insertions(+), 7 deletions(-) diff --git a/AUTHORS b/AUTHORS index a1956881cc..b96d20bb0d 100644 --- a/AUTHORS +++ b/AUTHORS @@ -242,6 +242,7 @@ answer newbie questions, and generally made Django that much better: Owen Griffiths Espen Grindhaug Mike Grouchy + Stanislas Guerra Janos Guljas Thomas Güttler Horst Gutmann diff --git a/django/contrib/admin/__init__.py b/django/contrib/admin/__init__.py index dc63d9b493..cf110ed97c 100644 --- a/django/contrib/admin/__init__.py +++ b/django/contrib/admin/__init__.py @@ -6,7 +6,8 @@ from django.contrib.admin.options import (HORIZONTAL, VERTICAL, ModelAdmin, StackedInline, TabularInline) from django.contrib.admin.filters import (ListFilter, SimpleListFilter, FieldListFilter, BooleanFieldListFilter, RelatedFieldListFilter, - ChoicesFieldListFilter, DateFieldListFilter, AllValuesFieldListFilter) + ChoicesFieldListFilter, DateFieldListFilter, AllValuesFieldListFilter, + RelatedOnlyFieldListFilter) from django.contrib.admin.sites import AdminSite, site from django.utils.module_loading import autodiscover_modules @@ -15,7 +16,7 @@ __all__ = [ "StackedInline", "TabularInline", "AdminSite", "site", "ListFilter", "SimpleListFilter", "FieldListFilter", "BooleanFieldListFilter", "RelatedFieldListFilter", "ChoicesFieldListFilter", "DateFieldListFilter", - "AllValuesFieldListFilter", "autodiscover", + "AllValuesFieldListFilter", "RelatedOnlyFieldListFilter", "autodiscover", ] diff --git a/django/contrib/admin/filters.py b/django/contrib/admin/filters.py index d5f31ab88c..08d34831cb 100644 --- a/django/contrib/admin/filters.py +++ b/django/contrib/admin/filters.py @@ -170,7 +170,7 @@ class RelatedFieldListFilter(FieldListFilter): 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) - self.lookup_choices = field.get_choices(include_blank=False) + self.lookup_choices = self.field_choices(field, request, model_admin) super(RelatedFieldListFilter, self).__init__( field, request, params, model, model_admin, field_path) if hasattr(field, 'verbose_name'): @@ -191,6 +191,9 @@ class RelatedFieldListFilter(FieldListFilter): def expected_parameters(self): 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): from django.contrib.admin.views.main import EMPTY_CHANGELIST_VALUE yield { @@ -410,3 +413,9 @@ class AllValuesFieldListFilter(FieldListFilter): } 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) diff --git a/django/db/models/fields/__init__.py b/django/db/models/fields/__init__.py index 0a90392e87..29017bed97 100644 --- a/django/db/models/fields/__init__.py +++ b/django/db/models/fields/__init__.py @@ -726,7 +726,7 @@ class Field(RegisterLookupMixin): def get_validator_unique_lookup_type(self): 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 as SelectField choices for this field.""" blank_defined = False @@ -743,15 +743,16 @@ class Field(RegisterLookupMixin): if self.choices: return first_choice + choices rel_model = self.rel.to + limit_choices_to = limit_choices_to or self.get_limit_choices_to() if hasattr(self.rel, 'get_related_field'): lst = [(getattr(x, self.rel.get_related_field().attname), smart_text(x)) for x in rel_model._default_manager.complex_filter( - self.get_limit_choices_to())] + limit_choices_to)] else: lst = [(x._get_pk_val(), smart_text(x)) for x in rel_model._default_manager.complex_filter( - self.get_limit_choices_to())] + limit_choices_to)] return first_choice + lst def get_choices_default(self): diff --git a/docs/ref/contrib/admin/index.txt b/docs/ref/contrib/admin/index.txt index 958551d660..8d69ac6e82 100644 --- a/docs/ref/contrib/admin/index.txt +++ b/docs/ref/contrib/admin/index.txt @@ -880,6 +880,20 @@ subclass:: ('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:: The ``FieldListFilter`` API is considered internal and might be diff --git a/docs/releases/1.8.txt b/docs/releases/1.8.txt index 07dd783d0b..9634b04db4 100644 --- a/docs/releases/1.8.txt +++ b/docs/releases/1.8.txt @@ -39,6 +39,11 @@ Minor features :attr:`~django.contrib.admin.InlineModelAdmin.show_change_link` that supports showing a link to an inline object's change form. +* Use the new ``django.contrib.admin.RelatedOnlyFieldListFilter`` in + :attr:`ModelAdmin.list_filter ` + to limit the ``list_filter`` choices to foreign objects which are attached to + those from the ``ModelAdmin``. + :mod:`django.contrib.auth` ^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/tests/admin_filters/tests.py b/tests/admin_filters/tests.py index 17c67929dc..23c6c10319 100644 --- a/tests/admin_filters/tests.py +++ b/tests/admin_filters/tests.py @@ -3,7 +3,7 @@ from __future__ import unicode_literals import datetime from django.contrib.admin import (site, ModelAdmin, SimpleListFilter, - BooleanFieldListFilter, AllValuesFieldListFilter) + BooleanFieldListFilter, AllValuesFieldListFilter, RelatedOnlyFieldListFilter) from django.contrib.admin.views.main import ChangeList from django.contrib.auth.admin import UserAdmin 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') +class BookAdminRelatedOnlyFilter(ModelAdmin): + list_filter = ( + 'year', 'is_best_seller', 'date_registered', 'no', + ('author', RelatedOnlyFieldListFilter), + ('contributors', RelatedOnlyFieldListFilter), + ) + ordering = ('-id',) + + class DecadeFilterBookAdmin(ModelAdmin): list_filter = ('author', DecadeListFilterWithTitleAndParameter) ordering = ('-id',) @@ -359,6 +368,13 @@ class ListFiltersTests(TestCase): def test_relatedfieldlistfilter_foreignkey(self): 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'}) changelist = self.get_changelist(request, Book, modeladmin) @@ -387,6 +403,13 @@ class ListFiltersTests(TestCase): def test_relatedfieldlistfilter_manytomany(self): 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'}) changelist = self.get_changelist(request, Book, modeladmin) @@ -464,6 +487,26 @@ class ListFiltersTests(TestCase): self.assertEqual(choice['selected'], True) 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): modeladmin = BookAdmin(Book, site) self.verify_booleanfieldlistfilter(modeladmin)