From 8a4e5067605e608c3fcbb5ca11e0019eac8b40aa Mon Sep 17 00:00:00 2001 From: Shreya Bamne Date: Wed, 8 Dec 2021 15:24:55 +0100 Subject: [PATCH] Fixed #19721 -- Allowed admin filters to customize the list separator. --- django/contrib/admin/filters.py | 3 +- django/contrib/admin/utils.py | 4 +-- docs/ref/contrib/admin/filters.txt | 19 ++++++++++++ docs/releases/4.1.txt | 4 +++ tests/admin_filters/tests.py | 46 +++++++++++++++++++++++++++++- 5 files changed, 72 insertions(+), 4 deletions(-) diff --git a/django/contrib/admin/filters.py b/django/contrib/admin/filters.py index 82dabe4d2c3..f6833e57cb6 100644 --- a/django/contrib/admin/filters.py +++ b/django/contrib/admin/filters.py @@ -118,6 +118,7 @@ class SimpleListFilter(ListFilter): class FieldListFilter(ListFilter): _field_list_filters = [] _take_priority_index = 0 + list_separator = ',' def __init__(self, field, request, params, model, model_admin, field_path): self.field = field @@ -127,7 +128,7 @@ class FieldListFilter(ListFilter): for p in self.expected_parameters(): if p in params: value = params.pop(p) - self.used_parameters[p] = prepare_lookup_value(p, value) + self.used_parameters[p] = prepare_lookup_value(p, value, self.list_separator) def has_output(self): return True diff --git a/django/contrib/admin/utils.py b/django/contrib/admin/utils.py index baaaa9e43ff..42d0d96ea6e 100644 --- a/django/contrib/admin/utils.py +++ b/django/contrib/admin/utils.py @@ -51,13 +51,13 @@ def lookup_spawns_duplicates(opts, lookup_path): return False -def prepare_lookup_value(key, value): +def prepare_lookup_value(key, value, separator=','): """ Return a lookup value prepared to be used in queryset filtering. """ # if key ends with __in, split parameter into separate values if key.endswith('__in'): - value = value.split(',') + value = value.split(separator) # if key ends with __isnull, special case '' and the string literals 'false' and '0' elif key.endswith('__isnull'): value = value.lower() not in ('', 'false', '0') diff --git a/docs/ref/contrib/admin/filters.txt b/docs/ref/contrib/admin/filters.txt index f78005936da..0dc119af3f3 100644 --- a/docs/ref/contrib/admin/filters.txt +++ b/docs/ref/contrib/admin/filters.txt @@ -176,6 +176,25 @@ allows to store:: ('title', admin.EmptyFieldListFilter), ) +By defining a filter using the ``__in`` lookup, it is possible to filter for +any of a group of values. You need to override the ``expected_parameters`` +method, and the specify the ``lookup_kwargs`` attribute with the appropriate +field name. By default, multiple values in the query string will be separated +with commas, but this can be customized via the ``list_separator`` attribute. +The following example shows such a filter using the vertical-pipe character as +the separator:: + + class FilterWithCustomSeparator(admin.FieldListFilter): + # custom list separator that should be used to separate values. + list_separator = '|' + + def __init__(self, field, request, params, model, model_admin, field_path): + self.lookup_kwarg = '%s__in' % field_path + super().__init__(field, request, params, model, model_admin, field_path) + + def expected_parameters(self): + return [self.lookup_kwarg] + .. note:: The :class:`~django.contrib.contenttypes.fields.GenericForeignKey` field is diff --git a/docs/releases/4.1.txt b/docs/releases/4.1.txt index 543c9caf6ed..c55e6386954 100644 --- a/docs/releases/4.1.txt +++ b/docs/releases/4.1.txt @@ -54,6 +54,10 @@ Minor features * The admin :ref:`dark mode CSS variables ` are now applied in a separate stylesheet and template block. +* :ref:`modeladmin-list-filters` providing custom ``FieldListFilter`` + subclasses can now control the query string value separator when filtering + for multiple values using the ``__in`` lookup. + :mod:`django.contrib.admindocs` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/tests/admin_filters/tests.py b/tests/admin_filters/tests.py index 17aa54a6f65..4f1ff26048c 100644 --- a/tests/admin_filters/tests.py +++ b/tests/admin_filters/tests.py @@ -4,7 +4,8 @@ import unittest from django.contrib.admin import ( AllValuesFieldListFilter, BooleanFieldListFilter, EmptyFieldListFilter, - ModelAdmin, RelatedOnlyFieldListFilter, SimpleListFilter, site, + FieldListFilter, ModelAdmin, RelatedOnlyFieldListFilter, SimpleListFilter, + site, ) from django.contrib.admin.options import IncorrectLookupParameters from django.contrib.auth.admin import UserAdmin @@ -135,6 +136,17 @@ class DepartmentListFilterLookupWithDynamicValue(DecadeListFilterWithTitleAndPar return (('the 80s', "the 1980's"), ('the 90s', "the 1990's"),) +class EmployeeNameCustomDividerFilter(FieldListFilter): + list_separator = '|' + + def __init__(self, field, request, params, model, model_admin, field_path): + self.lookup_kwarg = '%s__in' % field_path + super().__init__(field, request, params, model, model_admin, field_path) + + def expected_parameters(self): + return [self.lookup_kwarg] + + class CustomUserAdmin(UserAdmin): list_filter = ('books_authored', 'books_contributed') @@ -231,6 +243,12 @@ class EmployeeAdmin(ModelAdmin): list_filter = ['department'] +class EmployeeCustomDividerFilterAdmin(EmployeeAdmin): + list_filter = [ + ('name', EmployeeNameCustomDividerFilter), + ] + + class DepartmentFilterEmployeeAdmin(EmployeeAdmin): list_filter = [DepartmentListFilterLookupWithNonStringValue] @@ -1547,3 +1565,29 @@ class ListFiltersTests(TestCase): request.user = self.alfred with self.assertRaises(IncorrectLookupParameters): modeladmin.get_changelist_instance(request) + + def test_lookup_using_custom_divider(self): + """ + Filter __in lookups with a custom divider. + """ + jane = Employee.objects.create(name='Jane,Green', department=self.design) + modeladmin = EmployeeCustomDividerFilterAdmin(Employee, site) + employees = [jane, self.jack] + + request = self.request_factory.get( + '/', {'name__in': "|".join(e.name for e in employees)} + ) + # test for lookup with custom divider + request.user = self.alfred + changelist = modeladmin.get_changelist_instance(request) + # Make sure the correct queryset is returned + queryset = changelist.get_queryset(request) + self.assertEqual(list(queryset), employees) + + # test for lookup with comma in the lookup string + request = self.request_factory.get('/', {'name': jane.name}) + request.user = self.alfred + changelist = modeladmin.get_changelist_instance(request) + # Make sure the correct queryset is returned + queryset = changelist.get_queryset(request) + self.assertEqual(list(queryset), [jane])