Skip to content

Improvements for pagination, sorting, filtering, and exception h… #416

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
14 changes: 14 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,17 @@
v2.4.x - [Unreleased]
* Pagination updates:
* Improve existing documentation of pagination, mixins
* Document LimitOffsetPagination
* Describe how to override pagination class query parameter names.
* Remove reference to PAGINATE_BY_PARAM which was deprecated in DRF 3.2.
* Document SparseFieldsetsMixin
* Add new default settings for pagination query parameters and maximum size.
* Add combinable mixins for filter and sort query parameters and make MultiplIDMixin combinable.
* Exceptions updates:
* Document JSON_API_UNIFORM_EXCEPTIONS setting.
* handle missing fields exception thrown by new filter and sort Mixins as a 400 error.
* Catch all exceptions not caught by DRF and format as JSON API error objects instead of returning HTML error pages.

v2.4.0 - Released January 25, 2018

* Add support for Django REST Framework 3.7.x.
Expand Down
64 changes: 63 additions & 1 deletion docs/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,71 @@
## mixins
### MultipleIDMixin

Add this mixin to a view to override `get_queryset` to automatically filter
Add this mixin to a view to override `get_queryset` to filter
records by `ids[]=1&ids[]=2` in URL query params.

For example:
```http request
GET /widgets/?ids[]=123&ids[]=456
```

You might want to consider using the `FilterMixin` where the equivalent example is:
```http request
GET /widgets/?filter[id]=123,456
```

### FilterMixin

