Skip to content

Document how to use rest_framework.filters.SearchFilter #476

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Sep 17, 2018
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,9 @@ override ``settings.REST_FRAMEWORK``
'DEFAULT_FILTER_BACKENDS': (
'rest_framework_json_api.filters.JSONAPIOrderingFilter',
'rest_framework_json_api.django_filters.DjangoFilterBackend',
'rest_framework.filters.SearchFilter',
),
'SEARCH_PARAM': 'filter[search]',
'TEST_REQUEST_RENDERER_CLASSES': (
'rest_framework_json_api.renderers.JSONRenderer',
),
Expand Down
30 changes: 25 additions & 5 deletions docs/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,9 @@ REST_FRAMEWORK = {
'DEFAULT_FILTER_BACKENDS': (
'rest_framework_json_api.filters.JSONAPIOrderingFilter',
'rest_framework_json_api.django_filters.DjangoFilterBackend',
'rest_framework.filters.SearchFilter',
),
'SEARCH_PARAM': 'filter[search]',
'TEST_REQUEST_RENDERER_CLASSES': (
'rest_framework_json_api.renderers.JSONRenderer',
),
Expand Down Expand Up @@ -102,7 +104,8 @@ class MyLimitPagination(JsonApiLimitOffsetPagination):

### Filter Backends

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

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

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

The filter returns a `400 Bad Request` error for invalid filter query parameters as in this example
for `GET http://127.0.0.1:8000/nopage-entries?filter[bad]=1`:
Expand All @@ -173,6 +176,15 @@ for `GET http://127.0.0.1:8000/nopage-entries?filter[bad]=1`:
]
}
```
#### `SearchFilter`

To comply with JSON:API query parameter naming standards, DRF's
[SearchFilter](https://django-rest-framework.readthedocs.io/en/latest/api-guide/filtering/#searchfilter) should
be configured to use a `filter[_something_]` query parameter. This can be done by default by adding the
SearchFilter to `REST_FRAMEWORK['DEFAULT_FILTER_BACKENDS']` and setting `REST_FRAMEWORK['SEARCH_PARAM']` or
adding the `.search_param` attribute to a custom class derived from `SearchFilter`. If you do this and also
use [`DjangoFilterBackend`](#djangofilterbackend), make sure you set the same values for both classes.


#### Configuring Filter Backends

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

class MyViewset(ModelViewSet):
queryset = MyModel.objects.all()
serializer_class = MyModelSerializer
filter_backends = (filters.JSONAPIOrderingFilter, django_filters.DjangoFilterBackend,)
filter_backends = (filters.JSONAPIOrderingFilter, django_filters.DjangoFilterBackend, SearchFilter)
filterset_fields = {
'id': ('exact', 'lt', 'gt', 'gte', 'lte', 'in'),
'descriptuon': ('icontains', 'iexact', 'contains'),
'tagline': ('icontains', 'iexact', 'contains'),
}
search_fields = ('id', 'description', 'tagline',)
```


Expand Down
2 changes: 2 additions & 0 deletions example/settings/dev.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,9 @@
'DEFAULT_FILTER_BACKENDS': (
'rest_framework_json_api.filters.JSONAPIOrderingFilter',
'rest_framework_json_api.django_filters.DjangoFilterBackend',
'rest_framework.filters.SearchFilter',
),
'SEARCH_PARAM': 'filter[search]',
'TEST_REQUEST_RENDERER_CLASSES': (
'rest_framework_json_api.renderers.JSONRenderer',
),
Expand Down
30 changes: 30 additions & 0 deletions example/tests/test_filters.py
Original file line number Diff line number Diff line change
Expand Up @@ -338,3 +338,33 @@ def test_filter_missing_rvalue_equal(self):
dja_response = response.json()
self.assertEqual(dja_response['errors'][0]['detail'],
"missing filter[headline] test value")

def test_search_keywords(self):
"""
test for `filter[search]=keyword1...` (keyword1 [AND keyword2...])
"""
for keywords in ("research", "chemistry", "nonesuch",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test is a bit difficult to comprehend. It would be much easier to write it with pytest parametrize.

I ask myself though do we really need a test for SearchFilter? It is DRF functionality and as such it is already tested in DRF itself and we just do same work again. Hence I think we could remove this test.

"research seminar", "research nonesuch",
"barnard classic"):
response = self.client.get(self.url, data={'filter[search]': keywords})
self.assertEqual(response.status_code, 200, msg=response.content.decode("utf-8"))
dja_response = response.json()
# see the search_fields defined in views.py.
a = {}
b = {}
c = {}
d = {}
keys = keywords.split()
for key in keys:
a[key] = [str(k.id) for k in self.entries.filter(headline__icontains=key)]
b[key] = [str(k.id) for k in self.entries.filter(body_text__icontains=key)]
c[key] = [str(k.id) for k in self.entries.filter(blog__name__icontains=key)]
d[key] = [str(k.id) for k in self.entries.filter(blog__tagline__icontains=key)]
union = [] # a list of sets grouped by keyword
for key in keys:
union.append(set(a[key] + b[key] + c[key] + d[key]))
# all keywords must be present: intersect the keyword sets
inter = set.intersection(*union)
expected_len = len(inter)
self.assertEqual(len(dja_response['data']), expected_len)
self.assertEqual(set([k['id'] for k in dja_response['data']]), inter)
1 change: 1 addition & 0 deletions example/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@ class NonPaginatedEntryViewSet(EntryViewSet):
'blog__tagline': rels,
}
filter_fields = filterset_fields # django-filter<=1.1 (required for py27)
search_fields = ('headline', 'body_text', 'blog__name', 'blog__tagline')


class EntryFilter(filters.FilterSet):
Expand Down