Skip to content

Commit 812f254

Browse files
authored
SearchFilter to support JSONField and HStoreField (#7121)
* SearchFilter to support Custom query Transforms Since Some fields support `__` as a custom Transform for query lookups we needed to update the m2m checking code to handle search_fields that contain __ that are not relationships. * Update documentation on SearchFilter to include references to JSON and HStore Fields.
1 parent 13c0837 commit 812f254

File tree

3 files changed

+47
-0
lines changed

3 files changed

+47
-0
lines changed

docs/api-guide/filtering.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -205,6 +205,10 @@ This will allow the client to filter the items in the list by making queries suc
205205
You can also perform a related lookup on a ForeignKey or ManyToManyField with the lookup API double-underscore notation:
206206

207207
search_fields = ['username', 'email', 'profile__profession']
208+
209+
For [JSONField][JSONField] and [HStoreField][HStoreField] fields you can filter based on nested values within the data structure using the same double-underscore notation:
210+
211+
search_fields = ['data__breed', 'data__owner__other_pets__0__name']
208212

209213
By default, searches will use case-insensitive partial matches. The search parameter may contain multiple search terms, which should be whitespace and/or comma separated. If multiple search terms are used then objects will be returned in the list only if all the provided terms are matched.
210214

@@ -360,3 +364,5 @@ The [djangorestframework-word-filter][django-rest-framework-word-search-filter]
360364
[django-rest-framework-word-search-filter]: https://github.com/trollknurr/django-rest-framework-word-search-filter
361365
[django-url-filter]: https://github.com/miki725/django-url-filter
362366
[drf-url-filter]: https://github.com/manjitkumar/drf-url-filters
367+
[HStoreField]: https://docs.djangoproject.com/en/3.0/ref/contrib/postgres/fields/#hstorefield
368+
[JSONField]: https://docs.djangoproject.com/en/3.0/ref/contrib/postgres/fields/#jsonfield

rest_framework/filters.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,9 @@ def must_call_distinct(self, queryset, search_fields):
9696
if any(path.m2m for path in path_info):
9797
# This field is a m2m relation so we know we need to call distinct
9898
return True
99+
else:
100+
# This field has a custom __ query transform but is not a relational field.
101+
break
99102
return False
100103

101104
def filter_queryset(self, request, queryset, view):

tests/test_filters.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
import datetime
22
from importlib import reload as reload_module
33

4+
import django
45
import pytest
56
from django.core.exceptions import ImproperlyConfigured
67
from django.db import models
8+
from django.db.models import CharField, Transform
79
from django.db.models.functions import Concat, Upper
810
from django.test import TestCase
911
from django.test.utils import override_settings
@@ -189,6 +191,42 @@ def test_search_field_with_null_characters(self):
189191

190192
assert terms == ['asdf']
191193

194+
@pytest.mark.skipif(django.VERSION[:2] < (2, 2), reason="requires django 2.2 or higher")
195+
def test_search_field_with_additional_transforms(self):
196+
from django.test.utils import register_lookup
197+
198+
class SearchListView(generics.ListAPIView):
199+
queryset = SearchFilterModel.objects.all()
200+
serializer_class = SearchFilterSerializer
201+
filter_backends = (filters.SearchFilter,)
202+
search_fields = ('text__trim', )
203+
204+
view = SearchListView.as_view()
205+
206+
# an example custom transform, that trims `a` from the string.
207+
class TrimA(Transform):
208+
function = 'TRIM'
209+
lookup_name = 'trim'
210+
211+
def as_sql(self, compiler, connection):
212+
sql, params = compiler.compile(self.lhs)
213+
return "trim(%s, 'a')" % sql, params
214+
215+
with register_lookup(CharField, TrimA):
216+
# Search including `a`
217+
request = factory.get('/', {'search': 'abc'})
218+
219+
response = view(request)
220+
assert response.data == []
221+
222+
# Search excluding `a`
223+
request = factory.get('/', {'search': 'bc'})
224+
response = view(request)
225+
assert response.data == [
226+
{'id': 1, 'title': 'z', 'text': 'abc'},
227+
{'id': 2, 'title': 'zz', 'text': 'bcd'},
228+
]
229+
192230

193231
class AttributeModel(models.Model):
194232
label = models.CharField(max_length=32)

0 commit comments

Comments
 (0)