Skip to content
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
5 changes: 5 additions & 0 deletions .gitignore
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
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
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):

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

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