[3.1.x] Fixed #31597, #31603 -- Corrected admin clear all filters link behavior.

- Show "Clear all filters" only when any filter is selected.
- Preserve query string not related with filters.

Co-Authored-By: Hasan Ramezani <hasan.r67@gmail.com>

Backport of 4484bc1b2f from master
This commit is contained in:
Fran Hrzenjak 2020-05-19 09:03:04 +02:00 committed by Mariusz Felisiak
parent ff9cdb70cf
commit 47e21d339f
5 changed files with 110 additions and 16 deletions

View File

@ -60,8 +60,8 @@
{% if cl.has_filters %} {% if cl.has_filters %}
<div id="changelist-filter"> <div id="changelist-filter">
<h2>{% translate 'Filter' %}</h2> <h2>{% translate 'Filter' %}</h2>
{% if cl.preserved_filters %}<h3 id="changelist-filter-clear"> {% if cl.has_active_filters %}<h3 id="changelist-filter-clear">
<a href="?{% if cl.is_popup %}_popup=1{% endif %}">&#10006; {% translate "Clear all filters" %}</a> <a href="{{ cl.clear_all_filters_qs }}">&#10006; {% translate "Clear all filters" %}</a>
</h3>{% endif %} </h3>{% endif %}
{% for spec in cl.filter_specs %}{% admin_list_filter cl spec %}{% endfor %} {% for spec in cl.filter_specs %}{% admin_list_filter cl spec %}{% endfor %}
</div> </div>

View File

