Skip to content

Issue 430: pagination enhancement #434

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

Merged
merged 14 commits into from
May 17, 2018
Merged
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
v2.5.0 - [unreleased]
* Add new pagination classes based on JSON:API query parameter *recommendations*:
* JsonApiPageNumberPagination and JsonApiLimitOffsetPagination. See [usage docs](docs/usage.md#pagination).
* Deprecates PageNumberPagination and LimitOffsetPagination.

v2.4.0 - Released January 25, 2018

* Add support for Django REST Framework 3.7.x.
Expand Down
55 changes: 48 additions & 7 deletions docs/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ REST_FRAMEWORK = {
'PAGE_SIZE': 10,
'EXCEPTION_HANDLER': 'rest_framework_json_api.exceptions.exception_handler',
'DEFAULT_PAGINATION_CLASS':
'rest_framework_json_api.pagination.PageNumberPagination',
'rest_framework_json_api.pagination.JsonApiPageNumberPagination',
'DEFAULT_PARSER_CLASSES': (
'rest_framework_json_api.parsers.JSONParser',
'rest_framework.parsers.FormParser',
Expand All @@ -34,14 +34,55 @@ REST_FRAMEWORK = {
}
```

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

DJA pagination is based on [DRF pagination](http://www.django-rest-framework.org/api-guide/pagination/).

When pagination is enabled, 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
#### Configuring the Pagination Style

Pagination style can be set on a particular viewset with the `pagination_class` attribute or by default for all viewsets
by setting `REST_FRAMEWORK['DEFAULT_PAGINATION_CLASS']` and by setting `REST_FRAMEWORK['PAGE_SIZE']`.

You can configure fixed values for the page size or limit -- or allow the client to choose the size or limit
via query parameters.

Two pagination classes are available:
- `JsonApiPageNumberPagination` breaks a response up into pages that start at a given page number with a given size
(number of items per page). It can be configured with the following attributes:
- `page_query_param` (default `page[number]`)
- `page_size_query_param` (default `page[size]`) Set this to `None` if you don't want to allow the client
to specify the size.
- `max_page_size` (default `100`) enforces an upper bound on the `page_size_query_param`.
Set it to `None` if you don't want to enforce an upper bound.
- `JsonApiLimitOffsetPagination` breaks a response up into pages that start from an item's offset in the viewset for
a given number of items (the limit).
It can be configured with the following attributes:
- `offset_query_param` (default `page[offset]`).
- `limit_query_param` (default `page[limit]`).
- `max_limit` (default `100`) enforces an upper bound on the limit.
Set it to `None` if you don't want to enforce an upper bound.


These examples show how to configure the parameters to use non-standard names and different limits:

```python
from rest_framework_json_api.pagination import JsonApiPageNumberPagination, JsonApiLimitOffsetPagination

class MyPagePagination(JsonApiPageNumberPagination):
page_query_param = 'page_number'
page_size_query_param = 'page_size'
max_page_size = 1000

class MyLimitPagination(JsonApiLimitOffsetPagination):
offset_query_param = 'offset'
limit_query_param = 'limit'
max_limit = None
```

### 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
29 changes: 26 additions & 3 deletions example/tests/unit/test_pagination.py
Original file line number Diff line number Diff line change
@@ -1,21 +1,23 @@
import sys
from collections import OrderedDict

import pytest
from rest_framework.request import Request
from rest_framework.test import APIRequestFactory
from rest_framework.utils.urls import replace_query_param

from rest_framework_json_api.pagination import LimitOffsetPagination
from rest_framework_json_api import pagination

factory = APIRequestFactory()


class TestLimitOffset:
"""
Unit tests for `pagination.LimitOffsetPagination`.
Unit tests for `pagination.JsonApiLimitOffsetPagination`.
"""

def setup(self):
class ExamplePagination(LimitOffsetPagination):
class ExamplePagination(pagination.JsonApiLimitOffsetPagination):
default_limit = 10
max_limit = 15

Expand Down Expand Up @@ -76,3 +78,24 @@ def test_valid_offset_limit(self):

assert queryset == list(range(offset + 1, next_offset + 1))
assert content == expected_content

def test_limit_offset_deprecation(self):
with pytest.warns(DeprecationWarning) as record:
pagination.LimitOffsetPagination()
assert len(record) == 1
assert 'LimitOffsetPagination' in str(record[0].message)


# TODO: This test fails under py27 but it's not clear why so just leave it out for now.
Copy link
Member

Choose a reason for hiding this comment

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

There is a pytest feature called xfail. See https://docs.pytest.org/en/latest/skipping.html

So instead of commenting this out you can mark it as @pytest.mark.xfail and still leave you comment with TODO.

This is that CI doesn't fail even when test is not successful but it still counts towards coverage.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

done. Got caught by flake8 again (#436 will address this if and when done)

@pytest.mark.xfail((sys.version_info.major, sys.version_info.minor) == (2, 7),
reason="python2.7 fails for unknown reason")
class TestPageNumber:
"""
Unit tests for `pagination.JsonApiPageNumberPagination`.
TODO: add unit tests for changing query parameter names, limits, etc.
"""
def test_page_number_deprecation(self):
with pytest.warns(DeprecationWarning) as record:
pagination.PageNumberPagination()
assert len(record) == 1
assert 'PageNumberPagination' in str(record[0].message)
41 changes: 37 additions & 4 deletions rest_framework_json_api/pagination.py
Original file line number Diff line number Diff line change
@@ -1,19 +1,20 @@
"""
Pagination fields
"""
import warnings
from collections import OrderedDict

from rest_framework.pagination import LimitOffsetPagination, PageNumberPagination
from rest_framework.utils.urls import remove_query_param, replace_query_param
from rest_framework.views import Response


class PageNumberPagination(PageNumberPagination):
class JsonApiPageNumberPagination(PageNumberPagination):
"""
A json-api compatible pagination format
"""

page_size_query_param = 'page_size'
page_query_param = 'page[number]'
page_size_query_param = 'page[size]'
max_page_size = 100

def build_link(self, index):
Expand Down Expand Up @@ -49,14 +50,15 @@ def get_paginated_response(self, data):
})


class LimitOffsetPagination(LimitOffsetPagination):
class JsonApiLimitOffsetPagination(LimitOffsetPagination):
"""
A limit/offset based style. For example:
http://api.example.org/accounts/?page[limit]=100
http://api.example.org/accounts/?page[offset]=400&page[limit]=100
"""
limit_query_param = 'page[limit]'
offset_query_param = 'page[offset]'
max_limit = 100

def get_last_link(self):
if self.count == 0:
Expand Down Expand Up @@ -96,3 +98,34 @@ def get_paginated_response(self, data):
('prev', self.get_previous_link())
])
})


class PageNumberPagination(JsonApiPageNumberPagination):
"""
Deprecated paginator that uses different query parameters
"""
page_query_param = 'page'
page_size_query_param = 'page_size'

def __init__(self):
warnings.warn(
'PageNumberPagination is deprecated. Use JsonApiPageNumberPagination '
'or create custom pagination. See '
'http://django-rest-framework-json-api.readthedocs.io/en/stable/usage.html#pagination',
DeprecationWarning)
super(PageNumberPagination, self).__init__()


class LimitOffsetPagination(JsonApiLimitOffsetPagination):
"""
Deprecated paginator that uses a different max_limit
"""
max_limit = None

def __init__(self):
warnings.warn(
'LimitOffsetPagination is deprecated. Use JsonApiLimitOffsetPagination '
'or create custom pagination. See '
'http://django-rest-framework-json-api.readthedocs.io/en/stable/usage.html#pagination',
DeprecationWarning)
super(LimitOffsetPagination, self).__init__()