mirror of
https://github.com/encode/django-rest-framework.git
synced 2024-11-24 02:24:03 +03:00
Merge pull request #792 from maspwr/writable-nested-modelserializer
Writable nested modelserializer (merge in master)
This commit is contained in:
commit
eceae64804
14
docs/api-guide/authentication.md
Normal file → Executable file
14
docs/api-guide/authentication.md
Normal file → Executable file
|
@ -107,7 +107,7 @@ Unauthenticated responses that are denied permission will result in an `HTTP 401
|
||||||
|
|
||||||
WWW-Authenticate: Basic realm="api"
|
WWW-Authenticate: Basic realm="api"
|
||||||
|
|
||||||
**Note:** If you use `BasicAuthentication` in production you must ensure that your API is only available over `https` only. You should also ensure that your API clients will always re-request the username and password at login, and will never store those details to persistent storage.
|
**Note:** If you use `BasicAuthentication` in production you must ensure that your API is only available over `https`. You should also ensure that your API clients will always re-request the username and password at login, and will never store those details to persistent storage.
|
||||||
|
|
||||||
## TokenAuthentication
|
## TokenAuthentication
|
||||||
|
|
||||||
|
@ -120,6 +120,8 @@ To use the `TokenAuthentication` scheme, include `rest_framework.authtoken` in y
|
||||||
'rest_framework.authtoken'
|
'rest_framework.authtoken'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
Make sure to run `manage.py syncdb` after changing your settings.
|
||||||
|
|
||||||
You'll also need to create tokens for your users.
|
You'll also need to create tokens for your users.
|
||||||
|
|
||||||
from rest_framework.authtoken.models import Token
|
from rest_framework.authtoken.models import Token
|
||||||
|
@ -140,9 +142,13 @@ Unauthenticated responses that are denied permission will result in an `HTTP 401
|
||||||
|
|
||||||
WWW-Authenticate: Token
|
WWW-Authenticate: Token
|
||||||
|
|
||||||
|
The `curl` command line tool may be useful for testing token authenticated APIs. For example:
|
||||||
|
|
||||||
|
curl -X GET http://127.0.0.1:8000/api/example/ -H 'Authorization: Token 9944b09199c62bcf9418ad846dd0e4bbdfc6ee4b'
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
**Note:** If you use `TokenAuthentication` in production you must ensure that your API is only available over `https` only.
|
**Note:** If you use `TokenAuthentication` in production you must ensure that your API is only available over `https`.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
@ -253,7 +259,7 @@ Finally, sync your database.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
**Note:** If you use `OAuth2Authentication` in production you must ensure that your API is only available over `https` only.
|
**Note:** If you use `OAuth2Authentication` in production you must ensure that your API is only available over `https`.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
@ -294,7 +300,7 @@ The only thing needed to make the `OAuth2Authentication` class work is to insert
|
||||||
|
|
||||||
The command line to test the authentication looks like:
|
The command line to test the authentication looks like:
|
||||||
|
|
||||||
curl -H "Authorization: Bearer <your-access-token>" http://localhost:8000/api/?client_id=YOUR_CLIENT_ID\&client_secret=YOUR_CLIENT_SECRET
|
curl -H "Authorization: Bearer <your-access-token>" http://localhost:8000/api/
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
|
@ -197,12 +197,16 @@ If you want to override this behavior, you'll need to declare the `DateTimeField
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Comment
|
model = Comment
|
||||||
|
|
||||||
|
Note that by default, datetime representations are deteremined by the renderer in use, although this can be explicitly overridden as detailed below.
|
||||||
|
|
||||||
|
In the case of JSON this means the default datetime representation uses the [ECMA 262 date time string specification][ecma262]. This is a subset of ISO 8601 which uses millisecond precision, and includes the 'Z' suffix for the UTC timezone, for example: `2013-01-29T12:34:56.123Z`.
|
||||||
|
|
||||||
**Signature:** `DateTimeField(format=None, input_formats=None)`
|
**Signature:** `DateTimeField(format=None, input_formats=None)`
|
||||||
|
|
||||||
* `format` - A string representing the output format. If not specified, this defaults to `None`, which indicates that python `datetime` objects should be returned by `to_native`. In this case the datetime encoding will be determined by the renderer.
|
* `format` - A string representing the output format. If not specified, this defaults to `None`, which indicates that python `datetime` objects should be returned by `to_native`. In this case the datetime encoding will be determined by the renderer.
|
||||||
* `input_formats` - A list of strings representing the input formats which may be used to parse the date. If not specified, the `DATETIME_INPUT_FORMATS` setting will be used, which defaults to `['iso-8601']`.
|
* `input_formats` - A list of strings representing the input formats which may be used to parse the date. If not specified, the `DATETIME_INPUT_FORMATS` setting will be used, which defaults to `['iso-8601']`.
|
||||||
|
|
||||||
DateTime format strings may either be [python strftime formats][strftime] which explicitly specifiy the format, or the special string `'iso-8601'`, which indicates that [ISO 8601][iso8601] style datetimes should be used. (eg `'2013-01-29T12:34:56.000000'`)
|
DateTime format strings may either be [python strftime formats][strftime] which explicitly specifiy the format, or the special string `'iso-8601'`, which indicates that [ISO 8601][iso8601] style datetimes should be used. (eg `'2013-01-29T12:34:56.000000Z'`)
|
||||||
|
|
||||||
## DateField
|
## DateField
|
||||||
|
|
||||||
|
@ -318,5 +322,6 @@ As an example, let's create a field that can be used represent the class name of
|
||||||
|
|
||||||
[cite]: https://docs.djangoproject.com/en/dev/ref/forms/api/#django.forms.Form.cleaned_data
|
[cite]: https://docs.djangoproject.com/en/dev/ref/forms/api/#django.forms.Form.cleaned_data
|
||||||
[FILE_UPLOAD_HANDLERS]: https://docs.djangoproject.com/en/dev/ref/settings/#std:setting-FILE_UPLOAD_HANDLERS
|
[FILE_UPLOAD_HANDLERS]: https://docs.djangoproject.com/en/dev/ref/settings/#std:setting-FILE_UPLOAD_HANDLERS
|
||||||
|
[ecma262]: http://ecma-international.org/ecma-262/5.1/#sec-15.9.1.15
|
||||||
[strftime]: http://docs.python.org/2/library/datetime.html#strftime-and-strptime-behavior
|
[strftime]: http://docs.python.org/2/library/datetime.html#strftime-and-strptime-behavior
|
||||||
[iso8601]: http://www.w3.org/TR/NOTE-datetime
|
[iso8601]: http://www.w3.org/TR/NOTE-datetime
|
||||||
|
|
2
docs/api-guide/generic-views.md
Normal file → Executable file
2
docs/api-guide/generic-views.md
Normal file → Executable file
|
@ -200,7 +200,7 @@ Should be mixed in with any [GenericAPIView].
|
||||||
|
|
||||||
Provides a `.retrieve(request, *args, **kwargs)` method, that implements returning an existing model instance in a response.
|
Provides a `.retrieve(request, *args, **kwargs)` method, that implements returning an existing model instance in a response.
|
||||||
|
|
||||||
If an object can be retrieve this returns a `200 OK` response, with a serialized representation of the object as the body of the response. Otherwise it will return a `404 Not Found`.
|
If an object can be retrieved this returns a `200 OK` response, with a serialized representation of the object as the body of the response. Otherwise it will return a `404 Not Found`.
|
||||||
|
|
||||||
Should be mixed in with [SingleObjectAPIView].
|
Should be mixed in with [SingleObjectAPIView].
|
||||||
|
|
||||||
|
|
12
docs/api-guide/serializers.md
Normal file → Executable file
12
docs/api-guide/serializers.md
Normal file → Executable file
|
@ -242,21 +242,21 @@ This allows you to write views that update or create multiple items when a `PUT`
|
||||||
# True
|
# True
|
||||||
serialize.save() # `.save()` will be called on each updated or newly created instance.
|
serialize.save() # `.save()` will be called on each updated or newly created instance.
|
||||||
|
|
||||||
Bulk updates will update any instances that already exist, and create new instances for data items that do not have a corresponding instance.
|
By default bulk updates will be limited to updating instances that already exist in the provided queryset.
|
||||||
|
|
||||||
When performing a bulk update you may want any items that are not present in the incoming data to be deleted. To do so, pass `allow_add_remove=True` to the serializer.
|
When performing a bulk update you may want to allow new items to be created, and missing items to be deleted. To do so, pass `allow_add_remove=True` to the serializer.
|
||||||
|
|
||||||
serializer = BookSerializer(queryset, data=data, many=True, allow_add_remove=True)
|
serializer = BookSerializer(queryset, data=data, many=True, allow_add_remove=True)
|
||||||
serializer.is_valid()
|
serializer.is_valid()
|
||||||
# True
|
# True
|
||||||
serializer.save() # `.save()` will be called on each updated or newly created instance.
|
serializer.save() # `.save()` will be called on updated or newly created instances.
|
||||||
# `.delete()` will be called on any other items in the `queryset`.
|
# `.delete()` will be called on any other items in the `queryset`.
|
||||||
|
|
||||||
Passing `allow_add_remove=True` ensures that any update operations will completely overwrite the existing queryset, rather than simply updating any objects found in the incoming data.
|
Passing `allow_add_remove=True` ensures that any update operations will completely overwrite the existing queryset, rather than simply updating existing objects.
|
||||||
|
|
||||||
#### How identity is determined when performing bulk updates
|
#### How identity is determined when performing bulk updates
|
||||||
|
|
||||||
Performing a bulk update is slightly more complicated than performing a bulk creation, because the serializer needs a way of determining how the items in the incoming data should be matched against the existing object instances.
|
Performing a bulk update is slightly more complicated than performing a bulk creation, because the serializer needs a way to determine how the items in the incoming data should be matched against the existing object instances.
|
||||||
|
|
||||||
By default the serializer class will use the `id` key on the incoming data to determine the canonical identity of an object. If you need to change this behavior you should override the `get_identity` method on the `Serializer` class. For example:
|
By default the serializer class will use the `id` key on the incoming data to determine the canonical identity of an object. If you need to change this behavior you should override the `get_identity` method on the `Serializer` class. For example:
|
||||||
|
|
||||||
|
@ -353,7 +353,7 @@ The `depth` option should be set to an integer value that indicates the depth of
|
||||||
|
|
||||||
## Specifying which fields should be read-only
|
## Specifying which fields should be read-only
|
||||||
|
|
||||||
You may wish to specify multiple fields as read-only. Instead of adding each field explicitely with the `read_only=True` attribute, you may use the `read_only_fields` Meta option, like so:
|
You may wish to specify multiple fields as read-only. Instead of adding each field explicitly with the `read_only=True` attribute, you may use the `read_only_fields` Meta option, like so:
|
||||||
|
|
||||||
class AccountSerializer(serializers.ModelSerializer):
|
class AccountSerializer(serializers.ModelSerializer):
|
||||||
class Meta:
|
class Meta:
|
||||||
|
|
|
@ -277,3 +277,14 @@ footer a {
|
||||||
footer a:hover {
|
footer a:hover {
|
||||||
color: gray;
|
color: gray;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.btn-inverse {
|
||||||
|
background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#606060), to(#404040)) !important;
|
||||||
|
background-image: -webkit-linear-gradient(top, #606060, #404040) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-open .modal,.btn:focus{outline:none;}
|
||||||
|
|
||||||
|
@media (max-width: 650px) {
|
||||||
|
.repo-link.btn-inverse {display: none;}
|
||||||
|
}
|
||||||
|
|
|
@ -9,9 +9,9 @@
|
||||||
|
|
||||||
# Django REST framework
|
# Django REST framework
|
||||||
|
|
||||||
**Web APIs for Django, made easy.**
|
**Awesome web-browseable Web APIs.**
|
||||||
|
|
||||||
Django REST framework is a flexible, powerful library that makes it incredibly easy to build Web APIs. It is designed as a modular and easy to customize architecture, based on Django's class based views.
|
Django REST framework is a flexible, powerful Web API toolkit. It is designed as a modular and easy to customize architecture, based on Django's class based views.
|
||||||
|
|
||||||
APIs built using REST framework are fully self-describing and web browseable - a huge useability win for your developers. It also supports a wide range of media types, authentication and permission policies out of the box.
|
APIs built using REST framework are fully self-describing and web browseable - a huge useability win for your developers. It also supports a wide range of media types, authentication and permission policies out of the box.
|
||||||
|
|
||||||
|
|
|
@ -41,6 +41,9 @@
|
||||||
<div class="navbar-inner">
|
<div class="navbar-inner">
|
||||||
<div class="container-fluid">
|
<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/tomchristie/django-rest-framework/tree/master">GitHub</a>
|
||||||
|
<a class="repo-link btn btn-inverse btn-small {{ next_url_disabled }}" href="{{ next_url }}">Next <i class="icon-arrow-right icon-white"></i></a>
|
||||||
|
<a class="repo-link btn btn-inverse btn-small {{ prev_url_disabled }}" href="{{ prev_url }}"><i class="icon-arrow-left icon-white"></i> Previous</a>
|
||||||
|
<a class="repo-link btn btn-inverse btn-small" href="#searchModal" data-toggle="modal"><i class="icon-search icon-white"></i> Search</a>
|
||||||
<a class="btn btn-navbar" data-toggle="collapse" data-target=".nav-collapse">
|
<a class="btn btn-navbar" data-toggle="collapse" data-target=".nav-collapse">
|
||||||
<span class="icon-bar"></span>
|
<span class="icon-bar"></span>
|
||||||
<span class="icon-bar"></span>
|
<span class="icon-bar"></span>
|
||||||
|
@ -118,6 +121,34 @@
|
||||||
|
|
||||||
<div class="body-content">
|
<div class="body-content">
|
||||||
<div class="container-fluid">
|
<div class="container-fluid">
|
||||||
|
|
||||||
|
<!-- Search Modal -->
|
||||||
|
<div id="searchModal" class="modal hide fade" tabindex="-1" role="dialog" aria-labelledby="myModalLabel" aria-hidden="true">
|
||||||
|
<div class="modal-header">
|
||||||
|
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button>
|
||||||
|
<h3 id="myModalLabel">Documentation search</h3>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<!-- Custom google search -->
|
||||||
|
<script>
|
||||||
|
(function() {
|
||||||
|
var cx = '015016005043623903336:rxraeohqk6w';
|
||||||
|
var gcse = document.createElement('script');
|
||||||
|
gcse.type = 'text/javascript';
|
||||||
|
gcse.async = true;
|
||||||
|
gcse.src = (document.location.protocol == 'https:' ? 'https:' : 'http:') +
|
||||||
|
'//www.google.com/cse/cse.js?cx=' + cx;
|
||||||
|
var s = document.getElementsByTagName('script')[0];
|
||||||
|
s.parentNode.insertBefore(gcse, s);
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
<gcse:search></gcse:search>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button class="btn" data-dismiss="modal" aria-hidden="true">Close</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="row-fluid">
|
<div class="row-fluid">
|
||||||
|
|
||||||
<div class="span3">
|
<div class="span3">
|
||||||
|
|
|
@ -18,7 +18,7 @@ When answering questions make sure to help future contributors find their way ar
|
||||||
|
|
||||||
# Issues
|
# Issues
|
||||||
|
|
||||||
Usage questions should be directed to the [discussion group][google-group]. Feature requests, bug reports and other issues should be raised on the GitHub [issue tracker][issues].
|
It's really helpful if you make sure you address issues to the correct channel. Usage questions should be directed to the [discussion group][google-group]. Feature requests, bug reports and other issues should be raised on the GitHub [issue tracker][issues].
|
||||||
|
|
||||||
Some tips on good issue reporting:
|
Some tips on good issue reporting:
|
||||||
|
|
||||||
|
|
|
@ -112,6 +112,10 @@ The following people have helped make REST framework great.
|
||||||
* Bouke Haarsma - [bouke]
|
* Bouke Haarsma - [bouke]
|
||||||
* Pierre Dulac - [dulaccc]
|
* Pierre Dulac - [dulaccc]
|
||||||
* Dave Kuhn - [kuhnza]
|
* Dave Kuhn - [kuhnza]
|
||||||
|
* Sitong Peng - [stoneg]
|
||||||
|
* Victor Shih - [vshih]
|
||||||
|
* Atle Frenvik Sveen - [atlefren]
|
||||||
|
* J. Paul Reed - [preed]
|
||||||
|
|
||||||
Many thanks to everyone who's contributed to the project.
|
Many thanks to everyone who's contributed to the project.
|
||||||
|
|
||||||
|
@ -258,3 +262,7 @@ You can also contact [@_tomchristie][twitter] directly on twitter.
|
||||||
[bouke]: https://github.com/bouke
|
[bouke]: https://github.com/bouke
|
||||||
[dulaccc]: https://github.com/dulaccc
|
[dulaccc]: https://github.com/dulaccc
|
||||||
[kuhnza]: https://github.com/kuhnza
|
[kuhnza]: https://github.com/kuhnza
|
||||||
|
[stoneg]: https://github.com/stoneg
|
||||||
|
[vshih]: https://github.com/vshih
|
||||||
|
[atlefren]: https://github.com/atlefren
|
||||||
|
[preed]: https://github.com/preed
|
||||||
|
|
|
@ -40,8 +40,22 @@ You can determine your currently installed version using `pip freeze`:
|
||||||
|
|
||||||
## 2.2.x series
|
## 2.2.x series
|
||||||
|
|
||||||
### Master
|
### 2.2.6
|
||||||
|
|
||||||
|
**Date**: 4th April 2013
|
||||||
|
|
||||||
|
* OAuth2 authentication no longer requires unneccessary URL parameters in addition to the token.
|
||||||
|
* URL hyperlinking in browseable API now handles more cases correctly.
|
||||||
|
* Long HTTP headers in browsable API are broken in multiple lines when possible.
|
||||||
|
* Bugfix: Fix regression with DjangoFilterBackend not worthing correctly with single object views.
|
||||||
|
* Bugfix: OAuth should fail hard when invalid token used.
|
||||||
|
* Bugfix: Fix serializer potentially returning `None` object for models that define `__bool__` or `__len__`.
|
||||||
|
|
||||||
|
### 2.2.5
|
||||||
|
|
||||||
|
**Date**: 26th March 2013
|
||||||
|
|
||||||
|
* Serializer support for bulk create and bulk update operations.
|
||||||
* Regression fix: Date and time fields return date/time objects by default. Fixes regressions caused by 2.2.2. See [#743][743] for more details.
|
* Regression fix: Date and time fields return date/time objects by default. Fixes regressions caused by 2.2.2. See [#743][743] for more details.
|
||||||
* Bugfix: Fix 500 error is OAuth not attempted with OAuthAuthentication class installed.
|
* Bugfix: Fix 500 error is OAuth not attempted with OAuthAuthentication class installed.
|
||||||
* `Serializer.save()` now supports arbitrary keyword args which are passed through to the object `.save()` method. Mixins use `force_insert` and `force_update` where appropriate, resulting in one less database query.
|
* `Serializer.save()` now supports arbitrary keyword args which are passed through to the object `.save()` method. Mixins use `force_insert` and `force_update` where appropriate, resulting in one less database query.
|
||||||
|
|
|
@ -179,7 +179,7 @@ Now, if you open a browser again, you find that the 'DELETE' and 'PUT' actions o
|
||||||
|
|
||||||
## Authenticating with the API
|
## Authenticating with the API
|
||||||
|
|
||||||
Because we now have a set of permissions on the API, we need to authenticate our requests to it if we want to edit any snippets. We havn't set up any [authentication classes][authentication], so the defaults are currently applied, which are `SessionAuthentication` and `BasicAuthentication`.
|
Because we now have a set of permissions on the API, we need to authenticate our requests to it if we want to edit any snippets. We haven't set up any [authentication classes][authentication], so the defaults are currently applied, which are `SessionAuthentication` and `BasicAuthentication`.
|
||||||
|
|
||||||
When we interact with the API through the web browser, we can login, and the browser session will then provide the required authentication for the requests.
|
When we interact with the API through the web browser, we can login, and the browser session will then provide the required authentication for the requests.
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
# Tutorial 5 - Relationships & Hyperlinked APIs
|
# Tutorial 5: Relationships & Hyperlinked APIs
|
||||||
|
|
||||||
At the moment relationships within our API are represented by using primary keys. In this part of the tutorial we'll improve the cohesion and discoverability of our API, by instead using hyperlinking for relationships.
|
At the moment relationships within our API are represented by using primary keys. In this part of the tutorial we'll improve the cohesion and discoverability of our API, by instead using hyperlinking for relationships.
|
||||||
|
|
||||||
|
|
75
mkdocs.py
75
mkdocs.py
|
@ -37,6 +37,60 @@ page = open(os.path.join(docs_dir, 'template.html'), 'r').read()
|
||||||
# shutil.rmtree(target)
|
# shutil.rmtree(target)
|
||||||
# shutil.copytree(source, target)
|
# shutil.copytree(source, target)
|
||||||
|
|
||||||
|
|
||||||
|
# Hacky, but what the hell, it'll do the job
|
||||||
|
path_list = [
|
||||||
|
'index.md',
|
||||||
|
'tutorial/quickstart.md',
|
||||||
|
'tutorial/1-serialization.md',
|
||||||
|
'tutorial/2-requests-and-responses.md',
|
||||||
|
'tutorial/3-class-based-views.md',
|
||||||
|
'tutorial/4-authentication-and-permissions.md',
|
||||||
|
'tutorial/5-relationships-and-hyperlinked-apis.md',
|
||||||
|
'api-guide/requests.md',
|
||||||
|
'api-guide/responses.md',
|
||||||
|
'api-guide/views.md',
|
||||||
|
'api-guide/generic-views.md',
|
||||||
|
'api-guide/parsers.md',
|
||||||
|
'api-guide/renderers.md',
|
||||||
|
'api-guide/serializers.md',
|
||||||
|
'api-guide/fields.md',
|
||||||
|
'api-guide/relations.md',
|
||||||
|
'api-guide/authentication.md',
|
||||||
|
'api-guide/permissions.md',
|
||||||
|
'api-guide/throttling.md',
|
||||||
|
'api-guide/filtering.md',
|
||||||
|
'api-guide/pagination.md',
|
||||||
|
'api-guide/content-negotiation.md',
|
||||||
|
'api-guide/format-suffixes.md',
|
||||||
|
'api-guide/reverse.md',
|
||||||
|
'api-guide/exceptions.md',
|
||||||
|
'api-guide/status-codes.md',
|
||||||
|
'api-guide/settings.md',
|
||||||
|
'topics/ajax-csrf-cors.md',
|
||||||
|
'topics/browser-enhancements.md',
|
||||||
|
'topics/browsable-api.md',
|
||||||
|
'topics/rest-hypermedia-hateoas.md',
|
||||||
|
'topics/contributing.md',
|
||||||
|
'topics/rest-framework-2-announcement.md',
|
||||||
|
'topics/2.2-announcement.md',
|
||||||
|
'topics/release-notes.md',
|
||||||
|
'topics/credits.md',
|
||||||
|
]
|
||||||
|
|
||||||
|
prev_url_map = {}
|
||||||
|
next_url_map = {}
|
||||||
|
for idx in range(len(path_list)):
|
||||||
|
path = path_list[idx]
|
||||||
|
rel = '../' * path.count('/')
|
||||||
|
|
||||||
|
if idx > 0:
|
||||||
|
prev_url_map[path] = rel + path_list[idx - 1][:-3] + suffix
|
||||||
|
|
||||||
|
if idx < len(path_list) - 1:
|
||||||
|
next_url_map[path] = rel + path_list[idx + 1][:-3] + suffix
|
||||||
|
|
||||||
|
|
||||||
for (dirpath, dirnames, filenames) in os.walk(docs_dir):
|
for (dirpath, dirnames, filenames) in os.walk(docs_dir):
|
||||||
relative_dir = dirpath.replace(docs_dir, '').lstrip(os.path.sep)
|
relative_dir = dirpath.replace(docs_dir, '').lstrip(os.path.sep)
|
||||||
build_dir = os.path.join(html_dir, relative_dir)
|
build_dir = os.path.join(html_dir, relative_dir)
|
||||||
|
@ -46,6 +100,7 @@ for (dirpath, dirnames, filenames) in os.walk(docs_dir):
|
||||||
|
|
||||||
for filename in filenames:
|
for filename in filenames:
|
||||||
path = os.path.join(dirpath, filename)
|
path = os.path.join(dirpath, filename)
|
||||||
|
relative_path = os.path.join(relative_dir, filename)
|
||||||
|
|
||||||
if not filename.endswith('.md'):
|
if not filename.endswith('.md'):
|
||||||
if relative_dir:
|
if relative_dir:
|
||||||
|
@ -78,16 +133,34 @@ for (dirpath, dirnames, filenames) in os.walk(docs_dir):
|
||||||
toc += template + '\n'
|
toc += template + '\n'
|
||||||
|
|
||||||
if filename == 'index.md':
|
if filename == 'index.md':
|
||||||
main_title = 'Django REST framework - APIs made easy'
|
main_title = 'Django REST framework - Web Browseable APIs'
|
||||||
else:
|
else:
|
||||||
main_title = 'Django REST framework - ' + main_title
|
main_title = 'Django REST framework - ' + main_title
|
||||||
|
|
||||||
|
prev_url = prev_url_map.get(relative_path)
|
||||||
|
next_url = next_url_map.get(relative_path)
|
||||||
|
|
||||||
content = markdown.markdown(text, ['headerid'])
|
content = markdown.markdown(text, ['headerid'])
|
||||||
|
|
||||||
output = page.replace('{{ content }}', content).replace('{{ toc }}', toc).replace('{{ base_url }}', base_url).replace('{{ suffix }}', suffix).replace('{{ index }}', index)
|
output = page.replace('{{ content }}', content).replace('{{ toc }}', toc).replace('{{ base_url }}', base_url).replace('{{ suffix }}', suffix).replace('{{ index }}', index)
|
||||||
output = output.replace('{{ title }}', main_title)
|
output = output.replace('{{ title }}', main_title)
|
||||||
output = output.replace('{{ description }}', description)
|
output = output.replace('{{ description }}', description)
|
||||||
output = output.replace('{{ page_id }}', filename[:-3])
|
output = output.replace('{{ page_id }}', filename[:-3])
|
||||||
|
|
||||||
|
if prev_url:
|
||||||
|
output = output.replace('{{ prev_url }}', prev_url)
|
||||||
|
output = output.replace('{{ prev_url_disabled }}', '')
|
||||||
|
else:
|
||||||
|
output = output.replace('{{ prev_url }}', '#')
|
||||||
|
output = output.replace('{{ prev_url_disabled }}', 'disabled')
|
||||||
|
|
||||||
|
if next_url:
|
||||||
|
output = output.replace('{{ next_url }}', next_url)
|
||||||
|
output = output.replace('{{ next_url_disabled }}', '')
|
||||||
|
else:
|
||||||
|
output = output.replace('{{ next_url }}', '#')
|
||||||
|
output = output.replace('{{ next_url_disabled }}', 'disabled')
|
||||||
|
|
||||||
output = re.sub(r'a href="([^"]*)\.md"', r'a href="\1%s"' % suffix, output)
|
output = re.sub(r'a href="([^"]*)\.md"', r'a href="\1%s"' % suffix, output)
|
||||||
output = re.sub(r'<pre><code>:::bash', r'<pre class="prettyprint lang-bsh">', output)
|
output = re.sub(r'<pre><code>:::bash', r'<pre class="prettyprint lang-bsh">', output)
|
||||||
output = re.sub(r'<pre>', r'<pre class="prettyprint lang-py">', output)
|
output = re.sub(r'<pre>', r'<pre class="prettyprint lang-py">', output)
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
__version__ = '2.2.4'
|
__version__ = '2.2.6'
|
||||||
|
|
||||||
VERSION = __version__ # synonym
|
VERSION = __version__ # synonym
|
||||||
|
|
||||||
|
|
|
@ -2,14 +2,16 @@
|
||||||
Provides a set of pluggable authentication policies.
|
Provides a set of pluggable authentication policies.
|
||||||
"""
|
"""
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
import base64
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
from django.contrib.auth import authenticate
|
from django.contrib.auth import authenticate
|
||||||
from django.core.exceptions import ImproperlyConfigured
|
from django.core.exceptions import ImproperlyConfigured
|
||||||
from rest_framework import exceptions, HTTP_HEADER_ENCODING
|
from rest_framework import exceptions, HTTP_HEADER_ENCODING
|
||||||
from rest_framework.compat import CsrfViewMiddleware
|
from rest_framework.compat import CsrfViewMiddleware
|
||||||
from rest_framework.compat import oauth, oauth_provider, oauth_provider_store
|
from rest_framework.compat import oauth, oauth_provider, oauth_provider_store
|
||||||
from rest_framework.compat import oauth2_provider, oauth2_provider_forms, oauth2_provider_backends
|
from rest_framework.compat import oauth2_provider
|
||||||
from rest_framework.authtoken.models import Token
|
from rest_framework.authtoken.models import Token
|
||||||
import base64
|
|
||||||
|
|
||||||
|
|
||||||
def get_authorization_header(request):
|
def get_authorization_header(request):
|
||||||
|
@ -228,7 +230,7 @@ class OAuthAuthentication(BaseAuthentication):
|
||||||
try:
|
try:
|
||||||
consumer_key = oauth_request.get_parameter('oauth_consumer_key')
|
consumer_key = oauth_request.get_parameter('oauth_consumer_key')
|
||||||
consumer = oauth_provider_store.get_consumer(request, oauth_request, consumer_key)
|
consumer = oauth_provider_store.get_consumer(request, oauth_request, consumer_key)
|
||||||
except oauth_provider_store.InvalidConsumerError as err:
|
except oauth_provider.store.InvalidConsumerError as err:
|
||||||
raise exceptions.AuthenticationFailed(err)
|
raise exceptions.AuthenticationFailed(err)
|
||||||
|
|
||||||
if consumer.status != oauth_provider.consts.ACCEPTED:
|
if consumer.status != oauth_provider.consts.ACCEPTED:
|
||||||
|
@ -238,7 +240,7 @@ class OAuthAuthentication(BaseAuthentication):
|
||||||
try:
|
try:
|
||||||
token_param = oauth_request.get_parameter('oauth_token')
|
token_param = oauth_request.get_parameter('oauth_token')
|
||||||
token = oauth_provider_store.get_access_token(request, oauth_request, consumer, token_param)
|
token = oauth_provider_store.get_access_token(request, oauth_request, consumer, token_param)
|
||||||
except oauth_provider_store.InvalidTokenError:
|
except oauth_provider.store.InvalidTokenError:
|
||||||
msg = 'Invalid access token: %s' % oauth_request.get_parameter('oauth_token')
|
msg = 'Invalid access token: %s' % oauth_request.get_parameter('oauth_token')
|
||||||
raise exceptions.AuthenticationFailed(msg)
|
raise exceptions.AuthenticationFailed(msg)
|
||||||
|
|
||||||
|
@ -315,16 +317,12 @@ class OAuth2Authentication(BaseAuthentication):
|
||||||
Authenticate the request, given the access token.
|
Authenticate the request, given the access token.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# Authenticate the client
|
try:
|
||||||
oauth2_client_form = oauth2_provider_forms.ClientAuthForm(request.REQUEST)
|
token = oauth2_provider.models.AccessToken.objects.select_related('user')
|
||||||
if not oauth2_client_form.is_valid():
|
# TODO: Change to timezone aware datetime when oauth2_provider add
|
||||||
raise exceptions.AuthenticationFailed('Client could not be validated')
|
# support to it.
|
||||||
client = oauth2_client_form.cleaned_data.get('client')
|
token = token.get(token=access_token, expires__gt=datetime.now())
|
||||||
|
except oauth2_provider.models.AccessToken.DoesNotExist:
|
||||||
# Retrieve the `OAuth2AccessToken` instance from the access_token
|
|
||||||
auth_backend = oauth2_provider_backends.AccessTokenBackend()
|
|
||||||
token = auth_backend.authenticate(access_token, client)
|
|
||||||
if token is None:
|
|
||||||
raise exceptions.AuthenticationFailed('Invalid token')
|
raise exceptions.AuthenticationFailed('Invalid token')
|
||||||
|
|
||||||
user = token.user
|
user = token.user
|
||||||
|
@ -333,7 +331,7 @@ class OAuth2Authentication(BaseAuthentication):
|
||||||
msg = 'User inactive or deleted: %s' % user.username
|
msg = 'User inactive or deleted: %s' % user.username
|
||||||
raise exceptions.AuthenticationFailed(msg)
|
raise exceptions.AuthenticationFailed(msg)
|
||||||
|
|
||||||
return (token.user, token)
|
return (user, token)
|
||||||
|
|
||||||
def authenticate_header(self, request):
|
def authenticate_header(self, request):
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -395,6 +395,41 @@ except ImportError:
|
||||||
kw = dict((k, int(v)) for k, v in kw.iteritems() if v is not None)
|
kw = dict((k, int(v)) for k, v in kw.iteritems() if v is not None)
|
||||||
return datetime.datetime(**kw)
|
return datetime.datetime(**kw)
|
||||||
|
|
||||||
|
|
||||||
|
# smart_urlquote is new on Django 1.4
|
||||||
|
try:
|
||||||
|
from django.utils.html import smart_urlquote
|
||||||
|
except ImportError:
|
||||||
|
import re
|
||||||
|
from django.utils.encoding import smart_str
|
||||||
|
try:
|
||||||
|
from urllib.parse import quote, urlsplit, urlunsplit
|
||||||
|
except ImportError: # Python 2
|
||||||
|
from urllib import quote
|
||||||
|
from urlparse import urlsplit, urlunsplit
|
||||||
|
|
||||||
|
unquoted_percents_re = re.compile(r'%(?![0-9A-Fa-f]{2})')
|
||||||
|
|
||||||
|
def smart_urlquote(url):
|
||||||
|
"Quotes a URL if it isn't already quoted."
|
||||||
|
# Handle IDN before quoting.
|
||||||
|
scheme, netloc, path, query, fragment = urlsplit(url)
|
||||||
|
try:
|
||||||
|
netloc = netloc.encode('idna').decode('ascii') # IDN -> ACE
|
||||||
|
except UnicodeError: # invalid domain part
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
url = urlunsplit((scheme, netloc, path, query, fragment))
|
||||||
|
|
||||||
|
# An URL is considered unquoted if it contains no % characters or
|
||||||
|
# contains a % not followed by two hexadecimal digits. See #9655.
|
||||||
|
if '%' not in url or unquoted_percents_re.search(url):
|
||||||
|
# See http://bugs.python.org/issue2637
|
||||||
|
url = quote(smart_str(url), safe=b'!*\'();:@&=+$,/?#[]~')
|
||||||
|
|
||||||
|
return force_text(url)
|
||||||
|
|
||||||
|
|
||||||
# Markdown is optional
|
# Markdown is optional
|
||||||
try:
|
try:
|
||||||
import markdown
|
import markdown
|
||||||
|
@ -445,14 +480,12 @@ except ImportError:
|
||||||
# OAuth 2 support is optional
|
# OAuth 2 support is optional
|
||||||
try:
|
try:
|
||||||
import provider.oauth2 as oauth2_provider
|
import provider.oauth2 as oauth2_provider
|
||||||
from provider.oauth2 import backends as oauth2_provider_backends
|
|
||||||
from provider.oauth2 import models as oauth2_provider_models
|
from provider.oauth2 import models as oauth2_provider_models
|
||||||
from provider.oauth2 import forms as oauth2_provider_forms
|
from provider.oauth2 import forms as oauth2_provider_forms
|
||||||
from provider import scope as oauth2_provider_scope
|
from provider import scope as oauth2_provider_scope
|
||||||
from provider import constants as oauth2_constants
|
from provider import constants as oauth2_constants
|
||||||
except ImportError:
|
except ImportError:
|
||||||
oauth2_provider = None
|
oauth2_provider = None
|
||||||
oauth2_provider_backends = None
|
|
||||||
oauth2_provider_models = None
|
oauth2_provider_models = None
|
||||||
oauth2_provider_forms = None
|
oauth2_provider_forms = None
|
||||||
oauth2_provider_scope = None
|
oauth2_provider_scope = None
|
||||||
|
|
|
@ -55,6 +55,6 @@ class DjangoFilterBackend(BaseFilterBackend):
|
||||||
filter_class = self.get_filter_class(view)
|
filter_class = self.get_filter_class(view)
|
||||||
|
|
||||||
if filter_class:
|
if filter_class:
|
||||||
return filter_class(request.QUERY_PARAMS, queryset=queryset)
|
return filter_class(request.QUERY_PARAMS, queryset=queryset).qs
|
||||||
|
|
||||||
return queryset
|
return queryset
|
||||||
|
|
|
@ -130,6 +130,7 @@ class SingleObjectAPIView(SingleObjectMixin, GenericAPIView):
|
||||||
"""
|
"""
|
||||||
Override default to add support for object-level permissions.
|
Override default to add support for object-level permissions.
|
||||||
"""
|
"""
|
||||||
|
queryset = self.filter_queryset(self.get_queryset())
|
||||||
obj = super(SingleObjectAPIView, self).get_object(queryset)
|
obj = super(SingleObjectAPIView, self).get_object(queryset)
|
||||||
self.check_object_permissions(self.request, obj)
|
self.check_object_permissions(self.request, obj)
|
||||||
return obj
|
return obj
|
||||||
|
|
|
@ -97,9 +97,7 @@ class RetrieveModelMixin(object):
|
||||||
Should be mixed in with `SingleObjectAPIView`.
|
Should be mixed in with `SingleObjectAPIView`.
|
||||||
"""
|
"""
|
||||||
def retrieve(self, request, *args, **kwargs):
|
def retrieve(self, request, *args, **kwargs):
|
||||||
queryset = self.get_queryset()
|
self.object = self.get_object()
|
||||||
filtered_queryset = self.filter_queryset(queryset)
|
|
||||||
self.object = self.get_object(filtered_queryset)
|
|
||||||
serializer = self.get_serializer(self.object)
|
serializer = self.get_serializer(self.object)
|
||||||
return Response(serializer.data)
|
return Response(serializer.data)
|
||||||
|
|
||||||
|
|
|
@ -457,6 +457,10 @@ class BaseSerializer(WritableField):
|
||||||
# Determine which object we're updating
|
# Determine which object we're updating
|
||||||
identity = self.get_identity(item)
|
identity = self.get_identity(item)
|
||||||
self.object = identity_to_objects.pop(identity, None)
|
self.object = identity_to_objects.pop(identity, None)
|
||||||
|
if self.object is None and not self.allow_add_remove:
|
||||||
|
ret.append(None)
|
||||||
|
errors.append({'non_field_errors': ['Cannot create a new item, only existing items may be updated.']})
|
||||||
|
continue
|
||||||
|
|
||||||
ret.append(self.from_native(item, None))
|
ret.append(self.from_native(item, None))
|
||||||
errors.append(self._errors)
|
errors.append(self._errors)
|
||||||
|
@ -466,7 +470,7 @@ class BaseSerializer(WritableField):
|
||||||
|
|
||||||
self._errors = any(errors) and errors or []
|
self._errors = any(errors) and errors or []
|
||||||
else:
|
else:
|
||||||
self._errors = {'non_field_errors': ['Expected a list of items']}
|
self._errors = {'non_field_errors': ['Expected a list of items.']}
|
||||||
else:
|
else:
|
||||||
ret = self.from_native(data, files)
|
ret = self.from_native(data, files)
|
||||||
|
|
||||||
|
@ -766,7 +770,7 @@ class ModelSerializer(Serializer):
|
||||||
Override the default method to also include model field validation.
|
Override the default method to also include model field validation.
|
||||||
"""
|
"""
|
||||||
instance = super(ModelSerializer, self).from_native(data, files)
|
instance = super(ModelSerializer, self).from_native(data, files)
|
||||||
if instance:
|
if not self._errors:
|
||||||
return self.full_clean(instance)
|
return self.full_clean(instance)
|
||||||
|
|
||||||
def save_object(self, obj, **kwargs):
|
def save_object(self, obj, **kwargs):
|
||||||
|
|
|
@ -115,7 +115,7 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="response-info">
|
<div class="response-info">
|
||||||
<pre class="prettyprint"><div class="meta nocode"><b>HTTP {{ response.status_code }} {{ response.status_text }}</b>{% autoescape off %}
|
<pre class="prettyprint"><div class="meta nocode"><b>HTTP {{ response.status_code }} {{ response.status_text }}</b>{% autoescape off %}
|
||||||
{% for key, val in response.items %}<b>{{ key }}:</b> <span class="lit">{{ val|urlize_quoted_links }}</span>
|
{% for key, val in response.items %}<b>{{ key }}:</b> <span class="lit">{{ val|break_long_headers|urlize_quoted_links }}</span>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>{{ content|urlize_quoted_links }}</pre>{% endautoescape %}
|
</div>{{ content|urlize_quoted_links }}</pre>{% endautoescape %}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -4,11 +4,8 @@ from django.core.urlresolvers import reverse, NoReverseMatch
|
||||||
from django.http import QueryDict
|
from django.http import QueryDict
|
||||||
from django.utils.html import escape
|
from django.utils.html import escape
|
||||||
from django.utils.safestring import SafeData, mark_safe
|
from django.utils.safestring import SafeData, mark_safe
|
||||||
from rest_framework.compat import urlparse
|
from rest_framework.compat import urlparse, force_text, six, smart_urlquote
|
||||||
from rest_framework.compat import force_text
|
import re, string
|
||||||
from rest_framework.compat import six
|
|
||||||
import re
|
|
||||||
import string
|
|
||||||
|
|
||||||
register = template.Library()
|
register = template.Library()
|
||||||
|
|
||||||
|
@ -112,22 +109,6 @@ def replace_query_param(url, key, val):
|
||||||
class_re = re.compile(r'(?<=class=["\'])(.*)(?=["\'])')
|
class_re = re.compile(r'(?<=class=["\'])(.*)(?=["\'])')
|
||||||
|
|
||||||
|
|
||||||
# Bunch of stuff cloned from urlize
|
|
||||||
LEADING_PUNCTUATION = ['(', '<', '<', '"', "'"]
|
|
||||||
TRAILING_PUNCTUATION = ['.', ',', ')', '>', '\n', '>', '"', "'"]
|
|
||||||
DOTS = ['·', '*', '\xe2\x80\xa2', '•', '•', '•']
|
|
||||||
unencoded_ampersands_re = re.compile(r'&(?!(\w+|#\d+);)')
|
|
||||||
word_split_re = re.compile(r'(\s+)')
|
|
||||||
punctuation_re = re.compile('^(?P<lead>(?:%s)*)(?P<middle>.*?)(?P<trail>(?:%s)*)$' % \
|
|
||||||
('|'.join([re.escape(x) for x in LEADING_PUNCTUATION]),
|
|
||||||
'|'.join([re.escape(x) for x in TRAILING_PUNCTUATION])))
|
|
||||||
simple_email_re = re.compile(r'^\S+@[a-zA-Z0-9._-]+\.[a-zA-Z0-9._-]+$')
|
|
||||||
link_target_attribute_re = re.compile(r'(<a [^>]*?)target=[^\s>]+')
|
|
||||||
html_gunk_re = re.compile(r'(?:<br clear="all">|<i><\/i>|<b><\/b>|<em><\/em>|<strong><\/strong>|<\/?smallcaps>|<\/?uppercase>)', re.IGNORECASE)
|
|
||||||
hard_coded_bullets_re = re.compile(r'((?:<p>(?:%s).*?[a-zA-Z].*?</p>\s*)+)' % '|'.join([re.escape(x) for x in DOTS]), re.DOTALL)
|
|
||||||
trailing_empty_content_re = re.compile(r'(?:<p>(?: |\s|<br \/>)*?</p>\s*)+\Z')
|
|
||||||
|
|
||||||
|
|
||||||
# And the template tags themselves...
|
# And the template tags themselves...
|
||||||
|
|
||||||
@register.simple_tag
|
@register.simple_tag
|
||||||
|
@ -195,15 +176,25 @@ def add_class(value, css_class):
|
||||||
return value
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
# Bunch of stuff cloned from urlize
|
||||||
|
TRAILING_PUNCTUATION = ['.', ',', ':', ';', '.)', '"', "'"]
|
||||||
|
WRAPPING_PUNCTUATION = [('(', ')'), ('<', '>'), ('[', ']'), ('<', '>'),
|
||||||
|
('"', '"'), ("'", "'")]
|
||||||
|
word_split_re = re.compile(r'(\s+)')
|
||||||
|
simple_url_re = re.compile(r'^https?://\[?\w', re.IGNORECASE)
|
||||||
|
simple_url_2_re = re.compile(r'^www\.|^(?!http)\w[^@]+\.(com|edu|gov|int|mil|net|org)$', re.IGNORECASE)
|
||||||
|
simple_email_re = re.compile(r'^\S+@\S+\.\S+$')
|
||||||
|
|
||||||
|
|
||||||
@register.filter
|
@register.filter
|
||||||
def urlize_quoted_links(text, trim_url_limit=None, nofollow=True, autoescape=True):
|
def urlize_quoted_links(text, trim_url_limit=None, nofollow=True, autoescape=True):
|
||||||
"""
|
"""
|
||||||
Converts any URLs in text into clickable links.
|
Converts any URLs in text into clickable links.
|
||||||
|
|
||||||
Works on http://, https://, www. links and links ending in .org, .net or
|
Works on http://, https://, www. links, and also on links ending in one of
|
||||||
.com. Links can have trailing punctuation (periods, commas, close-parens)
|
the original seven gTLDs (.com, .edu, .gov, .int, .mil, .net, and .org).
|
||||||
and leading punctuation (opening parens) and it'll still do the right
|
Links can have trailing punctuation (periods, commas, close-parens) and
|
||||||
thing.
|
leading punctuation (opening parens) and it'll still do the right thing.
|
||||||
|
|
||||||
If trim_url_limit is not None, the URLs in link text longer than this limit
|
If trim_url_limit is not None, the URLs in link text longer than this limit
|
||||||
will truncated to trim_url_limit-3 characters and appended with an elipsis.
|
will truncated to trim_url_limit-3 characters and appended with an elipsis.
|
||||||
|
@ -216,24 +207,41 @@ def urlize_quoted_links(text, trim_url_limit=None, nofollow=True, autoescape=Tru
|
||||||
trim_url = lambda x, limit=trim_url_limit: limit is not None and (len(x) > limit and ('%s...' % x[:max(0, limit - 3)])) or x
|
trim_url = lambda x, limit=trim_url_limit: limit is not None and (len(x) > limit and ('%s...' % x[:max(0, limit - 3)])) or x
|
||||||
safe_input = isinstance(text, SafeData)
|
safe_input = isinstance(text, SafeData)
|
||||||
words = word_split_re.split(force_text(text))
|
words = word_split_re.split(force_text(text))
|
||||||
nofollow_attr = nofollow and ' rel="nofollow"' or ''
|
|
||||||
for i, word in enumerate(words):
|
for i, word in enumerate(words):
|
||||||
match = None
|
match = None
|
||||||
if '.' in word or '@' in word or ':' in word:
|
if '.' in word or '@' in word or ':' in word:
|
||||||
match = punctuation_re.match(word)
|
# Deal with punctuation.
|
||||||
if match:
|
lead, middle, trail = '', word, ''
|
||||||
lead, middle, trail = match.groups()
|
for punctuation in TRAILING_PUNCTUATION:
|
||||||
|
if middle.endswith(punctuation):
|
||||||
|
middle = middle[:-len(punctuation)]
|
||||||
|
trail = punctuation + trail
|
||||||
|
for opening, closing in WRAPPING_PUNCTUATION:
|
||||||
|
if middle.startswith(opening):
|
||||||
|
middle = middle[len(opening):]
|
||||||
|
lead = lead + opening
|
||||||
|
# Keep parentheses at the end only if they're balanced.
|
||||||
|
if (middle.endswith(closing)
|
||||||
|
and middle.count(closing) == middle.count(opening) + 1):
|
||||||
|
middle = middle[:-len(closing)]
|
||||||
|
trail = closing + trail
|
||||||
|
|
||||||
# Make URL we want to point to.
|
# Make URL we want to point to.
|
||||||
url = None
|
url = None
|
||||||
if middle.startswith('http://') or middle.startswith('https://'):
|
nofollow_attr = ' rel="nofollow"' if nofollow else ''
|
||||||
url = middle
|
if simple_url_re.match(middle):
|
||||||
elif middle.startswith('www.') or ('@' not in middle and \
|
url = smart_urlquote(middle)
|
||||||
middle and middle[0] in string.ascii_letters + string.digits and \
|
elif simple_url_2_re.match(middle):
|
||||||
(middle.endswith('.org') or middle.endswith('.net') or middle.endswith('.com'))):
|
url = smart_urlquote('http://%s' % middle)
|
||||||
url = 'http://%s' % middle
|
elif not ':' in middle and simple_email_re.match(middle):
|
||||||
elif '@' in middle and not ':' in middle and simple_email_re.match(middle):
|
local, domain = middle.rsplit('@', 1)
|
||||||
url = 'mailto:%s' % middle
|
try:
|
||||||
|
domain = domain.encode('idna').decode('ascii')
|
||||||
|
except UnicodeError:
|
||||||
|
continue
|
||||||
|
url = 'mailto:%s@%s' % (local, domain)
|
||||||
nofollow_attr = ''
|
nofollow_attr = ''
|
||||||
|
|
||||||
# Make link.
|
# Make link.
|
||||||
if url:
|
if url:
|
||||||
trimmed = trim_url(middle)
|
trimmed = trim_url(middle)
|
||||||
|
@ -251,4 +259,15 @@ def urlize_quoted_links(text, trim_url_limit=None, nofollow=True, autoescape=Tru
|
||||||
words[i] = mark_safe(word)
|
words[i] = mark_safe(word)
|
||||||
elif autoescape:
|
elif autoescape:
|
||||||
words[i] = escape(word)
|
words[i] = escape(word)
|
||||||
return mark_safe(''.join(words))
|
return ''.join(words)
|
||||||
|
|
||||||
|
|
||||||
|
@register.filter
|
||||||
|
def break_long_headers(header):
|
||||||
|
"""
|
||||||
|
Breaks headers longer than 160 characters (~page length)
|
||||||
|
when possible (are comma separated)
|
||||||
|
"""
|
||||||
|
if len(header) > 160 and ',' in header:
|
||||||
|
header = mark_safe('<br> ' + ', <br>'.join(header.split(',')))
|
||||||
|
return header
|
||||||
|
|
|
@ -466,17 +466,13 @@ class OAuth2Tests(TestCase):
|
||||||
def _create_authorization_header(self, token=None):
|
def _create_authorization_header(self, token=None):
|
||||||
return "Bearer {0}".format(token or self.access_token.token)
|
return "Bearer {0}".format(token or self.access_token.token)
|
||||||
|
|
||||||
def _client_credentials_params(self):
|
|
||||||
return {'client_id': self.CLIENT_ID, 'client_secret': self.CLIENT_SECRET}
|
|
||||||
|
|
||||||
@unittest.skipUnless(oauth2_provider, 'django-oauth2-provider not installed')
|
@unittest.skipUnless(oauth2_provider, 'django-oauth2-provider not installed')
|
||||||
def test_get_form_with_wrong_authorization_header_token_type_failing(self):
|
def test_get_form_with_wrong_authorization_header_token_type_failing(self):
|
||||||
"""Ensure that a wrong token type lead to the correct HTTP error status code"""
|
"""Ensure that a wrong token type lead to the correct HTTP error status code"""
|
||||||
auth = "Wrong token-type-obsviously"
|
auth = "Wrong token-type-obsviously"
|
||||||
response = self.csrf_client.get('/oauth2-test/', {}, HTTP_AUTHORIZATION=auth)
|
response = self.csrf_client.get('/oauth2-test/', {}, HTTP_AUTHORIZATION=auth)
|
||||||
self.assertEqual(response.status_code, 401)
|
self.assertEqual(response.status_code, 401)
|
||||||
params = self._client_credentials_params()
|
response = self.csrf_client.get('/oauth2-test/', HTTP_AUTHORIZATION=auth)
|
||||||
response = self.csrf_client.get('/oauth2-test/', params, HTTP_AUTHORIZATION=auth)
|
|
||||||
self.assertEqual(response.status_code, 401)
|
self.assertEqual(response.status_code, 401)
|
||||||
|
|
||||||
@unittest.skipUnless(oauth2_provider, 'django-oauth2-provider not installed')
|
@unittest.skipUnless(oauth2_provider, 'django-oauth2-provider not installed')
|
||||||
|
@ -485,8 +481,7 @@ class OAuth2Tests(TestCase):
|
||||||
auth = "Bearer wrong token format"
|
auth = "Bearer wrong token format"
|
||||||
response = self.csrf_client.get('/oauth2-test/', {}, HTTP_AUTHORIZATION=auth)
|
response = self.csrf_client.get('/oauth2-test/', {}, HTTP_AUTHORIZATION=auth)
|
||||||
self.assertEqual(response.status_code, 401)
|
self.assertEqual(response.status_code, 401)
|
||||||
params = self._client_credentials_params()
|
response = self.csrf_client.get('/oauth2-test/', HTTP_AUTHORIZATION=auth)
|
||||||
response = self.csrf_client.get('/oauth2-test/', params, HTTP_AUTHORIZATION=auth)
|
|
||||||
self.assertEqual(response.status_code, 401)
|
self.assertEqual(response.status_code, 401)
|
||||||
|
|
||||||
@unittest.skipUnless(oauth2_provider, 'django-oauth2-provider not installed')
|
@unittest.skipUnless(oauth2_provider, 'django-oauth2-provider not installed')
|
||||||
|
@ -495,33 +490,21 @@ class OAuth2Tests(TestCase):
|
||||||
auth = "Bearer wrong-token"
|
auth = "Bearer wrong-token"
|
||||||
response = self.csrf_client.get('/oauth2-test/', {}, HTTP_AUTHORIZATION=auth)
|
response = self.csrf_client.get('/oauth2-test/', {}, HTTP_AUTHORIZATION=auth)
|
||||||
self.assertEqual(response.status_code, 401)
|
self.assertEqual(response.status_code, 401)
|
||||||
params = self._client_credentials_params()
|
response = self.csrf_client.get('/oauth2-test/', HTTP_AUTHORIZATION=auth)
|
||||||
response = self.csrf_client.get('/oauth2-test/', params, HTTP_AUTHORIZATION=auth)
|
|
||||||
self.assertEqual(response.status_code, 401)
|
|
||||||
|
|
||||||
@unittest.skipUnless(oauth2_provider, 'django-oauth2-provider not installed')
|
|
||||||
def test_get_form_with_wrong_client_data_failing_auth(self):
|
|
||||||
"""Ensure GETing form over OAuth with incorrect client credentials fails"""
|
|
||||||
auth = self._create_authorization_header()
|
|
||||||
params = self._client_credentials_params()
|
|
||||||
params['client_id'] += 'a'
|
|
||||||
response = self.csrf_client.get('/oauth2-test/', params, HTTP_AUTHORIZATION=auth)
|
|
||||||
self.assertEqual(response.status_code, 401)
|
self.assertEqual(response.status_code, 401)
|
||||||
|
|
||||||
@unittest.skipUnless(oauth2_provider, 'django-oauth2-provider not installed')
|
@unittest.skipUnless(oauth2_provider, 'django-oauth2-provider not installed')
|
||||||
def test_get_form_passing_auth(self):
|
def test_get_form_passing_auth(self):
|
||||||
"""Ensure GETing form over OAuth with correct client credentials succeed"""
|
"""Ensure GETing form over OAuth with correct client credentials succeed"""
|
||||||
auth = self._create_authorization_header()
|
auth = self._create_authorization_header()
|
||||||
params = self._client_credentials_params()
|
response = self.csrf_client.get('/oauth2-test/', HTTP_AUTHORIZATION=auth)
|
||||||
response = self.csrf_client.get('/oauth2-test/', params, HTTP_AUTHORIZATION=auth)
|
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
@unittest.skipUnless(oauth2_provider, 'django-oauth2-provider not installed')
|
@unittest.skipUnless(oauth2_provider, 'django-oauth2-provider not installed')
|
||||||
def test_post_form_passing_auth(self):
|
def test_post_form_passing_auth(self):
|
||||||
"""Ensure POSTing form over OAuth with correct credentials passes and does not require CSRF"""
|
"""Ensure POSTing form over OAuth with correct credentials passes and does not require CSRF"""
|
||||||
auth = self._create_authorization_header()
|
auth = self._create_authorization_header()
|
||||||
params = self._client_credentials_params()
|
response = self.csrf_client.post('/oauth2-test/', HTTP_AUTHORIZATION=auth)
|
||||||
response = self.csrf_client.post('/oauth2-test/', params, HTTP_AUTHORIZATION=auth)
|
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
@unittest.skipUnless(oauth2_provider, 'django-oauth2-provider not installed')
|
@unittest.skipUnless(oauth2_provider, 'django-oauth2-provider not installed')
|
||||||
|
@ -529,16 +512,14 @@ class OAuth2Tests(TestCase):
|
||||||
"""Ensure POSTing when there is no OAuth access token in db fails"""
|
"""Ensure POSTing when there is no OAuth access token in db fails"""
|
||||||
self.access_token.delete()
|
self.access_token.delete()
|
||||||
auth = self._create_authorization_header()
|
auth = self._create_authorization_header()
|
||||||
params = self._client_credentials_params()
|
response = self.csrf_client.post('/oauth2-test/', HTTP_AUTHORIZATION=auth)
|
||||||
response = self.csrf_client.post('/oauth2-test/', params, HTTP_AUTHORIZATION=auth)
|
|
||||||
self.assertIn(response.status_code, (status.HTTP_401_UNAUTHORIZED, status.HTTP_403_FORBIDDEN))
|
self.assertIn(response.status_code, (status.HTTP_401_UNAUTHORIZED, status.HTTP_403_FORBIDDEN))
|
||||||
|
|
||||||
@unittest.skipUnless(oauth2_provider, 'django-oauth2-provider not installed')
|
@unittest.skipUnless(oauth2_provider, 'django-oauth2-provider not installed')
|
||||||
def test_post_form_with_refresh_token_failing_auth(self):
|
def test_post_form_with_refresh_token_failing_auth(self):
|
||||||
"""Ensure POSTing with refresh token instead of access token fails"""
|
"""Ensure POSTing with refresh token instead of access token fails"""
|
||||||
auth = self._create_authorization_header(token=self.refresh_token.token)
|
auth = self._create_authorization_header(token=self.refresh_token.token)
|
||||||
params = self._client_credentials_params()
|
response = self.csrf_client.post('/oauth2-test/', HTTP_AUTHORIZATION=auth)
|
||||||
response = self.csrf_client.post('/oauth2-test/', params, HTTP_AUTHORIZATION=auth)
|
|
||||||
self.assertIn(response.status_code, (status.HTTP_401_UNAUTHORIZED, status.HTTP_403_FORBIDDEN))
|
self.assertIn(response.status_code, (status.HTTP_401_UNAUTHORIZED, status.HTTP_403_FORBIDDEN))
|
||||||
|
|
||||||
@unittest.skipUnless(oauth2_provider, 'django-oauth2-provider not installed')
|
@unittest.skipUnless(oauth2_provider, 'django-oauth2-provider not installed')
|
||||||
|
@ -547,8 +528,7 @@ class OAuth2Tests(TestCase):
|
||||||
self.access_token.expires = datetime.datetime.now() - datetime.timedelta(seconds=10) # 10 seconds late
|
self.access_token.expires = datetime.datetime.now() - datetime.timedelta(seconds=10) # 10 seconds late
|
||||||
self.access_token.save()
|
self.access_token.save()
|
||||||
auth = self._create_authorization_header()
|
auth = self._create_authorization_header()
|
||||||
params = self._client_credentials_params()
|
response = self.csrf_client.post('/oauth2-test/', HTTP_AUTHORIZATION=auth)
|
||||||
response = self.csrf_client.post('/oauth2-test/', params, HTTP_AUTHORIZATION=auth)
|
|
||||||
self.assertIn(response.status_code, (status.HTTP_401_UNAUTHORIZED, status.HTTP_403_FORBIDDEN))
|
self.assertIn(response.status_code, (status.HTTP_401_UNAUTHORIZED, status.HTTP_403_FORBIDDEN))
|
||||||
self.assertIn('Invalid token', response.content)
|
self.assertIn('Invalid token', response.content)
|
||||||
|
|
||||||
|
@ -559,10 +539,9 @@ class OAuth2Tests(TestCase):
|
||||||
read_only_access_token.scope = oauth2_provider_scope.SCOPE_NAME_DICT['read']
|
read_only_access_token.scope = oauth2_provider_scope.SCOPE_NAME_DICT['read']
|
||||||
read_only_access_token.save()
|
read_only_access_token.save()
|
||||||
auth = self._create_authorization_header(token=read_only_access_token.token)
|
auth = self._create_authorization_header(token=read_only_access_token.token)
|
||||||
params = self._client_credentials_params()
|
response = self.csrf_client.get('/oauth2-with-scope-test/', HTTP_AUTHORIZATION=auth)
|
||||||
response = self.csrf_client.get('/oauth2-with-scope-test/', params, HTTP_AUTHORIZATION=auth)
|
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
response = self.csrf_client.post('/oauth2-with-scope-test/', params, HTTP_AUTHORIZATION=auth)
|
response = self.csrf_client.post('/oauth2-with-scope-test/', HTTP_AUTHORIZATION=auth)
|
||||||
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
|
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
|
||||||
|
|
||||||
@unittest.skipUnless(oauth2_provider, 'django-oauth2-provider not installed')
|
@unittest.skipUnless(oauth2_provider, 'django-oauth2-provider not installed')
|
||||||
|
@ -572,6 +551,5 @@ class OAuth2Tests(TestCase):
|
||||||
read_write_access_token.scope = oauth2_provider_scope.SCOPE_NAME_DICT['write']
|
read_write_access_token.scope = oauth2_provider_scope.SCOPE_NAME_DICT['write']
|
||||||
read_write_access_token.save()
|
read_write_access_token.save()
|
||||||
auth = self._create_authorization_header(token=read_write_access_token.token)
|
auth = self._create_authorization_header(token=read_write_access_token.token)
|
||||||
params = self._client_credentials_params()
|
response = self.csrf_client.post('/oauth2-with-scope-test/', HTTP_AUTHORIZATION=auth)
|
||||||
response = self.csrf_client.post('/oauth2-with-scope-test/', params, HTTP_AUTHORIZATION=auth)
|
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
|
@ -1,11 +1,12 @@
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
import datetime
|
import datetime
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
|
from django.core.urlresolvers import reverse
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
from django.test.client import RequestFactory
|
from django.test.client import RequestFactory
|
||||||
from django.utils import unittest
|
from django.utils import unittest
|
||||||
from rest_framework import generics, status, filters
|
from rest_framework import generics, status, filters
|
||||||
from rest_framework.compat import django_filters
|
from rest_framework.compat import django_filters, patterns, url
|
||||||
from rest_framework.tests.models import FilterableItem, BasicModel
|
from rest_framework.tests.models import FilterableItem, BasicModel
|
||||||
|
|
||||||
factory = RequestFactory()
|
factory = RequestFactory()
|
||||||
|
@ -46,11 +47,20 @@ if django_filters:
|
||||||
filter_class = MisconfiguredFilter
|
filter_class = MisconfiguredFilter
|
||||||
filter_backend = filters.DjangoFilterBackend
|
filter_backend = filters.DjangoFilterBackend
|
||||||
|
|
||||||
|
class FilterClassDetailView(generics.RetrieveAPIView):
|
||||||
|
model = FilterableItem
|
||||||
|
filter_class = SeveralFieldsFilter
|
||||||
|
filter_backend = filters.DjangoFilterBackend
|
||||||
|
|
||||||
class IntegrationTestFiltering(TestCase):
|
urlpatterns = patterns('',
|
||||||
"""
|
url(r'^(?P<pk>\d+)/$', FilterClassDetailView.as_view(), name='detail-view'),
|
||||||
Integration tests for filtered list views.
|
url(r'^$', FilterClassRootView.as_view(), name='root-view'),
|
||||||
"""
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class CommonFilteringTestCase(TestCase):
|
||||||
|
def _serialize_object(self, obj):
|
||||||
|
return {'id': obj.id, 'text': obj.text, 'decimal': obj.decimal, 'date': obj.date}
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
"""
|
"""
|
||||||
|
@ -65,10 +75,16 @@ class IntegrationTestFiltering(TestCase):
|
||||||
|
|
||||||
self.objects = FilterableItem.objects
|
self.objects = FilterableItem.objects
|
||||||
self.data = [
|
self.data = [
|
||||||
{'id': obj.id, 'text': obj.text, 'decimal': obj.decimal, 'date': obj.date}
|
self._serialize_object(obj)
|
||||||
for obj in self.objects.all()
|
for obj in self.objects.all()
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class IntegrationTestFiltering(CommonFilteringTestCase):
|
||||||
|
"""
|
||||||
|
Integration tests for filtered list views.
|
||||||
|
"""
|
||||||
|
|
||||||
@unittest.skipUnless(django_filters, 'django-filters not installed')
|
@unittest.skipUnless(django_filters, 'django-filters not installed')
|
||||||
def test_get_filtered_fields_root_view(self):
|
def test_get_filtered_fields_root_view(self):
|
||||||
"""
|
"""
|
||||||
|
@ -167,3 +183,50 @@ class IntegrationTestFiltering(TestCase):
|
||||||
request = factory.get('/?integer=%s' % search_integer)
|
request = factory.get('/?integer=%s' % search_integer)
|
||||||
response = view(request).render()
|
response = view(request).render()
|
||||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
|
|
||||||
|
|
||||||
|
class IntegrationTestDetailFiltering(CommonFilteringTestCase):
|
||||||
|
"""
|
||||||
|
Integration tests for filtered detail views.
|
||||||
|
"""
|
||||||
|
urls = 'rest_framework.tests.filterset'
|
||||||
|
|
||||||
|
def _get_url(self, item):
|
||||||
|
return reverse('detail-view', kwargs=dict(pk=item.pk))
|
||||||
|
|
||||||
|
@unittest.skipUnless(django_filters, 'django-filters not installed')
|
||||||
|
def test_get_filtered_detail_view(self):
|
||||||
|
"""
|
||||||
|
GET requests to filtered RetrieveAPIView that have a filter_class set
|
||||||
|
should return filtered results.
|
||||||
|
"""
|
||||||
|
item = self.objects.all()[0]
|
||||||
|
data = self._serialize_object(item)
|
||||||
|
|
||||||
|
# Basic test with no filter.
|
||||||
|
response = self.client.get(self._get_url(item))
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
|
self.assertEqual(response.data, data)
|
||||||
|
|
||||||
|
# Tests that the decimal filter set that should fail.
|
||||||
|
search_decimal = Decimal('4.25')
|
||||||
|
high_item = self.objects.filter(decimal__gt=search_decimal)[0]
|
||||||
|
response = self.client.get('{url}?decimal={param}'.format(url=self._get_url(high_item), param=search_decimal))
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
|
||||||
|
|
||||||
|
# Tests that the decimal filter set that should succeed.
|
||||||
|
search_decimal = Decimal('4.25')
|
||||||
|
low_item = self.objects.filter(decimal__lt=search_decimal)[0]
|
||||||
|
low_item_data = self._serialize_object(low_item)
|
||||||
|
response = self.client.get('{url}?decimal={param}'.format(url=self._get_url(low_item), param=search_decimal))
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
|
self.assertEqual(response.data, low_item_data)
|
||||||
|
|
||||||
|
# Tests that multiple filters works.
|
||||||
|
search_decimal = Decimal('5.25')
|
||||||
|
search_date = datetime.date(2012, 10, 2)
|
||||||
|
valid_item = self.objects.filter(decimal__lt=search_decimal, date__gt=search_date)[0]
|
||||||
|
valid_item_data = self._serialize_object(valid_item)
|
||||||
|
response = self.client.get('{url}?decimal={decimal}&date={date}'.format(url=self._get_url(valid_item), decimal=search_decimal, date=search_date))
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
|
self.assertEqual(response.data, valid_item_data)
|
||||||
|
|
|
@ -129,16 +129,6 @@ class IntegrationTestPaginationAndFiltering(TestCase):
|
||||||
view = FilterFieldsRootView.as_view()
|
view = FilterFieldsRootView.as_view()
|
||||||
|
|
||||||
EXPECTED_NUM_QUERIES = 2
|
EXPECTED_NUM_QUERIES = 2
|
||||||
if django.VERSION < (1, 4):
|
|
||||||
# On Django 1.3 we need to use django-filter 0.5.4
|
|
||||||
#
|
|
||||||
# The filter objects there don't expose a `.count()` method,
|
|
||||||
# which means we only make a single query *but* it's a single
|
|
||||||
# query across *all* of the queryset, instead of a COUNT and then
|
|
||||||
# a SELECT with a LIMIT.
|
|
||||||
#
|
|
||||||
# Although this is fewer queries, it's actually a regression.
|
|
||||||
EXPECTED_NUM_QUERIES = 1
|
|
||||||
|
|
||||||
request = factory.get('/?decimal=15.20')
|
request = factory.get('/?decimal=15.20')
|
||||||
with self.assertNumQueries(EXPECTED_NUM_QUERIES):
|
with self.assertNumQueries(EXPECTED_NUM_QUERIES):
|
||||||
|
|
|
@ -98,7 +98,7 @@ class BulkCreateSerializerTests(TestCase):
|
||||||
serializer = self.BookSerializer(data=data, many=True)
|
serializer = self.BookSerializer(data=data, many=True)
|
||||||
self.assertEqual(serializer.is_valid(), False)
|
self.assertEqual(serializer.is_valid(), False)
|
||||||
|
|
||||||
expected_errors = {'non_field_errors': ['Expected a list of items']}
|
expected_errors = {'non_field_errors': ['Expected a list of items.']}
|
||||||
|
|
||||||
self.assertEqual(serializer.errors, expected_errors)
|
self.assertEqual(serializer.errors, expected_errors)
|
||||||
|
|
||||||
|
@ -115,7 +115,7 @@ class BulkCreateSerializerTests(TestCase):
|
||||||
serializer = self.BookSerializer(data=data, many=True)
|
serializer = self.BookSerializer(data=data, many=True)
|
||||||
self.assertEqual(serializer.is_valid(), False)
|
self.assertEqual(serializer.is_valid(), False)
|
||||||
|
|
||||||
expected_errors = {'non_field_errors': ['Expected a list of items']}
|
expected_errors = {'non_field_errors': ['Expected a list of items.']}
|
||||||
|
|
||||||
self.assertEqual(serializer.errors, expected_errors)
|
self.assertEqual(serializer.errors, expected_errors)
|
||||||
|
|
||||||
|
@ -206,6 +206,7 @@ class BulkUpdateSerializerTests(TestCase):
|
||||||
self.assertEqual(serializer.data, data)
|
self.assertEqual(serializer.data, data)
|
||||||
serializer.save()
|
serializer.save()
|
||||||
new_data = self.BookSerializer(self.books(), many=True).data
|
new_data = self.BookSerializer(self.books(), many=True).data
|
||||||
|
|
||||||
self.assertEqual(data, new_data)
|
self.assertEqual(data, new_data)
|
||||||
|
|
||||||
def test_bulk_update_and_create(self):
|
def test_bulk_update_and_create(self):
|
||||||
|
@ -230,6 +231,29 @@ class BulkUpdateSerializerTests(TestCase):
|
||||||
new_data = self.BookSerializer(self.books(), many=True).data
|
new_data = self.BookSerializer(self.books(), many=True).data
|
||||||
self.assertEqual(data, new_data)
|
self.assertEqual(data, new_data)
|
||||||
|
|
||||||
|
def test_bulk_update_invalid_create(self):
|
||||||
|
"""
|
||||||
|
Bulk update serialization without allow_add_remove may not create items.
|
||||||
|
"""
|
||||||
|
data = [
|
||||||
|
{
|
||||||
|
'id': 0,
|
||||||
|
'title': 'The electric kool-aid acid test',
|
||||||
|
'author': 'Tom Wolfe'
|
||||||
|
}, {
|
||||||
|
'id': 3,
|
||||||
|
'title': 'Kafka on the shore',
|
||||||
|
'author': 'Haruki Murakami'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
expected_errors = [
|
||||||
|
{},
|
||||||
|
{'non_field_errors': ['Cannot create a new item, only existing items may be updated.']}
|
||||||
|
]
|
||||||
|
serializer = self.BookSerializer(self.books(), data=data, many=True)
|
||||||
|
self.assertEqual(serializer.is_valid(), False)
|
||||||
|
self.assertEqual(serializer.errors, expected_errors)
|
||||||
|
|
||||||
def test_bulk_update_error(self):
|
def test_bulk_update_error(self):
|
||||||
"""
|
"""
|
||||||
Incorrect bulk update serialization should return error data.
|
Incorrect bulk update serialization should return error data.
|
||||||
|
|
|
@ -3,7 +3,7 @@ Provides an APIView class that is used as the base of all class-based views.
|
||||||
"""
|
"""
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
from django.core.exceptions import PermissionDenied
|
from django.core.exceptions import PermissionDenied
|
||||||
from django.http import Http404
|
from django.http import Http404, HttpResponse
|
||||||
from django.utils.html import escape
|
from django.utils.html import escape
|
||||||
from django.utils.safestring import mark_safe
|
from django.utils.safestring import mark_safe
|
||||||
from django.views.decorators.csrf import csrf_exempt
|
from django.views.decorators.csrf import csrf_exempt
|
||||||
|
@ -327,6 +327,12 @@ class APIView(View):
|
||||||
"""
|
"""
|
||||||
Returns the final response object.
|
Returns the final response object.
|
||||||
"""
|
"""
|
||||||
|
# Make the error obvious if a proper response is not returned
|
||||||
|
assert isinstance(response, HttpResponse), (
|
||||||
|
'Expected a `Response` to be returned from the view, '
|
||||||
|
'but received a `%s`' % type(response)
|
||||||
|
)
|
||||||
|
|
||||||
if isinstance(response, Response):
|
if isinstance(response, Response):
|
||||||
if not getattr(request, 'accepted_renderer', None):
|
if not getattr(request, 'accepted_renderer', None):
|
||||||
neg = self.perform_content_negotiation(request, force=True)
|
neg = self.perform_content_negotiation(request, force=True)
|
||||||
|
|
Loading…
Reference in New Issue
Block a user