mirror of
https://github.com/encode/django-rest-framework.git
synced 2024-11-22 17:47:04 +03:00
Merge branch 'master' into version-3-6-3
This commit is contained in:
commit
a99f12f1c6
|
@ -35,7 +35,6 @@ matrix:
|
|||
|
||||
allow_failures:
|
||||
- env: DJANGO=master
|
||||
- env: DJANGO=1.11
|
||||
|
||||
install:
|
||||
- pip install tox tox-travis
|
||||
|
|
|
@ -50,7 +50,7 @@ Getting involved in triaging incoming issues is a good way to start contributing
|
|||
|
||||
To start developing on Django REST framework, clone the repo:
|
||||
|
||||
git clone git@github.com:tomchristie/django-rest-framework.git
|
||||
git clone git@github.com:encode/django-rest-framework.git
|
||||
|
||||
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.
|
||||
|
||||
|
@ -198,10 +198,10 @@ If you want to draw attention to a note or warning, use a pair of enclosing line
|
|||
[code-of-conduct]: https://www.djangoproject.com/conduct/
|
||||
[google-group]: https://groups.google.com/forum/?fromgroups#!forum/django-rest-framework
|
||||
[so-filter]: http://stackexchange.com/filters/66475/rest-framework
|
||||
[issues]: https://github.com/tomchristie/django-rest-framework/issues?state=open
|
||||
[issues]: https://github.com/encode/django-rest-framework/issues?state=open
|
||||
[pep-8]: http://www.python.org/dev/peps/pep-0008/
|
||||
[pull-requests]: https://help.github.com/articles/using-pull-requests
|
||||
[tox]: https://tox.readthedocs.io/en/latest/
|
||||
[markdown]: http://daringfireball.net/projects/markdown/basics
|
||||
[docs]: https://github.com/tomchristie/django-rest-framework/tree/master/docs
|
||||
[docs]: https://github.com/encode/django-rest-framework/tree/master/docs
|
||||
[mou]: http://mouapp.com/
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
*Note*: Before submitting this pull request, please review our [contributing guidelines](https://github.com/tomchristie/django-rest-framework/blob/master/CONTRIBUTING.md#pull-requests).
|
||||
*Note*: Before submitting this pull request, please review our [contributing guidelines](https://github.com/encode/django-rest-framework/blob/master/CONTRIBUTING.md#pull-requests).
|
||||
|
||||
## Description
|
||||
|
||||
|
|
24
README.md
24
README.md
|
@ -21,12 +21,12 @@ The initial aim is to provide a single full-time position on REST framework.
|
|||
*Every single sign-up makes a significant impact towards making that possible.*
|
||||
|
||||
<p align="center">
|
||||
<a href="http://jobs.rover.com/"><img src="https://raw.githubusercontent.com/tomchristie/django-rest-framework/master/docs/img/premium/rover-readme.png"/></a>
|
||||
<a href="https://getsentry.com/welcome/"><img src="https://raw.githubusercontent.com/tomchristie/django-rest-framework/master/docs/img/premium/sentry-readme.png"/></a>
|
||||
<a href="https://getstream.io/try-the-api/?utm_source=drf&utm_medium=banner&utm_campaign=drf"><img src="https://raw.githubusercontent.com/tomchristie/django-rest-framework/master/docs/img/premium/stream-readme.png"/></a>
|
||||
<a href="https://hello.machinalis.co.uk/"><img src="https://raw.githubusercontent.com/tomchristie/django-rest-framework/master/docs/img/premium/machinalis-readme.png"/></a>
|
||||
<a href="https://rollbar.com/"><img src="https://raw.githubusercontent.com/tomchristie/django-rest-framework/master/docs/img/premium/rollbar-readme.png"/></a>
|
||||
<a href="https://micropyramid.com/django-rest-framework-development-services/"><img src="https://raw.githubusercontent.com/tomchristie/django-rest-framework/master/docs/img/premium/micropyramid-readme.png"/></a>
|
||||
<a href="http://jobs.rover.com/"><img src="https://raw.githubusercontent.com/encode/django-rest-framework/master/docs/img/premium/rover-readme.png"/></a>
|
||||
<a href="https://getsentry.com/welcome/"><img src="https://raw.githubusercontent.com/encode/django-rest-framework/master/docs/img/premium/sentry-readme.png"/></a>
|
||||
<a href="https://getstream.io/try-the-api/?utm_source=drf&utm_medium=banner&utm_campaign=drf"><img src="https://raw.githubusercontent.com/encode/django-rest-framework/master/docs/img/premium/stream-readme.png"/></a>
|
||||
<a href="https://hello.machinalis.co.uk/"><img src="https://raw.githubusercontent.com/encode/django-rest-framework/master/docs/img/premium/machinalis-readme.png"/></a>
|
||||
<a href="https://rollbar.com/"><img src="https://raw.githubusercontent.com/encode/django-rest-framework/master/docs/img/premium/rollbar-readme.png"/></a>
|
||||
<a href="https://micropyramid.com/django-rest-framework-development-services/"><img src="https://raw.githubusercontent.com/encode/django-rest-framework/master/docs/img/premium/micropyramid-readme.png"/></a>
|
||||
</p>
|
||||
|
||||
*Many thanks to all our [wonderful sponsors][sponsors], and in particular to our premium backers, [Rover](http://jobs.rover.com/), [Sentry](https://getsentry.com/welcome/), [Stream](https://getstream.io/?utm_source=drf&utm_medium=banner&utm_campaign=drf), [Machinalis](https://hello.machinalis.co.uk/), [Rollbar](https://rollbar.com), and [MicroPyramid](https://micropyramid.com/django-rest-framework-development-services/).*
|
||||
|
@ -53,8 +53,8 @@ There is a live example API for testing purposes, [available here][sandbox].
|
|||
|
||||
# Requirements
|
||||
|
||||
* Python (2.7, 3.2, 3.3, 3.4, 3.5)
|
||||
* Django (1.8, 1.9, 1.10)
|
||||
* Python (2.7, 3.2, 3.3, 3.4, 3.5, 3.6)
|
||||
* Django (1.8, 1.9, 1.10, 1.11)
|
||||
|
||||
# Installation
|
||||
|
||||
|
@ -176,10 +176,10 @@ If you believe you've found something in Django REST framework which has securit
|
|||
|
||||
Send a description of the issue via email to [rest-framework-security@googlegroups.com][security-mail]. The project maintainers will then work with you to resolve any issues where required, prior to any public disclosure.
|
||||
|
||||
[build-status-image]: https://secure.travis-ci.org/tomchristie/django-rest-framework.svg?branch=master
|
||||
[travis]: http://travis-ci.org/tomchristie/django-rest-framework?branch=master
|
||||
[coverage-status-image]: https://img.shields.io/codecov/c/github/tomchristie/django-rest-framework/master.svg
|
||||
[codecov]: http://codecov.io/github/tomchristie/django-rest-framework?branch=master
|
||||
[build-status-image]: https://secure.travis-ci.org/encode/django-rest-framework.svg?branch=master
|
||||
[travis]: http://travis-ci.org/encode/django-rest-framework?branch=master
|
||||
[coverage-status-image]: https://img.shields.io/codecov/c/github/encode/django-rest-framework/master.svg
|
||||
[codecov]: http://codecov.io/github/encode/django-rest-framework?branch=master
|
||||
[pypi-version]: https://img.shields.io/pypi/v/djangorestframework.svg
|
||||
[pypi]: https://pypi.python.org/pypi/djangorestframework
|
||||
[twitter]: https://twitter.com/_tomchristie
|
||||
|
|
|
@ -356,6 +356,10 @@ HTTP Signature (currently a [IETF draft][http-signature-ietf-draft]) provides a
|
|||
|
||||
[Django-rest-knox][django-rest-knox] library provides models and views to handle token based authentication in a more secure and extensible way than the built-in TokenAuthentication scheme - with Single Page Applications and Mobile clients in mind. It provides per-client tokens, and views to generate them when provided some other authentication (usually basic authentication), to delete the token (providing a server enforced logout) and to delete all tokens (logs out all clients that a user is logged into).
|
||||
|
||||
## drfpasswordless
|
||||
|
||||
[drfpasswordless][drfpasswordless] adds (Medium, Square Cash inspired) passwordless support to Django REST Framework's own TokenAuthentication scheme. Users log in and sign up with a token sent to a contact point like an email address or a mobile number.
|
||||
|
||||
[cite]: http://jacobian.org/writing/rest-worst-practices/
|
||||
[http401]: http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.4.2
|
||||
[http403]: http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.4.4
|
||||
|
@ -396,3 +400,4 @@ HTTP Signature (currently a [IETF draft][http-signature-ietf-draft]) provides a
|
|||
[django-rest-auth]: https://github.com/Tivix/django-rest-auth
|
||||
[django-rest-framework-social-oauth2]: https://github.com/PhilipGarnero/django-rest-framework-social-oauth2
|
||||
[django-rest-knox]: https://github.com/James1345/django-rest-knox
|
||||
[drfpasswordless]: https://github.com/aaronn/django-rest-framework-passwordless
|
||||
|
|
|
@ -142,7 +142,7 @@ Note that you can use both an overridden `.get_queryset()` and generic filtering
|
|||
The `django-filter` library includes a `DjangoFilterBackend` class which
|
||||
supports highly customizable field filtering for REST framework.
|
||||
|
||||
To use `DjangoFilterBackend`, first install `django-filter`.
|
||||
To use `DjangoFilterBackend`, first install `django-filter`. Then add `django_filters` to Django's `INSTALLED_APPS`
|
||||
|
||||
pip install django-filter
|
||||
|
||||
|
@ -160,16 +160,6 @@ Or add the filter backend to an individual View or ViewSet.
|
|||
...
|
||||
filter_backends = (DjangoFilterBackend,)
|
||||
|
||||
If you are using the browsable API or admin API you may also want to install `django-crispy-forms`, which will enhance the presentation of the filter forms in HTML views, by allowing them to render Bootstrap 3 HTML.
|
||||
|
||||
pip install django-crispy-forms
|
||||
|
||||
With crispy forms installed and added to Django's `INSTALLED_APPS`, the browsable API will present a filtering control for `DjangoFilterBackend`, like so:
|
||||
|
||||
![Django Filter](../img/django-filter.png)
|
||||
|
||||
#### Specifying filter fields
|
||||
|
||||
If all you need is simple equality-based filtering, you can set a `filter_fields` attribute on the view, or viewset, listing the set of fields you wish to filter against.
|
||||
|
||||
class ProductList(generics.ListAPIView):
|
||||
|
@ -182,80 +172,10 @@ This will automatically create a `FilterSet` class for the given fields, and wil
|
|||
|
||||
http://example.com/api/products?category=clothing&in_stock=True
|
||||
|
||||
#### Specifying a FilterSet
|
||||
For more advanced filtering requirements you can specify a `FilterSet` class that should be used by the view.
|
||||
You can read more about `FilterSet`s in the [django-filter documentation][django-filter-docs].
|
||||
It's also recommended that you read the section on [DRF integration][django-filter-drf-docs].
|
||||
|
||||
For more advanced filtering requirements you can specify a `FilterSet` class that should be used by the view. For example:
|
||||
|
||||
import django_filters
|
||||
from myapp.models import Product
|
||||
from myapp.serializers import ProductSerializer
|
||||
from rest_framework import generics
|
||||
|
||||
class ProductFilter(django_filters.rest_framework.FilterSet):
|
||||
min_price = django_filters.NumberFilter(name="price", lookup_expr='gte')
|
||||
max_price = django_filters.NumberFilter(name="price", lookup_expr='lte')
|
||||
class Meta:
|
||||
model = Product
|
||||
fields = ['category', 'in_stock', 'min_price', 'max_price']
|
||||
|
||||
class ProductList(generics.ListAPIView):
|
||||
queryset = Product.objects.all()
|
||||
serializer_class = ProductSerializer
|
||||
filter_backends = (django_filters.rest_framework.DjangoFilterBackend,)
|
||||
filter_class = ProductFilter
|
||||
|
||||
|
||||
Which will allow you to make requests such as:
|
||||
|
||||
http://example.com/api/products?category=clothing&max_price=10.00
|
||||
|
||||
You can also span relationships using `django-filter`, let's assume that each
|
||||
product has foreign key to `Manufacturer` model, so we create filter that
|
||||
filters using `Manufacturer` name. For example:
|
||||
|
||||
import django_filters
|
||||
from myapp.models import Product
|
||||
from myapp.serializers import ProductSerializer
|
||||
from rest_framework import generics
|
||||
|
||||
class ProductFilter(django_filters.rest_framework.FilterSet):
|
||||
class Meta:
|
||||
model = Product
|
||||
fields = ['category', 'in_stock', 'manufacturer__name']
|
||||
|
||||
This enables us to make queries like:
|
||||
|
||||
http://example.com/api/products?manufacturer__name=foo
|
||||
|
||||
This is nice, but it exposes the Django's double underscore convention as part of the API. If you instead want to explicitly name the filter argument you can instead explicitly include it on the `FilterSet` class:
|
||||
|
||||
import django_filters
|
||||
from myapp.models import Product
|
||||
from myapp.serializers import ProductSerializer
|
||||
from rest_framework import generics
|
||||
|
||||
class ProductFilter(django_filters.rest_framework.FilterSet):
|
||||
manufacturer = django_filters.CharFilter(name="manufacturer__name")
|
||||
|
||||
class Meta:
|
||||
model = Product
|
||||
fields = ['category', 'in_stock', 'manufacturer']
|
||||
|
||||
And now you can execute:
|
||||
|
||||
http://example.com/api/products?manufacturer=foo
|
||||
|
||||
For more details on using filter sets see the [django-filter documentation][django-filter-docs].
|
||||
|
||||
---
|
||||
|
||||
**Hints & Tips**
|
||||
|
||||
* By default filtering is not enabled. If you want to use `DjangoFilterBackend` remember to make sure it is installed by using the `'DEFAULT_FILTER_BACKENDS'` setting.
|
||||
* When using boolean fields, you should use the values `True` and `False` in the URL query parameters, rather than `0`, `1`, `true` or `false`. (The allowed boolean values are currently hardwired in Django's [NullBooleanSelect implementation][nullbooleanselect].)
|
||||
* `django-filter` supports filtering across relationships, using Django's double-underscore syntax.
|
||||
|
||||
---
|
||||
|
||||
## SearchFilter
|
||||
|
||||
|
@ -432,8 +352,11 @@ The method should return a rendered HTML string.
|
|||
## Pagination & schemas
|
||||
|
||||
You can also make the filter controls available to the schema autogeneration
|
||||
that REST framework provides, by implementing a `get_schema_fields()` method,
|
||||
which should return a list of `coreapi.Field` instances.
|
||||
that REST framework provides, by implementing a `get_schema_fields()` method. This method should have the following signature:
|
||||
|
||||
`get_schema_fields(self, view)`
|
||||
|
||||
The method should return a list of `coreapi.Field` instances.
|
||||
|
||||
# Third party packages
|
||||
|
||||
|
@ -458,6 +381,7 @@ The [djangorestframework-word-filter][django-rest-framework-word-search-filter]
|
|||
[cite]: https://docs.djangoproject.com/en/stable/topics/db/queries/#retrieving-specific-objects-with-filters
|
||||
[django-filter]: https://github.com/alex/django-filter
|
||||
[django-filter-docs]: https://django-filter.readthedocs.io/en/latest/index.html
|
||||
[django-filter-drf-docs]: https://django-filter.readthedocs.io/en/develop/guide/rest_framework.html
|
||||
[guardian]: https://django-guardian.readthedocs.io/
|
||||
[view-permissions]: https://django-guardian.readthedocs.io/en/latest/userguide/assign.html
|
||||
[view-permissions-blogpost]: http://blog.nyaruka.com/adding-a-view-permission-to-django-models
|
||||
|
|
|
@ -279,8 +279,11 @@ API responses for list endpoints will now include a `Link` header, instead of in
|
|||
## Pagination & schemas
|
||||
|
||||
You can also make the pagination controls available to the schema autogeneration
|
||||
that REST framework provides, by implementing a `get_schema_fields()` method,
|
||||
which should return a list of `coreapi.Field` instances.
|
||||
that REST framework provides, by implementing a `get_schema_fields()` method. This method should have the following signature:
|
||||
|
||||
`get_schema_fields(self, view)`
|
||||
|
||||
The method should return a list of `coreapi.Field` instances.
|
||||
|
||||
---
|
||||
|
||||
|
|
|
@ -270,7 +270,7 @@ Let's take a look at the routes our `CustomReadOnlyRouter` would generate for a
|
|||
lookup_field = 'username'
|
||||
|
||||
@detail_route()
|
||||
def group_names(self, request):
|
||||
def group_names(self, request, pk=None):
|
||||
"""
|
||||
Returns a list of all the group names that the given
|
||||
user belongs to.
|
||||
|
|
|
@ -117,7 +117,7 @@ The simplest way to include a schema in your project is to use the
|
|||
Once the view has been added, you'll be able to make API requests to retrieve
|
||||
the auto-generated schema definition.
|
||||
|
||||
$ http http://127.0.0.1:8000/ Accept:application/vnd.coreapi+json
|
||||
$ http http://127.0.0.1:8000/ Accept:application/coreapi+json
|
||||
HTTP/1.0 200 OK
|
||||
Allow: GET, HEAD, OPTIONS
|
||||
Content-Type: application/vnd.coreapi+json
|
||||
|
@ -170,6 +170,22 @@ May be used to pass the set of renderer classes that can be used to render the A
|
|||
renderer_classes=[CoreJSONRenderer, APIBlueprintRenderer]
|
||||
)
|
||||
|
||||
#### `patterns`
|
||||
|
||||
List of url patterns to limit the schema introspection to. If you only want the `myproject.api` urls
|
||||
to be exposed in the schema:
|
||||
|
||||
schema_url_patterns = [
|
||||
url(r'^api/', include('myproject.api.urls')),
|
||||
]
|
||||
|
||||
schema_view = get_schema_view(
|
||||
title='Server Monitoring API',
|
||||
url='https://www.example.org/api/',
|
||||
patterns=schema_url_patterns,
|
||||
)
|
||||
|
||||
|
||||
## Using an explicit schema view
|
||||
|
||||
If you need a little more control than the `get_schema_view()` shortcut gives you,
|
||||
|
|
|
@ -162,7 +162,7 @@ The `credentials` method is appropriate for testing APIs that require authentica
|
|||
|
||||
#### .force_authenticate(user=None, token=None)
|
||||
|
||||
Sometimes you may want to bypass authentication, and simple force all requests by the test client to be automatically treated as authenticated.
|
||||
Sometimes you may want to bypass authentication entirely and force all requests by the test client to be automatically treated as authenticated.
|
||||
|
||||
This can be a useful shortcut if you're testing the API but don't want to have to construct valid authentication credentials in order to make test requests.
|
||||
|
||||
|
|
BIN
docs/img/books/hwa-cover.png
Normal file
BIN
docs/img/books/hwa-cover.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 30 KiB |
BIN
docs/img/books/tsd-cover.png
Normal file
BIN
docs/img/books/tsd-cover.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 116 KiB |
Binary file not shown.
Before Width: | Height: | Size: 13 KiB |
|
@ -18,10 +18,10 @@
|
|||
</style>
|
||||
|
||||
<p class="badges" height=20px>
|
||||
<iframe src="http://ghbtns.com/github-btn.html?user=tomchristie&repo=django-rest-framework&type=watch&count=true" class="github-star-button" allowtransparency="true" frameborder="0" scrolling="0" width="110px" height="20px"></iframe>
|
||||
<iframe src="http://ghbtns.com/github-btn.html?user=encode&repo=django-rest-framework&type=watch&count=true" class="github-star-button" allowtransparency="true" frameborder="0" scrolling="0" width="110px" height="20px"></iframe>
|
||||
|
||||
<a href="http://travis-ci.org/tomchristie/django-rest-framework?branch=master">
|
||||
<img src="https://secure.travis-ci.org/tomchristie/django-rest-framework.svg?branch=master" class="status-badge">
|
||||
<a href="http://travis-ci.org/encode/django-rest-framework?branch=master">
|
||||
<img src="https://secure.travis-ci.org/encode/django-rest-framework.svg?branch=master" class="status-badge">
|
||||
</a>
|
||||
|
||||
<a href="https://pypi.python.org/pypi/djangorestframework">
|
||||
|
@ -87,8 +87,8 @@ continued development by **[signing up for a paid plan][funding]**.
|
|||
|
||||
REST framework requires the following:
|
||||
|
||||
* Python (2.7, 3.2, 3.3, 3.4, 3.5)
|
||||
* Django (1.8, 1.9, 1.10)
|
||||
* Python (2.7, 3.2, 3.3, 3.4, 3.5, 3.6)
|
||||
* Django (1.8, 1.9, 1.10, 1.11)
|
||||
|
||||
The following packages are optional:
|
||||
|
||||
|
@ -108,7 +108,7 @@ Install using `pip`, including any optional packages you want...
|
|||
|
||||
...or clone the project from github.
|
||||
|
||||
git clone git@github.com:tomchristie/django-rest-framework.git
|
||||
git clone git@github.com:encode/django-rest-framework.git
|
||||
|
||||
Add `'rest_framework'` to your `INSTALLED_APPS` setting.
|
||||
|
||||
|
@ -310,8 +310,8 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|||
[markdown]: http://pypi.python.org/pypi/Markdown/
|
||||
[django-filter]: http://pypi.python.org/pypi/django-filter
|
||||
[django-crispy-forms]: https://github.com/maraujop/django-crispy-forms
|
||||
[django-guardian]: https://github.com/lukaszb/django-guardian
|
||||
[0.4]: https://github.com/tomchristie/django-rest-framework/tree/0.4.X
|
||||
[django-guardian]: https://github.com/django-guardian/django-guardian
|
||||
[0.4]: https://github.com/encode/django-rest-framework/tree/0.4.X
|
||||
[image]: img/quickstart.png
|
||||
[index]: .
|
||||
[oauth1-section]: api-guide/authentication/#django-rest-framework-oauth
|
||||
|
|
|
@ -155,5 +155,5 @@ From version 2.2 onwards, serializers with hyperlinked relationships *always* re
|
|||
[mailing-list]: https://groups.google.com/forum/?fromgroups#!forum/django-rest-framework
|
||||
[django-rest-framework-docs]: https://github.com/marcgibbons/django-rest-framework-docs
|
||||
[marcgibbons]: https://github.com/marcgibbons/
|
||||
[issues]: https://github.com/tomchristie/django-rest-framework/issues
|
||||
[564]: https://github.com/tomchristie/django-rest-framework/issues/564
|
||||
[issues]: https://github.com/encode/django-rest-framework/issues
|
||||
[564]: https://github.com/encode/django-rest-framework/issues/564
|
||||
|
|
|
@ -167,6 +167,6 @@ Once again, many thanks to all the generous [backers and sponsors][kickstarter-s
|
|||
[view-name-and-description-settings]: ../api-guide/settings#view-names-and-descriptions
|
||||
[client-ip-identification]: ../api-guide/throttling#how-clients-are-identified
|
||||
[2-3-announcement]: 2.3-announcement
|
||||
[github-labels]: https://github.com/tomchristie/django-rest-framework/issues
|
||||
[github-milestones]: https://github.com/tomchristie/django-rest-framework/milestones
|
||||
[github-labels]: https://github.com/encode/django-rest-framework/issues
|
||||
[github-milestones]: https://github.com/encode/django-rest-framework/milestones
|
||||
[kickstarter-sponsors]: kickstarter-announcement#sponsors
|
||||
|
|
|
@ -957,9 +957,9 @@ The 3.1 release is planned to address improvements in the following components:
|
|||
|
||||
The 3.2 release is planned to introduce an alternative admin-style interface to the browsable API.
|
||||
|
||||
You can follow development on the GitHub site, where we use [milestones to indicate planning timescales](https://github.com/tomchristie/django-rest-framework/milestones).
|
||||
You can follow development on the GitHub site, where we use [milestones to indicate planning timescales](https://github.com/encode/django-rest-framework/milestones).
|
||||
|
||||
[kickstarter]: http://kickstarter.com/projects/tomchristie/django-rest-framework-3
|
||||
[sponsors]: http://www.django-rest-framework.org/topics/kickstarter-announcement/#sponsors
|
||||
[mixins.py]: https://github.com/tomchristie/django-rest-framework/blob/master/rest_framework/mixins.py
|
||||
[mixins.py]: https://github.com/encode/django-rest-framework/blob/master/rest_framework/mixins.py
|
||||
[django-localization]: https://docs.djangoproject.com/en/stable/topics/i18n/translation/#localization-how-to-create-language-files
|
||||
|
|
|
@ -8,7 +8,7 @@ This interface is intended to act as a more user-friendly interface to the API.
|
|||
|
||||
We've also fixed a huge number of issues, and made numerous cleanups and improvements.
|
||||
|
||||
Over the course of the 3.1.x series we've [resolved nearly 600 tickets](https://github.com/tomchristie/django-rest-framework/issues?utf8=%E2%9C%93&q=closed%3A%3E2015-03-05) on our GitHub issue tracker. This means we're currently running at a rate of **closing around 100 issues or pull requests per month**.
|
||||
Over the course of the 3.1.x series we've [resolved nearly 600 tickets](https://github.com/encode/django-rest-framework/issues?utf8=%E2%9C%93&q=closed%3A%3E2015-03-05) on our GitHub issue tracker. This means we're currently running at a rate of **closing around 100 issues or pull requests per month**.
|
||||
|
||||
None of this would have been possible without the support of our wonderful Kickstarter backers. If you're looking for a job in Django development we'd strongly recommend taking [a look through our sponsors](http://www.django-rest-framework.org/topics/kickstarter-announcement/#sponsors) and finding out who's hiring.
|
||||
|
||||
|
|
|
@ -53,6 +53,6 @@ The following pagination view attributes and settings have been moved into attri
|
|||
The `ModelSerializer` and `HyperlinkedModelSerializer` classes should now include either a `fields` or `exclude` option, although the `fields = '__all__'` shortcut may be used. Failing to include either of these two options is currently pending deprecation, and will be removed entirely in the 3.5 release. This behavior brings `ModelSerializer` more closely in line with Django's `ModelForm` behavior.
|
||||
|
||||
[forms-api]: html-and-forms.md
|
||||
[ajax-form]: https://github.com/tomchristie/ajax-form
|
||||
[ajax-form]: https://github.com/encode/ajax-form
|
||||
[jsonfield]: ../../api-guide/fields#jsonfield
|
||||
[django-supported-versions]: https://www.djangoproject.com/download/#supported-versions
|
|
@ -188,7 +188,7 @@ The full set of itemized release notes [are available here][release-notes].
|
|||
[tut-7]: ../../tutorial/7-schemas-and-client-libraries/
|
||||
[schema-generation]: ../../api-guide/schemas/
|
||||
[api-clients]: api-clients.md
|
||||
[milestone]: https://github.com/tomchristie/django-rest-framework/milestone/35
|
||||
[milestone]: https://github.com/encode/django-rest-framework/milestone/35
|
||||
[release-notes]: release-notes#34
|
||||
[metadata]: ../../api-guide/metadata/#custom-metadata-classes
|
||||
[gh3751]: https://github.com/tomchristie/django-rest-framework/issues/3751
|
||||
[gh3751]: https://github.com/encode/django-rest-framework/issues/3751
|
||||
|
|
|
@ -261,6 +261,6 @@ in version 3.3 and raised a deprecation warning in 3.4. Its usage is now mandato
|
|||
[schema-generation-api]: ../api-guide/schemas/#schemagenerator
|
||||
[schema-docs]: ../api-guide/schemas/#schemas-as-documentation
|
||||
[schema-view]: ../api-guide/schemas/#the-get_schema_view-shortcut
|
||||
[django-rest-raml]: https://github.com/tomchristie/django-rest-raml
|
||||
[django-rest-raml]: https://github.com/encode/django-rest-raml
|
||||
[raml-image]: ../img/raml.png
|
||||
[raml-codec]: https://github.com/core-api/python-raml-codec
|
||||
|
|
|
@ -146,8 +146,6 @@ An alternative, but more complex option would be to replace the input with an au
|
|||
|
||||
There are [a variety of packages for autocomplete widgets][autocomplete-packages], such as [django-autocomplete-light][django-autocomplete-light], that you may want to refer to. Note that you will not be able to simply include these components as standard widgets, but will need to write the HTML template explicitly. This is because REST framework 3.0 no longer supports the `widget` keyword argument since it now uses templated HTML generation.
|
||||
|
||||
Better support for autocomplete inputs is planned in future versions.
|
||||
|
||||
---
|
||||
|
||||
[cite]: http://en.wikiquote.org/wiki/Alfred_North_Whitehead
|
||||
|
|
|
@ -81,7 +81,7 @@ was later [dropped from the spec][html5]. There remains
|
|||
as well as how to support content types other than form-encoded data.
|
||||
|
||||
[cite]: http://www.amazon.com/Restful-Web-Services-Leonard-Richardson/dp/0596529260
|
||||
[ajax-form]: https://github.com/tomchristie/ajax-form
|
||||
[ajax-form]: https://github.com/encode/ajax-form
|
||||
[rails]: http://guides.rubyonrails.org/form_helpers.html#how-do-forms-with-put-or-delete-methods-work
|
||||
[html5]: http://www.w3.org/TR/html5-diff/#changes-2010-06-24
|
||||
[put_delete]: http://amundsen.com/examples/put-delete-forms/
|
||||
|
|
|
@ -50,7 +50,7 @@ Getting involved in triaging incoming issues is a good way to start contributing
|
|||
|
||||
To start developing on Django REST framework, clone the repo:
|
||||
|
||||
git clone git@github.com:tomchristie/django-rest-framework.git
|
||||
git clone git@github.com:encode/django-rest-framework.git
|
||||
|
||||
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.
|
||||
|
||||
|
@ -202,11 +202,11 @@ If you want to draw attention to a note or warning, use a pair of enclosing line
|
|||
[code-of-conduct]: https://www.djangoproject.com/conduct/
|
||||
[google-group]: https://groups.google.com/forum/?fromgroups#!forum/django-rest-framework
|
||||
[so-filter]: http://stackexchange.com/filters/66475/rest-framework
|
||||
[issues]: https://github.com/tomchristie/django-rest-framework/issues?state=open
|
||||
[issues]: https://github.com/encode/django-rest-framework/issues?state=open
|
||||
[pep-8]: http://www.python.org/dev/peps/pep-0008/
|
||||
[travis-status]: ../img/travis-status.png
|
||||
[pull-requests]: https://help.github.com/articles/using-pull-requests
|
||||
[tox]: https://tox.readthedocs.io/en/latest/
|
||||
[markdown]: http://daringfireball.net/projects/markdown/basics
|
||||
[docs]: https://github.com/tomchristie/django-rest-framework/tree/master/docs
|
||||
[docs]: https://github.com/encode/django-rest-framework/tree/master/docs
|
||||
[mou]: http://mouapp.com/
|
||||
|
|
|
@ -329,7 +329,7 @@ For further enquires please contact <a href=mailto:funding@django-rest-framework
|
|||
|
||||
## Accountability
|
||||
|
||||
In an effort to keep the project as transparent as possible, we are releasing [monthly progress reports](http://www.encode.io/reports/february-2017) and regularly include financial reports and cost breakdowns.
|
||||
In an effort to keep the project as transparent as possible, we are releasing [monthly progress reports](http://www.encode.io/reports/march-2017) and regularly include financial reports and cost breakdowns.
|
||||
|
||||
<!-- Begin MailChimp Signup Form -->
|
||||
<link href="//cdn-images.mailchimp.com/embedcode/classic-10_7.css" rel="stylesheet" type="text/css">
|
||||
|
|
|
@ -81,7 +81,7 @@ If you're translating a new language you'll need to translate the existing REST
|
|||
|
||||
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
|
||||
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.
|
||||
|
@ -106,7 +106,7 @@ For API clients the most appropriate of these will typically be to use the `Acce
|
|||
[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-po-source]: https://raw.githubusercontent.com/encode/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
|
||||
|
|
|
@ -35,5 +35,5 @@ Wonder how else you can help? One of the best ways you can help Django REST Fram
|
|||
[remoteok-io]: https://remoteok.io/remote-django-jobs
|
||||
[remotepython-com]: https://www.remotepython.com/jobs/
|
||||
[drf-funding]: https://fund.django-rest-framework.org/topics/funding/
|
||||
[submit-pr]: https://github.com/tomchristie/django-rest-framework
|
||||
[submit-pr]: https://github.com/encode/django-rest-framework
|
||||
[anna-email]: mailto:anna@django-rest-framework.org
|
||||
|
|
|
@ -17,9 +17,9 @@ We have a quarterly maintenance cycle where new members may join the maintenance
|
|||
|
||||
#### Current team
|
||||
|
||||
The [maintenance team for Q4 2015](https://github.com/tomchristie/django-rest-framework/issues/2190):
|
||||
The [maintenance team for Q4 2015](https://github.com/encode/django-rest-framework/issues/2190):
|
||||
|
||||
* [@tomchristie](https://github.com/tomchristie/)
|
||||
* [@tomchristie](https://github.com/encode/)
|
||||
* [@xordoquy](https://github.com/xordoquy/) (Release manager.)
|
||||
* [@carltongibson](https://github.com/carltongibson/)
|
||||
* [@kevin-brown](https://github.com/kevin-brown/)
|
||||
|
@ -104,9 +104,9 @@ The following template should be used for the description of the issue, and serv
|
|||
|
||||
Checklist:
|
||||
|
||||
- [ ] Create pull request for [release notes](https://github.com/tomchristie/django-rest-framework/blob/master/docs/topics/release-notes.md) based on the [*.*.* milestone](https://github.com/tomchristie/django-rest-framework/milestones/***).
|
||||
- [ ] Create pull request for [release notes](https://github.com/encode/django-rest-framework/blob/master/docs/topics/release-notes.md) based on the [*.*.* milestone](https://github.com/encode/django-rest-framework/milestones/***).
|
||||
- [ ] Update the translations from [transifex](http://www.django-rest-framework.org/topics/project-management/#translations).
|
||||
- [ ] Ensure the pull request increments the version to `*.*.*` in [`restframework/__init__.py`](https://github.com/tomchristie/django-rest-framework/blob/master/rest_framework/__init__.py).
|
||||
- [ ] Ensure the pull request increments the version to `*.*.*` in [`restframework/__init__.py`](https://github.com/encode/django-rest-framework/blob/master/rest_framework/__init__.py).
|
||||
- [ ] Confirm with @tomchristie that release is finalized and ready to go.
|
||||
- [ ] Ensure that release date is included in pull request.
|
||||
- [ ] Merge the release pull request.
|
||||
|
@ -197,10 +197,10 @@ The following issues still need to be addressed:
|
|||
* Document ownership and management of the security mailing list.
|
||||
|
||||
[bus-factor]: http://en.wikipedia.org/wiki/Bus_factor
|
||||
[un-triaged]: https://github.com/tomchristie/django-rest-framework/issues?q=is%3Aopen+no%3Alabel
|
||||
[un-triaged]: https://github.com/encode/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
|
||||
[github-org]: https://github.com/encode/django-rest-framework/issues/2162
|
||||
[sandbox]: http://restframework.herokuapp.com/
|
||||
[mailing-list]: https://groups.google.com/forum/#!forum/django-rest-framework
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -190,6 +190,7 @@ To submit new content, [open an issue][drf-create-issue] or [create a pull reque
|
|||
* [djoser][djoser] - Provides a set of views to handle basic actions such as registration, login, logout, password reset and account activation.
|
||||
* [django-rest-auth][django-rest-auth] - Provides a set of REST API endpoints for registration, authentication (including social media authentication), password reset, retrieve and update user details, etc.
|
||||
* [drf-oidc-auth][drf-oidc-auth] - Implements OpenID Connect token authentication for DRF.
|
||||
* [drfpasswordless][drfpasswordless] - Adds (Medium, Square Cash inspired) passwordless logins and signups via email and mobile numbers.
|
||||
|
||||
### Permissions
|
||||
|
||||
|
@ -260,7 +261,7 @@ To submit new content, [open an issue][drf-create-issue] or [create a pull reque
|
|||
* [drf-haystack][drf-haystack] - Haystack search for Django Rest Framework
|
||||
* [django-rest-framework-version-transforms][django-rest-framework-version-transforms] - Enables the use of delta transformations for versioning of DRF resource representations.
|
||||
* [django-rest-messaging][django-rest-messaging], [django-rest-messaging-centrifugo][django-rest-messaging-centrifugo] and [django-rest-messaging-js][django-rest-messaging-js] - A real-time pluggable messaging service using DRM.
|
||||
|
||||
* [djangorest-alchemy][djangorest-alchemy] - SQLAlchemy support for REST framework.
|
||||
|
||||
[cite]: http://www.software-ecosystems.com/Software_Ecosystems/Ecosystems.html
|
||||
[cookiecutter]: https://github.com/jpadilla/cookiecutter-django-rest-framework
|
||||
|
@ -271,10 +272,10 @@ To submit new content, [open an issue][drf-create-issue] or [create a pull reque
|
|||
[pypi-register]: https://pypi.python.org/pypi?%3Aaction=register_form
|
||||
[semver]: http://semver.org/
|
||||
[tox-docs]: https://tox.readthedocs.io/en/latest/
|
||||
[drf-compat]: https://github.com/tomchristie/django-rest-framework/blob/master/rest_framework/compat.py
|
||||
[drf-compat]: https://github.com/encode/django-rest-framework/blob/master/rest_framework/compat.py
|
||||
[rest-framework-grid]: https://www.djangopackages.com/grids/g/django-rest-framework/
|
||||
[drf-create-pr]: https://github.com/tomchristie/django-rest-framework/compare
|
||||
[drf-create-issue]: https://github.com/tomchristie/django-rest-framework/issues/new
|
||||
[drf-create-pr]: https://github.com/encode/django-rest-framework/compare
|
||||
[drf-create-issue]: https://github.com/encode/django-rest-framework/issues/new
|
||||
[authentication]: ../api-guide/authentication.md
|
||||
[permissions]: ../api-guide/permissions.md
|
||||
[third-party-packages]: ../topics/third-party-packages/#existing-third-party-packages
|
||||
|
@ -330,3 +331,5 @@ To submit new content, [open an issue][drf-create-issue] or [create a pull reque
|
|||
[drf-oidc-auth]: https://github.com/ByteInternet/drf-oidc-auth
|
||||
[drf-serializer-extensions]: https://github.com/evenicoulddoit/django-rest-framework-serializer-extensions
|
||||
[djangorestframework-queryfields]: https://github.com/wimglenn/djangorestframework-queryfields
|
||||
[drfpasswordless]: https://github.com/aaronn/django-rest-framework-passwordless
|
||||
[djangorest-alchemy]: https://github.com/dealertrack/djangorest-alchemy
|
||||
|
|
|
@ -2,6 +2,17 @@
|
|||
|
||||
There are a wide range of resources available for learning and using Django REST framework. We try to keep a comprehensive list available here.
|
||||
|
||||
## Books
|
||||
|
||||
<div class="book-covers">
|
||||
<a class="book-cover" href="https://hellowebapp.com/order/">
|
||||
<img src="../../img/books/hwa-cover.png"/>
|
||||
</a>
|
||||
<a class="book-cover" href="https://www.twoscoopspress.com/products/two-scoops-of-django-1-11">
|
||||
<img src="../../img/books/tsd-cover.png"/>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
## Tutorials
|
||||
|
||||
* [Beginner's Guide to the Django REST Framework][beginners-guide-to-the-django-rest-framework]
|
||||
|
@ -56,10 +67,6 @@ There are a wide range of resources available for learning and using Django REST
|
|||
* [New Django Admin with DRF and EmberJS... What are the News?][new-django-admin-with-drf-and-emberjs]
|
||||
* [Blog posts about Django REST Framework][medium-django-rest-framework]
|
||||
|
||||
## Books
|
||||
|
||||
* [Hello Web App: Intermediate Concepts, Chapter 10][hello-web-app-intermediate]
|
||||
|
||||
### Documentations
|
||||
* [Classy Django REST Framework][cdrf.co]
|
||||
* [DRF-schema-adapter][drf-schema]
|
||||
|
@ -95,7 +102,6 @@ Want your Django REST Framework talk/tutorial/article to be added to our website
|
|||
[drf-schema]: http://drf-schema-adapter.readthedocs.io/en/latest/
|
||||
[creating-a-production-ready-api-with-python-and-drf-part1]: https://www.andreagrandi.it/2016/09/28/creating-production-ready-api-python-django-rest-framework-part-1/
|
||||
[creating-a-production-ready-api-with-python-and-drf-part2]: https://www.andreagrandi.it/2016/10/01/creating-a-production-ready-api-with-python-and-django-rest-framework-part-2/
|
||||
[hello-web-app-intermediate]: https://hellowebapp.com/order/
|
||||
[django-rest-api-so-easy]: https://www.youtube.com/watch?v=cqP758k1BaQ
|
||||
[full-fledged-rest-api-with-django-oauth-tookit]: https://www.youtube.com/watch?v=M6Ud3qC2tTk
|
||||
[drf-in-your-pjs]: https://www.youtube.com/watch?v=xMtHsWa72Ww
|
||||
|
@ -106,5 +112,5 @@ Want your Django REST Framework talk/tutorial/article to be added to our website
|
|||
[drf-an-intro]: https://realpython.com/blog/python/django-rest-framework-quick-start/
|
||||
[drf-tutorial]: https://tests4geeks.com/django-rest-framework-tutorial/
|
||||
[building-a-restful-api-with-drf]: http://agiliq.com/blog/2014/12/building-a-restful-api-with-django-rest-framework/
|
||||
[submit-pr]: https://github.com/tomchristie/django-rest-framework
|
||||
[submit-pr]: https://github.com/encode/django-rest-framework
|
||||
[anna-email]: mailto:anna@django-rest-framework.org
|
||||
|
|
|
@ -310,7 +310,7 @@ Quit out of the shell...
|
|||
Validating models...
|
||||
|
||||
0 errors found
|
||||
Django version 1.8.3, using settings 'tutorial.settings'
|
||||
Django version 1.11, using settings 'tutorial.settings'
|
||||
Development server is running at http://127.0.0.1:8000/
|
||||
Quit the server with CONTROL-C.
|
||||
|
||||
|
@ -373,7 +373,7 @@ Our API views don't do anything particularly special at the moment, beyond servi
|
|||
We'll see how we can start to improve things in [part 2 of the tutorial][tut-2].
|
||||
|
||||
[quickstart]: quickstart.md
|
||||
[repo]: https://github.com/tomchristie/rest-framework-tutorial
|
||||
[repo]: https://github.com/encode/rest-framework-tutorial
|
||||
[sandbox]: http://restframework.herokuapp.com/
|
||||
[virtualenv]: http://www.virtualenv.org/en/latest/index.html
|
||||
[tut-2]: 2-requests-and-responses.md
|
||||
|
|
|
@ -36,14 +36,16 @@ API schema.
|
|||
We can now include a schema for our API, by including an autogenerated schema
|
||||
view in our URL configuration.
|
||||
|
||||
```
|
||||
from rest_framework.schemas import get_schema_view
|
||||
|
||||
schema_view = get_schema_view(title='Pastebin API')
|
||||
|
||||
urlpatterns = [
|
||||
url(r'^schema/$', schema_view),
|
||||
url(r'^schema/$', schema_view),
|
||||
...
|
||||
]
|
||||
```
|
||||
|
||||
If you visit the API root endpoint in a browser you should now see `corejson`
|
||||
representation become available as an option.
|
||||
|
@ -221,8 +223,8 @@ We've reached the end of our tutorial. If you want to get more involved in the
|
|||
[coreapi]: http://www.coreapi.org
|
||||
[corejson]: http://www.coreapi.org/specification/encoding/#core-json-encoding
|
||||
[openapi]: https://openapis.org/
|
||||
[repo]: https://github.com/tomchristie/rest-framework-tutorial
|
||||
[repo]: https://github.com/encode/rest-framework-tutorial
|
||||
[sandbox]: http://restframework.herokuapp.com/
|
||||
[github]: https://github.com/tomchristie/django-rest-framework
|
||||
[github]: https://github.com/encode/django-rest-framework
|
||||
[group]: https://groups.google.com/forum/?fromgroups#!forum/django-rest-framework
|
||||
[twitter]: https://twitter.com/_tomchristie
|
||||
|
|
|
@ -417,3 +417,8 @@ ul.sponsor {
|
|||
.toclink {
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.book-cover img {
|
||||
margin: 0 !important;
|
||||
display: inline-block !important;
|
||||
}
|
||||
|
|
|
@ -114,7 +114,7 @@
|
|||
{% block content %}
|
||||
{% if page.meta.source %}
|
||||
{% for filename in page.meta.source %}
|
||||
<a class="github" href="https://github.com/tomchristie/django-rest-framework/tree/master/rest_framework/{{ filename }}">
|
||||
<a class="github" href="https://github.com/encode/django-rest-framework/tree/master/rest_framework/{{ filename }}">
|
||||
<span class="label label-info">{{ filename }}</span>
|
||||
</a>
|
||||
{% endfor %}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<div class="navbar navbar-inverse navbar-fixed-top">
|
||||
<div class="navbar-inner">
|
||||
<div class="container-fluid">
|
||||
<a class="repo-link btn btn-primary btn-small" href="https://github.com/tomchristie/django-rest-framework/tree/master">GitHub</a>
|
||||
<a class="repo-link btn btn-primary btn-small" href="https://github.com/encode/django-rest-framework/tree/master">GitHub</a>
|
||||
<a class="repo-link btn btn-inverse btn-small {% if not page.next_page %}disabled{% endif %}" rel="prev" {% if page.next_page %}href="{{ page.next_page.url }}"{% endif %}>
|
||||
Next <i class="icon-arrow-right icon-white"></i>
|
||||
</a>
|
||||
|
|
|
@ -2,7 +2,7 @@ site_name: Django REST framework
|
|||
site_url: http://www.django-rest-framework.org/
|
||||
site_description: Django REST framework - Web APIs for Django
|
||||
|
||||
repo_url: https://github.com/tomchristie/django-rest-framework
|
||||
repo_url: https://github.com/encode/django-rest-framework
|
||||
|
||||
theme_dir: docs_theme
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
# Optional packages which may be used with REST framework.
|
||||
markdown==2.6.4
|
||||
django-guardian==1.4.6
|
||||
django-filter==1.0.0
|
||||
django-guardian==1.4.8
|
||||
django-filter==1.0.2
|
||||
coreapi==2.2.4
|
||||
coreschema==0.0.4
|
||||
|
|
|
@ -24,7 +24,7 @@ class Token(models.Model):
|
|||
# https://code.djangoproject.com/ticket/19422
|
||||
#
|
||||
# Also see corresponding ticket:
|
||||
# https://github.com/tomchristie/django-rest-framework/issues/705
|
||||
# https://github.com/encode/django-rest-framework/issues/705
|
||||
abstract = 'rest_framework.authtoken' not in settings.INSTALLED_APPS
|
||||
verbose_name = _("Token")
|
||||
verbose_name_plural = _("Tokens")
|
||||
|
|
|
@ -275,6 +275,14 @@ except ImportError:
|
|||
def pygments_css(style):
|
||||
return None
|
||||
|
||||
|
||||
try:
|
||||
import pytz
|
||||
from pytz.exceptions import InvalidTimeError
|
||||
except ImportError:
|
||||
InvalidTimeError = Exception
|
||||
|
||||
|
||||
# `separators` argument to `json.dumps()` differs between 2.x and 3.x
|
||||
# See: http://bugs.python.org/issue22767
|
||||
if six.PY3:
|
||||
|
@ -339,6 +347,7 @@ def set_many(instance, field, value):
|
|||
field = getattr(instance, field)
|
||||
field.set(value)
|
||||
|
||||
|
||||
def include(module, namespace=None, app_name=None):
|
||||
from django.conf.urls import include
|
||||
if django.VERSION < (1,9):
|
||||
|
|
|
@ -3,10 +3,12 @@ from django.conf.urls import include, url
|
|||
from rest_framework.renderers import (
|
||||
CoreJSONRenderer, DocumentationRenderer, SchemaJSRenderer
|
||||
)
|
||||
from rest_framework.schemas import get_schema_view
|
||||
from rest_framework.schemas import SchemaGenerator, get_schema_view
|
||||
|
||||
|
||||
def get_docs_view(title=None, description=None, schema_url=None, public=True):
|
||||
def get_docs_view(
|
||||
title=None, description=None, schema_url=None, public=True,
|
||||
patterns=None, generator_class=SchemaGenerator):
|
||||
renderer_classes = [DocumentationRenderer, CoreJSONRenderer]
|
||||
|
||||
return get_schema_view(
|
||||
|
@ -14,11 +16,15 @@ def get_docs_view(title=None, description=None, schema_url=None, public=True):
|
|||
url=schema_url,
|
||||
description=description,
|
||||
renderer_classes=renderer_classes,
|
||||
public=public
|
||||
public=public,
|
||||
patterns=patterns,
|
||||
generator_class=generator_class,
|
||||
)
|
||||
|
||||
|
||||
def get_schemajs_view(title=None, description=None, schema_url=None, public=True):
|
||||
def get_schemajs_view(
|
||||
title=None, description=None, schema_url=None, public=True,
|
||||
patterns=None, generator_class=SchemaGenerator):
|
||||
renderer_classes = [SchemaJSRenderer]
|
||||
|
||||
return get_schema_view(
|
||||
|
@ -26,22 +32,30 @@ def get_schemajs_view(title=None, description=None, schema_url=None, public=True
|
|||
url=schema_url,
|
||||
description=description,
|
||||
renderer_classes=renderer_classes,
|
||||
public=public
|
||||
public=public,
|
||||
patterns=patterns,
|
||||
generator_class=generator_class,
|
||||
)
|
||||
|
||||
|
||||
def include_docs_urls(title=None, description=None, schema_url=None, public=True):
|
||||
def include_docs_urls(
|
||||
title=None, description=None, schema_url=None, public=True,
|
||||
patterns=None, generator_class=SchemaGenerator):
|
||||
docs_view = get_docs_view(
|
||||
title=title,
|
||||
description=description,
|
||||
schema_url=schema_url,
|
||||
public=public
|
||||
public=public,
|
||||
patterns=patterns,
|
||||
generator_class=generator_class,
|
||||
)
|
||||
schema_js_view = get_schemajs_view(
|
||||
title=title,
|
||||
description=description,
|
||||
schema_url=schema_url,
|
||||
public=public
|
||||
public=public,
|
||||
patterns=patterns,
|
||||
generator_class=generator_class,
|
||||
)
|
||||
urls = [
|
||||
url(r'^$', docs_view, name='docs-index'),
|
||||
|
|
|
@ -33,7 +33,8 @@ from django.utils.translation import ugettext_lazy as _
|
|||
|
||||
from rest_framework import ISO_8601
|
||||
from rest_framework.compat import (
|
||||
get_remote_field, unicode_repr, unicode_to_repr, value_from_object
|
||||
InvalidTimeError, get_remote_field, unicode_repr, unicode_to_repr,
|
||||
value_from_object
|
||||
)
|
||||
from rest_framework.exceptions import ErrorDetail, ValidationError
|
||||
from rest_framework.settings import api_settings
|
||||
|
@ -618,8 +619,8 @@ class Field(object):
|
|||
originally created with, rather than copying the complete state.
|
||||
"""
|
||||
# Treat regexes and validators as immutable.
|
||||
# See https://github.com/tomchristie/django-rest-framework/issues/1954
|
||||
# and https://github.com/tomchristie/django-rest-framework/pull/4489
|
||||
# See https://github.com/encode/django-rest-framework/issues/1954
|
||||
# and https://github.com/encode/django-rest-framework/pull/4489
|
||||
args = [
|
||||
copy.deepcopy(item) if not isinstance(item, REGEX_TYPE) else item
|
||||
for item in self._args
|
||||
|
@ -649,6 +650,7 @@ class BooleanField(Field):
|
|||
initial = False
|
||||
TRUE_VALUES = {
|
||||
't', 'T',
|
||||
'y', 'Y', 'yes', 'YES',
|
||||
'true', 'True', 'TRUE',
|
||||
'on', 'On', 'ON',
|
||||
'1', 1,
|
||||
|
@ -656,6 +658,7 @@ class BooleanField(Field):
|
|||
}
|
||||
FALSE_VALUES = {
|
||||
'f', 'F',
|
||||
'n', 'N', 'no', 'NO',
|
||||
'false', 'False', 'FALSE',
|
||||
'off', 'Off', 'OFF',
|
||||
'0', 0, 0.0,
|
||||
|
@ -1085,6 +1088,7 @@ class DateTimeField(Field):
|
|||
default_error_messages = {
|
||||
'invalid': _('Datetime has wrong format. Use one of these formats instead: {format}.'),
|
||||
'date': _('Expected a datetime but got a date.'),
|
||||
'make_aware': _('Invalid datetime for the timezone "{timezone}".')
|
||||
}
|
||||
datetime_parser = datetime.datetime.strptime
|
||||
|
||||
|
@ -1105,7 +1109,10 @@ class DateTimeField(Field):
|
|||
field_timezone = getattr(self, 'timezone', self.default_timezone())
|
||||
|
||||
if (field_timezone is not None) and not timezone.is_aware(value):
|
||||
return timezone.make_aware(value, field_timezone)
|
||||
try:
|
||||
return timezone.make_aware(value, field_timezone)
|
||||
except InvalidTimeError:
|
||||
self.fail('make_aware', timezone=field_timezone)
|
||||
elif (field_timezone is None) and timezone.is_aware(value):
|
||||
return timezone.make_naive(value, utc)
|
||||
return value
|
||||
|
|
|
@ -50,12 +50,17 @@ if django_filters:
|
|||
DeprecationWarning
|
||||
)
|
||||
return super(FilterSet, self).__init__(*args, **kwargs)
|
||||
|
||||
DFBase = django_filters.rest_framework.DjangoFilterBackend
|
||||
|
||||
else:
|
||||
def FilterSet():
|
||||
assert False, 'django-filter must be installed to use the `FilterSet` class'
|
||||
|
||||
DFBase = BaseFilterBackend
|
||||
|
||||
class DjangoFilterBackend(BaseFilterBackend):
|
||||
|
||||
class DjangoFilterBackend(DFBase):
|
||||
"""
|
||||
A filter backend that uses django-filter.
|
||||
"""
|
||||
|
@ -69,9 +74,7 @@ class DjangoFilterBackend(BaseFilterBackend):
|
|||
DeprecationWarning
|
||||
)
|
||||
|
||||
from django_filters.rest_framework import DjangoFilterBackend
|
||||
|
||||
return DjangoFilterBackend(*args, **kwargs)
|
||||
return super(DjangoFilterBackend, cls).__new__(cls, *args, **kwargs)
|
||||
|
||||
|
||||
class SearchFilter(BaseFilterBackend):
|
||||
|
@ -323,7 +326,7 @@ class DjangoObjectPermissionsFilter(BaseFilterBackend):
|
|||
|
||||
def filter_queryset(self, request, queryset, view):
|
||||
# We want to defer this import until run-time, rather than import-time.
|
||||
# See https://github.com/tomchristie/django-rest-framework/issues/4608
|
||||
# See https://github.com/encode/django-rest-framework/issues/4608
|
||||
# (Also see #1624 for why we need to make this import explicitly)
|
||||
from guardian.shortcuts import get_objects_for_user
|
||||
|
||||
|
|
|
@ -3,6 +3,7 @@ Generic views that provide commonly needed behaviour.
|
|||
"""
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.core.exceptions import ValidationError
|
||||
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
|
||||
|
@ -18,7 +19,7 @@ def get_object_or_404(queryset, *filter_args, **filter_kwargs):
|
|||
"""
|
||||
try:
|
||||
return _get_object_or_404(queryset, *filter_args, **filter_kwargs)
|
||||
except (TypeError, ValueError):
|
||||
except (TypeError, ValueError, ValidationError):
|
||||
raise Http404
|
||||
|
||||
|
||||
|
|
|
@ -332,12 +332,12 @@ class LimitOffsetPagination(BasePagination):
|
|||
template = 'rest_framework/pagination/numbers.html'
|
||||
|
||||
def paginate_queryset(self, queryset, request, view=None):
|
||||
self.count = _get_count(queryset)
|
||||
self.limit = self.get_limit(request)
|
||||
if self.limit is None:
|
||||
return None
|
||||
|
||||
self.offset = self.get_offset(request)
|
||||
self.count = _get_count(queryset)
|
||||
self.request = request
|
||||
if self.count > self.limit and self.template is not None:
|
||||
self.display_page_controls = True
|
||||
|
|
|
@ -7,7 +7,9 @@ from django.core.exceptions import ImproperlyConfigured, ObjectDoesNotExist
|
|||
from django.db.models import Manager
|
||||
from django.db.models.query import QuerySet
|
||||
from django.utils import six
|
||||
from django.utils.encoding import python_2_unicode_compatible, smart_text
|
||||
from django.utils.encoding import (
|
||||
python_2_unicode_compatible, smart_text, uri_to_iri
|
||||
)
|
||||
from django.utils.six.moves.urllib import parse as urlparse
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
|
@ -324,6 +326,8 @@ class HyperlinkedRelatedField(RelatedField):
|
|||
if data.startswith(prefix):
|
||||
data = '/' + data[len(prefix):]
|
||||
|
||||
data = uri_to_iri(data)
|
||||
|
||||
try:
|
||||
match = resolve(data)
|
||||
except Resolver404:
|
||||
|
|
|
@ -316,6 +316,7 @@ class DefaultRouter(SimpleRouter):
|
|||
default_schema_renderers = None
|
||||
APIRootView = APIRootView
|
||||
APISchemaView = SchemaView
|
||||
SchemaGenerator = SchemaGenerator
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
if 'schema_title' in kwargs:
|
||||
|
@ -342,7 +343,7 @@ class DefaultRouter(SimpleRouter):
|
|||
"""
|
||||
Return a schema root view.
|
||||
"""
|
||||
schema_generator = SchemaGenerator(
|
||||
schema_generator = self.SchemaGenerator(
|
||||
title=self.schema_title,
|
||||
url=self.schema_url,
|
||||
patterns=api_urls
|
||||
|
|
|
@ -246,7 +246,9 @@ class EndpointInspector(object):
|
|||
Return a list of the valid HTTP methods for this endpoint.
|
||||
"""
|
||||
if hasattr(callback, 'actions'):
|
||||
return [method.upper() for method in callback.actions.keys()]
|
||||
actions = set(callback.actions.keys())
|
||||
http_method_names = set(callback.cls.http_method_names)
|
||||
return [method.upper() for method in actions & http_method_names]
|
||||
|
||||
return [
|
||||
method for method in
|
||||
|
@ -604,7 +606,7 @@ class SchemaGenerator(object):
|
|||
return []
|
||||
|
||||
pagination = getattr(view, 'pagination_class', None)
|
||||
if not pagination or not pagination.page_size:
|
||||
if not pagination or not getattr(pagination, 'page_size', None):
|
||||
return []
|
||||
|
||||
paginator = view.pagination_class()
|
||||
|
@ -694,11 +696,16 @@ class SchemaView(APIView):
|
|||
return Response(schema)
|
||||
|
||||
|
||||
def get_schema_view(title=None, url=None, description=None, urlconf=None, renderer_classes=None, public=False):
|
||||
def get_schema_view(
|
||||
title=None, url=None, description=None, urlconf=None, renderer_classes=None,
|
||||
public=False, patterns=None, generator_class=SchemaGenerator):
|
||||
"""
|
||||
Return a schema view.
|
||||
"""
|
||||
generator = SchemaGenerator(title=title, url=url, description=description, urlconf=urlconf)
|
||||
generator = generator_class(
|
||||
title=title, url=url, description=description,
|
||||
urlconf=urlconf, patterns=patterns,
|
||||
)
|
||||
return SchemaView.as_view(
|
||||
renderer_classes=renderer_classes,
|
||||
schema_generator=generator,
|
||||
|
|
|
@ -38,7 +38,8 @@ from rest_framework.utils.field_mapping import (
|
|||
get_relation_kwargs, get_url_kwargs
|
||||
)
|
||||
from rest_framework.utils.serializer_helpers import (
|
||||
BindingDict, BoundField, NestedBoundField, ReturnDict, ReturnList
|
||||
BindingDict, BoundField, JSONBoundField, NestedBoundField, ReturnDict,
|
||||
ReturnList
|
||||
)
|
||||
from rest_framework.validators import (
|
||||
UniqueForDateValidator, UniqueForMonthValidator, UniqueForYearValidator,
|
||||
|
@ -521,6 +522,8 @@ class Serializer(BaseSerializer):
|
|||
error = self.errors.get(key) if hasattr(self, '_errors') else None
|
||||
if isinstance(field, Serializer):
|
||||
return NestedBoundField(field, value, error)
|
||||
if isinstance(field, JSONField):
|
||||
return JSONBoundField(field, value, error)
|
||||
return BoundField(field, value, error)
|
||||
|
||||
# Include a backlink to the serializer class on return objects.
|
||||
|
@ -562,6 +565,10 @@ class ListSerializer(BaseSerializer):
|
|||
super(ListSerializer, self).__init__(*args, **kwargs)
|
||||
self.child.bind(field_name='', parent=self)
|
||||
|
||||
def bind(self, field_name, parent):
|
||||
super(ListSerializer, self).bind(field_name, parent)
|
||||
self.partial = self.parent.partial
|
||||
|
||||
def get_initial(self):
|
||||
if hasattr(self, 'initial_data'):
|
||||
return self.to_representation(self.initial_data)
|
||||
|
@ -613,6 +620,9 @@ class ListSerializer(BaseSerializer):
|
|||
}, code='not_a_list')
|
||||
|
||||
if not self.allow_empty and len(data) == 0:
|
||||
if self.parent and self.partial:
|
||||
raise SkipField()
|
||||
|
||||
message = self.error_messages['empty']
|
||||
raise ValidationError({
|
||||
api_settings.NON_FIELD_ERRORS_KEY: [message]
|
||||
|
|
|
@ -9,7 +9,8 @@ REST_FRAMEWORK = {
|
|||
)
|
||||
'DEFAULT_PARSER_CLASSES': (
|
||||
'rest_framework.parsers.JSONParser',
|
||||
'rest_framework.parsers.TemplateHTMLRenderer',
|
||||
'rest_framework.parsers.FormParser',
|
||||
'rest_framework.parsers.MultiPartParser'
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -24,11 +24,12 @@ def get_breadcrumbs(url, request=None):
|
|||
# Check if this is a REST framework view,
|
||||
# and if so add it to the breadcrumbs
|
||||
cls = getattr(view, 'cls', None)
|
||||
initkwargs = getattr(view, 'initkwargs', {})
|
||||
if cls is not None and issubclass(cls, APIView):
|
||||
# Don't list the same view twice in a row.
|
||||
# Probably an optional trailing slash.
|
||||
if not seen or seen[-1] != view:
|
||||
c = cls()
|
||||
c = cls(**initkwargs)
|
||||
c.suffix = getattr(view, 'suffix', None)
|
||||
name = c.get_view_name()
|
||||
insert_url = preserve_builtin_query_params(prefix + url, request)
|
||||
|
|
|
@ -8,7 +8,7 @@ from django.core import validators
|
|||
from django.db import models
|
||||
from django.utils.text import capfirst
|
||||
|
||||
from rest_framework.compat import DecimalValidator
|
||||
from rest_framework.compat import DecimalValidator, JSONField
|
||||
from rest_framework.validators import UniqueValidator
|
||||
|
||||
NUMERIC_FIELD_TYPES = (
|
||||
|
@ -88,7 +88,7 @@ def get_field_kwargs(field_name, model_field):
|
|||
if decimal_places is not None:
|
||||
kwargs['decimal_places'] = decimal_places
|
||||
|
||||
if isinstance(model_field, models.TextField):
|
||||
if isinstance(model_field, models.TextField) or (JSONField and isinstance(model_field, JSONField)):
|
||||
kwargs['style'] = {'base_template': 'textarea.html'}
|
||||
|
||||
if isinstance(model_field, models.AutoField) or not model_field.editable:
|
||||
|
@ -123,18 +123,70 @@ def get_field_kwargs(field_name, model_field):
|
|||
kwargs['allow_folders'] = model_field.allow_folders
|
||||
|
||||
if model_field.choices:
|
||||
# If this model field contains choices, then return early.
|
||||
# Further keyword arguments are not valid.
|
||||
kwargs['choices'] = model_field.choices
|
||||
return kwargs
|
||||
else:
|
||||
# Ensure that max_value is passed explicitly as a keyword arg,
|
||||
# rather than as a validator.
|
||||
max_value = next((
|
||||
validator.limit_value for validator in validator_kwarg
|
||||
if isinstance(validator, validators.MaxValueValidator)
|
||||
), None)
|
||||
if max_value is not None and isinstance(model_field, NUMERIC_FIELD_TYPES):
|
||||
kwargs['max_value'] = max_value
|
||||
validator_kwarg = [
|
||||
validator for validator in validator_kwarg
|
||||
if not isinstance(validator, validators.MaxValueValidator)
|
||||
]
|
||||
|
||||
# Our decimal validation is handled in the field code, not validator code.
|
||||
# (In Django 1.9+ this differs from previous style)
|
||||
if isinstance(model_field, models.DecimalField) and DecimalValidator:
|
||||
validator_kwarg = [
|
||||
validator for validator in validator_kwarg
|
||||
if not isinstance(validator, DecimalValidator)
|
||||
]
|
||||
# Ensure that min_value is passed explicitly as a keyword arg,
|
||||
# rather than as a validator.
|
||||
min_value = next((
|
||||
validator.limit_value for validator in validator_kwarg
|
||||
if isinstance(validator, validators.MinValueValidator)
|
||||
), None)
|
||||
if min_value is not None and isinstance(model_field, NUMERIC_FIELD_TYPES):
|
||||
kwargs['min_value'] = min_value
|
||||
validator_kwarg = [
|
||||
validator for validator in validator_kwarg
|
||||
if not isinstance(validator, validators.MinValueValidator)
|
||||
]
|
||||
|
||||
# URLField does not need to include the URLValidator argument,
|
||||
# as it is explicitly added in.
|
||||
if isinstance(model_field, models.URLField):
|
||||
validator_kwarg = [
|
||||
validator for validator in validator_kwarg
|
||||
if not isinstance(validator, validators.URLValidator)
|
||||
]
|
||||
|
||||
# EmailField does not need to include the validate_email argument,
|
||||
# as it is explicitly added in.
|
||||
if isinstance(model_field, models.EmailField):
|
||||
validator_kwarg = [
|
||||
validator for validator in validator_kwarg
|
||||
if validator is not validators.validate_email
|
||||
]
|
||||
|
||||
# SlugField do not need to include the 'validate_slug' argument,
|
||||
if isinstance(model_field, models.SlugField):
|
||||
validator_kwarg = [
|
||||
validator for validator in validator_kwarg
|
||||
if validator is not validators.validate_slug
|
||||
]
|
||||
|
||||
# IPAddressField do not need to include the 'validate_ipv46_address' argument,
|
||||
if isinstance(model_field, models.GenericIPAddressField):
|
||||
validator_kwarg = [
|
||||
validator for validator in validator_kwarg
|
||||
if validator is not validators.validate_ipv46_address
|
||||
]
|
||||
# Our decimal validation is handled in the field code, not validator code.
|
||||
# (In Django 1.9+ this differs from previous style)
|
||||
if isinstance(model_field, models.DecimalField) and DecimalValidator:
|
||||
validator_kwarg = [
|
||||
validator for validator in validator_kwarg
|
||||
if not isinstance(validator, DecimalValidator)
|
||||
]
|
||||
|
||||
# Ensure that max_length is passed explicitly as a keyword arg,
|
||||
# rather than as a validator.
|
||||
|
@ -160,62 +212,6 @@ def get_field_kwargs(field_name, model_field):
|
|||
if not isinstance(validator, validators.MinLengthValidator)
|
||||
]
|
||||
|
||||
# Ensure that max_value is passed explicitly as a keyword arg,
|
||||
# rather than as a validator.
|
||||
max_value = next((
|
||||
validator.limit_value for validator in validator_kwarg
|
||||
if isinstance(validator, validators.MaxValueValidator)
|
||||
), None)
|
||||
if max_value is not None and isinstance(model_field, NUMERIC_FIELD_TYPES):
|
||||
kwargs['max_value'] = max_value
|
||||
validator_kwarg = [
|
||||
validator for validator in validator_kwarg
|
||||
if not isinstance(validator, validators.MaxValueValidator)
|
||||
]
|
||||
|
||||
# Ensure that max_value is passed explicitly as a keyword arg,
|
||||
# rather than as a validator.
|
||||
min_value = next((
|
||||
validator.limit_value for validator in validator_kwarg
|
||||
if isinstance(validator, validators.MinValueValidator)
|
||||
), None)
|
||||
if min_value is not None and isinstance(model_field, NUMERIC_FIELD_TYPES):
|
||||
kwargs['min_value'] = min_value
|
||||
validator_kwarg = [
|
||||
validator for validator in validator_kwarg
|
||||
if not isinstance(validator, validators.MinValueValidator)
|
||||
]
|
||||
|
||||
# URLField does not need to include the URLValidator argument,
|
||||
# as it is explicitly added in.
|
||||
if isinstance(model_field, models.URLField):
|
||||
validator_kwarg = [
|
||||
validator for validator in validator_kwarg
|
||||
if not isinstance(validator, validators.URLValidator)
|
||||
]
|
||||
|
||||
# EmailField does not need to include the validate_email argument,
|
||||
# as it is explicitly added in.
|
||||
if isinstance(model_field, models.EmailField):
|
||||
validator_kwarg = [
|
||||
validator for validator in validator_kwarg
|
||||
if validator is not validators.validate_email
|
||||
]
|
||||
|
||||
# SlugField do not need to include the 'validate_slug' argument,
|
||||
if isinstance(model_field, models.SlugField):
|
||||
validator_kwarg = [
|
||||
validator for validator in validator_kwarg
|
||||
if validator is not validators.validate_slug
|
||||
]
|
||||
|
||||
# IPAddressField do not need to include the 'validate_ipv46_address' argument,
|
||||
if isinstance(model_field, models.GenericIPAddressField):
|
||||
validator_kwarg = [
|
||||
validator for validator in validator_kwarg
|
||||
if validator is not validators.validate_ipv46_address
|
||||
]
|
||||
|
||||
if getattr(model_field, 'unique', False):
|
||||
unique_error_message = model_field.error_messages.get('unique', None)
|
||||
if unique_error_message:
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
from __future__ import unicode_literals
|
||||
|
||||
import collections
|
||||
import json
|
||||
from collections import OrderedDict
|
||||
|
||||
from django.utils.encoding import force_text
|
||||
|
@ -82,6 +83,16 @@ class BoundField(object):
|
|||
return self.__class__(self._field, value, self.errors, self._prefix)
|
||||
|
||||
|
||||
class JSONBoundField(BoundField):
|
||||
def as_form_field(self):
|
||||
value = self.value
|
||||
try:
|
||||
value = json.dumps(self.value, sort_keys=True, indent=4)
|
||||
except TypeError:
|
||||
pass
|
||||
return self.__class__(self._field, value, self.errors, self._prefix)
|
||||
|
||||
|
||||
class NestedBoundField(BoundField):
|
||||
"""
|
||||
This `BoundField` additionally implements __iter__ and __getitem__
|
||||
|
@ -101,7 +112,7 @@ class NestedBoundField(BoundField):
|
|||
def __getitem__(self, key):
|
||||
field = self.fields[key]
|
||||
value = self.value.get(key) if self.value else None
|
||||
error = self.errors.get(key) if self.errors else None
|
||||
error = self.errors.get(key) if isinstance(self.errors, dict) else None
|
||||
if hasattr(field, 'fields'):
|
||||
return NestedBoundField(field, value, error, prefix=self.name + '.')
|
||||
return BoundField(field, value, error, prefix=self.name + '.')
|
||||
|
|
|
@ -18,7 +18,7 @@ from rest_framework.utils.representation import smart_repr
|
|||
|
||||
# Robust filter and exist implementations. Ensures that queryset.exists() for
|
||||
# an invalid value returns `False`, rather than raising an error.
|
||||
# Refs https://github.com/tomchristie/django-rest-framework/issues/3381
|
||||
# Refs https://github.com/encode/django-rest-framework/issues/3381
|
||||
|
||||
def qs_exists(queryset):
|
||||
try:
|
||||
|
|
|
@ -9,6 +9,7 @@ from django.db import models
|
|||
from django.http import Http404
|
||||
from django.http.response import HttpResponseBase
|
||||
from django.utils import six
|
||||
from django.utils.cache import cc_delim_re, patch_vary_headers
|
||||
from django.utils.encoding import smart_text
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
|
@ -290,7 +291,7 @@ class APIView(View):
|
|||
"""
|
||||
Returns the exception handler that this view uses.
|
||||
"""
|
||||
return api_settings.EXCEPTION_HANDLER
|
||||
return self.settings.EXCEPTION_HANDLER
|
||||
|
||||
# API policy implementation methods
|
||||
|
||||
|
@ -414,6 +415,11 @@ class APIView(View):
|
|||
response.accepted_media_type = request.accepted_media_type
|
||||
response.renderer_context = self.get_renderer_context()
|
||||
|
||||
# Add new vary headers to the response instead of overwriting.
|
||||
vary_headers = self.headers.pop('Vary', None)
|
||||
if vary_headers is not None:
|
||||
patch_vary_headers(response, cc_delim_re.split(vary_headers))
|
||||
|
||||
for key, value in self.headers.items():
|
||||
response[key] = value
|
||||
|
||||
|
|
1
setup.py
1
setup.py
|
@ -95,6 +95,7 @@ setup(
|
|||
'Framework :: Django :: 1.8',
|
||||
'Framework :: Django :: 1.9',
|
||||
'Framework :: Django :: 1.10',
|
||||
'Framework :: Django :: 1.11',
|
||||
'Intended Audience :: Developers',
|
||||
'License :: OSI Approved :: BSD License',
|
||||
'Operating System :: OS Independent',
|
||||
|
|
|
@ -44,7 +44,7 @@ class TestManyPostView(TestCase):
|
|||
POST request to a view that returns a list of objects should
|
||||
still successfully return the browsable API with a rendered form.
|
||||
|
||||
Regression test for https://github.com/tomchristie/django-rest-framework/pull/3164
|
||||
Regression test for https://github.com/encode/django-rest-framework/pull/3164
|
||||
"""
|
||||
data = {}
|
||||
request = factory.post('/', data, format='json')
|
||||
|
|
|
@ -127,7 +127,7 @@ class BasicAuthTests(TestCase):
|
|||
def test_regression_handle_bad_base64_basic_auth_header(self):
|
||||
"""Ensure POSTing JSON over basic auth with incorrectly padded Base64 string is handled correctly"""
|
||||
# regression test for issue in 'rest_framework.authentication.BasicAuthentication.authenticate'
|
||||
# https://github.com/tomchristie/django-rest-framework/issues/4089
|
||||
# https://github.com/encode/django-rest-framework/issues/4089
|
||||
auth = 'Basic =a='
|
||||
response = self.csrf_client.post(
|
||||
'/basic/',
|
||||
|
@ -185,7 +185,7 @@ class SessionAuthTests(TestCase):
|
|||
"""
|
||||
Ensure the login template renders for a basic GET.
|
||||
|
||||
cf. [#1810](https://github.com/tomchristie/django-rest-framework/pull/1810)
|
||||
cf. [#1810](https://github.com/encode/django-rest-framework/pull/1810)
|
||||
"""
|
||||
response = self.csrf_client.get('/auth/login/')
|
||||
content = response.content.decode('utf8')
|
||||
|
|
|
@ -96,7 +96,7 @@ class TestViewNamesAndDescriptions(TestCase):
|
|||
Ensure a view may have a docstring that is actually a lazily evaluated
|
||||
class that can be converted to a string.
|
||||
|
||||
See: https://github.com/tomchristie/django-rest-framework/issues/1708
|
||||
See: https://github.com/encode/django-rest-framework/issues/1708
|
||||
"""
|
||||
# use a mock object instead of gettext_lazy to ensure that we can't end
|
||||
# up with a test case string in our l10n catalog
|
||||
|
|
|
@ -12,7 +12,7 @@ from django.utils import six
|
|||
from django.utils.timezone import utc
|
||||
|
||||
import rest_framework
|
||||
from rest_framework import serializers
|
||||
from rest_framework import compat, serializers
|
||||
from rest_framework.fields import is_simple_callable
|
||||
|
||||
try:
|
||||
|
@ -1205,6 +1205,30 @@ class TestNaiveDateTimeField(FieldValues):
|
|||
field = serializers.DateTimeField(default_timezone=None)
|
||||
|
||||
|
||||
class TestNaiveDayLightSavingTimeTimeZoneDateTimeField(FieldValues):
|
||||
"""
|
||||
Invalid values for `DateTimeField` with datetime in DST shift (non-existing or ambiguous) and timezone with DST.
|
||||
Timezone America/New_York has DST shift from 2017-03-12T02:00:00 to 2017-03-12T03:00:00 and
|
||||
from 2017-11-05T02:00:00 to 2017-11-05T01:00:00 in 2017.
|
||||
"""
|
||||
valid_inputs = {}
|
||||
invalid_inputs = {
|
||||
'2017-03-12T02:30:00': ['Invalid datetime for the timezone "America/New_York".'],
|
||||
'2017-11-05T01:30:00': ['Invalid datetime for the timezone "America/New_York".']
|
||||
}
|
||||
outputs = {}
|
||||
|
||||
class MockTimezone:
|
||||
@staticmethod
|
||||
def localize(value, is_dst):
|
||||
raise compat.InvalidTimeError()
|
||||
|
||||
def __str__(self):
|
||||
return 'America/New_York'
|
||||
|
||||
field = serializers.DateTimeField(default_timezone=MockTimezone())
|
||||
|
||||
|
||||
class TestTimeField(FieldValues):
|
||||
"""
|
||||
Valid and invalid values for `TimeField`.
|
||||
|
|
|
@ -201,6 +201,21 @@ class IntegrationTestFiltering(CommonFilteringTestCase):
|
|||
assert response.data == self.data
|
||||
assert len(w) == 0
|
||||
|
||||
@unittest.skipUnless(django_filters, 'django-filter not installed')
|
||||
def test_backend_mro(self):
|
||||
class CustomBackend(filters.DjangoFilterBackend):
|
||||
def filter_queryset(self, request, queryset, view):
|
||||
assert False, "custom filter_queryset should run"
|
||||
|
||||
class DFFilterFieldsRootView(FilterFieldsRootView):
|
||||
filter_backends = (CustomBackend,)
|
||||
|
||||
view = DFFilterFieldsRootView.as_view()
|
||||
request = factory.get('/')
|
||||
|
||||
with pytest.raises(AssertionError, message="custom filter_queryset should run"):
|
||||
view(request).render()
|
||||
|
||||
@unittest.skipUnless(django_filters, 'django-filter not installed')
|
||||
def test_get_filtered_fields_root_view(self):
|
||||
"""
|
||||
|
|
|
@ -2,6 +2,7 @@ from __future__ import unicode_literals
|
|||
|
||||
import pytest
|
||||
from django.db import models
|
||||
from django.http import Http404
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django.test import TestCase
|
||||
from django.utils import six
|
||||
|
@ -10,7 +11,8 @@ from rest_framework import generics, renderers, serializers, status
|
|||
from rest_framework.response import Response
|
||||
from rest_framework.test import APIRequestFactory
|
||||
from tests.models import (
|
||||
BasicModel, ForeignKeySource, ForeignKeyTarget, RESTFrameworkModel
|
||||
BasicModel, ForeignKeySource, ForeignKeyTarget, RESTFrameworkModel,
|
||||
UUIDForeignKeyTarget
|
||||
)
|
||||
|
||||
factory = APIRequestFactory()
|
||||
|
@ -398,7 +400,7 @@ class TestCreateModelWithAutoNowAddField(TestCase):
|
|||
"""
|
||||
Regression test for #285
|
||||
|
||||
https://github.com/tomchristie/django-rest-framework/issues/285
|
||||
https://github.com/encode/django-rest-framework/issues/285
|
||||
"""
|
||||
data = {'email': 'foobar@example.com', 'content': 'foobar'}
|
||||
request = factory.post('/', data, format='json')
|
||||
|
@ -647,3 +649,19 @@ class ApiViewsTests(TestCase):
|
|||
view.delete('test request', 'test arg', test_kwarg='test')
|
||||
assert view.called is True
|
||||
assert view.call_args == data
|
||||
|
||||
|
||||
class GetObjectOr404Tests(TestCase):
|
||||
def setUp(self):
|
||||
super(GetObjectOr404Tests, self).setUp()
|
||||
self.uuid_object = UUIDForeignKeyTarget.objects.create(name='bar')
|
||||
|
||||
def test_get_object_or_404_with_valid_uuid(self):
|
||||
obj = generics.get_object_or_404(
|
||||
UUIDForeignKeyTarget, pk=self.uuid_object.pk
|
||||
)
|
||||
assert obj == self.uuid_object
|
||||
|
||||
def test_get_object_or_404_with_invalid_string_for_uuid(self):
|
||||
with pytest.raises(Http404):
|
||||
generics.get_object_or_404(UUIDForeignKeyTarget, pk='not-a-uuid')
|
||||
|
|
|
@ -99,6 +99,15 @@ class Issue3674ChildModel(models.Model):
|
|||
value = models.CharField(primary_key=True, max_length=64)
|
||||
|
||||
|
||||
class UniqueChoiceModel(models.Model):
|
||||
CHOICES = (
|
||||
('choice1', 'choice 1'),
|
||||
('choice2', 'choice 1'),
|
||||
)
|
||||
|
||||
name = models.CharField(max_length=254, unique=True, choices=CHOICES)
|
||||
|
||||
|
||||
class TestModelSerializer(TestCase):
|
||||
def test_create_method(self):
|
||||
class TestSerializer(serializers.ModelSerializer):
|
||||
|
@ -1080,3 +1089,16 @@ class Issue4897TestCase(TestCase):
|
|||
with pytest.raises(AssertionError) as cm:
|
||||
TestSerializer(obj).fields
|
||||
cm.match(r'readonly_fields')
|
||||
|
||||
|
||||
class Test5004UniqueChoiceField(TestCase):
|
||||
def test_unique_choice_field(self):
|
||||
class TestUniqueChoiceSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = UniqueChoiceModel
|
||||
fields = '__all__'
|
||||
|
||||
UniqueChoiceModel.objects.create(name='choice1')
|
||||
serializer = TestUniqueChoiceSerializer(data={'name': 'choice1'})
|
||||
assert not serializer.is_valid()
|
||||
assert serializer.errors == {'name': ['unique choice model with this name already exists.']}
|
||||
|
|
|
@ -43,7 +43,7 @@ class TestPrefetchRelatedUpdates(TestCase):
|
|||
|
||||
def test_prefetch_related_excluding_instance_from_original_queryset(self):
|
||||
"""
|
||||
Regression test for https://github.com/tomchristie/django-rest-framework/issues/4661
|
||||
Regression test for https://github.com/encode/django-rest-framework/issues/4661
|
||||
"""
|
||||
view = UserUpdate.as_view()
|
||||
pk = self.user.pk
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
import uuid
|
||||
|
||||
import pytest
|
||||
from django.conf.urls import url
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
from django.test import override_settings
|
||||
from django.utils.datastructures import MultiValueDict
|
||||
|
||||
from rest_framework import serializers
|
||||
|
@ -87,10 +89,21 @@ class TestProxiedPrimaryKeyRelatedField(APISimpleTestCase):
|
|||
assert representation == self.instance.pk.int
|
||||
|
||||
|
||||
@override_settings(ROOT_URLCONF=[
|
||||
url(r'^example/(?P<name>.+)/$', lambda: None, name='example'),
|
||||
])
|
||||
class TestHyperlinkedRelatedField(APISimpleTestCase):
|
||||
def setUp(self):
|
||||
self.queryset = MockQueryset([
|
||||
MockObject(pk=1, name='foobar'),
|
||||
MockObject(pk=2, name='baz qux'),
|
||||
])
|
||||
self.field = serializers.HyperlinkedRelatedField(
|
||||
view_name='example', read_only=True)
|
||||
view_name='example',
|
||||
lookup_field='name',
|
||||
lookup_url_kwarg='name',
|
||||
queryset=self.queryset,
|
||||
)
|
||||
self.field.reverse = mock_reverse
|
||||
self.field._context = {'request': True}
|
||||
|
||||
|
@ -98,6 +111,20 @@ class TestHyperlinkedRelatedField(APISimpleTestCase):
|
|||
representation = self.field.to_representation(MockObject(pk=''))
|
||||
assert representation is None
|
||||
|
||||
def test_hyperlinked_related_lookup_exists(self):
|
||||
instance = self.field.to_internal_value('http://example.org/example/foobar/')
|
||||
assert instance is self.queryset.items[0]
|
||||
|
||||
def test_hyperlinked_related_lookup_url_encoded_exists(self):
|
||||
instance = self.field.to_internal_value('http://example.org/example/baz%20qux/')
|
||||
assert instance is self.queryset.items[1]
|
||||
|
||||
def test_hyperlinked_related_lookup_does_not_exist(self):
|
||||
with pytest.raises(serializers.ValidationError) as excinfo:
|
||||
self.field.to_internal_value('http://example.org/example/doesnotexist/')
|
||||
msg = excinfo.value.detail[0]
|
||||
assert msg == 'Invalid hyperlink - Object does not exist.'
|
||||
|
||||
|
||||
class TestHyperlinkedIdentityField(APISimpleTestCase):
|
||||
def setUp(self):
|
||||
|
|
|
@ -335,7 +335,7 @@ class PKForeignKeyTests(TestCase):
|
|||
"""
|
||||
Regression test for #1072
|
||||
|
||||
https://github.com/tomchristie/django-rest-framework/issues/1072
|
||||
https://github.com/encode/django-rest-framework/issues/1072
|
||||
"""
|
||||
serializer = NullableForeignKeySourceSerializer()
|
||||
assert serializer.data['target'] is None
|
||||
|
|
|
@ -242,7 +242,7 @@ class RendererEndToEndTests(TestCase):
|
|||
"""
|
||||
Regression test for #1196
|
||||
|
||||
https://github.com/tomchristie/django-rest-framework/issues/1196
|
||||
https://github.com/encode/django-rest-framework/issues/1196
|
||||
"""
|
||||
resp = self.client.get('/empty')
|
||||
self.assertEqual(resp.get('Content-Type', None), None)
|
||||
|
|
|
@ -156,6 +156,7 @@ class TestCustomLookupFields(TestCase):
|
|||
"""
|
||||
def setUp(self):
|
||||
RouterTestModel.objects.create(uuid='123', text='foo bar')
|
||||
RouterTestModel.objects.create(uuid='a b', text='baz qux')
|
||||
|
||||
def test_custom_lookup_field_route(self):
|
||||
detail_route = notes_router.urls[-1]
|
||||
|
@ -164,12 +165,19 @@ class TestCustomLookupFields(TestCase):
|
|||
|
||||
def test_retrieve_lookup_field_list_view(self):
|
||||
response = self.client.get('/example/notes/')
|
||||
assert response.data == [{"url": "http://testserver/example/notes/123/", "uuid": "123", "text": "foo bar"}]
|
||||
assert response.data == [
|
||||
{"url": "http://testserver/example/notes/123/", "uuid": "123", "text": "foo bar"},
|
||||
{"url": "http://testserver/example/notes/a%20b/", "uuid": "a b", "text": "baz qux"},
|
||||
]
|
||||
|
||||
def test_retrieve_lookup_field_detail_view(self):
|
||||
response = self.client.get('/example/notes/123/')
|
||||
assert response.data == {"url": "http://testserver/example/notes/123/", "uuid": "123", "text": "foo bar"}
|
||||
|
||||
def test_retrieve_lookup_field_url_encoded_detail_view_(self):
|
||||
response = self.client.get('/example/notes/a%20b/')
|
||||
assert response.data == {"url": "http://testserver/example/notes/a%20b/", "uuid": "a b", "text": "baz qux"}
|
||||
|
||||
|
||||
class TestLookupValueRegex(TestCase):
|
||||
"""
|
||||
|
@ -211,6 +219,10 @@ class TestLookupUrlKwargs(TestCase):
|
|||
response = self.client.get('/example2/notes/fo/')
|
||||
assert response.data == {"url": "http://testserver/example/notes/123/", "uuid": "123", "text": "foo bar"}
|
||||
|
||||
def test_retrieve_lookup_url_encoded_kwarg_detail_view(self):
|
||||
response = self.client.get('/example2/notes/foo%20bar/')
|
||||
assert response.data == {"url": "http://testserver/example/notes/123/", "uuid": "123", "text": "foo bar"}
|
||||
|
||||
|
||||
class TestTrailingSlashIncluded(TestCase):
|
||||
def setUp(self):
|
||||
|
|
|
@ -246,6 +246,11 @@ class PermissionDeniedExampleViewSet(ExampleViewSet):
|
|||
permission_classes = [DenyAllUsingPermissionDenied]
|
||||
|
||||
|
||||
class MethodLimitedViewSet(ExampleViewSet):
|
||||
permission_classes = []
|
||||
http_method_names = ['get', 'head', 'options']
|
||||
|
||||
|
||||
class ExampleListView(APIView):
|
||||
permission_classes = [permissions.IsAuthenticatedOrReadOnly]
|
||||
|
||||
|
@ -368,6 +373,61 @@ class TestSchemaGeneratorNotAtRoot(TestCase):
|
|||
assert schema == expected
|
||||
|
||||
|
||||
@unittest.skipUnless(coreapi, 'coreapi is not installed')
|
||||
class TestSchemaGeneratorWithMethodLimitedViewSets(TestCase):
|
||||
def setUp(self):
|
||||
router = DefaultRouter()
|
||||
router.register('example1', MethodLimitedViewSet, base_name='example1')
|
||||
self.patterns = [
|
||||
url(r'^', include(router.urls))
|
||||
]
|
||||
|
||||
def test_schema_for_regular_views(self):
|
||||
"""
|
||||
Ensure that schema generation works for ViewSet classes
|
||||
with method limitation by Django CBV's http_method_names attribute
|
||||
"""
|
||||
generator = SchemaGenerator(title='Example API', patterns=self.patterns)
|
||||
request = factory.get('/example1/')
|
||||
schema = generator.get_schema(Request(request))
|
||||
|
||||
expected = coreapi.Document(
|
||||
url='http://testserver/example1/',
|
||||
title='Example API',
|
||||
content={
|
||||
'example1': {
|
||||
'list': coreapi.Link(
|
||||
url='/example1/',
|
||||
action='get',
|
||||
fields=[
|
||||
coreapi.Field('page', required=False, location='query', schema=coreschema.Integer(title='Page', description='A page number within the paginated result set.')),
|
||||
coreapi.Field('page_size', required=False, location='query', schema=coreschema.Integer(title='Page size', description='Number of results to return per page.')),
|
||||
coreapi.Field('ordering', required=False, location='query', schema=coreschema.String(title='Ordering', description='Which field to use when ordering the results.'))
|
||||
]
|
||||
),
|
||||
'custom_list_action': coreapi.Link(
|
||||
url='/example1/custom_list_action/',
|
||||
action='get'
|
||||
),
|
||||
'custom_list_action_multiple_methods': {
|
||||
'read': coreapi.Link(
|
||||
url='/example1/custom_list_action_multiple_methods/',
|
||||
action='get'
|
||||
)
|
||||
},
|
||||
'read': coreapi.Link(
|
||||
url='/example1/{id}/',
|
||||
action='get',
|
||||
fields=[
|
||||
coreapi.Field('id', required=True, location='path', schema=coreschema.String())
|
||||
]
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
assert schema == expected
|
||||
|
||||
|
||||
@unittest.skipUnless(coreapi, 'coreapi is not installed')
|
||||
class TestSchemaGeneratorWithRestrictedViewSets(TestCase):
|
||||
def setUp(self):
|
||||
|
|
|
@ -318,3 +318,217 @@ class TestSerializerPartialUsage:
|
|||
assert serializer.is_valid()
|
||||
assert serializer.validated_data == {}
|
||||
assert serializer.errors == {}
|
||||
|
||||
def test_allow_empty_true(self):
|
||||
class ListSerializer(serializers.Serializer):
|
||||
update_field = serializers.IntegerField()
|
||||
store_field = serializers.IntegerField()
|
||||
|
||||
instance = [
|
||||
{'update_field': 11, 'store_field': 12},
|
||||
{'update_field': 21, 'store_field': 22},
|
||||
]
|
||||
|
||||
serializer = ListSerializer(instance, data=[], partial=True, many=True)
|
||||
assert serializer.is_valid()
|
||||
assert serializer.validated_data == []
|
||||
assert serializer.errors == []
|
||||
|
||||
def test_update_allow_empty_true(self):
|
||||
class ListSerializer(serializers.Serializer):
|
||||
update_field = serializers.IntegerField()
|
||||
store_field = serializers.IntegerField()
|
||||
|
||||
instance = [
|
||||
{'update_field': 11, 'store_field': 12},
|
||||
{'update_field': 21, 'store_field': 22},
|
||||
]
|
||||
input_data = [{'update_field': 31}, {'update_field': 41}]
|
||||
updated_data_list = [
|
||||
{'update_field': 31, 'store_field': 12},
|
||||
{'update_field': 41, 'store_field': 22},
|
||||
]
|
||||
|
||||
serializer = ListSerializer(
|
||||
instance, data=input_data, partial=True, many=True)
|
||||
assert serializer.is_valid()
|
||||
|
||||
for index, data in enumerate(serializer.validated_data):
|
||||
for key, value in data.items():
|
||||
assert value == updated_data_list[index][key]
|
||||
|
||||
assert serializer.errors == []
|
||||
|
||||
def test_allow_empty_false(self):
|
||||
class ListSerializer(serializers.Serializer):
|
||||
update_field = serializers.IntegerField()
|
||||
store_field = serializers.IntegerField()
|
||||
|
||||
instance = [
|
||||
{'update_field': 11, 'store_field': 12},
|
||||
{'update_field': 21, 'store_field': 22},
|
||||
]
|
||||
|
||||
serializer = ListSerializer(
|
||||
instance, data=[], allow_empty=False, partial=True, many=True)
|
||||
assert not serializer.is_valid()
|
||||
assert serializer.validated_data == []
|
||||
assert len(serializer.errors) == 1
|
||||
assert serializer.errors['non_field_errors'][0] == 'This list may not be empty.'
|
||||
|
||||
def test_update_allow_empty_false(self):
|
||||
class ListSerializer(serializers.Serializer):
|
||||
update_field = serializers.IntegerField()
|
||||
store_field = serializers.IntegerField()
|
||||
|
||||
instance = [
|
||||
{'update_field': 11, 'store_field': 12},
|
||||
{'update_field': 21, 'store_field': 22},
|
||||
]
|
||||
input_data = [{'update_field': 31}, {'update_field': 41}]
|
||||
updated_data_list = [
|
||||
{'update_field': 31, 'store_field': 12},
|
||||
{'update_field': 41, 'store_field': 22},
|
||||
]
|
||||
|
||||
serializer = ListSerializer(
|
||||
instance, data=input_data, allow_empty=False, partial=True, many=True)
|
||||
assert serializer.is_valid()
|
||||
|
||||
for index, data in enumerate(serializer.validated_data):
|
||||
for key, value in data.items():
|
||||
assert value == updated_data_list[index][key]
|
||||
|
||||
assert serializer.errors == []
|
||||
|
||||
def test_as_field_allow_empty_true(self):
|
||||
class ListSerializer(serializers.Serializer):
|
||||
update_field = serializers.IntegerField()
|
||||
store_field = serializers.IntegerField()
|
||||
|
||||
class Serializer(serializers.Serializer):
|
||||
extra_field = serializers.IntegerField()
|
||||
list_field = ListSerializer(many=True)
|
||||
|
||||
instance = {
|
||||
'extra_field': 1,
|
||||
'list_field': [
|
||||
{'update_field': 11, 'store_field': 12},
|
||||
{'update_field': 21, 'store_field': 22},
|
||||
]
|
||||
}
|
||||
|
||||
serializer = Serializer(instance, data={}, partial=True)
|
||||
assert serializer.is_valid()
|
||||
assert serializer.validated_data == {}
|
||||
assert serializer.errors == {}
|
||||
|
||||
def test_udate_as_field_allow_empty_true(self):
|
||||
class ListSerializer(serializers.Serializer):
|
||||
update_field = serializers.IntegerField()
|
||||
store_field = serializers.IntegerField()
|
||||
|
||||
class Serializer(serializers.Serializer):
|
||||
extra_field = serializers.IntegerField()
|
||||
list_field = ListSerializer(many=True)
|
||||
|
||||
instance = {
|
||||
'extra_field': 1,
|
||||
'list_field': [
|
||||
{'update_field': 11, 'store_field': 12},
|
||||
{'update_field': 21, 'store_field': 22},
|
||||
]
|
||||
}
|
||||
input_data_1 = {'extra_field': 2}
|
||||
input_data_2 = {
|
||||
'list_field': [
|
||||
{'update_field': 31},
|
||||
{'update_field': 41},
|
||||
]
|
||||
}
|
||||
|
||||
# data_1
|
||||
serializer = Serializer(instance, data=input_data_1, partial=True)
|
||||
assert serializer.is_valid()
|
||||
assert len(serializer.validated_data) == 1
|
||||
assert serializer.validated_data['extra_field'] == 2
|
||||
assert serializer.errors == {}
|
||||
|
||||
# data_2
|
||||
serializer = Serializer(instance, data=input_data_2, partial=True)
|
||||
assert serializer.is_valid()
|
||||
|
||||
updated_data_list = [
|
||||
{'update_field': 31, 'store_field': 12},
|
||||
{'update_field': 41, 'store_field': 22},
|
||||
]
|
||||
for index, data in enumerate(serializer.validated_data['list_field']):
|
||||
for key, value in data.items():
|
||||
assert value == updated_data_list[index][key]
|
||||
|
||||
assert serializer.errors == {}
|
||||
|
||||
def test_as_field_allow_empty_false(self):
|
||||
class ListSerializer(serializers.Serializer):
|
||||
update_field = serializers.IntegerField()
|
||||
store_field = serializers.IntegerField()
|
||||
|
||||
class Serializer(serializers.Serializer):
|
||||
extra_field = serializers.IntegerField()
|
||||
list_field = ListSerializer(many=True, allow_empty=False)
|
||||
|
||||
instance = {
|
||||
'extra_field': 1,
|
||||
'list_field': [
|
||||
{'update_field': 11, 'store_field': 12},
|
||||
{'update_field': 21, 'store_field': 22},
|
||||
]
|
||||
}
|
||||
|
||||
serializer = Serializer(instance, data={}, partial=True)
|
||||
assert serializer.is_valid()
|
||||
assert serializer.validated_data == {}
|
||||
assert serializer.errors == {}
|
||||
|
||||
def test_update_as_field_allow_empty_false(self):
|
||||
class ListSerializer(serializers.Serializer):
|
||||
update_field = serializers.IntegerField()
|
||||
store_field = serializers.IntegerField()
|
||||
|
||||
class Serializer(serializers.Serializer):
|
||||
extra_field = serializers.IntegerField()
|
||||
list_field = ListSerializer(many=True, allow_empty=False)
|
||||
|
||||
instance = {
|
||||
'extra_field': 1,
|
||||
'list_field': [
|
||||
{'update_field': 11, 'store_field': 12},
|
||||
{'update_field': 21, 'store_field': 22},
|
||||
]
|
||||
}
|
||||
input_data_1 = {'extra_field': 2}
|
||||
input_data_2 = {
|
||||
'list_field': [
|
||||
{'update_field': 31},
|
||||
{'update_field': 41},
|
||||
]
|
||||
}
|
||||
updated_data_list = [
|
||||
{'update_field': 31, 'store_field': 12},
|
||||
{'update_field': 41, 'store_field': 22},
|
||||
]
|
||||
|
||||
# data_1
|
||||
serializer = Serializer(instance, data=input_data_1, partial=True)
|
||||
assert serializer.is_valid()
|
||||
assert serializer.errors == {}
|
||||
|
||||
# data_2
|
||||
serializer = Serializer(instance, data=input_data_2, partial=True)
|
||||
assert serializer.is_valid()
|
||||
|
||||
for index, data in enumerate(serializer.validated_data['list_field']):
|
||||
for key, value in data.items():
|
||||
assert value == updated_data_list[index][key]
|
||||
|
||||
assert serializer.errors == {}
|
||||
|
|
|
@ -8,7 +8,7 @@ from django.test import TestCase
|
|||
from rest_framework import status
|
||||
from rest_framework.decorators import api_view
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.settings import api_settings
|
||||
from rest_framework.settings import APISettings, api_settings
|
||||
from rest_framework.test import APIRequestFactory
|
||||
from rest_framework.views import APIView
|
||||
|
||||
|
@ -45,6 +45,19 @@ class ErrorView(APIView):
|
|||
raise Exception
|
||||
|
||||
|
||||
def custom_handler(exc, context):
|
||||
if isinstance(exc, SyntaxError):
|
||||
return Response({'error': 'SyntaxError'}, status=400)
|
||||
return Response({'error': 'UnknownError'}, status=500)
|
||||
|
||||
|
||||
class OverridenSettingsView(APIView):
|
||||
settings = APISettings({'EXCEPTION_HANDLER': custom_handler})
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
raise SyntaxError('request is invalid syntax')
|
||||
|
||||
|
||||
@api_view(['GET'])
|
||||
def error_view(request):
|
||||
raise Exception
|
||||
|
@ -118,3 +131,14 @@ class TestCustomExceptionHandler(TestCase):
|
|||
expected = 'Error!'
|
||||
assert response.status_code == status.HTTP_400_BAD_REQUEST
|
||||
assert response.data == expected
|
||||
|
||||
|
||||
class TestCustomSettings(TestCase):
|
||||
def setUp(self):
|
||||
self.view = OverridenSettingsView.as_view()
|
||||
|
||||
def test_get_exception_handler(self):
|
||||
request = factory.get('/', content_type='application/json')
|
||||
response = self.view(request)
|
||||
assert response.status_code == 400
|
||||
assert response.data == {'error': 'SyntaxError'}
|
||||
|
|
2
tox.ini
2
tox.ini
|
@ -26,7 +26,7 @@ deps =
|
|||
django18: Django>=1.8,<1.9
|
||||
django19: Django>=1.9,<1.10
|
||||
django110: Django>=1.10,<1.11
|
||||
django111: Django>=1.11rc1,<2.0
|
||||
django111: Django>=1.11,<2.0
|
||||
djangomaster: https://github.com/django/django/archive/master.tar.gz
|
||||
-rrequirements/requirements-testing.txt
|
||||
-rrequirements/requirements-optionals.txt
|
||||
|
|
Loading…
Reference in New Issue
Block a user