From 98e8da3709e400d549e87c7ff1450a2685c0adcf Mon Sep 17 00:00:00 2001
From: Stanislas Guerra <stanislas.guerra@gmail.com>
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 <http://grindhaug.org/>
     Mike Grouchy <http://mikegrouchy.com/>
+    Stanislas Guerra <stan@slashdev.me>
     Janos Guljas
     Thomas Güttler <hv@tbz-pariv.de>
     Horst Gutmann <zerok@zerokspot.com>
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 <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`
 ^^^^^^^^^^^^^^^^^^^^^^^^^^
 
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)