Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit 5d98c0b

Browse files
n2ygksliverc
authored andcommitted
Document how to use rest_framework.filters.SearchFilter (#476)
1 parent 59c439d commit 5d98c0b

File tree

5 files changed

+169
-4
lines changed

5 files changed

+169
-4
lines changed

‎README.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,7 +175,9 @@ override ``settings.REST_FRAMEWORK``
175175
'DEFAULT_FILTER_BACKENDS': (
176176
'rest_framework_json_api.filters.OrderingFilter',
177177
'rest_framework_json_api.django_filters.DjangoFilterBackend',
178+
'rest_framework.filters.SearchFilter',
178179
),
180+
'SEARCH_PARAM': 'filter[search]',
179181
'TEST_REQUEST_RENDERER_CLASSES': (
180182
'rest_framework_json_api.renderers.JSONRenderer',
181183
),

‎docs/usage.md

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,9 @@ REST_FRAMEWORK = {
3535
'DEFAULT_FILTER_BACKENDS': (
3636
'rest_framework_json_api.filters.OrderingFilter',
3737
'rest_framework_json_api.django_filters.DjangoFilterBackend',
38+
'rest_framework.filters.SearchFilter',
3839
),
40+
'SEARCH_PARAM': 'filter[search]',
3941
'TEST_REQUEST_RENDERER_CLASSES': (
4042
'rest_framework_json_api.renderers.JSONRenderer',
4143
),
@@ -102,7 +104,8 @@ class MyLimitPagination(JsonApiLimitOffsetPagination):
102104

103105
### Filter Backends
104106

105-
_There are several anticipated JSON:API-specific filter backends in development. The first two are described below._
107+
Following are descriptions for two JSON:API-specific filter backends and documentation on suggested usage
108+
for a standard DRF keyword-search filter backend that makes it consistent with JSON:API.
106109

107110
#### `OrderingFilter`
108111
`OrderingFilter` implements the [JSON:API `sort`](http://jsonapi.org/format/#fetching-sorting) and uses
@@ -151,12 +154,12 @@ Filters can be:
151154
- A related resource path can be used:
152155
`?filter[inventory.item.partNum]=123456` (where `inventory.item` is the relationship path)
153156

154-
If you are also using [`rest_framework.filters.SearchFilter`](https://django-rest-framework.readthedocs.io/en/latest/api-guide/filtering/#searchfilter)
155-
(which performs single parameter searchs across multiple fields) you'll want to customize the name of the query
157+
If you are also using [`SearchFilter`](#searchfilter)
158+
(which performs single parameter searches across multiple fields) you'll want to customize the name of the query
156159
parameter for searching to make sure it doesn't conflict with a field name defined in the filterset.
157160
The recommended value is: `search_param="filter[search]"` but just make sure it's
158161
`filter[_something_]` to comply with the JSON:API spec requirement to use the filter
159-
keyword. The default is "search" unless overriden.
162+
keyword. The default is `REST_FRAMEWORK['SEARCH_PARAM']` unless overriden.
160163

161164
The filter returns a `400 Bad Request` error for invalid filter query parameters as in this example
162165
for `GET http://127.0.0.1:8000/nopage-entries?filter[bad]=1`:
@@ -173,6 +176,15 @@ for `GET http://127.0.0.1:8000/nopage-entries?filter[bad]=1`:
173176
]
174177
}
175178
```
179+
#### `SearchFilter`
180+
181+
To comply with JSON:API query parameter naming standards, DRF's
182+
[SearchFilter](https://django-rest-framework.readthedocs.io/en/latest/api-guide/filtering/#searchfilter) should
183+
be configured to use a `filter[_something_]` query parameter. This can be done by default by adding the
184+
SearchFilter to `REST_FRAMEWORK['DEFAULT_FILTER_BACKENDS']` and setting `REST_FRAMEWORK['SEARCH_PARAM']` or
185+
adding the `.search_param` attribute to a custom class derived from `SearchFilter`. If you do this and also
186+
use [`DjangoFilterBackend`](#djangofilterbackend), make sure you set the same values for both classes.
187+
176188

177189
#### Configuring Filter Backends
178190

@@ -182,11 +194,19 @@ in the [example settings](#configuration) or individually add them as `.filter_b
182194
```python
183195
from rest_framework_json_api import filters
184196
from rest_framework_json_api import django_filters
197+
from rest_framework import SearchFilter
198+
from models import MyModel
185199

186200
class MyViewset(ModelViewSet):
187201
queryset = MyModel.objects.all()
188202
serializer_class = MyModelSerializer
189203
filter_backends = (filters.OrderingFilter, django_filters.DjangoFilterBackend,)
204+
filterset_fields = {
205+
'id': ('exact', 'lt', 'gt', 'gte', 'lte', 'in'),
206+
'descriptuon': ('icontains', 'iexact', 'contains'),
207+
'tagline': ('icontains', 'iexact', 'contains'),
208+
}
209+
search_fields = ('id', 'description', 'tagline',)
190210
```
191211

192212

‎example/settings/dev.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,9 @@
9292
'DEFAULT_FILTER_BACKENDS': (
9393
'rest_framework_json_api.filters.OrderingFilter',
9494
'rest_framework_json_api.django_filters.DjangoFilterBackend',
95+
'rest_framework.filters.SearchFilter',
9596
),
97+
'SEARCH_PARAM': 'filter[search]',
9698
'TEST_REQUEST_RENDERER_CLASSES': (
9799
'rest_framework_json_api.renderers.JSONRenderer',
98100
),

‎example/tests/test_filters.py

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -338,3 +338,143 @@ def test_filter_missing_rvalue_equal(self):
338338
dja_response = response.json()
339339
self.assertEqual(dja_response['errors'][0]['detail'],
340340
"missing filter[headline] test value")
341+
342+
def test_search_keywords(self):
343+
"""
344+
test for `filter[search]="keywords"` where some of the keywords are in the entry and
345+
others are in the related blog.
346+
"""
347+
response = self.client.get(self.url, data={'filter[search]': 'barnard field research'})
348+
expected_result = {
349+
'data': [
350+
{
351+
'type': 'posts',
352+
'id': '7',
353+
'attributes': {
354+
'headline': 'ANTH3868X',
355+
'bodyText': 'ETHNOGRAPHIC FIELD RESEARCH IN NYC',
356+
'pubDate': None,
357+
'modDate': None},
358+
'relationships': {
359+
'blog': {
360+
'data': {
361+
'type': 'blogs',
362+
'id': '1'
363+
}
364+
},
365+
'blogHyperlinked': {
366+
'links': {
367+
'self': 'http://testserver/entries/7/relationships/blog_hyperlinked', # noqa: E501
368+
'related': 'http://testserver/entries/7/blog'}
369+
},
370+
'authors': {
371+
'meta': {
372+
'count': 0
373+
},
374+
'data': []
375+
},
376+
'comments': {
377+
'meta': {
378+
'count': 0
379+
},
380+
'data': []
381+
},
382+
'commentsHyperlinked': {
383+
'links': {
384+
'self': 'http://testserver/entries/7/relationships/comments_hyperlinked', # noqa: E501
385+
'related': 'http://testserver/entries/7/comments'
386+
}
387+
},
388+
'suggested': {
389+
'links': {
390+
'self': 'http://testserver/entries/7/relationships/suggested',
391+
'related': 'http://testserver/entries/7/suggested/'
392+
},
393+
'data': [
394+
{'type': 'entries', 'id': '1'},
395+
{'type': 'entries', 'id': '2'},
396+
{'type': 'entries', 'id': '3'},
397+
{'type': 'entries', 'id': '4'},
398+
{'type': 'entries', 'id': '5'},
399+
{'type': 'entries', 'id': '6'},
400+
{'type': 'entries', 'id': '8'},
401+
{'type': 'entries', 'id': '9'},
402+
{'type': 'entries', 'id': '10'},
403+
{'type': 'entries', 'id': '11'},
404+
{'type': 'entries', 'id': '12'}
405+
]
406+
},
407+
'suggestedHyperlinked': {
408+
'links': {
409+
'self': 'http://testserver/entries/7/relationships/suggested_hyperlinked', # noqa: E501
410+
'related': 'http://testserver/entries/7/suggested/'}
411+
},
412+
'tags': {
413+
'data': []
414+
},
415+
'featuredHyperlinked': {
416+
'links': {
417+
'self': 'http://testserver/entries/7/relationships/featured_hyperlinked', # noqa: E501
418+
'related': 'http://testserver/entries/7/featured'
419+
}
420+
}
421+
},
422+
'meta': {
423+
'bodyFormat': 'text'
424+
}
425+
}
426+
]
427+
}
428+
assert response.json() == expected_result
429+
430+
def test_search_multiple_keywords(self):
431+
"""
432+
test for `filter[search]=keyword1...` (keyword1 [AND keyword2...])
433+
434+
See the four search_fields defined in views.py which demonstrate both searching
435+
direct fields (entry) and following ORM links to related fields (blog):
436+
`search_fields = ('headline', 'body_text', 'blog__name', 'blog__tagline')`
437+
438+
SearchFilter searches for items that match all whitespace separated keywords across
439+
the many fields.
440+
441+
This code tests that functionality by comparing the result of the GET request
442+
with the equivalent results used by filtering the test data via the model manager.
443+
To do so, iterate over the list of given searches:
444+
1. For each keyword, search the 4 search_fields for a match and then get the result
445+
set which is the union of all results for the given keyword.
446+
2. Intersect those results sets such that *all* keywords are represented.
447+
See `example/fixtures/blogentry.json` for the test content that the searches are based on.
448+
The searches test for both direct entries and related blogs across multiple fields.
449+
"""
450+
for searches in ("research", "chemistry", "nonesuch",
451+
"research seminar", "research nonesuch",
452+
"barnard classic", "barnard ethnographic field research"):
453+
response = self.client.get(self.url, data={'filter[search]': searches})
454+
self.assertEqual(response.status_code, 200, msg=response.content.decode("utf-8"))
455+
dja_response = response.json()
456+
keys = searches.split()
457+
# dicts keyed by the search keys for the 4 search_fields:
458+
headline = {} # list of entry ids where key is in entry__headline
459+
body_text = {} # list of entry ids where key is in entry__body_text
460+
blog_name = {} # list of entry ids where key is in entry__blog__name
461+
blog_tagline = {} # list of entry ids where key is in entry__blog__tagline
462+
for key in keys:
463+
headline[key] = [str(k.id) for k in
464+
self.entries.filter(headline__icontains=key)]
465+
body_text[key] = [str(k.id) for k in
466+
self.entries.filter(body_text__icontains=key)]
467+
blog_name[key] = [str(k.id) for k in
468+
self.entries.filter(blog__name__icontains=key)]
469+
blog_tagline[key] = [str(k.id) for k in
470+
self.entries.filter(blog__tagline__icontains=key)]
471+
union = [] # each list item is a set of entry ids matching the given key
472+
for key in keys:
473+
union.append(set(headline[key] + body_text[key] +
474+
blog_name[key] + blog_tagline[key]))
475+
# all keywords must be present: intersect the keyword sets
476+
expected_ids = set.intersection(*union)
477+
expected_len = len(expected_ids)
478+
self.assertEqual(len(dja_response['data']), expected_len)
479+
returned_ids = set([k['id'] for k in dja_response['data']])
480+
self.assertEqual(returned_ids, expected_ids)

‎example/views.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,7 @@ class NonPaginatedEntryViewSet(EntryViewSet):
104104
'blog__tagline': rels,
105105
}
106106
filter_fields = filterset_fields # django-filter<=1.1 (required for py27)
107+
search_fields = ('headline', 'body_text', 'blog__name', 'blog__tagline')
107108

108109

109110
class EntryFilter(filters.FilterSet):

0 commit comments

Comments
(0)

AltStyle によって変換されたページ (->オリジナル) /