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 54cabcd

Browse files
Add filter feature per JSON API specs.
Added filter feature. This uses DjangoFilterBackend which either comes from `rest-framework-filter` or `django-filter`. The former is a package which adds more features to the latter. All that is done is reformatting the request query parameters to be compatible with DjangoFilterBackend. The spec recommend the pattern `filter[field]=values`. JsonApiFilterBackend takes that pattern and reformats it to become `field=value` which will then work as typical filtering in DRF. The specs only rule towards filtering is that the `filter` keyword should be reserved. It doesn't however say about the exact format. It only **recommends** `filter[field]=value` but this can also be `filter{field}=value` or `filter(field)=value`. Therefore, a new setting for DJA is introduced: JSON_API_FILTER_KEYWORD. This is a regex which controls the filter format. Its default value is: ``` JSON_API_FILTER_KEYWORD = 'filter\[(?P<field>\w+)\]' ``` It can be changed to anything as long as the `field` keyword is included in the regex. The docs have been updated. Unfortunately, the editor's beautifier was run and made lots of other changes to the code style. No harm was done though, that's why TDD exists :D
1 parent 1623942 commit 54cabcd

File tree

10 files changed

+272
-13
lines changed

10 files changed

+272
-13
lines changed

‎.gitignore‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,3 +37,8 @@ pip-delete-this-directory.txt
3737

3838
# VirtualEnv
3939
.venv/
40+
41+
#python3 pyvenv
42+
bin/
43+
lib64
44+
pyvenv.cfg

‎docs/usage.md‎

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11

22
# Usage
33

4-
The DJA package implements a custom renderer, parser, exception handler, and
5-
pagination. To get started enable the pieces in `settings.py` that you want to use.
4+
The DJA package implements a custom renderer, parser, exception handler, pagination and filter backend. To get started enable the pieces in `settings.py` that you want to use.
65

