Skip to content
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