mirror of
https://github.com/encode/django-rest-framework.git
synced 2025-02-02 20:54:42 +03:00
Merge pull request #2395 from tomchristie/pagination-api
First pass at 3.1 pagination API
This commit is contained in:
commit
12de43fc0c
125
CONTRIBUTING.md
125
CONTRIBUTING.md
|
@ -10,9 +10,9 @@ There are many ways you can contribute to Django REST framework. We'd like it t
|
|||
|
||||
The most important thing you can do to help push the REST framework project forward is to be actively involved wherever possible. Code contributions are often overvalued as being the primary way to get involved in a project, we don't believe that needs to be the case.
|
||||
|
||||
If you use REST framework, we'd love you to be vocal about your experiences with it - you might consider writing a blog post about using REST framework, or publishing a tutorial about building a project with a particular Javascript framework. Experiences from beginners can be particularly helpful because you'll be in the best position to assess which bits of REST framework are more difficult to understand and work with.
|
||||
If you use REST framework, we'd love you to be vocal about your experiences with it - you might consider writing a blog post about using REST framework, or publishing a tutorial about building a project with a particular JavaScript framework. Experiences from beginners can be particularly helpful because you'll be in the best position to assess which bits of REST framework are more difficult to understand and work with.
|
||||
|
||||
Other really great ways you can help move the community forward include helping answer questions on the [discussion group][google-group], or setting up an [email alert on StackOverflow][so-filter] so that you get notified of any new questions with the `django-rest-framework` tag.
|
||||
Other really great ways you can help move the community forward include helping to answer questions on the [discussion group][google-group], or setting up an [email alert on StackOverflow][so-filter] so that you get notified of any new questions with the `django-rest-framework` tag.
|
||||
|
||||
When answering questions make sure to help future contributors find their way around by hyperlinking wherever possible to related threads and tickets, and include backlinks from those items if relevant.
|
||||
|
||||
|
@ -52,7 +52,7 @@ To start developing on Django REST framework, clone the repo:
|
|||
|
||||
git clone git@github.com:tomchristie/django-rest-framework.git
|
||||
|
||||
Changes should broadly follow the [PEP 8][pep-8] style conventions, and we recommend you setup your editor to automatically indicated non-conforming styles.
|
||||
Changes should broadly follow the [PEP 8][pep-8] style conventions, and we recommend you set up your editor to automatically indicate non-conforming styles.
|
||||
|
||||
## Testing
|
||||
|
||||
|
@ -60,13 +60,47 @@ To run the tests, clone the repository, and then:
|
|||
|
||||
# Setup the virtual environment
|
||||
virtualenv env
|
||||
env/bin/activate
|
||||
source env/bin/activate
|
||||
pip install -r requirements.txt
|
||||
|
||||
# Run the tests
|
||||
./runtests.py
|
||||
|
||||
You can also use the excellent [`tox`][tox] testing tool to run the tests against all supported versions of Python and Django. Install `tox` globally, and then simply run:
|
||||
### Test options
|
||||
|
||||
Run using a more concise output style.
|
||||
|
||||
./runtests.py -q
|
||||
|
||||
Run the tests using a more concise output style, no coverage, no flake8.
|
||||
|
||||
./runtests.py --fast
|
||||
|
||||
Don't run the flake8 code linting.
|
||||
|
||||
./runtests.py --nolint
|
||||
|
||||
Only run the flake8 code linting, don't run the tests.
|
||||
|
||||
./runtests.py --lintonly
|
||||
|
||||
Run the tests for a given test case.
|
||||
|
||||
./runtests.py MyTestCase
|
||||
|
||||
Run the tests for a given test method.
|
||||
|
||||
./runtests.py MyTestCase.test_this_method
|
||||
|
||||
Shorter form to run the tests for a given test method.
|
||||
|
||||
./runtests.py test_this_method
|
||||
|
||||
Note: The test case and test method matching is fuzzy and will sometimes run other tests that contain a partial string match to the given command line input.
|
||||
|
||||
### Running against multiple environments
|
||||
|
||||
You can also use the excellent [tox][tox] testing tool to run the tests against all supported versions of Python and Django. Install `tox` globally, and then simply run:
|
||||
|
||||
tox
|
||||
|
||||
|
@ -82,7 +116,7 @@ GitHub's documentation for working on pull requests is [available here][pull-req
|
|||
|
||||
Always run the tests before submitting pull requests, and ideally run `tox` in order to check that your modifications are compatible with both Python 2 and Python 3, and that they run properly on all supported versions of Django.
|
||||
|
||||
Once you've made a pull request take a look at the travis build status in the GitHub interface and make sure the tests are running as you'd expect.
|
||||
Once you've made a pull request take a look at the Travis build status in the GitHub interface and make sure the tests are running as you'd expect.
|
||||
|
||||
![Travis status][travis-status]
|
||||
|
||||
|
@ -96,7 +130,7 @@ Sometimes, in order to ensure your code works on various different versions of D
|
|||
|
||||
The documentation for REST framework is built from the [Markdown][markdown] source files in [the docs directory][docs].
|
||||
|
||||
There are many great markdown editors that make working with the documentation really easy. The [Mou editor for Mac][mou] is one such editor that comes highly recommended.
|
||||
There are many great Markdown editors that make working with the documentation really easy. The [Mou editor for Mac][mou] is one such editor that comes highly recommended.
|
||||
|
||||
## Building the documentation
|
||||
|
||||
|
@ -104,7 +138,7 @@ To build the documentation, install MkDocs with `pip install mkdocs` and then ru
|
|||
|
||||
mkdocs build
|
||||
|
||||
This will build the html output into the `html` directory.
|
||||
This will build the documentation into the `site` directory.
|
||||
|
||||
You can build the documentation and open a preview in a browser window by using the `serve` command.
|
||||
|
||||
|
@ -117,8 +151,7 @@ Documentation should be in American English. The tone of the documentation is v
|
|||
Some other tips:
|
||||
|
||||
* Keep paragraphs reasonably short.
|
||||
* Use double spacing after the end of sentences.
|
||||
* Don't use the abbreviations such as 'e.g.' but instead use long form, such as 'For example'.
|
||||
* Don't use abbreviations such as 'e.g.' but instead use the long form, such as 'For example'.
|
||||
|
||||
## Markdown style
|
||||
|
||||
|
@ -151,7 +184,7 @@ If you are hyperlinking to another REST framework document, you should use a rel
|
|||
|
||||
[authentication]: ../api-guide/authentication.md
|
||||
|
||||
Linking in this style means you'll be able to click the hyperlink in your markdown editor to open the referenced document. When the documentation is built, these links will be converted into regular links to HTML pages.
|
||||
Linking in this style means you'll be able to click the hyperlink in your Markdown editor to open the referenced document. When the documentation is built, these links will be converted into regular links to HTML pages.
|
||||
|
||||
##### 3. Notes
|
||||
|
||||
|
@ -163,70 +196,6 @@ If you want to draw attention to a note or warning, use a pair of enclosing line
|
|||
|
||||
---
|
||||
|
||||
# Third party packages
|
||||
|
||||
New features to REST framework are generally recommended to be implemented as third party libraries that are developed outside of the core framework. Ideally third party libraries should be properly documented and packaged, and made available on PyPI.
|
||||
|
||||
## Getting started
|
||||
|
||||
If you have some functionality that you would like to implement as a third party package it's worth contacting the [discussion group][google-group] as others may be willing to get involved. We strongly encourage third party package development and will always try to prioritize time spent helping their development, documentation and packaging.
|
||||
|
||||
We recommend the [`django-reusable-app`][django-reusable-app] template as a good resource for getting up and running with implementing a third party Django package.
|
||||
|
||||
## Linking to your package
|
||||
|
||||
Once your package is decently documented and available on PyPI open a pull request or issue, and we'll add a link to it from the main REST framework documentation.
|
||||
|
||||
# Translations
|
||||
|
||||
If REST framework isn't translated into your language you can request that it is at the [Transifex project][transifex].
|
||||
|
||||
## Managing Transfiex
|
||||
The [official Transifex client][transifex-client] is used to upload and download translations to Transifex. The client is installed using pip:
|
||||
|
||||
```
|
||||
pip install transifex-client
|
||||
```
|
||||
|
||||
To use it you'll need a login to Transifex which has a password, and you'll need to have administrative access to the Transifex project. You'll need to create a `~/.transifexrc` file which contains your authentication information:
|
||||
|
||||
```
|
||||
[https://www.transifex.com]
|
||||
username = user
|
||||
token =
|
||||
password = p@ssw0rd
|
||||
hostname = https://www.transifex.com
|
||||
```
|
||||
|
||||
## Upload new source translations
|
||||
When any user-visible strings are changed, they should be uploaded to Transifex so that the translators can start to translate them. To do this, just run:
|
||||
|
||||
```
|
||||
cd rest_framework
|
||||
django-admin.py makemessages -l en_US
|
||||
cd ..
|
||||
tx push -s
|
||||
```
|
||||
|
||||
When pushing source files, Transifex will update the source strings of a resource to match those from the new source file.
|
||||
|
||||
Here's how differences between the old and new source files will be handled:
|
||||
|
||||
* New strings will be added.
|
||||
* Modified strings will be added as well.
|
||||
* Strings which do not exist in the new source file will be removed from the database, along with their translations. If that source strings gets re-added later then [Transifex Translation Memory][translation-memory] will automatically restore the translated string too.
|
||||
|
||||
|
||||
## Get translations
|
||||
When a translator has finished translating their work needs to be downloaded from Transifex into the source repo. To do this, run:
|
||||
|
||||
```
|
||||
tx pull -a
|
||||
cd rest_framework
|
||||
django-admin.py compilemessages
|
||||
```
|
||||
|
||||
You can then commit as normal.
|
||||
|
||||
[cite]: http://www.w3.org/People/Berners-Lee/FAQ.html
|
||||
[code-of-conduct]: https://www.djangoproject.com/conduct/
|
||||
|
@ -234,13 +203,9 @@ You can then commit as normal.
|
|||
[so-filter]: http://stackexchange.com/filters/66475/rest-framework
|
||||
[issues]: https://github.com/tomchristie/django-rest-framework/issues?state=open
|
||||
[pep-8]: http://www.python.org/dev/peps/pep-0008/
|
||||
[travis-status]: https://raw.github.com/tomchristie/django-rest-framework/master/docs/img/travis-status.png
|
||||
[travis-status]: ../img/travis-status.png
|
||||
[pull-requests]: https://help.github.com/articles/using-pull-requests
|
||||
[tox]: http://tox.readthedocs.org/en/latest/
|
||||
[markdown]: http://daringfireball.net/projects/markdown/basics
|
||||
[docs]: https://github.com/tomchristie/django-rest-framework/tree/master/docs
|
||||
[mou]: http://mouapp.com/
|
||||
[django-reusable-app]: https://github.com/dabapps/django-reusable-app
|
||||
[transifex]: https://www.transifex.com/projects/p/django-rest-framework/
|
||||
[transifex-client]: https://pypi.python.org/pypi/transifex-client
|
||||
[translation-memory]: http://docs.transifex.com/guides/tm#let-tm-automatically-populate-translations
|
|
@ -6,148 +6,101 @@ source: pagination.py
|
|||
>
|
||||
> — [Django documentation][cite]
|
||||
|
||||
REST framework includes a `PaginationSerializer` class that makes it easy to return paginated data in a way that can then be rendered to arbitrary media types.
|
||||
REST framework includes support for customizable pagination styles. This allows you to modify how large result sets are split into individual pages of data.
|
||||
|
||||
## Paginating basic data
|
||||
The pagination API can support either:
|
||||
|
||||
Let's start by taking a look at an example from the Django documentation.
|
||||
* Pagination links that are provided as part of the content of the response.
|
||||
* Pagination links that are included in response headers, such as `Content-Range` or `Link`.
|
||||
|
||||
from django.core.paginator import Paginator
|
||||
The built-in styles currently all use links included as part of the content of the response. This style is more accessible when using the browsable API.
|
||||
|
||||
objects = ['john', 'paul', 'george', 'ringo']
|
||||
paginator = Paginator(objects, 2)
|
||||
page = paginator.page(1)
|
||||
page.object_list
|
||||
# ['john', 'paul']
|
||||
Pagination is only performed automatically if you're using the generic views or viewsets. If you're using a regular `APIView`, you'll need to call into the pagination API yourself to ensure you return a paginated response. See the source code for the `mixins.ListMixin` and `generics.GenericAPIView` classes for an example.
|
||||
|
||||
At this point we've got a page object. If we wanted to return this page object as a JSON response, we'd need to provide the client with context such as next and previous links, so that it would be able to page through the remaining results.
|
||||
## Setting the pagination style
|
||||
|
||||
from rest_framework.pagination import PaginationSerializer
|
||||
|
||||
serializer = PaginationSerializer(instance=page)
|
||||
serializer.data
|
||||
# {'count': 4, 'next': '?page=2', 'previous': None, 'results': [u'john', u'paul']}
|
||||
|
||||
The `context` argument of the `PaginationSerializer` class may optionally include the request. If the request is included in the context then the next and previous links returned by the serializer will use absolute URLs instead of relative URLs.
|
||||
|
||||
request = RequestFactory().get('/foobar')
|
||||
serializer = PaginationSerializer(instance=page, context={'request': request})
|
||||
serializer.data
|
||||
# {'count': 4, 'next': 'http://testserver/foobar?page=2', 'previous': None, 'results': [u'john', u'paul']}
|
||||
|
||||
We could now return that data in a `Response` object, and it would be rendered into the correct media type.
|
||||
|
||||
## Paginating QuerySets
|
||||
|
||||
Our first example worked because we were using primitive objects. If we wanted to paginate a queryset or other complex data, we'd need to specify a serializer to use to serialize the result set itself.
|
||||
|
||||
We can do this using the `object_serializer_class` attribute on the inner `Meta` class of the pagination serializer. For example.
|
||||
|
||||
class UserSerializer(serializers.ModelSerializer):
|
||||
"""
|
||||
Serializes user querysets.
|
||||
"""
|
||||
class Meta:
|
||||
model = User
|
||||
fields = ('username', 'email')
|
||||
|
||||
class PaginatedUserSerializer(pagination.PaginationSerializer):
|
||||
"""
|
||||
Serializes page objects of user querysets.
|
||||
"""
|
||||
class Meta:
|
||||
object_serializer_class = UserSerializer
|
||||
|
||||
We could now use our pagination serializer in a view like this.
|
||||
|
||||
@api_view('GET')
|
||||
def user_list(request):
|
||||
queryset = User.objects.all()
|
||||
paginator = Paginator(queryset, 20)
|
||||
|
||||
page = request.QUERY_PARAMS.get('page')
|
||||
try:
|
||||
users = paginator.page(page)
|
||||
except PageNotAnInteger:
|
||||
# If page is not an integer, deliver first page.
|
||||
users = paginator.page(1)
|
||||
except EmptyPage:
|
||||
# If page is out of range (e.g. 9999),
|
||||
# deliver last page of results.
|
||||
users = paginator.page(paginator.num_pages)
|
||||
|
||||
serializer_context = {'request': request}
|
||||
serializer = PaginatedUserSerializer(users,
|
||||
context=serializer_context)
|
||||
return Response(serializer.data)
|
||||
|
||||
## Pagination in the generic views
|
||||
|
||||
The generic class based views `ListAPIView` and `ListCreateAPIView` provide pagination of the returned querysets by default. You can customise this behaviour by altering the pagination style, by modifying the default number of results, by allowing clients to override the page size using a query parameter, or by turning pagination off completely.
|
||||
|
||||
The default pagination style may be set globally, using the `DEFAULT_PAGINATION_SERIALIZER_CLASS`, `PAGINATE_BY`, `PAGINATE_BY_PARAM`, and `MAX_PAGINATE_BY` settings. For example.
|
||||
The default pagination style may be set globally, using the `DEFAULT_PAGINATION_CLASS` settings key. For example, to use the built-in limit/offset pagination, you would do:
|
||||
|
||||
REST_FRAMEWORK = {
|
||||
'PAGINATE_BY': 10, # Default to 10
|
||||
'PAGINATE_BY_PARAM': 'page_size', # Allow client to override, using `?page_size=xxx`.
|
||||
'MAX_PAGINATE_BY': 100 # Maximum limit allowed when using `?page_size=xxx`.
|
||||
'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.LimitOffsetPagination'
|
||||
}
|
||||
|
||||
You can also set the pagination style on a per-view basis, using the `ListAPIView` generic class-based view.
|
||||
You can also set the pagination class on an individual view by using the `pagination_class` attribute. Typically you'll want to use the same pagination style throughout your API, although you might want to vary individual aspects of the pagination, such as default or maximum page size, on a per-view basis.
|
||||
|
||||
class PaginatedListView(ListAPIView):
|
||||
queryset = ExampleModel.objects.all()
|
||||
serializer_class = ExampleModelSerializer
|
||||
paginate_by = 10
|
||||
## Modifying the pagination style
|
||||
|
||||
If you want to modify particular aspects of the pagination style, you'll want to override one of the pagination classes, and set the attributes that you want to change.
|
||||
|
||||
class LargeResultsSetPagination(PageNumberPagination):
|
||||
paginate_by = 1000
|
||||
paginate_by_param = 'page_size'
|
||||
max_paginate_by = 100
|
||||
max_paginate_by = 10000
|
||||
|
||||
Note that using a `paginate_by` value of `None` will turn off pagination for the view.
|
||||
Note if you use the `PAGINATE_BY_PARAM` settings, you also have to set the `paginate_by_param` attribute in your view to `None` in order to turn off pagination for those requests that contain the `paginate_by_param` parameter.
|
||||
class StandardResultsSetPagination(PageNumberPagination):
|
||||
paginate_by = 100
|
||||
paginate_by_param = 'page_size'
|
||||
max_paginate_by = 1000
|
||||
|
||||
For more complex requirements such as serialization that differs depending on the requested media type you can override the `.get_paginate_by()` and `.get_pagination_serializer_class()` methods.
|
||||
You can then apply your new style to a view using the `.pagination_class` attribute:
|
||||
|
||||
class BillingRecordsView(generics.ListAPIView):
|
||||
queryset = Billing.objects.all()
|
||||
serializer = BillingRecordsSerializer
|
||||
pagination_class = LargeResultsSetPagination
|
||||
|
||||
Or apply the style globally, using the `DEFAULT_PAGINATION_CLASS` settings key. For example:
|
||||
|
||||
REST_FRAMEWORK = {
|
||||
'DEFAULT_PAGINATION_CLASS': 'apps.core.pagination.StandardResultsSetPagination'
}
|
||||
|
||||
# API Reference
|
||||
|
||||
## PageNumberPagination
|
||||
|
||||
## LimitOffsetPagination
|
||||
|
||||
---
|
||||
|
||||
# Custom pagination serializers
|
||||
# Custom pagination styles
|
||||
|
||||
To create a custom pagination serializer class you should override `pagination.BasePaginationSerializer` and set the fields that you want the serializer to return.
|
||||
To create a custom pagination serializer class you should subclass `pagination.BasePagination` and override the `paginate_queryset(self, queryset, request, view)` and `get_paginated_response(self, data)` methods:
|
||||
|
||||
You can also override the name used for the object list field, by setting the `results_field` attribute, which defaults to `'results'`.
|
||||
* The `paginate_queryset` method is passed the initial queryset and should return an iterable object that contains only the data in the requested page.
|
||||
* The `get_paginated_response` method is passed the serialized page data and should return a `Response` instance.
|
||||
|
||||
Note that the `paginate_queryset` method may set state on the pagination instance, that may later be used by the `get_paginated_response` method.
|
||||
|
||||
## Example
|
||||
|
||||
For example, to nest a pair of links labelled 'prev' and 'next', and set the name for the results field to 'objects', you might use something like this.
|
||||
Let's modify the built-in `PageNumberPagination` style, so that instead of include the pagination links in the body of the response, we'll instead include a `Link` header, in a [similar style to the GitHub API][github-link-pagination].
|
||||
|
||||
from rest_framework import pagination
|
||||
from rest_framework import serializers
|
||||
class LinkHeaderPagination(PageNumberPagination)
|
||||
def get_paginated_response(self, data):
|
||||
next_url = self.get_next_link()
previous_url = self.get_previous_link()
|
||||
|
||||
class LinksSerializer(serializers.Serializer):
|
||||
next = pagination.NextPageField(source='*')
|
||||
prev = pagination.PreviousPageField(source='*')
|
||||
if next_url is not None and previous_url is not None:
|
||||
link = '<{next_url}; rel="next">, <{previous_url}; rel="prev">'
|
||||
elif next_url is not None:
|
||||
link = '<{next_url}; rel="next">'
|
||||
elif prev_url is not None:
|
||||
link = '<{previous_url}; rel="prev">'
|
||||
else:
|
||||
link = ''
|
||||
|
||||
class CustomPaginationSerializer(pagination.BasePaginationSerializer):
|
||||
links = LinksSerializer(source='*') # Takes the page object as the source
|
||||
total_results = serializers.ReadOnlyField(source='paginator.count')
|
||||
link = link.format(next_url=next_url, previous_url=previous_url)
|
||||
headers = {'Link': link} if link else {}
|
||||
|
||||
results_field = 'objects'
|
||||
return Response(data, headers=headers)
|
||||
|
||||
## Using your custom pagination serializer
|
||||
## Using your custom pagination class
|
||||
|
||||
To have your custom pagination serializer be used by default, use the `DEFAULT_PAGINATION_SERIALIZER_CLASS` setting:
|
||||
To have your custom pagination class be used by default, use the `DEFAULT_PAGINATION_CLASS` setting:
|
||||
|
||||
REST_FRAMEWORK = {
|
||||
'DEFAULT_PAGINATION_SERIALIZER_CLASS':
|
||||
'example_app.pagination.CustomPaginationSerializer',
|
||||
'DEFAULT_PAGINATION_CLASS':
|
||||
'my_project.apps.core.pagination.LinkHeaderPagination',
|
||||
}
|
||||
|
||||
Alternatively, to set your custom pagination serializer on a per-view basis, use the `pagination_serializer_class` attribute on a generic class based view:
|
||||
|
||||
class PaginatedListView(generics.ListAPIView):
|
||||
model = ExampleModel
|
||||
pagination_serializer_class = CustomPaginationSerializer
|
||||
paginate_by = 10
|
||||
|
||||
# Third party packages
|
||||
|
||||
The following third party packages are also available.
|
||||
|
@ -157,5 +110,6 @@ The following third party packages are also available.
|
|||
The [`DRF-extensions` package][drf-extensions] includes a [`PaginateByMaxMixin` mixin class][paginate-by-max-mixin] that allows your API clients to specify `?page_size=max` to obtain the maximum allowed page size.
|
||||
|
||||
[cite]: https://docs.djangoproject.com/en/dev/topics/pagination/
|
||||
[github-link-pagination]: https://developer.github.com/guides/traversing-with-pagination/
|
||||
[drf-extensions]: http://chibisov.github.io/drf-extensions/docs/
|
||||
[paginate-by-max-mixin]: http://chibisov.github.io/drf-extensions/docs/#paginatebymaxmixin
|
||||
|
|
|
@ -305,6 +305,7 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|||
[settings]: api-guide/settings.md
|
||||
|
||||
[documenting-your-api]: topics/documenting-your-api.md
|
||||
[internationalization]: topics/documenting-your-api.md
|
||||
[ajax-csrf-cors]: topics/ajax-csrf-cors.md
|
||||
[browser-enhancements]: topics/browser-enhancements.md
|
||||
[browsableapi]: topics/browsable-api.md
|
||||
|
|
7
docs/topics/3.1-announcement.md
Normal file
7
docs/topics/3.1-announcement.md
Normal file
|
@ -0,0 +1,7 @@
|
|||
# Versioning
|
||||
|
||||
# Pagination
|
||||
|
||||
# Internationalization
|
||||
|
||||
# ModelSerializer API
|
|
@ -1,95 +0,0 @@
|
|||
# Internationalisation
|
||||
REST framework ships with translatable error messages. You can make these appear in your language enabling [Django's standard translation mechanisms][django-translation] and by translating the messages into your language.
|
||||
|
||||
## How to translate REST Framework errors
|
||||
|
||||
REST framework translations are managed online using [Transifex.com][transifex]. To get started, checkout the guide in the [CONTRIBUTING.md guide][contributing].
|
||||
|
||||
Sometimes you may want to use REST Framework in a language which has not been translated yet on Transifex. If that is the case then you should translate the error messages locally.
|
||||
|
||||
#### How to translate REST Framework error messages locally:
|
||||
|
||||
This guide assumes you are already familiar with how to translate a Django app. If you're not, start by reading [Django's translation docs][django-translation].
|
||||
|
||||
1. Make a new folder where you want to store the translated errors. Add this
|
||||
path to your [`LOCALE_PATHS`][django-locale-paths] setting.
|
||||
|
||||
---
|
||||
|
||||
**Note:** For the rest of
|
||||
this document we will assume the path you created was
|
||||
`/home/www/project/conf/locale/`, and that you have updated your `settings.py` to include the setting:
|
||||
|
||||
```
|
||||
LOCALE_PATHS = (
|
||||
'/home/www/project/conf/locale/',
|
||||
)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
2. Now create a subfolder for the language you want to translate. The folder should be named using [locale
|
||||
name][django-locale-name] notation. E.g. `de`, `pt_BR`, `es_AR`, etc.
|
||||
|
||||
```
|
||||
mkdir /home/www/project/conf/locale/pt_BR/LC_MESSAGES
|
||||
```
|
||||
|
||||
3. Now copy the base translations file from the REST framework source code
|
||||
into your translations folder
|
||||
|
||||
```
|
||||
cp /home/user/.virtualenvs/myproject/lib/python2.7/site-packages/rest_framework/locale/en_US/LC_MESSAGES/django.po
|
||||
/home/www/project/conf/locale/pt_BR/LC_MESSAGES
|
||||
```
|
||||
|
||||
This should create the file
|
||||
`/home/www/project/conf/locale/pt_BR/LC_MESSAGES/django.po`
|
||||
|
||||
---
|
||||
|
||||
**Note:** To find out where `rest_framework` is installed, run
|
||||
|
||||
```
|
||||
python -c "from distutils.sysconfig import get_python_lib; print(get_python_lib())"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
|
||||
4. Edit `/home/www/project/conf/locale/pt_BR/LC_MESSAGES/django.po` and
|
||||
translate all the error messages.
|
||||
|
||||
5. Run `manage.py compilemessages -l pt_BR` to make the translations
|
||||
available for Django to use. You should see a message
|
||||
|
||||
```
|
||||
processing file django.po in /home/www/project/conf/locale/pt_BR/LC_MESSAGES
|
||||
```
|
||||
|
||||
6. Restart your server.
|
||||
|
||||
|
||||
|
||||
## How Django chooses which language to use
|
||||
REST framework will use the same preferences to select which language to
|
||||
display as Django does. You can find more info in the [Django docs on discovering language preferences][django-language-preference]. For reference, these are
|
||||
|
||||
1. First, it looks for the language prefix in the requested URL
|
||||
2. Failing that, it looks for the `LANGUAGE_SESSION_KEY` key in the current user’s session.
|
||||
3. Failing that, it looks for a cookie
|
||||
4. Failing that, it looks at the `Accept-Language` HTTP header.
|
||||
5. Failing that, it uses the global `LANGUAGE_CODE` setting.
|
||||
|
||||
---
|
||||
|
||||
**Note:** You'll need to include the `django.middleware.locale.LocaleMiddleware` to enable any of the per-request language preferences.
|
||||
|
||||
---
|
||||
|
||||
|
||||
[django-translation]: https://docs.djangoproject.com/en/1.7/topics/i18n/translation
|
||||
[django-language-preference]: https://docs.djangoproject.com/en/1.7/topics/i18n/translation/#how-django-discovers-language-preference
|
||||
[django-locale-paths]: https://docs.djangoproject.com/en/1.7/ref/settings/#std:setting-LOCALE_PATHS
|
||||
[django-locale-name]: https://docs.djangoproject.com/en/1.7/topics/i18n/#term-locale-name
|
||||
[contributing]: ../../CONTRIBUTING.md
|
72
docs/topics/internationalization.md
Normal file
72
docs/topics/internationalization.md
Normal file
|
@ -0,0 +1,72 @@
|
|||
# Internationalization
|
||||
|
||||
> Supporting internationalization is not optional. It must be a core feature.
|
||||
>
|
||||
> — [Jannis Leidel, speaking at Django Under the Hood, 2015][cite].
|
||||
|
||||
REST framework ships with translatable error messages. You can make these appear in your language enabling [Django's standard translation mechanisms][django-translation].
|
||||
|
||||
Doing so will allow you to:
|
||||
|
||||
* Select a language other than English as the default, using the standard `LANGUAGE_CODE` Django setting.
|
||||
* Allow clients to choose a language themselves, using the `LocaleMiddleware` included with Django. A typical usage for API clients would be to include an `Accept-Language` request header.
|
||||
|
||||
Note that the translations only apply to the error strings themselves. The format of error messages, and the keys of field names will remain the same. An example `400 Bad Request` response body might look like this:
|
||||
|
||||
{"detail": {"username": ["Esse campo deve ser unico."]}}
|
||||
|
||||
If you want to use different string for parts of the response such as `detail` and `non_field_errors` then you can modify this behavior by using a [custom exception handler][custom-exception-handler].
|
||||
|
||||
## Adding new translations
|
||||
|
||||
REST framework translations are managed online using [Transifex][transifex-project]. You can use the Transifex service to add new translation languages. The maintenance team will then ensure that these translation strings are included in the REST framework package.
|
||||
|
||||
Sometimes you may need to add translation strings to your project locally. You may need to do this if:
|
||||
|
||||
* You want to use REST Framework in a language which has not been translated yet on Transifex.
|
||||
* Your project includes custom error messages, which are not part of REST framework's default translation strings.
|
||||
|
||||
#### Translating a new language locally
|
||||
|
||||
This guide assumes you are already familiar with how to translate a Django app. If you're not, start by reading [Django's translation docs][django-translation].
|
||||
|
||||
If you're translating a new language you'll need to translate the existing REST framework error messages:
|
||||
|
||||
1. Make a new folder where you want to store the internationalization resources. Add this path to your [`LOCALE_PATHS`][django-locale-paths] setting.
|
||||
|
||||
2. Now create a subfolder for the language you want to translate. The folder should be named using [locale name][django-locale-name] notation. For example: `de`, `pt_BR`, `es_AR`.
|
||||
|
||||
3. Now copy the [base translations file][django-po-source] from the REST framework source code into your translations folder.
|
||||
|
||||
4. Edit the `django.po` file you've just copied, translating all the error messages.
|
||||
|
||||
5. Run `manage.py compilemessages -l pt_BR` to make the translations
|
||||
available for Django to use. You should see a message like `processing file django.po in <...>/locale/pt_BR/LC_MESSAGES`.
|
||||
|
||||
6. Restart your development server to see the changes take effect.
|
||||
|
||||
If you're only translating custom error messages that exist inside your project codebase you don't need to copy the REST framework source `django.po` file into a `LOCALE_PATHS` folder, and can instead simply run Django's standard `makemessages` process.
|
||||
|
||||
## How the language is determined
|
||||
|
||||
If you want to allow per-request language preferences you'll need to include `django.middleware.locale.LocaleMiddleware` in your `MIDDLEWARE_CLASSES` setting.
|
||||
|
||||
You can find more information on how the language preference is determined in the [Django documentation][django-language-preference]. For reference, the method is:
|
||||
|
||||
1. First, it looks for the language prefix in the requested URL.
|
||||
2. Failing that, it looks for the `LANGUAGE_SESSION_KEY` key in the current user’s session.
|
||||
3. Failing that, it looks for a cookie.
|
||||
4. Failing that, it looks at the `Accept-Language` HTTP header.
|
||||
5. Failing that, it uses the global `LANGUAGE_CODE` setting.
|
||||
|
||||
For API clients the most appropriate of these will typically be to use the `Accept-Language` header; Sessions and cookies will not be available unless using session authentication, and generally better practice to prefer an `Accept-Language` header for API clients rather than using language URL prefixes.
|
||||
|
||||
[cite]: http://youtu.be/Wa0VfS2q94Y
|
||||
[django-translation]: https://docs.djangoproject.com/en/1.7/topics/i18n/translation
|
||||
[custom-exception-handler]: ../api-guide/exceptions.md#custom-exception-handling
|
||||
[transifex-project]: https://www.transifex.com/projects/p/django-rest-framework/
|
||||
[django-po-source]: https://raw.githubusercontent.com/tomchristie/django-rest-framework/master/rest_framework/locale/en_US/LC_MESSAGES/django.po
|
||||
[django-language-preference]: https://docs.djangoproject.com/en/1.7/topics/i18n/translation/#how-django-discovers-language-preference
|
||||
[django-locale-paths]: https://docs.djangoproject.com/en/1.7/ref/settings/#std:setting-LOCALE_PATHS
|
||||
[django-locale-name]: https://docs.djangoproject.com/en/1.7/topics/i18n/#term-locale-name
|
||||
[contributing]: ../../CONTRIBUTING.md
|
|
@ -63,10 +63,11 @@ The following template should be used for the description of the issue, and serv
|
|||
|
||||
Team members have the following responsibilities.
|
||||
|
||||
* Add triage labels and milestones to tickets.
|
||||
* Close invalid or resolved tickets.
|
||||
* Add triage labels and milestones to tickets.
|
||||
* Merge finalized pull requests.
|
||||
* Build and deploy the documentation, using `mkdocs gh-deploy`.
|
||||
* Build and update the included translation packs.
|
||||
|
||||
Further notes for maintainers:
|
||||
|
||||
|
@ -112,6 +113,55 @@ When pushing the release to PyPI ensure that your environment has been installed
|
|||
|
||||
---
|
||||
|
||||
## Translations
|
||||
|
||||
The maintenance team are responsible for managing the translation packs include in REST framework. Translating the source strings into multiple languages is managed through the [transifex service][transifex-project].
|
||||
|
||||
### Managing Transifex
|
||||
|
||||
The [official Transifex client][transifex-client] is used to upload and download translations to Transifex. The client is installed using pip:
|
||||
|
||||
pip install transifex-client
|
||||
|
||||
To use it you'll need a login to Transifex which has a password, and you'll need to have administrative access to the Transifex project. You'll need to create a `~/.transifexrc` file which contains your credentials.
|
||||
|
||||
[https://www.transifex.com]
|
||||
username = ***
|
||||
token = ***
|
||||
password = ***
|
||||
hostname = https://www.transifex.com
|
||||
|
||||
### Upload new source files
|
||||
|
||||
When any user visible strings are changed, they should be uploaded to Transifex so that the translators can start to translate them. To do this, just run:
|
||||
|
||||
# 1. Update the source django.po file, which is the US English version.
|
||||
cd rest_framework
|
||||
django-admin.py makemessages -l en_US
|
||||
# 2. Push the source django.po file to Transifex.
|
||||
cd ..
|
||||
tx push -s
|
||||
|
||||
When pushing source files, Transifex will update the source strings of a resource to match those from the new source file.
|
||||
|
||||
Here's how differences between the old and new source files will be handled:
|
||||
|
||||
* New strings will be added.
|
||||
* Modified strings will be added as well.
|
||||
* Strings which do not exist in the new source file will be removed from the database, along with their translations. If that source strings gets re-added later then [Transifex Translation Memory][translation-memory] will automatically include the translation string.
|
||||
|
||||
### Download translations
|
||||
|
||||
When a translator has finished translating their work needs to be downloaded from Transifex into the REST framework repository. To do this, run:
|
||||
|
||||
# 3. Pull the translated django.po files from Transifex.
|
||||
tx pull -a
|
||||
cd rest_framework
|
||||
# 4. Compile the binary .mo files for all supported languages.
|
||||
django-admin.py compilemessages
|
||||
|
||||
---
|
||||
|
||||
## Project ownership
|
||||
|
||||
The PyPI package is owned by `@tomchristie`. As a backup `@j4mie` also has ownership of the package.
|
||||
|
@ -129,6 +179,9 @@ The following issues still need to be addressed:
|
|||
|
||||
[bus-factor]: http://en.wikipedia.org/wiki/Bus_factor
|
||||
[un-triaged]: https://github.com/tomchristie/django-rest-framework/issues?q=is%3Aopen+no%3Alabel
|
||||
[transifex-project]: https://www.transifex.com/projects/p/django-rest-framework/
|
||||
[transifex-client]: https://pypi.python.org/pypi/transifex-client
|
||||
[translation-memory]: http://docs.transifex.com/guides/tm#let-tm-automatically-populate-translations
|
||||
[github-org]: https://github.com/tomchristie/django-rest-framework/issues/2162
|
||||
[sandbox]: http://restframework.herokuapp.com/
|
||||
[mailing-list]: https://groups.google.com/forum/#!forum/django-rest-framework
|
||||
|
|
|
@ -171,6 +171,21 @@ body{
|
|||
background-attachment: fixed;
|
||||
}
|
||||
|
||||
|
||||
#main-content h1:first-of-type {
|
||||
margin-top: 0
|
||||
}
|
||||
|
||||
#main-content h1, #main-content h2 {
|
||||
font-weight: 300;
|
||||
margin-top: 20px
|
||||
}
|
||||
|
||||
#main-content h3, #main-content h4, #main-content h5 {
|
||||
font-weight: 500;
|
||||
margin-top: 15px
|
||||
}
|
||||
|
||||
/* custom navigation styles */
|
||||
|
||||
.navbar .navbar-inner{
|
||||
|
|
|
@ -42,6 +42,7 @@ pages:
|
|||
- ['api-guide/testing.md', 'API Guide', 'Testing']
|
||||
- ['api-guide/settings.md', 'API Guide', 'Settings']
|
||||
- ['topics/documenting-your-api.md', 'Topics', 'Documenting your API']
|
||||
- ['topics/internationalization.md', 'Topics', 'Internationalization']
|
||||
- ['topics/ajax-csrf-cors.md', 'Topics', 'AJAX, CSRF & CORS']
|
||||
- ['topics/browser-enhancements.md', 'Topics',]
|
||||
- ['topics/browsable-api.md', 'Topics', 'The Browsable API']
|
||||
|
|
|
@ -2,29 +2,13 @@
|
|||
Generic views that provide commonly needed behaviour.
|
||||
"""
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.core.paginator import Paginator, InvalidPage
|
||||
from django.db.models.query import QuerySet
|
||||
from django.http import Http404
|
||||
from django.shortcuts import get_object_or_404 as _get_object_or_404
|
||||
from django.utils import six
|
||||
from django.utils.translation import ugettext as _
|
||||
from rest_framework import views, mixins
|
||||
from rest_framework.settings import api_settings
|
||||
|
||||
|
||||
def strict_positive_int(integer_string, cutoff=None):
|
||||
"""
|
||||
Cast a string to a strictly positive integer.
|
||||
"""
|
||||
ret = int(integer_string)
|
||||
if ret <= 0:
|
||||
raise ValueError()
|
||||
if cutoff:
|
||||
ret = min(ret, cutoff)
|
||||
return ret
|
||||
|
||||
|
||||
def get_object_or_404(queryset, *filter_args, **filter_kwargs):
|
||||
"""
|
||||
Same as Django's standard shortcut, but make sure to also raise 404
|
||||
|
@ -40,7 +24,6 @@ class GenericAPIView(views.APIView):
|
|||
"""
|
||||
Base class for all other generic views.
|
||||
"""
|
||||
|
||||
# You'll need to either set these attributes,
|
||||
# or override `get_queryset()`/`get_serializer_class()`.
|
||||
# If you are overriding a view method, it is important that you call
|
||||
|
@ -50,146 +33,16 @@ class GenericAPIView(views.APIView):
|
|||
queryset = None
|
||||
serializer_class = None
|
||||
|
||||
# If you want to use object lookups other than pk, set this attribute.
|
||||
# If you want to use object lookups other than pk, set 'lookup_field'.
|
||||
# For more complex lookup requirements override `get_object()`.
|
||||
lookup_field = 'pk'
|
||||
lookup_url_kwarg = None
|
||||
|
||||
# Pagination settings
|
||||
paginate_by = api_settings.PAGINATE_BY
|
||||
paginate_by_param = api_settings.PAGINATE_BY_PARAM
|
||||
max_paginate_by = api_settings.MAX_PAGINATE_BY
|
||||
pagination_serializer_class = api_settings.DEFAULT_PAGINATION_SERIALIZER_CLASS
|
||||
page_kwarg = 'page'
|
||||
|
||||
# The filter backend classes to use for queryset filtering
|
||||
filter_backends = api_settings.DEFAULT_FILTER_BACKENDS
|
||||
|
||||
# The following attribute may be subject to change,
|
||||
# and should be considered private API.
|
||||
paginator_class = Paginator
|
||||
|
||||
def get_serializer_context(self):
|
||||
"""
|
||||
Extra context provided to the serializer class.
|
||||
"""
|
||||
return {
|
||||
'request': self.request,
|
||||
'format': self.format_kwarg,
|
||||
'view': self
|
||||
}
|
||||
|
||||
def get_serializer(self, *args, **kwargs):
|
||||
"""
|
||||
Return the serializer instance that should be used for validating and
|
||||
deserializing input, and for serializing output.
|
||||
"""
|
||||
serializer_class = self.get_serializer_class()
|
||||
kwargs['context'] = self.get_serializer_context()
|
||||
return serializer_class(*args, **kwargs)
|
||||
|
||||
def get_pagination_serializer(self, page):
|
||||
"""
|
||||
Return a serializer instance to use with paginated data.
|
||||
"""
|
||||
class SerializerClass(self.pagination_serializer_class):
|
||||
class Meta:
|
||||
object_serializer_class = self.get_serializer_class()
|
||||
|
||||
pagination_serializer_class = SerializerClass
|
||||
context = self.get_serializer_context()
|
||||
return pagination_serializer_class(instance=page, context=context)
|
||||
|
||||
def paginate_queryset(self, queryset):
|
||||
"""
|
||||
Paginate a queryset if required, either returning a page object,
|
||||
or `None` if pagination is not configured for this view.
|
||||
"""
|
||||
page_size = self.get_paginate_by()
|
||||
if not page_size:
|
||||
return None
|
||||
|
||||
paginator = self.paginator_class(queryset, page_size)
|
||||
page_kwarg = self.kwargs.get(self.page_kwarg)
|
||||
page_query_param = self.request.query_params.get(self.page_kwarg)
|
||||
page = page_kwarg or page_query_param or 1
|
||||
try:
|
||||
page_number = paginator.validate_number(page)
|
||||
except InvalidPage:
|
||||
if page == 'last':
|
||||
page_number = paginator.num_pages
|
||||
else:
|
||||
raise Http404(_('Choose a valid page number. Page numbers must be a whole number, or must be the string "last".'))
|
||||
|
||||
try:
|
||||
page = paginator.page(page_number)
|
||||
except InvalidPage as exc:
|
||||
error_format = _('Invalid page "{page_number}": {message}.')
|
||||
raise Http404(error_format.format(
|
||||
page_number=page_number, message=six.text_type(exc)
|
||||
))
|
||||
|
||||
return page
|
||||
|
||||
def filter_queryset(self, queryset):
|
||||
"""
|
||||
Given a queryset, filter it with whichever filter backend is in use.
|
||||
|
||||
You are unlikely to want to override this method, although you may need
|
||||
to call it either from a list view, or from a custom `get_object`
|
||||
method if you want to apply the configured filtering backend to the
|
||||
default queryset.
|
||||
"""
|
||||
for backend in self.get_filter_backends():
|
||||
queryset = backend().filter_queryset(self.request, queryset, self)
|
||||
return queryset
|
||||
|
||||
def get_filter_backends(self):
|
||||
"""
|
||||
Returns the list of filter backends that this view requires.
|
||||
"""
|
||||
return list(self.filter_backends)
|
||||
|
||||
# The following methods provide default implementations
|
||||
# that you may want to override for more complex cases.
|
||||
|
||||
def get_paginate_by(self):
|
||||
"""
|
||||
Return the size of pages to use with pagination.
|
||||
|
||||
If `PAGINATE_BY_PARAM` is set it will attempt to get the page size
|
||||
from a named query parameter in the url, eg. ?page_size=100
|
||||
|
||||
Otherwise defaults to using `self.paginate_by`.
|
||||
"""
|
||||
if self.paginate_by_param:
|
||||
try:
|
||||
return strict_positive_int(
|
||||
self.request.query_params[self.paginate_by_param],
|
||||
cutoff=self.max_paginate_by
|
||||
)
|
||||
except (KeyError, ValueError):
|
||||
pass
|
||||
|
||||
return self.paginate_by
|
||||
|
||||
def get_serializer_class(self):
|
||||
"""
|
||||
Return the class to use for the serializer.
|
||||
Defaults to using `self.serializer_class`.
|
||||
|
||||
You may want to override this if you need to provide different
|
||||
serializations depending on the incoming request.
|
||||
|
||||
(Eg. admins get full serialization, others get basic serialization)
|
||||
"""
|
||||
assert self.serializer_class is not None, (
|
||||
"'%s' should either include a `serializer_class` attribute, "
|
||||
"or override the `get_serializer_class()` method."
|
||||
% self.__class__.__name__
|
||||
)
|
||||
|
||||
return self.serializer_class
|
||||
# The style to use for queryset pagination.
|
||||
pagination_class = api_settings.DEFAULT_PAGINATION_CLASS
|
||||
|
||||
def get_queryset(self):
|
||||
"""
|
||||
|
@ -246,6 +99,73 @@ class GenericAPIView(views.APIView):
|
|||
|
||||
return obj
|
||||
|
||||
def get_serializer(self, *args, **kwargs):
|
||||
"""
|
||||
Return the serializer instance that should be used for validating and
|
||||
deserializing input, and for serializing output.
|
||||
"""
|
||||
serializer_class = self.get_serializer_class()
|
||||
kwargs['context'] = self.get_serializer_context()
|
||||
return serializer_class(*args, **kwargs)
|
||||
|
||||
def get_serializer_class(self):
|
||||
"""
|
||||
Return the class to use for the serializer.
|
||||
Defaults to using `self.serializer_class`.
|
||||
|
||||
You may want to override this if you need to provide different
|
||||
serializations depending on the incoming request.
|
||||
|
||||
(Eg. admins get full serialization, others get basic serialization)
|
||||
"""
|
||||
assert self.serializer_class is not None, (
|
||||
"'%s' should either include a `serializer_class` attribute, "
|
||||
"or override the `get_serializer_class()` method."
|
||||
% self.__class__.__name__
|
||||
)
|
||||
|
||||
return self.serializer_class
|
||||
|
||||
def get_serializer_context(self):
|
||||
"""
|
||||
Extra context provided to the serializer class.
|
||||
"""
|
||||
return {
|
||||
'request': self.request,
|
||||
'format': self.format_kwarg,
|
||||
'view': self
|
||||
}
|
||||
|
||||
def filter_queryset(self, queryset):
|
||||
"""
|
||||
Given a queryset, filter it with whichever filter backend is in use.
|
||||
|
||||
You are unlikely to want to override this method, although you may need
|
||||
to call it either from a list view, or from a custom `get_object`
|
||||
method if you want to apply the configured filtering backend to the
|
||||
default queryset.
|
||||
"""
|
||||
for backend in list(self.filter_backends):
|
||||
queryset = backend().filter_queryset(self.request, queryset, self)
|
||||
return queryset
|
||||
|
||||
@property
|
||||
def pager(self):
|
||||
if not hasattr(self, '_pager'):
|
||||
if self.pagination_class is None:
|
||||
self._pager = None
|
||||
else:
|
||||
self._pager = self.pagination_class()
|
||||
return self._pager
|
||||
|
||||
def paginate_queryset(self, queryset):
|
||||
if self.pager is None:
|
||||
return queryset
|
||||
return self.pager.paginate_queryset(queryset, self.request, view=self)
|
||||
|
||||
def get_paginated_response(self, data):
|
||||
return self.pager.get_paginated_response(data)
|
||||
|
||||
|
||||
# Concrete view classes that provide method handlers
|
||||
# by composing the mixin classes with the base view.
|
||||
|
|
|
@ -5,7 +5,6 @@ We don't bind behaviour to http method handlers yet,
|
|||
which allows mixin classes to be composed in interesting ways.
|
||||
"""
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from rest_framework import status
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.settings import api_settings
|
||||
|
@ -37,12 +36,14 @@ class ListModelMixin(object):
|
|||
List a queryset.
|
||||
"""
|
||||
def list(self, request, *args, **kwargs):
|
||||
instance = self.filter_queryset(self.get_queryset())
|
||||
page = self.paginate_queryset(instance)
|
||||
queryset = self.filter_queryset(self.get_queryset())
|
||||
|
||||
page = self.paginate_queryset(queryset)
|
||||
if page is not None:
|
||||
serializer = self.get_pagination_serializer(page)
|
||||
else:
|
||||
serializer = self.get_serializer(instance, many=True)
|
||||
serializer = self.get_serializer(page, many=True)
|
||||
return self.get_paginated_response(serializer.data)
|
||||
|
||||
serializer = self.get_serializer(queryset, many=True)
|
||||
return Response(serializer.data)
|
||||
|
||||
|
||||
|
|
|
@ -3,87 +3,202 @@ Pagination serializers determine the structure of the output that should
|
|||
be used for paginated responses.
|
||||
"""
|
||||
from __future__ import unicode_literals
|
||||
from rest_framework import serializers
|
||||
from django.core.paginator import InvalidPage, Paginator as DjangoPaginator
|
||||
from django.utils import six
|
||||
from django.utils.translation import ugettext as _
|
||||
from rest_framework.compat import OrderedDict
|
||||
from rest_framework.exceptions import NotFound
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.settings import api_settings
|
||||
from rest_framework.templatetags.rest_framework import replace_query_param
|
||||
|
||||
|
||||
class NextPageField(serializers.Field):
|
||||
def _strict_positive_int(integer_string, cutoff=None):
|
||||
"""
|
||||
Field that returns a link to the next page in paginated results.
|
||||
Cast a string to a strictly positive integer.
|
||||
"""
|
||||
page_field = 'page'
|
||||
|
||||
def to_representation(self, value):
|
||||
if not value.has_next():
|
||||
return None
|
||||
page = value.next_page_number()
|
||||
request = self.context.get('request')
|
||||
url = request and request.build_absolute_uri() or ''
|
||||
return replace_query_param(url, self.page_field, page)
|
||||
ret = int(integer_string)
|
||||
if ret <= 0:
|
||||
raise ValueError()
|
||||
if cutoff:
|
||||
ret = min(ret, cutoff)
|
||||
return ret
|
||||
|
||||
|
||||
class PreviousPageField(serializers.Field):
|
||||
def _get_count(queryset):
|
||||
"""
|
||||
Field that returns a link to the previous page in paginated results.
|
||||
Determine an object count, supporting either querysets or regular lists.
|
||||
"""
|
||||
page_field = 'page'
|
||||
|
||||
def to_representation(self, value):
|
||||
if not value.has_previous():
|
||||
return None
|
||||
page = value.previous_page_number()
|
||||
request = self.context.get('request')
|
||||
url = request and request.build_absolute_uri() or ''
|
||||
return replace_query_param(url, self.page_field, page)
|
||||
try:
|
||||
return queryset.count()
|
||||
except AttributeError:
|
||||
return len(queryset)
|
||||
|
||||
|
||||
class DefaultObjectSerializer(serializers.ReadOnlyField):
|
||||
class BasePagination(object):
|
||||
def paginate_queryset(self, queryset, request, view):
|
||||
raise NotImplemented('paginate_queryset() must be implemented.')
|
||||
|
||||
def get_paginated_response(self, data):
|
||||
raise NotImplemented('get_paginated_response() must be implemented.')
|
||||
|
||||
|
||||
class PageNumberPagination(BasePagination):
|
||||
"""
|
||||
If no object serializer is specified, then this serializer will be applied
|
||||
as the default.
|
||||
A simple page number based style that supports page numbers as
|
||||
query parameters. For example:
|
||||
|
||||
http://api.example.org/accounts/?page=4
|
||||
http://api.example.org/accounts/?page=4&page_size=100
|
||||
"""
|
||||
# The default page size.
|
||||
# Defaults to `None`, meaning pagination is disabled.
|
||||
paginate_by = api_settings.PAGINATE_BY
|
||||
|
||||
def __init__(self, source=None, many=None, context=None):
|
||||
# Note: Swallow context and many kwargs - only required for
|
||||
# eg. ModelSerializer.
|
||||
super(DefaultObjectSerializer, self).__init__(source=source)
|
||||
# Client can control the page using this query parameter.
|
||||
page_query_param = 'page'
|
||||
|
||||
# Client can control the page size using this query parameter.
|
||||
# Default is 'None'. Set to eg 'page_size' to enable usage.
|
||||
paginate_by_param = api_settings.PAGINATE_BY_PARAM
|
||||
|
||||
class BasePaginationSerializer(serializers.Serializer):
|
||||
"""
|
||||
A base class for pagination serializers to inherit from,
|
||||
to make implementing custom serializers more easy.
|
||||
"""
|
||||
results_field = 'results'
|
||||
# Set to an integer to limit the maximum page size the client may request.
|
||||
# Only relevant if 'paginate_by_param' has also been set.
|
||||
max_paginate_by = api_settings.MAX_PAGINATE_BY
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
def paginate_queryset(self, queryset, request, view):
|
||||
"""
|
||||
Override init to add in the object serializer field on-the-fly.
|
||||
Paginate a queryset if required, either returning a
|
||||
page object, or `None` if pagination is not configured for this view.
|
||||
"""
|
||||
super(BasePaginationSerializer, self).__init__(*args, **kwargs)
|
||||
results_field = self.results_field
|
||||
for attr in (
|
||||
'paginate_by', 'page_query_param',
|
||||
'paginate_by_param', 'max_paginate_by'
|
||||
):
|
||||
if hasattr(view, attr):
|
||||
setattr(self, attr, getattr(view, attr))
|
||||
|
||||
page_size = self.get_page_size(request)
|
||||
if not page_size:
|
||||
return None
|
||||
|
||||
paginator = DjangoPaginator(queryset, page_size)
|
||||
page_string = request.query_params.get(self.page_query_param, 1)
|
||||
try:
|
||||
page_number = paginator.validate_number(page_string)
|
||||
except InvalidPage:
|
||||
if page_string == 'last':
|
||||
page_number = paginator.num_pages
|
||||
else:
|
||||
msg = _(
|
||||
'Choose a valid page number. Page numbers must be a '
|
||||
'whole number, or must be the string "last".'
|
||||
)
|
||||
raise NotFound(msg)
|
||||
|
||||
try:
|
||||
object_serializer = self.Meta.object_serializer_class
|
||||
except AttributeError:
|
||||
object_serializer = DefaultObjectSerializer
|
||||
self.page = paginator.page(page_number)
|
||||
except InvalidPage as exc:
|
||||
msg = _('Invalid page "{page_number}": {message}.').format(
|
||||
page_number=page_number, message=six.text_type(exc)
|
||||
)
|
||||
raise NotFound(msg)
|
||||
|
||||
self.request = request
|
||||
return self.page
|
||||
|
||||
def get_paginated_response(self, data):
|
||||
return Response(OrderedDict([
|
||||
('count', self.page.paginator.count),
|
||||
('next', self.get_next_link()),
|
||||
('previous', self.get_previous_link()),
|
||||
('results', data)
|
||||
]))
|
||||
|
||||
def get_page_size(self, request):
|
||||
if self.paginate_by_param:
|
||||
try:
|
||||
return _strict_positive_int(
|
||||
request.query_params[self.paginate_by_param],
|
||||
cutoff=self.max_paginate_by
|
||||
)
|
||||
except (KeyError, ValueError):
|
||||
pass
|
||||
|
||||
return self.paginate_by
|
||||
|
||||
def get_next_link(self):
|
||||
if not self.page.has_next():
|
||||
return None
|
||||
url = self.request.build_absolute_uri()
|
||||
page_number = self.page.next_page_number()
|
||||
return replace_query_param(url, self.page_query_param, page_number)
|
||||
|
||||
def get_previous_link(self):
|
||||
if not self.page.has_previous():
|
||||
return None
|
||||
url = self.request.build_absolute_uri()
|
||||
page_number = self.page.previous_page_number()
|
||||
return replace_query_param(url, self.page_query_param, page_number)
|
||||
|
||||
|
||||
class LimitOffsetPagination(BasePagination):
|
||||
"""
|
||||
A limit/offset based style. For example:
|
||||
|
||||
http://api.example.org/accounts/?limit=100
|
||||
http://api.example.org/accounts/?offset=400&limit=100
|
||||
"""
|
||||
default_limit = api_settings.PAGINATE_BY
|
||||
limit_query_param = 'limit'
|
||||
offset_query_param = 'offset'
|
||||
max_limit = None
|
||||
|
||||
def paginate_queryset(self, queryset, request, view):
|
||||
self.limit = self.get_limit(request)
|
||||
self.offset = self.get_offset(request)
|
||||
self.count = _get_count(queryset)
|
||||
self.request = request
|
||||
return queryset[self.offset:self.offset + self.limit]
|
||||
|
||||
def get_paginated_response(self, data):
|
||||
return Response(OrderedDict([
|
||||
('count', self.count),
|
||||
('next', self.get_next_link()),
|
||||
('previous', self.get_previous_link()),
|
||||
('results', data)
|
||||
]))
|
||||
|
||||
def get_limit(self, request):
|
||||
if self.limit_query_param:
|
||||
try:
|
||||
return _strict_positive_int(
|
||||
request.query_params[self.limit_query_param],
|
||||
cutoff=self.max_limit
|
||||
)
|
||||
except (KeyError, ValueError):
|
||||
pass
|
||||
|
||||
return self.default_limit
|
||||
|
||||
def get_offset(self, request):
|
||||
try:
|
||||
list_serializer_class = object_serializer.Meta.list_serializer_class
|
||||
except AttributeError:
|
||||
list_serializer_class = serializers.ListSerializer
|
||||
return _strict_positive_int(
|
||||
request.query_params[self.offset_query_param],
|
||||
)
|
||||
except (KeyError, ValueError):
|
||||
return 0
|
||||
|
||||
self.fields[results_field] = list_serializer_class(
|
||||
child=object_serializer(),
|
||||
source='object_list'
|
||||
)
|
||||
self.fields[results_field].bind(field_name=results_field, parent=self)
|
||||
def get_next_link(self, page):
|
||||
if self.offset + self.limit >= self.count:
|
||||
return None
|
||||
url = self.request.build_absolute_uri()
|
||||
offset = self.offset + self.limit
|
||||
return replace_query_param(url, self.offset_query_param, offset)
|
||||
|
||||
|
||||
class PaginationSerializer(BasePaginationSerializer):
|
||||
"""
|
||||
A default implementation of a pagination serializer.
|
||||
"""
|
||||
count = serializers.ReadOnlyField(source='paginator.count')
|
||||
next = NextPageField(source='*')
|
||||
previous = PreviousPageField(source='*')
|
||||
def get_previous_link(self, page):
|
||||
if self.offset - self.limit < 0:
|
||||
return None
|
||||
url = self.request.build_absolute_uri()
|
||||
offset = self.offset - self.limit
|
||||
return replace_query_param(url, self.offset_query_param, offset)
|
||||
|
|
|
@ -49,7 +49,7 @@ DEFAULTS = {
|
|||
'DEFAULT_VERSIONING_CLASS': None,
|
||||
|
||||
# Generic view behavior
|
||||
'DEFAULT_PAGINATION_SERIALIZER_CLASS': 'rest_framework.pagination.PaginationSerializer',
|
||||
'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination',
|
||||
'DEFAULT_FILTER_BACKENDS': (),
|
||||
|
||||
# Throttling
|
||||
|
@ -130,7 +130,7 @@ IMPORT_STRINGS = (
|
|||
'DEFAULT_CONTENT_NEGOTIATION_CLASS',
|
||||
'DEFAULT_METADATA_CLASS',
|
||||
'DEFAULT_VERSIONING_CLASS',
|
||||
'DEFAULT_PAGINATION_SERIALIZER_CLASS',
|
||||
'DEFAULT_PAGINATION_CLASS',
|
||||
'DEFAULT_FILTER_BACKENDS',
|
||||
'EXCEPTION_HANDLER',
|
||||
'TEST_REQUEST_RENDERER_CLASSES',
|
||||
|
|
|
@ -1,10 +1,9 @@
|
|||
from __future__ import unicode_literals
|
||||
import datetime
|
||||
from decimal import Decimal
|
||||
from django.core.paginator import Paginator
|
||||
from django.test import TestCase
|
||||
from django.utils import unittest
|
||||
from rest_framework import generics, serializers, status, pagination, filters
|
||||
from rest_framework import generics, serializers, status, filters
|
||||
from rest_framework.compat import django_filters
|
||||
from rest_framework.test import APIRequestFactory
|
||||
from .models import BasicModel, FilterableItem
|
||||
|
@ -238,45 +237,6 @@ class IntegrationTestPaginationAndFiltering(TestCase):
|
|||
self.assertEqual(response.data['previous'], None)
|
||||
|
||||
|
||||
class PassOnContextPaginationSerializer(pagination.PaginationSerializer):
|
||||
class Meta:
|
||||
object_serializer_class = serializers.Serializer
|
||||
|
||||
|
||||
class UnitTestPagination(TestCase):
|
||||
"""
|
||||
Unit tests for pagination of primitive objects.
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
self.objects = [char * 3 for char in 'abcdefghijklmnopqrstuvwxyz']
|
||||
paginator = Paginator(self.objects, 10)
|
||||
self.first_page = paginator.page(1)
|
||||
self.last_page = paginator.page(3)
|
||||
|
||||
def test_native_pagination(self):
|
||||
serializer = pagination.PaginationSerializer(self.first_page)
|
||||
self.assertEqual(serializer.data['count'], 26)
|
||||
self.assertEqual(serializer.data['next'], '?page=2')
|
||||
self.assertEqual(serializer.data['previous'], None)
|
||||
self.assertEqual(serializer.data['results'], self.objects[:10])
|
||||
|
||||
serializer = pagination.PaginationSerializer(self.last_page)
|
||||
self.assertEqual(serializer.data['count'], 26)
|
||||
self.assertEqual(serializer.data['next'], None)
|
||||
self.assertEqual(serializer.data['previous'], '?page=2')
|
||||
self.assertEqual(serializer.data['results'], self.objects[20:])
|
||||
|
||||
def test_context_available_in_result(self):
|
||||
"""
|
||||
Ensure context gets passed through to the object serializer.
|
||||
"""
|
||||
serializer = PassOnContextPaginationSerializer(self.first_page, context={'foo': 'bar'})
|
||||
serializer.data
|
||||
results = serializer.fields[serializer.results_field]
|
||||
self.assertEqual(serializer.context, results.context)
|
||||
|
||||
|
||||
class TestUnpaginated(TestCase):
|
||||
"""
|
||||
Tests for list views without pagination.
|
||||
|
@ -377,177 +337,3 @@ class TestMaxPaginateByParam(TestCase):
|
|||
request = factory.get('/')
|
||||
response = self.view(request).render()
|
||||
self.assertEqual(response.data['results'], self.data[:3])
|
||||
|
||||
|
||||
# Tests for context in pagination serializers
|
||||
|
||||
class CustomField(serializers.ReadOnlyField):
|
||||
def to_native(self, value):
|
||||
if 'view' not in self.context:
|
||||
raise RuntimeError("context isn't getting passed into custom field")
|
||||
return "value"
|
||||
|
||||
|
||||
class BasicModelSerializer(serializers.Serializer):
|
||||
text = CustomField()
|
||||
|
||||
def to_native(self, value):
|
||||
if 'view' not in self.context:
|
||||
raise RuntimeError("context isn't getting passed into serializer")
|
||||
return super(BasicSerializer, self).to_native(value)
|
||||
|
||||
|
||||
class TestContextPassedToCustomField(TestCase):
|
||||
def setUp(self):
|
||||
BasicModel.objects.create(text='ala ma kota')
|
||||
|
||||
def test_with_pagination(self):
|
||||
class ListView(generics.ListCreateAPIView):
|
||||
queryset = BasicModel.objects.all()
|
||||
serializer_class = BasicModelSerializer
|
||||
paginate_by = 1
|
||||
|
||||
self.view = ListView.as_view()
|
||||
request = factory.get('/')
|
||||
response = self.view(request).render()
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
|
||||
|
||||
# Tests for custom pagination serializers
|
||||
|
||||
class LinksSerializer(serializers.Serializer):
|
||||
next = pagination.NextPageField(source='*')
|
||||
prev = pagination.PreviousPageField(source='*')
|
||||
|
||||
|
||||
class CustomPaginationSerializer(pagination.BasePaginationSerializer):
|
||||
links = LinksSerializer(source='*') # Takes the page object as the source
|
||||
total_results = serializers.ReadOnlyField(source='paginator.count')
|
||||
|
||||
results_field = 'objects'
|
||||
|
||||
|
||||
class CustomFooSerializer(serializers.Serializer):
|
||||
foo = serializers.CharField()
|
||||
|
||||
|
||||
class CustomFooPaginationSerializer(pagination.PaginationSerializer):
|
||||
class Meta:
|
||||
object_serializer_class = CustomFooSerializer
|
||||
|
||||
|
||||
class TestCustomPaginationSerializer(TestCase):
|
||||
def setUp(self):
|
||||
objects = ['john', 'paul', 'george', 'ringo']
|
||||
paginator = Paginator(objects, 2)
|
||||
self.page = paginator.page(1)
|
||||
|
||||
def test_custom_pagination_serializer(self):
|
||||
request = APIRequestFactory().get('/foobar')
|
||||
serializer = CustomPaginationSerializer(
|
||||
instance=self.page,
|
||||
context={'request': request}
|
||||
)
|
||||
expected = {
|
||||
'links': {
|
||||
'next': 'http://testserver/foobar?page=2',
|
||||
'prev': None
|
||||
},
|
||||
'total_results': 4,
|
||||
'objects': ['john', 'paul']
|
||||
}
|
||||
self.assertEqual(serializer.data, expected)
|
||||
|
||||
def test_custom_pagination_serializer_with_custom_object_serializer(self):
|
||||
objects = [
|
||||
{'foo': 'bar'},
|
||||
{'foo': 'spam'}
|
||||
]
|
||||
paginator = Paginator(objects, 1)
|
||||
page = paginator.page(1)
|
||||
serializer = CustomFooPaginationSerializer(page)
|
||||
serializer.data
|
||||
|
||||
|
||||
class NonIntegerPage(object):
|
||||
|
||||
def __init__(self, paginator, object_list, prev_token, token, next_token):
|
||||
self.paginator = paginator
|
||||
self.object_list = object_list
|
||||
self.prev_token = prev_token
|
||||
self.token = token
|
||||
self.next_token = next_token
|
||||
|
||||
def has_next(self):
|
||||
return not not self.next_token
|
||||
|
||||
def next_page_number(self):
|
||||
return self.next_token
|
||||
|
||||
def has_previous(self):
|
||||
return not not self.prev_token
|
||||
|
||||
def previous_page_number(self):
|
||||
return self.prev_token
|
||||
|
||||
|
||||
class NonIntegerPaginator(object):
|
||||
|
||||
def __init__(self, object_list, per_page):
|
||||
self.object_list = object_list
|
||||
self.per_page = per_page
|
||||
|
||||
def count(self):
|
||||
# pretend like we don't know how many pages we have
|
||||
return None
|
||||
|
||||
def page(self, token=None):
|
||||
if token:
|
||||
try:
|
||||
first = self.object_list.index(token)
|
||||
except ValueError:
|
||||
first = 0
|
||||
else:
|
||||
first = 0
|
||||
n = len(self.object_list)
|
||||
last = min(first + self.per_page, n)
|
||||
prev_token = self.object_list[last - (2 * self.per_page)] if first else None
|
||||
next_token = self.object_list[last] if last < n else None
|
||||
return NonIntegerPage(self, self.object_list[first:last], prev_token, token, next_token)
|
||||
|
||||
|
||||
class TestNonIntegerPagination(TestCase):
|
||||
def test_custom_pagination_serializer(self):
|
||||
objects = ['john', 'paul', 'george', 'ringo']
|
||||
paginator = NonIntegerPaginator(objects, 2)
|
||||
|
||||
request = APIRequestFactory().get('/foobar')
|
||||
serializer = CustomPaginationSerializer(
|
||||
instance=paginator.page(),
|
||||
context={'request': request}
|
||||
)
|
||||
expected = {
|
||||
'links': {
|
||||
'next': 'http://testserver/foobar?page={0}'.format(objects[2]),
|
||||
'prev': None
|
||||
},
|
||||
'total_results': None,
|
||||
'objects': objects[:2]
|
||||
}
|
||||
self.assertEqual(serializer.data, expected)
|
||||
|
||||
request = APIRequestFactory().get('/foobar')
|
||||
serializer = CustomPaginationSerializer(
|
||||
instance=paginator.page('george'),
|
||||
context={'request': request}
|
||||
)
|
||||
expected = {
|
||||
'links': {
|
||||
'next': None,
|
||||
'prev': 'http://testserver/foobar?page={0}'.format(objects[0]),
|
||||
},
|
||||
'total_results': None,
|
||||
'objects': objects[2:]
|
||||
}
|
||||
self.assertEqual(serializer.data, expected)
|
||||
|
|
Loading…
Reference in New Issue
Block a user