76
Many features of the JSON:API format standard have been implemented using Mixin classes in `serializers.py`.
87
The easiest way to make use of those features is to import ModelSerializer variants
@@ -26,6 +25,10 @@ REST_FRAMEWORK = {
2625
'rest_framework.renderers.BrowsableAPIRenderer',
2726
),
2827
'DEFAULT_METADATA_CLASS': 'rest_framework_json_api.metadata.JSONAPIMetadata',
28+
'DEFAULT_FILTER_BACKENDS': (
29+
'rest_framework_json_api.filters.JsonApiFilterBackend',
30+
)
31+
2932
}
3033
```
3134

@@ -462,3 +465,27 @@ Related links will be created automatically when using the Relationship View.
462465
### Included
463466
### Errors
464467
-->
468+
469+
### Filtering
470+
471+
JSON API spefications is agnostic towards filtering. However, it instructs that the `filter` keyword should be reserved for querying filtered resources, nothing other than that. Although, the specs recommend the following pattern for filtering:
472+
473+
```
474+
GET /comments?filter[post]=1 HTTP/1.1
475+
```
476+
477+
DJA package implements its own filter backend (JsonApiFilterBackend) which can be enabled by configuring 'DEFAULT_FILTER_BACKENDS' (as included in the beginning). The backend depends on the DRF's own filtering which depends on [`django-filter`](https://github.com/carltongibson/django-filter). A substitute for `django-filter` is [`django-rest-framework-filters`](https://github.com/philipn/django-rest-framework-filters). The DJA provided filter backend can use both packages.
478+
479+
The default filter format is set to match the above recommendation. This can be changed from the settings by modifying 'JSON_API_FILTER_KEYWORD' which is a simple regex. If for example, the square brackets need to be replaced by round parenthesis, the setting can be set to:
480+
```
481+
JSON_API_FILTER_KEYWORD = 'filter\((?P<field>\w+)\)'
482+
```
483+
484+
Now the query should look like:
485+
```
486+
GET /comments?filter(post)=1 HTTP/1.1
487+
```
488+
489+
The backend basically takes the request query paramters, which are formatted as the specs recommend, and reformats them in order to be used by DRF filtering.
490+
491+
How the filtering actually works and how to deal with queries such as: `GET /comments?filter[post]=1,2,3 HTTP/1.1` is something user dependent and beyond the scope of DJA.

‎example/filters.py‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import django_filters
2+
3+
from example.models import Comment
4+
5+
6+
class CommentFilter(django_filters.FilterSet):
7+
8+
class Meta:
9+
model = Comment
10+
fileds = {'body': ['exact', 'in', 'icontains', 'contains'],
11+
'author': ['exact', 'gte', 'lte'],
12+
}

‎example/settings/dev.py‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,8 @@
6161

6262
JSON_API_FORMAT_KEYS = 'camelize'
6363
JSON_API_FORMAT_TYPES = 'camelize'
64+
JSON_API_FILTER_KEYWORD = 'filter\[(?P<field>\w+)\]'
65+
6466
REST_FRAMEWORK = {
6567
'PAGE_SIZE': 5,
6668
'EXCEPTION_HANDLER': 'rest_framework_json_api.exceptions.exception_handler',
@@ -76,4 +78,8 @@
7678
'rest_framework.renderers.BrowsableAPIRenderer',
7779
),
7880
'DEFAULT_METADATA_CLASS': 'rest_framework_json_api.metadata.JSONAPIMetadata',
81+
'DEFAULT_FILTER_BACKENDS': (
82+
'rest_framework_json_api.filters.JsonApiFilterBackend',
83+
)
84+
7985
}

‎example/tests/test_filters.py‎

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
from django.urls import reverse
2+
3+
from rest_framework.settings import api_settings
4+
5+
import pytest
6+
7+
from example.tests.utils import dump_json, redump_json
8+
9+
pytestmark = pytest.mark.django_db
10+
11+
12+
class TestJsonApiFilter(object):
13+
14+
def test_request_without_filter(self, client, comment_factory):
15+
comment = comment_factory()
16+
comment2 = comment_factory()
17+
18+
expected = {
19+
"links": {
20+
"first": "http://testserver/comments?page=1",
21+
"last": "http://testserver/comments?page=2",
22+
"next": "http://testserver/comments?page=2",
23+
"prev": None
24+
},
25+
"data": [
26+
{
27+
"type": "comments",
28+
"id": str(comment.pk),
29+
"attributes": {
30+
"body": comment.body
31+
},
32+
"relationships": {
33+
"entry": {
34+
"data": {
35+
"type": "entries",
36+
"id": str(comment.entry.pk)
37+
}
38+
},
39+
"author": {
40+
"data": {
41+
"type": "authors",
42+
"id": str(comment.author.pk)
43+
}
44+
},
45+
}
46+
}
47+
],
48+
"meta": {
49+
"pagination": {
50+
"page": 1,
51+
"pages": 2,
52+
"count": 2
53+
}
54+
}
55+
}
56+
57+
response = client.get('/comments')
58+
# assert 0
59+
60+
assert response.status_code == 200
61+
actual = redump_json(response.content)
62+
expected_json = dump_json(expected)
63+
assert actual == expected_json
64+
65+
def test_request_with_filter(self, client, comment_factory):
66+
comment = comment_factory(body='Body for comment 1')
67+
comment2 = comment_factory()
68+
69+
expected = {
70+
"links": {
71+
"first": "http://testserver/comments?filter%5Bbody%5D=Body+for+comment+1&page=1",
72+
"last": "http://testserver/comments?filter%5Bbody%5D=Body+for+comment+1&page=1",
73+
"next": None,
74+
"prev": None
75+
},
76+
"data": [
77+
{
78+
"type": "comments",
79+
"id": str(comment.pk),
80+
"attributes": {
81+
"body": comment.body
82+
},
83+
"relationships": {
84+
"entry": {
85+
"data": {
86+
"type": "entries",
87+
"id": str(comment.entry.pk)
88+
}
89+
},
90+
"author": {
91+
"data": {
92+
"type": "authors",
93+
"id": str(comment.author.pk)
94+
}
95+
},
96+
}
97+
}
98+
],
99+
"meta": {
100+
"pagination": {
101+
"page": 1,
102+
"pages": 1,
103+
"count": 1
104+
}
105+
}
106+
}
107+
108+
response = client.get('/comments?filter[body]=Body for comment 1')
109+
110+
assert response.status_code == 200
111+
actual = redump_json(response.content)
112+
expected_json = dump_json(expected)
113+
assert actual == expected_json
114+
115+
def test_failed_request_with_filter(self, client, comment_factory):
116+
comment = comment_factory(body='Body for comment 1')
117+
comment2 = comment_factory()
118+
119+
expected = {
120+
"links": {
121+
"first": "http://testserver/comments?filter%5Bbody%5D=random+comment&page=1",
122+
"last": "http://testserver/comments?filter%5Bbody%5D=random+comment&page=1",
123+
"next": None,
124+
"prev": None
125+
},
126+
"data": [],
127+
"meta": {
128+
"pagination": {
129+
"page": 1,
130+
"pages": 1,
131+
"count": 0
132+
}
133+
}
134+
}
135+
136+
response = client.get('/comments?filter[body]=random comment')
137+
assert response.status_code == 200
138+
actual = redump_json(response.content)
139+
expected_json = dump_json(expected)
140+
assert actual == expected_json

‎example/tests/test_utils.py‎

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,12 @@
11
"""
22
Test rest_framework_json_api's utils functions.
33
"""
4+
from django.http import QueryDict
5+
46
from rest_framework_json_api import utils
57

8+
import pytest
9+
610
from ..serializers import EntrySerializer
711
from ..tests import TestBase
812

@@ -29,3 +33,16 @@ def test_m2m_relation(self):
2933
field = serializer.fields['authors']
3034

3135
self.assertEqual(utils.get_related_resource_type(field), 'authors')
36+
37+
38+
def test_format_query_params(settings):
39+
query_params = QueryDict(
40+
'filter[name]=Smith&filter[age]=50&other_random_param=10',
41+
mutable=True)
42+
43+
new_params = utils.format_query_params(query_params)
44+
45+
expected_params = QueryDict('name=Smith&age=50&other_random_param=10')
46+
47+
for key, value in new_params.items():
48+
assert expected_params[key] == new_params[key]

‎example/views.py‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
from example.models import Blog, Entry, Author, Comment
1010
from example.serializers import (
1111
BlogSerializer, EntrySerializer, AuthorSerializer, CommentSerializer)
12+
from example.filters import CommentFilter
1213

1314
from rest_framework_json_api.utils import format_drf_errors
1415

@@ -70,6 +71,7 @@ class AuthorViewSet(viewsets.ModelViewSet):
7071
class CommentViewSet(viewsets.ModelViewSet):
7172
queryset = Comment.objects.all()
7273
serializer_class = CommentSerializer
74+
filter_class = CommentFilter
7375

7476

7577
class EntryRelationshipView(RelationshipView):

‎requirements-development.txt‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,6 @@ pytest>=2.9.0,<3.0
33
pytest-django
44
pytest-factoryboy
55
fake-factory
6+
django-filter
67
tox
78
mock

‎rest_framework_json_api/filters.py‎

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
try:
2+
import rest_framework_filters
3+
DjangoFilterBackend = rest_framework_filters.backends.DjangoFilterBackend
4+
except ImportError:
5+
from rest_framework import filters
6+
DjangoFilterBackend = filters.DjangoFilterBackend
7+
8+
from rest_framework_json_api.utils import format_query_params
9+
10+
class JsonApiFilterBackend(DjangoFilterBackend):
11+
12+
def filter_queryset(self, request, queryset, view):
13+
14+
filter_class = self.get_filter_class(view, queryset)
15+
print(request.query_params)
16+
new_query_params = format_query_params(request.query_params)
17+
if filter_class:
18+
return filter_class(new_query_params, queryset=queryset).qs
19+
20+
return queryset

0 commit comments

Comments
(0)

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