mirror of https://github.com/django/django.git
- 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:
parent
ff9cdb70cf
commit
47e21d339f
|
@ -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 %}">✖ {% translate "Clear all filters" %}</a>
|
<a href="{{ cl.clear_all_filters_qs }}">✖ {% 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>
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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']
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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="?">✖ 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, '✖ Clear all filters')
|
||||||
|
link = '<a href="%s">✖ 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, '✖ Clear all filters')
|
||||||
|
link = '<a href="%s">✖ 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 = '>✖ 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')
|
||||||
|
|
Loading…
Reference in New Issue