@ -59,6 +59,8 @@ class ChangeList:
self.list_display_links = list_display_links self.list_display_links = list_display_links
self.list_filter = list_filter self.list_filter = list_filter
self.has_filters = None self.has_filters = None
self.has_active_filters = None
self.clear_all_filters_qs = None
self.date_hierarchy = date_hierarchy self.date_hierarchy = date_hierarchy
self.search_fields = search_fields self.search_fields = search_fields
self.list_select_related = list_select_related self.list_select_related = list_select_related
@ -121,6 +123,7 @@ class ChangeList:
def get_filters(self, request): def get_filters(self, request):
lookup_params = self.get_filters_params() lookup_params = self.get_filters_params()
use_distinct = False use_distinct = False
has_active_filters = False
for key, value in lookup_params.items(): for key, value in lookup_params.items():
if not self.model_admin.lookup_allowed(key, value): if not self.model_admin.lookup_allowed(key, value):
@ -128,6 +131,7 @@ class ChangeList:
filter_specs = [] filter_specs = []
for list_filter in self.list_filter: for list_filter in self.list_filter:
lookup_params_count = len(lookup_params)
if callable(list_filter): if callable(list_filter):
# This is simply a custom list filter class. # This is simply a custom list filter class.
spec = list_filter(request, lookup_params, self.model, self.model_admin) spec = list_filter(request, lookup_params, self.model, self.model_admin)
@ -145,7 +149,6 @@ class ChangeList:
field_path = field field_path = field
field = get_fields_from_path(self.model, field_path)[-1] field = get_fields_from_path(self.model, field_path)[-1]
lookup_params_count = len(lookup_params)
spec = field_list_filter_class( spec = field_list_filter_class(
field, request, lookup_params, field, request, lookup_params,
self.model, self.model_admin, field_path=field_path, self.model, self.model_admin, field_path=field_path,
@ -157,6 +160,8 @@ class ChangeList:
use_distinct = use_distinct or lookup_needs_distinct(self.lookup_opts, field_path) use_distinct = use_distinct or lookup_needs_distinct(self.lookup_opts, field_path)
if spec and spec.has_output(): if spec and spec.has_output():
filter_specs.append(spec) filter_specs.append(spec)
if lookup_params_count > len(lookup_params):
has_active_filters = True
if self.date_hierarchy: if self.date_hierarchy:
# Create bounded lookup parameters so that the query is more # Create bounded lookup parameters so that the query is more
@ -199,7 +204,10 @@ class ChangeList:
for key, value in lookup_params.items(): for key, value in lookup_params.items():
lookup_params[key] = prepare_lookup_value(key, value) lookup_params[key] = prepare_lookup_value(key, value)
use_distinct = use_distinct or lookup_needs_distinct(self.lookup_opts, key) use_distinct = use_distinct or lookup_needs_distinct(self.lookup_opts, key)
return filter_specs, bool(filter_specs), lookup_params, use_distinct return (
filter_specs, bool(filter_specs), lookup_params, use_distinct,
has_active_filters,
)
except FieldDoesNotExist as e: except FieldDoesNotExist as e:
raise IncorrectLookupParameters(e) from e raise IncorrectLookupParameters(e) from e
@ -433,9 +441,13 @@ class ChangeList:
def get_queryset(self, request): def get_queryset(self, request):
# First, we collect all the declared list filters. # First, we collect all the declared list filters.
(self.filter_specs, self.has_filters, remaining_lookup_params, (
filters_use_distinct) = self.get_filters(request) self.filter_specs,
self.has_filters,
remaining_lookup_params,
filters_use_distinct,
self.has_active_filters,
) = self.get_filters(request)
# Then, we let every list filter modify the queryset to its liking. # Then, we let every list filter modify the queryset to its liking.
qs = self.root_queryset qs = self.root_queryset
for filter_spec in self.filter_specs: for filter_spec in self.filter_specs:
@ -470,6 +482,11 @@ class ChangeList:
# Apply search results # Apply search results
qs, search_use_distinct = self.model_admin.get_search_results(request, qs, self.query) qs, search_use_distinct = self.model_admin.get_search_results(request, qs, self.query)
# Set query string for clearing all filters.
self.clear_all_filters_qs = self.get_query_string(
new_params=remaining_lookup_params,
remove=self.get_filters_params(),
)
# Remove duplicates from results, if necessary # Remove duplicates from results, if necessary
if filters_use_distinct | search_use_distinct: if filters_use_distinct | search_use_distinct:
return qs.distinct() return qs.distinct()

View File

@ -3,7 +3,7 @@ from django.contrib.auth.admin import UserAdmin
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.core.paginator import Paginator from django.core.paginator import Paginator
from .models import Child, Event, Parent, Swallow from .models import Band, Child, Event, Parent, Swallow
site = admin.AdminSite(name="admin") site = admin.AdminSite(name="admin")
@ -59,6 +59,31 @@ class BandAdmin(admin.ModelAdmin):
list_filter = ['genres'] list_filter = ['genres']
class NrOfMembersFilter(admin.SimpleListFilter):
title = 'number of members'
parameter_name = 'nr_of_members_partition'
def lookups(self, request, model_admin):
return [
('5', '0 - 5'),
('more', 'more than 5'),
]
def queryset(self, request, queryset):
value = self.value()
if value == '5':
return queryset.filter(nr_of_members__lte=5)
if value == 'more':
return queryset.filter(nr_of_members__gt=5)
class BandCallableFilterAdmin(admin.ModelAdmin):
list_filter = [NrOfMembersFilter]
site.register(Band, BandCallableFilterAdmin)
class GroupAdmin(admin.ModelAdmin): class GroupAdmin(admin.ModelAdmin):
list_filter = ['members'] list_filter = ['members']

View File

@ -21,7 +21,7 @@ class DateHierarchyTests(TestCase):
request = self.factory.get('/', query) request = self.factory.get('/', query)
request.user = self.superuser request.user = self.superuser
changelist = EventAdmin(Event, custom_site).get_changelist_instance(request) changelist = EventAdmin(Event, custom_site).get_changelist_instance(request)
_, _, lookup_params, _ = changelist.get_filters(request) _, _, lookup_params, *_ = changelist.get_filters(request)
self.assertEqual(lookup_params['date__gte'], expected_from_date) self.assertEqual(lookup_params['date__gte'], expected_from_date)
self.assertEqual(lookup_params['date__lt'], expected_to_date) self.assertEqual(lookup_params['date__lt'], expected_to_date)

View File

@ -5,7 +5,9 @@ from django.contrib.admin.models import LogEntry
from django.contrib.admin.options import IncorrectLookupParameters from django.contrib.admin.options import IncorrectLookupParameters
from django.contrib.admin.templatetags.admin_list import pagination from django.contrib.admin.templatetags.admin_list import pagination
from django.contrib.admin.tests import AdminSeleniumTestCase from django.contrib.admin.tests import AdminSeleniumTestCase
from django.contrib.admin.views.main import ALL_VAR, SEARCH_VAR from django.contrib.admin.views.main import (
ALL_VAR, IS_POPUP_VAR, ORDER_VAR, PAGE_VAR, SEARCH_VAR, TO_FIELD_VAR,
)
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from django.contrib.messages.storage.cookie import CookieStorage from django.contrib.messages.storage.cookie import CookieStorage
@ -708,15 +710,65 @@ class ChangeListTests(TestCase):
def test_clear_all_filters_link(self): def test_clear_all_filters_link(self):
self.client.force_login(self.superuser) self.client.force_login(self.superuser)
link = '<a href="?">&#10006; Clear all filters</a>' url = reverse('admin:auth_user_changelist')
response = self.client.get(reverse('admin:auth_user_changelist')) response = self.client.get(url)
self.assertNotContains(response, link) self.assertNotContains(response, '&#10006; Clear all filters')
link = '<a href="%s">&#10006; Clear all filters</a>'
for data, href in (
({'is_staff__exact': '0'}, '?'),
(
{'is_staff__exact': '0', 'username__startswith': 'test'},
'?username__startswith=test',
),
(
{'is_staff__exact': '0', SEARCH_VAR: 'test'},
'?%s=test' % SEARCH_VAR,
),
(
{'is_staff__exact': '0', IS_POPUP_VAR: 'id'},
'?%s=id' % IS_POPUP_VAR,
),
):
with self.subTest(data=data):
response = self.client.get(url, data=data)
self.assertContains(response, link % href)
def test_clear_all_filters_link_callable_filter(self):
self.client.force_login(self.superuser)
url = reverse('admin:admin_changelist_band_changelist')
response = self.client.get(url)
self.assertNotContains(response, '&#10006; Clear all filters')
link = '<a href="%s">&#10006; Clear all filters</a>'
for data, href in (
({'nr_of_members_partition': '5'}, '?'),
(
{'nr_of_members_partition': 'more', 'name__startswith': 'test'},
'?name__startswith=test',
),
(
{'nr_of_members_partition': '5', IS_POPUP_VAR: 'id'},
'?%s=id' % IS_POPUP_VAR,
),
):
with self.subTest(data=data):
response = self.client.get(url, data=data)
self.assertContains(response, link % href)
def test_no_clear_all_filters_link(self):
self.client.force_login(self.superuser)
url = reverse('admin:auth_user_changelist')
link = '>&#10006; Clear all filters</a>'
for data in ( for data in (
{SEARCH_VAR: 'test'}, {SEARCH_VAR: 'test'},
{'is_staff__exact': '0'}, {ORDER_VAR: '-1'},
{TO_FIELD_VAR: 'id'},
{PAGE_VAR: '1'},
{IS_POPUP_VAR: '1'},
{'username__startswith': 'test'},
): ):
response = self.client.get(reverse('admin:auth_user_changelist'), data=data) with self.subTest(data=data):
self.assertContains(response, link) response = self.client.get(url, data=data)
self.assertNotContains(response, link)
def test_tuple_list_display(self): def test_tuple_list_display(self):
swallow = Swallow.objects.create(origin='Africa', load='12.34', speed='22.2') swallow = Swallow.objects.create(origin='Africa', load='12.34', speed='22.2')