From 5bdc44bc894b1b1c606d59a48434133ea1c95f98 Mon Sep 17 00:00:00 2001 From: Adam Dobrawy Date: Tue, 10 Jan 2023 09:22:05 +0100 Subject: [PATCH 1/6] Add search_fields attribute to SearchFilter --- rest_framework/filters.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/rest_framework/filters.py b/rest_framework/filters.py index 1ffd9edc02..aaa476005a 100644 --- a/rest_framework/filters.py +++ b/rest_framework/filters.py @@ -48,6 +48,7 @@ class SearchFilter(BaseFilterBackend): } search_title = _('Search') search_description = _('A search term.') + search_fields = None def get_search_fields(self, view, request): """ @@ -55,7 +56,7 @@ def get_search_fields(self, view, request): passed to this method. Sub-classes can override this method to dynamically change the search fields based on request content. """ - return getattr(view, 'search_fields', None) + return getattr(view, 'search_fields', getattr(self, 'search_fields')) def get_search_terms(self, request): """ From 854e22b6bca7b3866f1f3cade8b9676342a9553b Mon Sep 17 00:00:00 2001 From: Adam Dobrawy Date: Tue, 7 Feb 2023 11:48:41 +0100 Subject: [PATCH 2/6] Apply review commnents --- docs/api-guide/filtering.md | 17 ++++++++++++++++- rest_framework/filters.py | 4 ++-- rest_framework/mixins.py | 1 - tests/test_filters.py | 38 +++++++++++++++++++++++++++++++++++++ 4 files changed, 56 insertions(+), 4 deletions(-) diff --git a/docs/api-guide/filtering.md b/docs/api-guide/filtering.md index be8d10e9c1..afb44ea3f2 100644 --- a/docs/api-guide/filtering.md +++ b/docs/api-guide/filtering.md @@ -196,7 +196,7 @@ When in use, the browsable API will include a `SearchFilter` control: ![Search Filter](../img/search-filter.png) -The `SearchFilter` class will only be applied if the view has a `search_fields` attribute set. The `search_fields` attribute should be a list of names of text type fields on the model, such as `CharField` or `TextField`. +The `SearchFilter` class will only be applied if the view or `SearchFilter` class itself has a `search_fields` attribute set. The `search_fields` attribute should be a list of names of text type fields on the model, such as `CharField` or `TextField`. from rest_framework import filters @@ -243,6 +243,21 @@ To dynamically change search fields based on request content, it's possible to s return ['title'] return super().get_search_fields(view, request) +To use use multiple filters in the same view, override `search_param` and `search_fields` attribute. Many filters can be applied simultaneously on the same view. For example, the following subclass will search on `title` if the query parameter `search_title` is used and search on `text` if the query parameter `search_title` is used: + + from rest_framework import filters + + class TitleSearchFilter(filters.SearchFilter): + search_param = 'search_title' + search_fields = ('$title', ) + + class TextSearchFilter(filters.SearchFilter): + search_param = 'search_text' + search_fields = ('$text', ) + + class SearchListView(generics.ListAPIView): + filter_backends = (TitleSearchFilter, TextSearchFilter) + For more details, see the [Django documentation][search-django-admin]. --- diff --git a/rest_framework/filters.py b/rest_framework/filters.py index aaa476005a..a0f480190e 100644 --- a/rest_framework/filters.py +++ b/rest_framework/filters.py @@ -52,8 +52,8 @@ class SearchFilter(BaseFilterBackend): def get_search_fields(self, view, request): """ - Search fields are obtained from the view, but the request is always - passed to this method. Sub-classes can override this method to + Search fields are obtained from the view / search backend, but the request is + always passed to this method. Sub-classes can override this method to dynamically change the search fields based on request content. """ return getattr(view, 'search_fields', getattr(self, 'search_fields')) diff --git a/rest_framework/mixins.py b/rest_framework/mixins.py index 7fa8947cb9..3828aed071 100644 --- a/rest_framework/mixins.py +++ b/rest_framework/mixins.py @@ -36,7 +36,6 @@ class ListModelMixin: """ def list(self, request, *args, **kwargs): queryset = self.filter_queryset(self.get_queryset()) - page = self.paginate_queryset(queryset) if page is not None: serializer = self.get_serializer(page, many=True) diff --git a/tests/test_filters.py b/tests/test_filters.py index 37ae4c7cf3..910ec58a01 100644 --- a/tests/test_filters.py +++ b/tests/test_filters.py @@ -181,6 +181,44 @@ class SearchListView(generics.ListAPIView): {'id': 3, 'title': 'zzz', 'text': 'cde'} ] + def test_search_with_filter_multiple(self): + class TitleSearchFilter(filters.SearchFilter): + search_param = 'search_title' + search_fields = ('$title', ) + + class TextSearchFilter(filters.SearchFilter): + search_param = 'search_text' + search_fields = ('$text', ) + + class SearchListView(generics.ListAPIView): + queryset = SearchFilterModel.objects.all() + serializer_class = SearchFilterSerializer + filter_backends = (TitleSearchFilter, TextSearchFilter) + + view = SearchListView.as_view() + request = factory.get('/', {TitleSearchFilter.search_param: r'^z{3}$'}) + response = view(request) + assert response.data == [ + {'id': 3, 'title': 'zzz', 'text': 'cde'} + ] + + request = factory.get('/', {TextSearchFilter.search_param: r'^cde$'}) + response = view(request) + assert response.data == [ + {'id': 3, 'title': 'zzz', 'text': 'cde'} + ] + + request = factory.get('/', { + TitleSearchFilter.search_param: r'^(z{3}|z{2})$', + TextSearchFilter.search_param: r'^\w{3}$' + }) + response = view(request) + assert response.data == [ + {'id': 2, 'title': 'zz', 'text': 'bcd'}, + {'id': 3, 'title': 'zzz', 'text': 'cde'} + ] + + def test_search_field_with_null_characters(self): view = generics.GenericAPIView() request = factory.get('/?search=\0as%00d\x00f') From a0aefacb1d7b7a3832621708bcb57aaf72cb0ee7 Mon Sep 17 00:00:00 2001 From: Adam Dobrawy Date: Sat, 11 Feb 2023 02:39:09 +0100 Subject: [PATCH 3/6] Fix lint --- rest_framework/filters.py | 2 +- tests/test_filters.py | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/rest_framework/filters.py b/rest_framework/filters.py index a0f480190e..6397abdfe7 100644 --- a/rest_framework/filters.py +++ b/rest_framework/filters.py @@ -52,7 +52,7 @@ class SearchFilter(BaseFilterBackend): def get_search_fields(self, view, request): """ - Search fields are obtained from the view / search backend, but the request is + Search fields are obtained from the view / search backend, but the request is always passed to this method. Sub-classes can override this method to dynamically change the search fields based on request content. """ diff --git a/tests/test_filters.py b/tests/test_filters.py index 910ec58a01..6973012132 100644 --- a/tests/test_filters.py +++ b/tests/test_filters.py @@ -218,7 +218,6 @@ class SearchListView(generics.ListAPIView): {'id': 3, 'title': 'zzz', 'text': 'cde'} ] - def test_search_field_with_null_characters(self): view = generics.GenericAPIView() request = factory.get('/?search=\0as%00d\x00f') From c2921b1eca4682fc7e10dbdf34065d2611d4fc4a Mon Sep 17 00:00:00 2001 From: Adam Dobrawy Date: Thu, 23 Feb 2023 04:28:00 +0100 Subject: [PATCH 4/6] Reduce diff --- rest_framework/mixins.py | 1 + 1 file changed, 1 insertion(+) diff --git a/rest_framework/mixins.py b/rest_framework/mixins.py index 3828aed071..7fa8947cb9 100644 --- a/rest_framework/mixins.py +++ b/rest_framework/mixins.py @@ -36,6 +36,7 @@ class ListModelMixin: """ def list(self, request, *args, **kwargs): queryset = self.filter_queryset(self.get_queryset()) + page = self.paginate_queryset(queryset) if page is not None: serializer = self.get_serializer(page, many=True) From a82e23c0ff31827094d442a9f21c35c5e75bad7f Mon Sep 17 00:00:00 2001 From: Adam Dobrawy Date: Fri, 24 Feb 2023 19:21:55 +0100 Subject: [PATCH 5/6] apply review comments --- docs/api-guide/filtering.md | 2 +- rest_framework/filters.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/api-guide/filtering.md b/docs/api-guide/filtering.md index afb44ea3f2..65b745a38a 100644 --- a/docs/api-guide/filtering.md +++ b/docs/api-guide/filtering.md @@ -196,7 +196,7 @@ When in use, the browsable API will include a `SearchFilter` control: ![Search Filter](../img/search-filter.png) -The `SearchFilter` class will only be applied if the view or `SearchFilter` class itself has a `search_fields` attribute set. The `search_fields` attribute should be a list of names of text type fields on the model, such as `CharField` or `TextField`. +The `SearchFilter` class will only be applied if `SearchFilter` class itself or the view has a `search_fields` attribute set. The `search_fields` attribute should be a list of names of text type fields on the model, such as `CharField` or `TextField`. from rest_framework import filters diff --git a/rest_framework/filters.py b/rest_framework/filters.py index 6397abdfe7..52258f1d23 100644 --- a/rest_framework/filters.py +++ b/rest_framework/filters.py @@ -56,7 +56,7 @@ def get_search_fields(self, view, request): always passed to this method. Sub-classes can override this method to dynamically change the search fields based on request content. """ - return getattr(view, 'search_fields', getattr(self, 'search_fields')) + return getattr(self, 'search_fields', getattr(view, 'search_fields')) def get_search_terms(self, request): """ From 731c63fb0f917e1a3a9317122c98c19633a3a588 Mon Sep 17 00:00:00 2001 From: Adam Dobrawy Date: Tue, 28 Mar 2023 16:43:53 +0200 Subject: [PATCH 6/6] Fix tests --- rest_framework/filters.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/rest_framework/filters.py b/rest_framework/filters.py index 52258f1d23..2f3493841d 100644 --- a/rest_framework/filters.py +++ b/rest_framework/filters.py @@ -52,11 +52,11 @@ class SearchFilter(BaseFilterBackend): def get_search_fields(self, view, request): """ - Search fields are obtained from the view / search backend, but the request is + Search fields are obtained from the search backend / view, but the request is always passed to this method. Sub-classes can override this method to dynamically change the search fields based on request content. """ - return getattr(self, 'search_fields', getattr(view, 'search_fields')) + return getattr(self, 'search_fields') or getattr(view, 'search_fields', None) def get_search_terms(self, request): """