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

Add filter feature per JSON API specs. #286

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

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
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
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
  • Loading branch information
abdulhaq-e committed Oct 21, 2016
commit d8f848f0165fb53e7df3af5d78e4f93786761a1c
5 changes: 5 additions & 0 deletions .gitignore
View file Open in desktop
Original file line number Diff line number Diff line change
Expand Up @@ -37,3 +37,8 @@ pip-delete-this-directory.txt

# VirtualEnv
.venv/

#python3 pyvenv
bin/
lib64
pyvenv.cfg
31 changes: 29 additions & 2 deletions docs/usage.md
View file Open in desktop
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@

# Usage

The DJA package implements a custom renderer, parser, exception handler, and
pagination. To get started enable the pieces in `settings.py` that you want to use.
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.

Many features of the JSON:API format standard have been implemented using Mixin classes in `serializers.py`.
The easiest way to make use of those features is to import ModelSerializer variants
Expand All @@ -26,6 +25,10 @@ REST_FRAMEWORK = {
'rest_framework.renderers.BrowsableAPIRenderer',
),
'DEFAULT_METADATA_CLASS': 'rest_framework_json_api.metadata.JSONAPIMetadata',
'DEFAULT_FILTER_BACKENDS': (
'rest_framework_json_api.filters.JsonApiFilterBackend',
)

}
```

Expand Down Expand Up @@ -462,3 +465,27 @@ Related links will be created automatically when using the Relationship View.
### Included
### Errors
-->

### Filtering

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:

```
GET /comments?filter[post]=1 HTTP/1.1
```

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.

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:
```
JSON_API_FILTER_KEYWORD = 'filter\((?P<field>\w+)\)'
```

Now the query should look like:
```
GET /comments?filter(post)=1 HTTP/1.1
```

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.

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.
12 changes: 12 additions & 0 deletions example/filters.py
View file Open in desktop
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import django_filters

from example.models import Comment


class CommentFilter(django_filters.FilterSet):
Copy link

@khornberg khornberg Oct 21, 2016

Choose a reason for hiding this comment

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

There does not seem to be an example of using DRF filters

Copy link
Contributor Author

@abdulhaq-e abdulhaq-e Oct 21, 2016

Choose a reason for hiding this comment

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

Can you provide a link that shows an example?

DRF filtering depends on django_filter and similar examples are found in DRF docs.


class Meta:
model = Comment
fileds = {'body': ['exact', 'in', 'icontains', 'contains'],
'author': ['exact', 'gte', 'lte'],
}
6 changes: 6 additions & 0 deletions example/settings/dev.py
View file Open in desktop
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,8 @@

JSON_API_FORMAT_KEYS = 'camelize'
JSON_API_FORMAT_TYPES = 'camelize'
JSON_API_FILTER_KEYWORD = 'filter\[(?P<field>\w+)\]'

REST_FRAMEWORK = {
'PAGE_SIZE': 5,
'EXCEPTION_HANDLER': 'rest_framework_json_api.exceptions.exception_handler',
Expand All @@ -76,4 +78,8 @@
'rest_framework.renderers.BrowsableAPIRenderer',
),
'DEFAULT_METADATA_CLASS': 'rest_framework_json_api.metadata.JSONAPIMetadata',
'DEFAULT_FILTER_BACKENDS': (
'rest_framework_json_api.filters.JsonApiFilterBackend',
)

}
140 changes: 140 additions & 0 deletions example/tests/test_filters.py
View file Open in desktop
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
from django.core.urlresolvers import reverse

from rest_framework.settings import api_settings

import pytest

from example.tests.utils import dump_json, redump_json

pytestmark = pytest.mark.django_db


class TestJsonApiFilter(object):

def test_request_without_filter(self, client, comment_factory):
comment = comment_factory()
comment2 = comment_factory()

expected = {
"links": {
"first": "http://testserver/comments?page=1",
"last": "http://testserver/comments?page=2",
"next": "http://testserver/comments?page=2",
"prev": None
},
"data": [
{
"type": "comments",
"id": str(comment.pk),
"attributes": {
"body": comment.body
},
"relationships": {
"entry": {
"data": {
"type": "entries",
"id": str(comment.entry.pk)
}
},
"author": {
"data": {
"type": "authors",
"id": str(comment.author.pk)
}
},
}
}
],
"meta": {
"pagination": {
"page": 1,
"pages": 2,
"count": 2
}
}
}

response = client.get('/comments')
# assert 0

assert response.status_code == 200
actual = redump_json(response.content)
expected_json = dump_json(expected)
assert actual == expected_json

def test_request_with_filter(self, client, comment_factory):
comment = comment_factory(body='Body for comment 1')
comment2 = comment_factory()

expected = {
"links": {
"first": "http://testserver/comments?filter%5Bbody%5D=Body+for+comment+1&page=1",
"last": "http://testserver/comments?filter%5Bbody%5D=Body+for+comment+1&page=1",
"next": None,
"prev": None
},
"data": [
{
"type": "comments",
"id": str(comment.pk),
"attributes": {
"body": comment.body
},
"relationships": {
"entry": {
"data": {
"type": "entries",
"id": str(comment.entry.pk)
}
},
"author": {
"data": {
"type": "authors",
"id": str(comment.author.pk)
}
},
}
}
],
"meta": {
"pagination": {
"page": 1,
"pages": 1,
"count": 1
}
}
}

response = client.get('/comments?filter[body]=Body for comment 1')

assert response.status_code == 200
actual = redump_json(response.content)
expected_json = dump_json(expected)
assert actual == expected_json

def test_failed_request_with_filter(self, client, comment_factory):
comment = comment_factory(body='Body for comment 1')
comment2 = comment_factory()

expected = {
"links": {
"first": "http://testserver/comments?filter%5Bbody%5D=random+comment&page=1",
"last": "http://testserver/comments?filter%5Bbody%5D=random+comment&page=1",
"next": None,
"prev": None
},
"data": [],
"meta": {
"pagination": {
"page": 1,
"pages": 1,
"count": 0
}
}
}

response = client.get('/comments?filter[body]=random comment')
assert response.status_code == 200
actual = redump_json(response.content)
expected_json = dump_json(expected)
assert actual == expected_json
17 changes: 17 additions & 0 deletions example/tests/test_utils.py
View file Open in desktop
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
"""
Test rest_framework_json_api's utils functions.
"""
from django.http import QueryDict

from rest_framework_json_api import utils

import pytest

from ..serializers import EntrySerializer
from ..tests import TestBase

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

self.assertEqual(utils.get_related_resource_type(field), 'authors')


def test_format_query_params(settings):
query_params = QueryDict(
'filter[name]=Smith&filter[age]=50&other_random_param=10',
mutable=True)

new_params = utils.format_query_params(query_params)

expected_params = QueryDict('name=Smith&age=50&other_random_param=10')

for key, value in new_params.items():
assert expected_params[key] == new_params[key]
2 changes: 2 additions & 0 deletions example/views.py
View file Open in desktop
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from example.models import Blog, Entry, Author, Comment
from example.serializers import (
BlogSerializer, EntrySerializer, AuthorSerializer, CommentSerializer)
from example.filters import CommentFilter

from rest_framework_json_api.utils import format_drf_errors

Expand Down Expand Up @@ -70,6 +71,7 @@ class AuthorViewSet(viewsets.ModelViewSet):
class CommentViewSet(viewsets.ModelViewSet):
queryset = Comment.objects.all()
serializer_class = CommentSerializer
filter_class = CommentFilter


class EntryRelationshipView(RelationshipView):
Expand Down
1 change: 1 addition & 0 deletions requirements-development.txt
View file Open in desktop
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,6 @@ pytest>=2.9.0,<3.0
pytest-django
pytest-factoryboy
fake-factory
django-filter
tox
mock
19 changes: 19 additions & 0 deletions rest_framework_json_api/filters.py
View file Open in desktop
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
try:
import rest_framework_filters
DjangoFilterBackend = rest_framework_filters.backends.DjangoFilterBackend
except ImportError:
from rest_framework import filters
DjangoFilterBackend = filters.DjangoFilterBackend

from rest_framework_json_api.utils import format_query_params

class JsonApiFilterBackend(DjangoFilterBackend):

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

filter_class = self.get_filter_class(view, queryset)
new_query_params = format_query_params(request.query_params)
if filter_class:
return filter_class(new_query_params, queryset=queryset).qs

return queryset
Loading

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