This ViewSet mixin augments `get_queryset` to provide JSON API filter query parameter support
per the [recommendation](http://jsonapi.org/recommendations/#filtering).

The `filter` syntax is `filter[name1]=list,of,alternative,values&filter[name2]=more,alternatives...`
which can be interpreted as `(name1 in [list,of,alternative,values]) and (name2 in [more,alternatives])`
`name` can be `id` or attributes fields.

For example:

```http request
GET /widgets/?filter[name]=can+opener,tap&filter[color]=red
```

### SortMixin

This ViewSet mixin augments `get_queryset` to provide JSON API sort query parameter support
per the [recommendation](http://jsonapi.org/format/#fetching-sorting).

The `sort` syntax is `sort=-field1,field2,...`

For example:

```http request
GET /widgets/?sort=-name,color
```

### SparseFieldsetsMixin

This Serializer mixin implements [sparse fieldsets](http://jsonapi.org/format/#fetching-sparse-fieldsets)
with the `fields[type]=` parameter. It is included by default in the HyperLinkedModelSerializer and
ModelSerializer classes.

For example:

```http request
GET /widgets/?fields[widgets]=name
```

### IncludedResourcesValidationMixin

This Serializer mixin implements [included compound documents](http://jsonapi.org/format/#document-compound-documents)
with the `include=` parameter. It is included by default in the HyperLinkedModelSerializer and
ModelSerializer classes.

For example:

```http request
GET /widgets/?included=locations
```

## rest_framework_json_api.renderers.JSONRenderer

The `JSONRenderer` exposes a number of methods that you may override if you need
Expand Down
95 changes: 89 additions & 6 deletions docs/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,14 +34,73 @@ REST_FRAMEWORK = {
}
```

If `PAGE_SIZE` is set the renderer will return a `meta` object with
### Pagination settings

If `REST_FRAMEWORK['PAGE_SIZE']` is set the renderer will return a `meta` object with
record count and a `links` object with the next, previous, first, and last links.
Pages can be selected with the `page` GET parameter. The query parameter used to
retrieve the page can be customized by subclassing `PageNumberPagination` and
overriding the `page_query_param`. Page size can be controlled per request via
the `PAGINATE_BY_PARAM` query parameter (`page_size` by default).

#### Performance Testing
#### Subclassing paginators

The JSON API pagination classes can be subclassed to override settings as
described in the [DRF pagination documentation](http://www.django-rest-framework.org/api-guide/pagination/).

The default values are shown in these examples:

```python
from rest_framework_json_api.pagination import PageNumberPagination, LimitOffsetPagination

class MyPagePagination(PageNumberPagination):
page_query_param = 'page'
page_size_query_param = 'page_size'
max_page_size = 100

class MyLimitPagination(LimitOffsetPagination):
offset_query_param = 'page[offset]'
limit_query_param = 'page[limit]'
max_limit = None
```

As shown above, pages can be selected with the `page` or `page[limit]` GET query parameter when using
the PageNumberPagination or LimitOffsetPagination class, respectively.

If you want to use the PageNumberPagination query parameter names shown
as an example in the JSON API [specification](http://jsonapi.org/format/#fetching-pagination),
set them as follows:
```python
class MyPagePagination(PageNumberPagination):
page_query_param = 'page[number]'
page_size_query_param = 'page[size]'
```

#### Setting global defaults for pagination

Set global defaults for the `PageNumberPagination` class with these settings:
- `JSON_API_PAGE_NUMBER_PARAM` sets the name of the page number query parameter (default: "page").
- `JSON_API_PAGE_SIZE_PARAM` sets the name of the page size query parameter (default: "page_size").
- `JSON_API_MAX_PAGE_SIZE` sets an upper limit for the page size query parameter (default: 100).

Set global defaults for the `LimitOffsetPagination` class with these settings:
- `JSON_API_PAGE_OFFSET_PARAM` sets the name of the page offset query parameter (default: "page\[offset\]").
- `JSON_API_PAGE_LIMIT_PARAM` sets the name of the page limit query parameter (default: "page\[limit\]").
- `JSON_API_MAX_PAGE_LIMIT` sets an upper limit for the page limit query parameter (default: None).

If you want to set the default PageNumberPagination query parameter names shown
as an example in the JSON API [specification](http://jsonapi.org/format/#fetching-pagination),
set them as follows:
```python
JSON_API_PAGE_NUMBER_PARAM = 'page[number]'
JSON_API_PAGE_SIZE_PARAM = 'page[size]'
```

### Exception handling

For the `exception_handler` class, if the optional `JSON_API_UNIFORM_EXCEPTIONS` is set to True,
all exceptions will respond with the JSON API [error format](http://jsonapi.org/format/#error-objects).

When `JSON_API_UNIFORM_EXCEPTIONS` is False (the default), non-JSON API views will respond
with the normal DRF error format.

### Performance Testing

If you are trying to see if your viewsets are configured properly to optimize performance,
it is preferable to use `example.utils.BrowsableAPIRendererWithoutForms` instead of the default `BrowsableAPIRenderer`
Expand Down Expand Up @@ -606,6 +665,30 @@ with 1e18 rows which will likely exhaust any available memory and
slow your database to crawl.

The prefetch_related case will issue 4 queries, but they will be small and fast queries.

### Filtering and Sorting views and viewsets

To add JSON API-style [filtering](http://jsonapi.org/recommendations/#filtering)
or [sorting](http://jsonapi.org/format/#fetching-sorting) query parameters for GET requests,
add the FilterMixin or SortMixin to a view or viewset. For example:

```python
from djangorestframework_jsonapi.mixins import FilterMixin, SortMixin
from rest_framework_json_api.views import viewsets

class WidgetViewSet(SortMixin, FilterMixin, viewsets.ModelViewSet):
queryset = Widget.objects.all()
serializer_class = WidgetSerializer
```

### Sparse Fieldsets

The HyperlinkedModelSerializer already includes the mixin for the
`field[type]=attr1,attr2,...` parameter
([sparse fieldsets](http://jsonapi.org/format/#fetching-sparse-fieldsets)).



<!--
### Relationships
### Errors
Expand Down
70 changes: 70 additions & 0 deletions example/tests/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -280,3 +280,73 @@ def test_no_content_response(self):
response = self.client.delete(url)
assert response.status_code == 204, response.rendered_content.decode()
assert len(response.rendered_content) == 0, response.rendered_content.decode()


class TestSortFilterViewSet(TestBase):
def setUp(self):
self.blog = Blog.objects.create(name='Some Blog', tagline="Also a blog")
self.entry1 = Entry.objects.create(blog=self.blog,
headline="common header",
body_text="1st body text")
self.entry2 = Entry.objects.create(blog=self.blog,
headline="differ header",
body_text="2nd body text")
self.entry3 = Entry.objects.create(blog=self.blog,
headline="common header",
body_text="3rd body text")

def test_sort_view(self):
url = '/entries'
querystring = 'sort=-headline,body_text'
response = self.client.get(url + '?page_size=10&' + querystring)
assert response.status_code == 200, response.content.decode('utf-8')
j = json.loads(response.content.decode('utf-8'))
assert len(j['data']) == 3
assert j['data'][0]['attributes']['headline'] == 'differ header'
assert j['data'][1]['attributes']['headline'] == 'common header'
assert j['data'][2]['attributes']['headline'] == 'common header'
assert j['data'][0]['attributes']['bodyText'] == '2nd body text'
assert j['data'][1]['attributes']['bodyText'] == '1st body text'
assert j['data'][2]['attributes']['bodyText'] == '3rd body text'

def test_filter_view(self):
url = '/entries'
querystring = 'filter[headline]=common header'
response = self.client.get(url + '?page_size=10&' + querystring)
assert response.status_code == 200, response.content.decode()
j = json.loads(response.content.decode('utf-8'))
assert len(j['data']) == 2
assert j['data'][0]['attributes']['headline'] == 'common header'
assert j['data'][1]['attributes']['headline'] == 'common header'

def test_sort_filter_fields_view(self):
url = '/entries'
# TODO: this is a pre-existing bug in SparseFieldSetsMixin:
# fields[body_text] vs. camelized bodyText in response.
querystring = 'sort=-headline,body_text&filter[headline]=common header'\
'&fields[entries]=body_text,mod_date'
response = self.client.get(url + '?page_size=10&' + querystring)
assert response.status_code == 200, response.content.decode()
j = json.loads(response.content.decode('utf-8'))
assert len(j['data']) == 2
assert j['data'][0]['attributes']['bodyText'] == '1st body text'
assert j['data'][1]['attributes']['bodyText'] == '3rd body text'
assert 'headline' not in j['data'][0]['attributes']

def test_sort_bad_key(self):
url = '/entries'
querystring = 'sort=-body_text,XxX'
response = self.client.get(url + '?page_size=10&' + querystring)
assert response.status_code == 400, response.content.decode()
j = json.loads(response.content.decode('utf-8'))
assert j['errors'][0]['code'] == 'field_error'
assert j['errors'][0]['source']['parameter'] == 'XxX'

def test_filter_bad_key(self):
url = '/entries'
querystring = 'filter[XxX]=43'
response = self.client.get(url + '?page_size=10&' + querystring)
assert response.status_code == 400, response.content.decode()
j = json.loads(response.content.decode('utf-8'))
assert j['errors'][0]['code'] == 'field_error'
assert j['errors'][0]['source']['parameter'] == 'XxX'
19 changes: 12 additions & 7 deletions example/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import rest_framework_json_api.metadata
import rest_framework_json_api.parsers
import rest_framework_json_api.renderers
from rest_framework_json_api.mixins import FilterMixin, MultipleIDMixin, SortMixin
from rest_framework_json_api.pagination import PageNumberPagination
from rest_framework_json_api.utils import format_drf_errors
from rest_framework_json_api.views import ModelViewSet, RelationshipView
Expand All @@ -22,12 +23,16 @@
HTTP_422_UNPROCESSABLE_ENTITY = 422


class BlogViewSet(ModelViewSet):
class BaseModelViewSet(SortMixin, FilterMixin, MultipleIDMixin, ModelViewSet):
pass


class BlogViewSet(BaseModelViewSet):
queryset = Blog.objects.all()
serializer_class = BlogSerializer


class JsonApiViewSet(ModelViewSet):
class JsonApiViewSet(BaseModelViewSet):
"""
This is an example on how to configure DRF-jsonapi from
within a class. It allows using DRF-jsonapi alongside
Expand Down Expand Up @@ -61,7 +66,7 @@ class BlogCustomViewSet(JsonApiViewSet):
serializer_class = BlogSerializer


class EntryViewSet(ModelViewSet):
class EntryViewSet(BaseModelViewSet):
queryset = Entry.objects.all()
resource_name = 'posts'

Expand All @@ -77,12 +82,12 @@ class NonPaginatedEntryViewSet(EntryViewSet):
pagination_class = NoPagination


class AuthorViewSet(ModelViewSet):
class AuthorViewSet(BaseModelViewSet):
queryset = Author.objects.all()
serializer_class = AuthorSerializer


class CommentViewSet(ModelViewSet):
class CommentViewSet(BaseModelViewSet):
queryset = Comment.objects.all()
serializer_class = CommentSerializer
prefetch_for_includes = {
Expand All @@ -91,12 +96,12 @@ class CommentViewSet(ModelViewSet):
}


class CompanyViewset(ModelViewSet):
class CompanyViewset(BaseModelViewSet):
queryset = Company.objects.all()
serializer_class = CompanySerializer


class ProjectViewset(ModelViewSet):
class ProjectViewset(BaseModelViewSet):
queryset = Project.objects.all()
serializer_class = ProjectSerializer

Expand Down
Loading