mirror of
https://github.com/encode/django-rest-framework.git
synced 2025-08-08 14:24:48 +03:00
Merge remote-tracking branch 'reference/master' into bug/3354
Conflicts: rest_framework/compat.py
This commit is contained in:
commit
026e114665
25
.travis.yml
25
.travis.yml
|
@ -5,6 +5,9 @@ sudo: false
|
|||
env:
|
||||
- TOX_ENV=py27-lint
|
||||
- TOX_ENV=py27-docs
|
||||
- TOX_ENV=py35-django19
|
||||
- TOX_ENV=py34-django19
|
||||
- TOX_ENV=py27-django19
|
||||
- TOX_ENV=py34-django18
|
||||
- TOX_ENV=py33-django18
|
||||
- TOX_ENV=py32-django18
|
||||
|
@ -13,28 +16,12 @@ env:
|
|||
- TOX_ENV=py33-django17
|
||||
- TOX_ENV=py32-django17
|
||||
- TOX_ENV=py27-django17
|
||||
- TOX_ENV=py34-django16
|
||||
- TOX_ENV=py33-django16
|
||||
- TOX_ENV=py32-django16
|
||||
- TOX_ENV=py27-django16
|
||||
- TOX_ENV=py26-django16
|
||||
- TOX_ENV=py34-django15
|
||||
- TOX_ENV=py33-django15
|
||||
- TOX_ENV=py32-django15
|
||||
- TOX_ENV=py27-django15
|
||||
- TOX_ENV=py26-django15
|
||||
- TOX_ENV=py27-djangomaster
|
||||
- TOX_ENV=py32-djangomaster
|
||||
- TOX_ENV=py33-djangomaster
|
||||
- TOX_ENV=py34-djangomaster
|
||||
|
||||
matrix:
|
||||
# Python 3.5 not yet available on travis, watch this to see when it is.
|
||||
fast_finish: true
|
||||
allow_failures:
|
||||
- env: TOX_ENV=py27-djangomaster
|
||||
- env: TOX_ENV=py32-djangomaster
|
||||
- env: TOX_ENV=py33-djangomaster
|
||||
- env: TOX_ENV=py34-djangomaster
|
||||
- env: TOX_ENV=py35-django19
|
||||
|
||||
install:
|
||||
- pip install tox
|
||||
|
@ -44,4 +31,4 @@ script:
|
|||
|
||||
after_success:
|
||||
- pip install codecov
|
||||
- codecov
|
||||
- codecov -e TOX_ENV
|
||||
|
|
|
@ -36,8 +36,8 @@ There is a live example API for testing purposes, [available here][sandbox].
|
|||
|
||||
# Requirements
|
||||
|
||||
* Python (2.6.5+, 2.7, 3.2, 3.3, 3.4)
|
||||
* Django (1.5.6+, 1.6.3+, 1.7, 1.8)
|
||||
* Python (2.7, 3.2, 3.3, 3.4, 3.5)
|
||||
* Django (1.7, 1.8, 1.9)
|
||||
|
||||
# Installation
|
||||
|
||||
|
|
|
@ -360,6 +360,10 @@ HTTP Signature (currently a [IETF draft][http-signature-ietf-draft]) provides a
|
|||
|
||||
[Django-rest-auth][django-rest-auth] library provides a set of REST API endpoints for registration, authentication (including social media authentication), password reset, retrieve and update user details, etc. By having these API endpoints, your client apps such as AngularJS, iOS, Android, and others can communicate to your Django backend site independently via REST APIs for user management.
|
||||
|
||||
## django-rest-framework-social-oauth2
|
||||
|
||||
[Django-rest-framework-social-oauth2][django-rest-framework-social-oauth2] library provides an easy way to integrate social plugins (facebook, twitter, google, etc.) to your authentication system and an easy oauth2 setup. With this library, you will be able to authenticate users based on external tokens (e.g. facebook access token), convert these tokens to "in-house" oauth2 tokens and use and generate oauth2 tokens to authenticate your users.
|
||||
|
||||
## django-rest-knox
|
||||
|
||||
[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).
|
||||
|
@ -404,4 +408,5 @@ HTTP Signature (currently a [IETF draft][http-signature-ietf-draft]) provides a
|
|||
[mac]: http://tools.ietf.org/html/draft-hammer-oauth-v2-mac-token-05
|
||||
[djoser]: https://github.com/sunscrapers/djoser
|
||||
[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
|
||||
|
|
|
@ -57,7 +57,7 @@ Note that setting a `default` value implies that the field is not required. Incl
|
|||
|
||||
### `source`
|
||||
|
||||
The name of the attribute that will be used to populate the field. May be a method that only takes a `self` argument, such as `URLField('get_absolute_url')`, or may use dotted notation to traverse attributes, such as `EmailField(source='user.email')`.
|
||||
The name of the attribute that will be used to populate the field. May be a method that only takes a `self` argument, such as `URLField(source='get_absolute_url')`, or may use dotted notation to traverse attributes, such as `EmailField(source='user.email')`.
|
||||
|
||||
The value `source='*'` has a special meaning, and is used to indicate that the entire object should be passed through to the field. This can be useful for creating nested representations, or for fields which require access to the complete object in order to determine the output representation.
|
||||
|
||||
|
@ -459,6 +459,14 @@ You can also use the declarative style, as with `ListField`. For example:
|
|||
class DocumentField(DictField):
|
||||
child = CharField()
|
||||
|
||||
## JSONField
|
||||
|
||||
A field class that validates that the incoming data structure consists of valid JSON primitives. In its alternate binary mode, it will represent and validate JSON-encoded binary strings.
|
||||
|
||||
**Signature**: `JSONField(binary)`
|
||||
|
||||
- `binary` - If set to `True` then the field will output and validate a JSON encoded string, rather that a primitive data structure. Defaults to `False`.
|
||||
|
||||
---
|
||||
|
||||
# Miscellaneous fields
|
||||
|
|
|
@ -69,6 +69,16 @@ If using the `i18n_patterns` function provided by Django, as well as `format_suf
|
|||
|
||||
---
|
||||
|
||||
## Query parameter formats
|
||||
|
||||
An alternative to the format suffixes is to include the requested format in a query parameter. REST framework provides this option by default, and it is used in the browsable API to switch between differing available representations.
|
||||
|
||||
To select a representation using its short format, use the `format` query parameter. For example: `http://example.com/organizations/?format=csv`.
|
||||
|
||||
The name of this query parameter can be modified using the `URL_FORMAT_OVERRIDE` setting. Set the value to `None` to disable this behavior.
|
||||
|
||||
---
|
||||
|
||||
## Accept headers vs. format suffixes
|
||||
|
||||
There seems to be a view among some of the Web community that filename extensions are not a RESTful pattern, and that `HTTP Accept` headers should always be used instead.
|
||||
|
|
|
@ -287,7 +287,7 @@ Similarly if a nested representation should be a list of items, you should pass
|
|||
|
||||
## Writable nested representations
|
||||
|
||||
When dealing with nested representations that support deserializing the data, an errors with nested objects will be nested under the field name of the nested object.
|
||||
When dealing with nested representations that support deserializing the data, any errors with nested objects will be nested under the field name of the nested object.
|
||||
|
||||
serializer = CommentSerializer(data={'user': {'email': 'foobar', 'username': 'doe'}, 'content': 'baz'})
|
||||
serializer.is_valid()
|
||||
|
@ -356,7 +356,7 @@ It is possible that a third party package, providing automatic support some kind
|
|||
|
||||
#### Handling saving related instances in model manager classes
|
||||
|
||||
An alternative to saving multiple related instances in the serializer is to write custom model manager classes handle creating the correct instances.
|
||||
An alternative to saving multiple related instances in the serializer is to write custom model manager classes that handle creating the correct instances.
|
||||
|
||||
For example, suppose we wanted to ensure that `User` instances and `Profile` instances are always created together as a pair. We might write a custom manager class that looks something like this:
|
||||
|
||||
|
@ -438,6 +438,7 @@ Declaring a `ModelSerializer` looks like this:
|
|||
class AccountSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = Account
|
||||
fields = ('id', 'account_name', 'users', 'created')
|
||||
|
||||
By default, all the model fields on the class will be mapped to a corresponding serializer fields.
|
||||
|
||||
|
@ -459,7 +460,7 @@ To do so, open the Django shell, using `python manage.py shell`, then import the
|
|||
|
||||
## Specifying which fields to include
|
||||
|
||||
If you only want a subset of the default fields to be used in a model serializer, you can do so using `fields` or `exclude` options, just as you would with a `ModelForm`.
|
||||
If you only want a subset of the default fields to be used in a model serializer, you can do so using `fields` or `exclude` options, just as you would with a `ModelForm`. It is strongly recommended that you explicitly set all fields that should be serialized using the `fields` attribute. This will make it less likely to result in unintentionally exposing data when your models change.
|
||||
|
||||
For example:
|
||||
|
||||
|
@ -468,7 +469,27 @@ For example:
|
|||
model = Account
|
||||
fields = ('id', 'account_name', 'users', 'created')
|
||||
|
||||
The names in the `fields` option will normally map to model fields on the model class.
|
||||
You can also set the `fields` attribute to the special value `'__all__'` to indicate that all fields in the model should be used.
|
||||
|
||||
For example:
|
||||
|
||||
class AccountSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = Account
|
||||
fields = '__all__'
|
||||
|
||||
You can set the `exclude` attribute to a list of fields to be excluded from the serializer.
|
||||
|
||||
For example:
|
||||
|
||||
class AccountSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = Account
|
||||
exclude = ('users',)
|
||||
|
||||
In the example above, if the `Account` model had 3 fields `account_name`, `users`, and `created`, this will result in the fields `account_name` and `created` to be serialized.
|
||||
|
||||
The names in the `fields` and `exclude` attributes will normally map to model fields on the model class.
|
||||
|
||||
Alternatively names in the `fields` options can map to properties or methods which take no arguments that exist on the model class.
|
||||
|
||||
|
@ -530,7 +551,7 @@ Please review the [Validators Documentation](/api-guide/validators/) for details
|
|||
|
||||
## Additional keyword arguments
|
||||
|
||||
There is also a shortcut allowing you to specify arbitrary additional keyword arguments on fields, using the `extra_kwargs` option. Similarly to `read_only_fields` this means you do not need to explicitly declare the field on the serializer.
|
||||
There is also a shortcut allowing you to specify arbitrary additional keyword arguments on fields, using the `extra_kwargs` option. As in the case of `read_only_fields`, this means you do not need to explicitly declare the field on the serializer.
|
||||
|
||||
This option is a dictionary, mapping field names to a dictionary of keyword arguments. For example:
|
||||
|
||||
|
@ -811,7 +832,7 @@ This class implements the same basic API as the `Serializer` class:
|
|||
* `.data` - Returns the outgoing primitive representation.
|
||||
* `.is_valid()` - Deserializes and validates incoming data.
|
||||
* `.validated_data` - Returns the validated incoming data.
|
||||
* `.errors` - Returns an errors during validation.
|
||||
* `.errors` - Returns any errors during validation.
|
||||
* `.save()` - Persists the validated data into an object instance.
|
||||
|
||||
There are four methods that can be overridden, depending on what functionality you want the serializer class to support:
|
||||
|
|
|
@ -249,47 +249,23 @@ Default:
|
|||
|
||||
---
|
||||
|
||||
## Browser overrides
|
||||
|
||||
*The following settings provide URL or form-based overrides of the default browser behavior.*
|
||||
|
||||
#### FORM_METHOD_OVERRIDE
|
||||
|
||||
The name of a form field that may be used to override the HTTP method of the form.
|
||||
|
||||
If the value of this setting is `None` then form method overloading will be disabled.
|
||||
|
||||
Default: `'_method'`
|
||||
|
||||
#### FORM_CONTENT_OVERRIDE
|
||||
|
||||
The name of a form field that may be used to override the content of the form payload. Must be used together with `FORM_CONTENTTYPE_OVERRIDE`.
|
||||
|
||||
If either setting is `None` then form content overloading will be disabled.
|
||||
|
||||
Default: `'_content'`
|
||||
|
||||
#### FORM_CONTENTTYPE_OVERRIDE
|
||||
|
||||
The name of a form field that may be used to override the content type of the form payload. Must be used together with `FORM_CONTENT_OVERRIDE`.
|
||||
|
||||
If either setting is `None` then form content overloading will be disabled.
|
||||
|
||||
Default: `'_content_type'`
|
||||
|
||||
#### URL_ACCEPT_OVERRIDE
|
||||
|
||||
The name of a URL parameter that may be used to override the HTTP `Accept` header.
|
||||
|
||||
If the value of this setting is `None` then URL accept overloading will be disabled.
|
||||
|
||||
Default: `'accept'`
|
||||
## Content type controls
|
||||
|
||||
#### URL_FORMAT_OVERRIDE
|
||||
|
||||
The name of a URL parameter that may be used to override the default `Accept` header based content negotiation.
|
||||
The name of a URL parameter that may be used to override the default content negotiation `Accept` header behavior, by using a `format=…` query parameter in the request URL.
|
||||
|
||||
If the value of this setting is `None` then URL format overloading will be disabled.
|
||||
For example: `http://example.com/organizations/?format=csv`
|
||||
|
||||
If the value of this setting is `None` then URL format overrides will be disabled.
|
||||
|
||||
Default: `'format'`
|
||||
|
||||
#### FORMAT_SUFFIX_KWARG
|
||||
|
||||
The name of a parameter in the URL conf that may be used to provide a format suffix. This setting is applied when using `format_suffix_patterns` to include suffixed URL patterns.
|
||||
|
||||
For example: `http://example.com/organizations.csv/`
|
||||
|
||||
Default: `'format'`
|
||||
|
||||
|
@ -451,12 +427,6 @@ A string representing the key that should be used for the URL fields generated b
|
|||
|
||||
Default: `'url'`
|
||||
|
||||
#### FORMAT_SUFFIX_KWARG
|
||||
|
||||
The name of a parameter in the URL conf that may be used to provide a format suffix.
|
||||
|
||||
Default: `'format'`
|
||||
|
||||
#### NUM_PROXIES
|
||||
|
||||
An integer of 0 or more, that may be used to specify the number of application proxies that the API runs behind. This allows throttling to more accurately identify client IP addresses. If set to `None` then less strict IP matching will be used by the throttle classes.
|
||||
|
|
|
@ -184,7 +184,7 @@ If the `.wait()` method is implemented and the request is throttled, then a `Ret
|
|||
|
||||
The following is an example of a rate throttle, that will randomly throttle 1 in every 10 requests.
|
||||
|
||||
class RandomRateThrottle(throttles.BaseThrottle):
|
||||
class RandomRateThrottle(throttling.BaseThrottle):
|
||||
def allow_request(self, request, view):
|
||||
return random.randint(1, 10) == 1
|
||||
|
||||
|
|
|
@ -72,7 +72,7 @@ The following settings keys are also used to control versioning:
|
|||
|
||||
* `DEFAULT_VERSION`. The value that should be used for `request.version` when no versioning information is present. Defaults to `None`.
|
||||
* `ALLOWED_VERSIONS`. If set, this value will restrict the set of versions that may be returned by the versioning scheme, and will raise an error if the provided version if not in this set. Note that the value used for the `DEFAULT_VERSION` setting is always considered to be part of the `ALLOWED_VERSIONS` set. Defaults to `None`.
|
||||
* `VERSION_PARAMETER`. The string that should used for any versioning parameters, such as in the media type or URL query parameters. Defaults to `'version'`.
|
||||
* `VERSION_PARAM`. The string that should used for any versioning parameters, such as in the media type or URL query parameters. Defaults to `'version'`.
|
||||
|
||||
You can also set your versioning class plus those three values on a per-view or a per-viewset basis by defining your own versioning scheme and using the `default_version`, `allowed_versions` and `version_param` class variables. For example, if you want to use `URLPathVersioning`:
|
||||
|
||||
|
|
|
@ -52,8 +52,8 @@ Some reasons you might want to use REST framework:
|
|||
|
||||
REST framework requires the following:
|
||||
|
||||
* Python (2.6.5+, 2.7, 3.2, 3.3, 3.4)
|
||||
* Django (1.5.6+, 1.6.3+, 1.7+, 1.8)
|
||||
* Python (2.6.5+, 2.7, 3.2, 3.3, 3.4, 3.5)
|
||||
* Django (1.7+, 1.8, 1.9)
|
||||
|
||||
The following packages are optional:
|
||||
|
||||
|
|
|
@ -4,58 +4,36 @@
|
|||
>
|
||||
> — [RESTful Web Services][cite], Leonard Richardson & Sam Ruby.
|
||||
|
||||
In order to allow the browsable API to function, there are a couple of browser enhancements that REST framework needs to provide.
|
||||
|
||||
As of version 3.3.0 onwards these are enabled with javascript, using the [ajax-form][ajax-form] library.
|
||||
|
||||
## Browser based PUT, DELETE, etc...
|
||||
|
||||
REST framework supports browser-based `PUT`, `DELETE` and other methods, by
|
||||
overloading `POST` requests using a hidden form field.
|
||||
The [AJAX form library][ajax-form] supports browser-based `PUT`, `DELETE` and other methods on HTML forms.
|
||||
|
||||
Note that this is the same strategy as is used in [Ruby on Rails][rails].
|
||||
After including the library, use the `data-method` attribute on the form, like so:
|
||||
|
||||
For example, given the following form:
|
||||
|
||||
<form action="/news-items/5" method="POST">
|
||||
<input type="hidden" name="_method" value="DELETE">
|
||||
<form action="/" data-method="PUT">
|
||||
<input name='foo'/>
|
||||
...
|
||||
</form>
|
||||
|
||||
`request.method` would return `"DELETE"`.
|
||||
|
||||
## HTTP header based method overriding
|
||||
|
||||
REST framework also supports method overriding via the semi-standard `X-HTTP-Method-Override` header. This can be useful if you are working with non-form content such as JSON and are working with an older web server and/or hosting provider that doesn't recognise particular HTTP methods such as `PATCH`. For example [Amazon Web Services ELB][aws_elb].
|
||||
|
||||
To use it, make a `POST` request, setting the `X-HTTP-Method-Override` header.
|
||||
|
||||
For example, making a `PATCH` request via `POST` in jQuery:
|
||||
|
||||
$.ajax({
|
||||
url: '/myresource/',
|
||||
method: 'POST',
|
||||
headers: {'X-HTTP-Method-Override': 'PATCH'},
|
||||
...
|
||||
});
|
||||
Note that prior to 3.3.0, this support was server-side rather than javascript based. The method overloading style (as used in [Ruby on Rails][rails]) is no longer supported due to subtle issues that it introduces in request parsing.
|
||||
|
||||
## Browser based submission of non-form content
|
||||
|
||||
Browser-based submission of content types other than form are supported by
|
||||
using form fields named `_content` and `_content_type`:
|
||||
Browser-based submission of content types such as JSON are supported by the [AJAX form library][ajax-form], using form fields with `data-override='content-type'` and `data-override='content'` attributes.
|
||||
|
||||
For example, given the following form:
|
||||
For example:
|
||||
|
||||
<form action="/news-items/5" method="PUT">
|
||||
<input type="hidden" name="_content_type" value="application/json">
|
||||
<input name="_content" value="{'count': 1}">
|
||||
<form action="/">
|
||||
<input data-override='content-type' value='application/json' type='hidden'/>
|
||||
<textarea data-override='content'>{}</textarea>
|
||||
<input type="submit"/>
|
||||
</form>
|
||||
|
||||
`request.content_type` would return `"application/json"`, and
|
||||
`request.stream` would return `"{'count': 1}"`
|
||||
|
||||
## URL based accept headers
|
||||
|
||||
REST framework can take `?accept=application/json` style URL parameters,
|
||||
which allow the `Accept` header to be overridden.
|
||||
|
||||
This can be useful for testing the API from a web browser, where you don't
|
||||
have any control over what is sent in the `Accept` header.
|
||||
Note that prior to 3.3.0, this support was server-side rather than javascript based.
|
||||
|
||||
## URL based format suffixes
|
||||
|
||||
|
@ -63,8 +41,37 @@ REST framework can take `?format=json` style URL parameters, which can be a
|
|||
useful shortcut for determining which content type should be returned from
|
||||
the view.
|
||||
|
||||
This is a more concise than using the `accept` override, but it also gives
|
||||
you less control. (For example you can't specify any media type parameters)
|
||||
This behavior is controlled using the `URL_FORMAT_OVERRIDE` setting.
|
||||
|
||||
## HTTP header based method overriding
|
||||
|
||||
Prior to version 3.3.0 the semi extension header `X-HTTP-Method-Override` was supported for overriding the request method. This behavior is no longer in core, but can be adding if needed using middleware.
|
||||
|
||||
For example:
|
||||
|
||||
METHOD_OVERRIDE_HEADER = 'HTTP_X_HTTP_METHOD_OVERRIDE'
|
||||
|
||||
class MethodOverrideMiddleware(object):
|
||||
def process_view(self, request, callback, callback_args, callback_kwargs):
|
||||
if request.method != 'POST':
|
||||
return
|
||||
if METHOD_OVERRIDE_HEADER not in request.META:
|
||||
return
|
||||
request.method = request.META[METHOD_OVERRIDE_HEADER]
|
||||
|
||||
## URL based accept headers
|
||||
|
||||
Until version 3.3.0 REST framework included built-in support for `?accept=application/json` style URL parameters, which would allow the `Accept` header to be overridden.
|
||||
|
||||
Since the introduction of the content negotiation API this behavior is no longer included in core, but may be added using a custom content negotiation class, if needed.
|
||||
|
||||
For example:
|
||||
|
||||
class AcceptQueryParamOverride()
|
||||
def get_accept_list(self, request):
|
||||
header = request.META.get('HTTP_ACCEPT', '*/*')
|
||||
header = request.query_params.get('_accept', header)
|
||||
return [token.strip() for token in header.split(',')]
|
||||
|
||||
## Doesn't HTML5 support PUT and DELETE forms?
|
||||
|
||||
|
@ -74,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
|
||||
[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/
|
||||
[aws_elb]: https://forums.aws.amazon.com/thread.jspa?messageID=400724
|
||||
|
|
|
@ -38,6 +38,14 @@ You can determine your currently installed version using `pip freeze`:
|
|||
|
||||
---
|
||||
|
||||
## 3.3.x series
|
||||
|
||||
### 3.3.0
|
||||
|
||||
**Date**: NOT YET RELEASED
|
||||
|
||||
* Removed support for Django Versions 1.5 & 1.6 ([#3421][gh3421], [#3429][gh3429])
|
||||
|
||||
## 3.2.x series
|
||||
|
||||
### 3.2.4
|
||||
|
@ -533,3 +541,7 @@ For older release notes, [please see the version 2.x documentation][old-release-
|
|||
[gh3361]: https://github.com/tomchristie/django-rest-framework/issues/3361
|
||||
[gh3364]: https://github.com/tomchristie/django-rest-framework/issues/3364
|
||||
[gh3415]: https://github.com/tomchristie/django-rest-framework/issues/3415
|
||||
|
||||
<!-- 3.3.0 -->
|
||||
[gh3421]: https://github.com/tomchristie/django-rest-framework/pulls/3421
|
||||
[gh3429]: https://github.com/tomchristie/django-rest-framework/pull/3429
|
||||
|
|
|
@ -237,6 +237,7 @@ To submit new content, [open an issue][drf-create-issue] or [create a pull reque
|
|||
|
||||
### Misc
|
||||
|
||||
* [cookiecutter-django-rest][cookiecutter-django-rest] - A cookiecutter template that takes care of the setup and configuration so you can focus on making your REST apis awesome.
|
||||
* [djangorestrelationalhyperlink][djangorestrelationalhyperlink] - A hyperlinked serialiser that can can be used to alter relationships via hyperlinks, but otherwise like a hyperlink model serializer.
|
||||
* [django-rest-swagger][django-rest-swagger] - An API documentation generator for Swagger UI.
|
||||
* [django-rest-framework-proxy][django-rest-framework-proxy] - Proxy to redirect incoming request to another API server.
|
||||
|
@ -246,6 +247,7 @@ To submit new content, [open an issue][drf-create-issue] or [create a pull reque
|
|||
* [django-versatileimagefield][django-versatileimagefield] - Provides a drop-in replacement for Django's stock `ImageField` that makes it easy to serve images in multiple sizes/renditions from a single field. For DRF-specific implementation docs, [click here][django-versatileimagefield-drf-docs].
|
||||
* [drf-tracking][drf-tracking] - Utilities to track requests to DRF API views.
|
||||
* [django-rest-framework-braces][django-rest-framework-braces] - Collection of utilities for working with Django Rest Framework. The most notable ones are [FormSerializer](https://django-rest-framework-braces.readthedocs.org/en/latest/overview.html#formserializer) and [SerializerForm](https://django-rest-framework-braces.readthedocs.org/en/latest/overview.html#serializerform), which are adapters between DRF serializers and Django forms.
|
||||
* [drf-haystack][drf-haystack] - Haystack search for Django Rest Framework
|
||||
|
||||
## Other Resources
|
||||
|
||||
|
@ -345,3 +347,5 @@ To submit new content, [open an issue][drf-create-issue] or [create a pull reque
|
|||
[django-rest-framework-braces]: https://github.com/dealertrack/django-rest-framework-braces
|
||||
[dry-rest-permissions]: https://github.com/Helioscene/dry-rest-permissions
|
||||
[django-url-filter]: https://github.com/miki725/django-url-filter
|
||||
[cookiecutter-django-rest]: https://github.com/agconti/cookiecutter-django-rest
|
||||
[drf-haystack]: http://drf-haystack.readthedocs.org/en/latest/
|
||||
|
|
|
@ -136,7 +136,7 @@
|
|||
</div> <!--/.wrapper -->
|
||||
|
||||
<footer class="span12">
|
||||
<p>Documentation built with <a href="http://www.mkdocs.org/">MkDocs</a>.</a>
|
||||
<p>Documentation built with <a href="http://www.mkdocs.org/">MkDocs</a>.
|
||||
</p>
|
||||
</footer>
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
from django.contrib.auth import authenticate
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from rest_framework import exceptions, serializers
|
||||
from rest_framework import serializers
|
||||
|
||||
|
||||
class AuthTokenSerializer(serializers.Serializer):
|
||||
|
@ -18,13 +18,13 @@ class AuthTokenSerializer(serializers.Serializer):
|
|||
if user:
|
||||
if not user.is_active:
|
||||
msg = _('User account is disabled.')
|
||||
raise exceptions.ValidationError(msg)
|
||||
raise serializers.ValidationError(msg)
|
||||
else:
|
||||
msg = _('Unable to log in with provided credentials.')
|
||||
raise exceptions.ValidationError(msg)
|
||||
raise serializers.ValidationError(msg)
|
||||
else:
|
||||
msg = _('Must include "username" and "password".')
|
||||
raise exceptions.ValidationError(msg)
|
||||
raise serializers.ValidationError(msg)
|
||||
|
||||
attrs['user'] = user
|
||||
return attrs
|
||||
|
|
|
@ -1,60 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from south.db import db
|
||||
from south.v2 import SchemaMigration
|
||||
|
||||
try:
|
||||
from django.contrib.auth import get_user_model
|
||||
except ImportError: # django < 1.5
|
||||
from django.contrib.auth.models import User
|
||||
else:
|
||||
User = get_user_model()
|
||||
|
||||
|
||||
class Migration(SchemaMigration):
|
||||
|
||||
def forwards(self, orm):
|
||||
# Adding model 'Token'
|
||||
db.create_table('authtoken_token', (
|
||||
('key', self.gf('django.db.models.fields.CharField')(max_length=40, primary_key=True)),
|
||||
('user', self.gf('django.db.models.fields.related.OneToOneField')(related_name='auth_token', unique=True, to=orm['%s.%s' % (User._meta.app_label, User._meta.object_name)])),
|
||||
('created', self.gf('django.db.models.fields.DateTimeField')(auto_now_add=True, blank=True)),
|
||||
))
|
||||
db.send_create_signal('authtoken', ['Token'])
|
||||
|
||||
def backwards(self, orm):
|
||||
# Deleting model 'Token'
|
||||
db.delete_table('authtoken_token')
|
||||
|
||||
models = {
|
||||
'auth.group': {
|
||||
'Meta': {'object_name': 'Group'},
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
|
||||
'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
|
||||
},
|
||||
'auth.permission': {
|
||||
'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'},
|
||||
'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
|
||||
'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
|
||||
},
|
||||
"%s.%s" % (User._meta.app_label, User._meta.module_name): {
|
||||
'Meta': {'object_name': User._meta.module_name, 'db_table': repr(User._meta.db_table)},
|
||||
},
|
||||
'authtoken.token': {
|
||||
'Meta': {'object_name': 'Token'},
|
||||
'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
|
||||
'key': ('django.db.models.fields.CharField', [], {'max_length': '40', 'primary_key': 'True'}),
|
||||
'user': ('django.db.models.fields.related.OneToOneField', [], {'related_name': "'auth_token'", 'unique': 'True', 'to': "orm['%s.%s']" % (User._meta.app_label, User._meta.object_name)})
|
||||
},
|
||||
'contenttypes.contenttype': {
|
||||
'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
|
||||
'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
|
||||
'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
|
||||
}
|
||||
}
|
||||
|
||||
complete_apps = ['authtoken']
|
|
@ -62,16 +62,6 @@ def distinct(queryset, base):
|
|||
return queryset.distinct()
|
||||
|
||||
|
||||
# OrderedDict only available in Python 2.7.
|
||||
# This will always be the case in Django 1.7 and above, as these versions
|
||||
# no longer support Python 2.6.
|
||||
# For Django <= 1.6 and Python 2.6 fall back to SortedDict.
|
||||
try:
|
||||
from collections import OrderedDict
|
||||
except ImportError:
|
||||
from django.utils.datastructures import SortedDict as OrderedDict
|
||||
|
||||
|
||||
# contrib.postgres only supported from 1.8 onwards.
|
||||
try:
|
||||
from django.contrib.postgres import fields as postgres_fields
|
||||
|
@ -79,41 +69,30 @@ except ImportError:
|
|||
postgres_fields = None
|
||||
|
||||
|
||||
# JSONField is only supported from 1.9 onwards
|
||||
try:
|
||||
from django.contrib.postgres.fields import JSONField
|
||||
except ImportError:
|
||||
JSONField = None
|
||||
|
||||
|
||||
# django-filter is optional
|
||||
try:
|
||||
import django_filters
|
||||
except ImportError:
|
||||
django_filters = None
|
||||
|
||||
if django.VERSION >= (1, 6):
|
||||
def clean_manytomany_helptext(text):
|
||||
return text
|
||||
else:
|
||||
# Up to version 1.5 many to many fields automatically suffix
|
||||
# the `help_text` attribute with hardcoded text.
|
||||
def clean_manytomany_helptext(text):
|
||||
if text.endswith(' Hold down "Control", or "Command" on a Mac, to select more than one.'):
|
||||
text = text[:-69]
|
||||
return text
|
||||
|
||||
# Django-guardian is optional. Import only if guardian is in INSTALLED_APPS
|
||||
# Fixes (#1712). We keep the try/except for the test suite.
|
||||
guardian = None
|
||||
try:
|
||||
if 'guardian' in settings.INSTALLED_APPS:
|
||||
import guardian
|
||||
import guardian.shortcuts # Fixes #1624
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
|
||||
def get_model_name(model_cls):
|
||||
try:
|
||||
return model_cls._meta.model_name
|
||||
except AttributeError:
|
||||
# < 1.6 used module_name instead of model_name
|
||||
return model_cls._meta.module_name
|
||||
|
||||
|
||||
class CustomValidatorMessage(object):
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.message = kwargs.pop('message', self.message)
|
||||
|
@ -136,32 +115,6 @@ class MaxLengthValidator(CustomValidatorMessage, MaxLengthValidator):
|
|||
pass
|
||||
|
||||
|
||||
# URLValidator only accepts `message` in 1.6+
|
||||
if django.VERSION >= (1, 6):
|
||||
from django.core.validators import URLValidator
|
||||
else:
|
||||
from django.core.validators import URLValidator as DjangoURLValidator
|
||||
|
||||
|
||||
class URLValidator(DjangoURLValidator):
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.message = kwargs.pop('message', self.message)
|
||||
super(URLValidator, self).__init__(*args, **kwargs)
|
||||
|
||||
|
||||
# EmailValidator requires explicit regex prior to 1.6+
|
||||
if django.VERSION >= (1, 6):
|
||||
from django.core.validators import EmailValidator
|
||||
else:
|
||||
from django.core.validators import EmailValidator as DjangoEmailValidator
|
||||
from django.core.validators import email_re
|
||||
|
||||
|
||||
class EmailValidator(DjangoEmailValidator):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(EmailValidator, self).__init__(email_re, *args, **kwargs)
|
||||
|
||||
|
||||
# PATCH method is not implemented by Django
|
||||
if 'patch' not in View.http_method_names:
|
||||
View.http_method_names = View.http_method_names + ['patch']
|
||||
|
|
|
@ -30,10 +30,10 @@ def _force_text_recursive(data):
|
|||
return ReturnList(ret, serializer=data.serializer)
|
||||
return data
|
||||
elif isinstance(data, dict):
|
||||
ret = dict([
|
||||
(key, _force_text_recursive(value))
|
||||
ret = {
|
||||
key: _force_text_recursive(value)
|
||||
for key, value in data.items()
|
||||
])
|
||||
}
|
||||
if isinstance(data, ReturnDict):
|
||||
return ReturnDict(ret, serializer=data.serializer)
|
||||
return data
|
||||
|
|
|
@ -5,13 +5,17 @@ import copy
|
|||
import datetime
|
||||
import decimal
|
||||
import inspect
|
||||
import json
|
||||
import re
|
||||
import uuid
|
||||
from collections import OrderedDict
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.exceptions import ValidationError as DjangoValidationError
|
||||
from django.core.exceptions import ObjectDoesNotExist
|
||||
from django.core.validators import RegexValidator, ip_address_validators
|
||||
from django.core.validators import (
|
||||
EmailValidator, RegexValidator, URLValidator, ip_address_validators
|
||||
)
|
||||
from django.forms import FilePathField as DjangoFilePathField
|
||||
from django.forms import ImageField as DjangoImageField
|
||||
from django.utils import six, timezone
|
||||
|
@ -23,9 +27,9 @@ from django.utils.translation import ugettext_lazy as _
|
|||
|
||||
from rest_framework import ISO_8601
|
||||
from rest_framework.compat import (
|
||||
EmailValidator, MaxLengthValidator, MaxValueValidator, MinLengthValidator,
|
||||
MinValueValidator, OrderedDict, URLValidator, duration_string,
|
||||
parse_duration, unicode_repr, unicode_to_repr
|
||||
MaxLengthValidator, MaxValueValidator, MinLengthValidator,
|
||||
MinValueValidator, duration_string, parse_duration, unicode_repr,
|
||||
unicode_to_repr
|
||||
)
|
||||
from rest_framework.exceptions import ValidationError
|
||||
from rest_framework.settings import api_settings
|
||||
|
@ -600,8 +604,8 @@ class BooleanField(Field):
|
|||
}
|
||||
default_empty_html = False
|
||||
initial = False
|
||||
TRUE_VALUES = set(('t', 'T', 'true', 'True', 'TRUE', '1', 1, True))
|
||||
FALSE_VALUES = set(('f', 'F', 'false', 'False', 'FALSE', '0', 0, 0.0, False))
|
||||
TRUE_VALUES = {'t', 'T', 'true', 'True', 'TRUE', '1', 1, True}
|
||||
FALSE_VALUES = {'f', 'F', 'false', 'False', 'FALSE', '0', 0, 0.0, False}
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
assert 'allow_null' not in kwargs, '`allow_null` is not a valid option. Use `NullBooleanField` instead.'
|
||||
|
@ -630,9 +634,9 @@ class NullBooleanField(Field):
|
|||
'invalid': _('"{input}" is not a valid boolean.')
|
||||
}
|
||||
initial = None
|
||||
TRUE_VALUES = set(('t', 'T', 'true', 'True', 'TRUE', '1', 1, True))
|
||||
FALSE_VALUES = set(('f', 'F', 'false', 'False', 'FALSE', '0', 0, 0.0, False))
|
||||
NULL_VALUES = set(('n', 'N', 'null', 'Null', 'NULL', '', None))
|
||||
TRUE_VALUES = {'t', 'T', 'true', 'True', 'TRUE', '1', 1, True}
|
||||
FALSE_VALUES = {'f', 'F', 'false', 'False', 'FALSE', '0', 0, 0.0, False}
|
||||
NULL_VALUES = {'n', 'N', 'null', 'Null', 'NULL', '', None}
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
assert 'allow_null' not in kwargs, '`allow_null` is not a valid option.'
|
||||
|
@ -885,12 +889,11 @@ class DecimalField(Field):
|
|||
}
|
||||
MAX_STRING_LENGTH = 1000 # Guard against malicious string inputs.
|
||||
|
||||
coerce_to_string = api_settings.COERCE_DECIMAL_TO_STRING
|
||||
|
||||
def __init__(self, max_digits, decimal_places, coerce_to_string=None, max_value=None, min_value=None, **kwargs):
|
||||
self.max_digits = max_digits
|
||||
self.decimal_places = decimal_places
|
||||
self.coerce_to_string = coerce_to_string if (coerce_to_string is not None) else self.coerce_to_string
|
||||
if coerce_to_string is not None:
|
||||
self.coerce_to_string = coerce_to_string
|
||||
|
||||
self.max_value = max_value
|
||||
self.min_value = min_value
|
||||
|
@ -970,12 +973,14 @@ class DecimalField(Field):
|
|||
return value
|
||||
|
||||
def to_representation(self, value):
|
||||
coerce_to_string = getattr(self, 'coerce_to_string', api_settings.COERCE_DECIMAL_TO_STRING)
|
||||
|
||||
if not isinstance(value, decimal.Decimal):
|
||||
value = decimal.Decimal(six.text_type(value).strip())
|
||||
|
||||
quantized = self.quantize(value)
|
||||
|
||||
if not self.coerce_to_string:
|
||||
if not coerce_to_string:
|
||||
return quantized
|
||||
return '{0:f}'.format(quantized)
|
||||
|
||||
|
@ -997,15 +1002,15 @@ class DateTimeField(Field):
|
|||
'invalid': _('Datetime has wrong format. Use one of these formats instead: {format}.'),
|
||||
'date': _('Expected a datetime but got a date.'),
|
||||
}
|
||||
format = api_settings.DATETIME_FORMAT
|
||||
input_formats = api_settings.DATETIME_INPUT_FORMATS
|
||||
default_timezone = timezone.get_default_timezone() if settings.USE_TZ else None
|
||||
datetime_parser = datetime.datetime.strptime
|
||||
|
||||
def __init__(self, format=empty, input_formats=None, default_timezone=None, *args, **kwargs):
|
||||
self.format = format if format is not empty else self.format
|
||||
self.input_formats = input_formats if input_formats is not None else self.input_formats
|
||||
self.default_timezone = default_timezone if default_timezone is not None else self.default_timezone
|
||||
if format is not empty:
|
||||
self.format = format
|
||||
if input_formats is not None:
|
||||
self.input_formats = input_formats
|
||||
if default_timezone is not None:
|
||||
self.timezone = default_timezone
|
||||
super(DateTimeField, self).__init__(*args, **kwargs)
|
||||
|
||||
def enforce_timezone(self, value):
|
||||
|
@ -1013,21 +1018,28 @@ class DateTimeField(Field):
|
|||
When `self.default_timezone` is `None`, always return naive datetimes.
|
||||
When `self.default_timezone` is not `None`, always return aware datetimes.
|
||||
"""
|
||||
if (self.default_timezone is not None) and not timezone.is_aware(value):
|
||||
return timezone.make_aware(value, self.default_timezone)
|
||||
elif (self.default_timezone is None) and timezone.is_aware(value):
|
||||
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)
|
||||
elif (field_timezone is None) and timezone.is_aware(value):
|
||||
return timezone.make_naive(value, timezone.UTC())
|
||||
return value
|
||||
|
||||
def default_timezone(self):
|
||||
return timezone.get_default_timezone() if settings.USE_TZ else None
|
||||
|
||||
def to_internal_value(self, value):
|
||||
input_formats = getattr(self, 'input_formats', api_settings.DATETIME_INPUT_FORMATS)
|
||||
|
||||
if isinstance(value, datetime.date) and not isinstance(value, datetime.datetime):
|
||||
self.fail('date')
|
||||
|
||||
if isinstance(value, datetime.datetime):
|
||||
return self.enforce_timezone(value)
|
||||
|
||||
for format in self.input_formats:
|
||||
if format.lower() == ISO_8601:
|
||||
for input_format in input_formats:
|
||||
if input_format.lower() == ISO_8601:
|
||||
try:
|
||||
parsed = parse_datetime(value)
|
||||
except (ValueError, TypeError):
|
||||
|
@ -1037,25 +1049,27 @@ class DateTimeField(Field):
|
|||
return self.enforce_timezone(parsed)
|
||||
else:
|
||||
try:
|
||||
parsed = self.datetime_parser(value, format)
|
||||
parsed = self.datetime_parser(value, input_format)
|
||||
except (ValueError, TypeError):
|
||||
pass
|
||||
else:
|
||||
return self.enforce_timezone(parsed)
|
||||
|
||||
humanized_format = humanize_datetime.datetime_formats(self.input_formats)
|
||||
humanized_format = humanize_datetime.datetime_formats(input_formats)
|
||||
self.fail('invalid', format=humanized_format)
|
||||
|
||||
def to_representation(self, value):
|
||||
if self.format is None:
|
||||
output_format = getattr(self, 'format', api_settings.DATETIME_FORMAT)
|
||||
|
||||
if output_format is None:
|
||||
return value
|
||||
|
||||
if self.format.lower() == ISO_8601:
|
||||
if output_format.lower() == ISO_8601:
|
||||
value = value.isoformat()
|
||||
if value.endswith('+00:00'):
|
||||
value = value[:-6] + 'Z'
|
||||
return value
|
||||
return value.strftime(self.format)
|
||||
return value.strftime(output_format)
|
||||
|
||||
|
||||
class DateField(Field):
|
||||
|
@ -1063,24 +1077,26 @@ class DateField(Field):
|
|||
'invalid': _('Date has wrong format. Use one of these formats instead: {format}.'),
|
||||
'datetime': _('Expected a date but got a datetime.'),
|
||||
}
|
||||
format = api_settings.DATE_FORMAT
|
||||
input_formats = api_settings.DATE_INPUT_FORMATS
|
||||
datetime_parser = datetime.datetime.strptime
|
||||
|
||||
def __init__(self, format=empty, input_formats=None, *args, **kwargs):
|
||||
self.format = format if format is not empty else self.format
|
||||
self.input_formats = input_formats if input_formats is not None else self.input_formats
|
||||
if format is not empty:
|
||||
self.format = format
|
||||
if input_formats is not None:
|
||||
self.input_formats = input_formats
|
||||
super(DateField, self).__init__(*args, **kwargs)
|
||||
|
||||
def to_internal_value(self, value):
|
||||
input_formats = getattr(self, 'input_formats', api_settings.DATE_INPUT_FORMATS)
|
||||
|
||||
if isinstance(value, datetime.datetime):
|
||||
self.fail('datetime')
|
||||
|
||||
if isinstance(value, datetime.date):
|
||||
return value
|
||||
|
||||
for format in self.input_formats:
|
||||
if format.lower() == ISO_8601:
|
||||
for input_format in input_formats:
|
||||
if input_format.lower() == ISO_8601:
|
||||
try:
|
||||
parsed = parse_date(value)
|
||||
except (ValueError, TypeError):
|
||||
|
@ -1090,20 +1106,22 @@ class DateField(Field):
|
|||
return parsed
|
||||
else:
|
||||
try:
|
||||
parsed = self.datetime_parser(value, format)
|
||||
parsed = self.datetime_parser(value, input_format)
|
||||
except (ValueError, TypeError):
|
||||
pass
|
||||
else:
|
||||
return parsed.date()
|
||||
|
||||
humanized_format = humanize_datetime.date_formats(self.input_formats)
|
||||
humanized_format = humanize_datetime.date_formats(input_formats)
|
||||
self.fail('invalid', format=humanized_format)
|
||||
|
||||
def to_representation(self, value):
|
||||
output_format = getattr(self, 'format', api_settings.DATE_FORMAT)
|
||||
|
||||
if not value:
|
||||
return None
|
||||
|
||||
if self.format is None:
|
||||
if output_format is None:
|
||||
return value
|
||||
|
||||
# Applying a `DateField` to a datetime value is almost always
|
||||
|
@ -1115,33 +1133,35 @@ class DateField(Field):
|
|||
'read-only field and deal with timezone issues explicitly.'
|
||||
)
|
||||
|
||||
if self.format.lower() == ISO_8601:
|
||||
if output_format.lower() == ISO_8601:
|
||||
if (isinstance(value, str)):
|
||||
value = datetime.datetime.strptime(value, '%Y-%m-%d').date()
|
||||
return value.isoformat()
|
||||
|
||||
return value.strftime(self.format)
|
||||
return value.strftime(output_format)
|
||||
|
||||
|
||||
class TimeField(Field):
|
||||
default_error_messages = {
|
||||
'invalid': _('Time has wrong format. Use one of these formats instead: {format}.'),
|
||||
}
|
||||
format = api_settings.TIME_FORMAT
|
||||
input_formats = api_settings.TIME_INPUT_FORMATS
|
||||
datetime_parser = datetime.datetime.strptime
|
||||
|
||||
def __init__(self, format=empty, input_formats=None, *args, **kwargs):
|
||||
self.format = format if format is not empty else self.format
|
||||
self.input_formats = input_formats if input_formats is not None else self.input_formats
|
||||
if format is not empty:
|
||||
self.format = format
|
||||
if input_formats is not None:
|
||||
self.input_formats = input_formats
|
||||
super(TimeField, self).__init__(*args, **kwargs)
|
||||
|
||||
def to_internal_value(self, value):
|
||||
input_formats = getattr(self, 'input_formats', api_settings.TIME_INPUT_FORMATS)
|
||||
|
||||
if isinstance(value, datetime.time):
|
||||
return value
|
||||
|
||||
for format in self.input_formats:
|
||||
if format.lower() == ISO_8601:
|
||||
for input_format in input_formats:
|
||||
if input_format.lower() == ISO_8601:
|
||||
try:
|
||||
parsed = parse_time(value)
|
||||
except (ValueError, TypeError):
|
||||
|
@ -1151,17 +1171,19 @@ class TimeField(Field):
|
|||
return parsed
|
||||
else:
|
||||
try:
|
||||
parsed = self.datetime_parser(value, format)
|
||||
parsed = self.datetime_parser(value, input_format)
|
||||
except (ValueError, TypeError):
|
||||
pass
|
||||
else:
|
||||
return parsed.time()
|
||||
|
||||
humanized_format = humanize_datetime.time_formats(self.input_formats)
|
||||
humanized_format = humanize_datetime.time_formats(input_formats)
|
||||
self.fail('invalid', format=humanized_format)
|
||||
|
||||
def to_representation(self, value):
|
||||
if self.format is None:
|
||||
output_format = getattr(self, 'format', api_settings.TIME_FORMAT)
|
||||
|
||||
if output_format is None:
|
||||
return value
|
||||
|
||||
# Applying a `TimeField` to a datetime value is almost always
|
||||
|
@ -1173,9 +1195,9 @@ class TimeField(Field):
|
|||
'read-only field and deal with timezone issues explicitly.'
|
||||
)
|
||||
|
||||
if self.format.lower() == ISO_8601:
|
||||
if output_format.lower() == ISO_8601:
|
||||
return value.isoformat()
|
||||
return value.strftime(self.format)
|
||||
return value.strftime(output_format)
|
||||
|
||||
|
||||
class DurationField(Field):
|
||||
|
@ -1219,9 +1241,9 @@ class ChoiceField(Field):
|
|||
# Map the string representation of choices to the underlying value.
|
||||
# Allows us to deal with eg. integer choices while supporting either
|
||||
# integer or string input, but still get the correct datatype out.
|
||||
self.choice_strings_to_values = dict([
|
||||
(six.text_type(key), key) for key in self.choices.keys()
|
||||
])
|
||||
self.choice_strings_to_values = {
|
||||
six.text_type(key): key for key in self.choices.keys()
|
||||
}
|
||||
|
||||
self.allow_blank = kwargs.pop('allow_blank', False)
|
||||
|
||||
|
@ -1280,15 +1302,15 @@ class MultipleChoiceField(ChoiceField):
|
|||
if not self.allow_empty and len(data) == 0:
|
||||
self.fail('empty')
|
||||
|
||||
return set([
|
||||
return {
|
||||
super(MultipleChoiceField, self).to_internal_value(item)
|
||||
for item in data
|
||||
])
|
||||
}
|
||||
|
||||
def to_representation(self, value):
|
||||
return set([
|
||||
return {
|
||||
self.choice_strings_to_values.get(six.text_type(item), item) for item in value
|
||||
])
|
||||
}
|
||||
|
||||
|
||||
class FilePathField(ChoiceField):
|
||||
|
@ -1318,12 +1340,12 @@ class FileField(Field):
|
|||
'empty': _('The submitted file is empty.'),
|
||||
'max_length': _('Ensure this filename has at most {max_length} characters (it has {length}).'),
|
||||
}
|
||||
use_url = api_settings.UPLOADED_FILES_USE_URL
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.max_length = kwargs.pop('max_length', None)
|
||||
self.allow_empty_file = kwargs.pop('allow_empty_file', False)
|
||||
self.use_url = kwargs.pop('use_url', self.use_url)
|
||||
if 'use_url' in kwargs:
|
||||
self.use_url = kwargs.pop('use_url')
|
||||
super(FileField, self).__init__(*args, **kwargs)
|
||||
|
||||
def to_internal_value(self, data):
|
||||
|
@ -1344,10 +1366,12 @@ class FileField(Field):
|
|||
return data
|
||||
|
||||
def to_representation(self, value):
|
||||
use_url = getattr(self, 'use_url', api_settings.UPLOADED_FILES_USE_URL)
|
||||
|
||||
if not value:
|
||||
return None
|
||||
|
||||
if self.use_url:
|
||||
if use_url:
|
||||
if not getattr(value, 'url', None):
|
||||
# If the file has not been saved it may not have a URL.
|
||||
return None
|
||||
|
@ -1484,19 +1508,50 @@ class DictField(Field):
|
|||
data = html.parse_html_dict(data)
|
||||
if not isinstance(data, dict):
|
||||
self.fail('not_a_dict', input_type=type(data).__name__)
|
||||
return dict([
|
||||
(six.text_type(key), self.child.run_validation(value))
|
||||
return {
|
||||
six.text_type(key): self.child.run_validation(value)
|
||||
for key, value in data.items()
|
||||
])
|
||||
}
|
||||
|
||||
def to_representation(self, value):
|
||||
"""
|
||||
List of object instances -> List of dicts of primitive datatypes.
|
||||
"""
|
||||
return dict([
|
||||
(six.text_type(key), self.child.to_representation(val))
|
||||
return {
|
||||
six.text_type(key): self.child.to_representation(val)
|
||||
for key, val in value.items()
|
||||
])
|
||||
}
|
||||
|
||||
|
||||
class JSONField(Field):
|
||||
default_error_messages = {
|
||||
'invalid': _('Value must be valid JSON.')
|
||||
}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.binary = kwargs.pop('binary', False)
|
||||
super(JSONField, self).__init__(*args, **kwargs)
|
||||
|
||||
def to_internal_value(self, data):
|
||||
try:
|
||||
if self.binary:
|
||||
if isinstance(data, six.binary_type):
|
||||
data = data.decode('utf-8')
|
||||
return json.loads(data)
|
||||
else:
|
||||
json.dumps(data)
|
||||
except (TypeError, ValueError):
|
||||
self.fail('invalid')
|
||||
return data
|
||||
|
||||
def to_representation(self, value):
|
||||
if self.binary:
|
||||
value = json.dumps(value)
|
||||
# On python 2.x the return type for json.dumps() is underspecified.
|
||||
# On python 3.x json.dumps() returns unicode strings.
|
||||
if isinstance(value, six.text_type):
|
||||
value = bytes(value.encode('utf-8'))
|
||||
return value
|
||||
|
||||
|
||||
# Miscellaneous field types...
|
||||
|
|
|
@ -11,9 +11,7 @@ from django.core.exceptions import ImproperlyConfigured
|
|||
from django.db import models
|
||||
from django.utils import six
|
||||
|
||||
from rest_framework.compat import (
|
||||
distinct, django_filters, get_model_name, guardian
|
||||
)
|
||||
from rest_framework.compat import distinct, django_filters, guardian
|
||||
from rest_framework.settings import api_settings
|
||||
|
||||
FilterSet = django_filters and django_filters.FilterSet or None
|
||||
|
@ -202,7 +200,7 @@ class DjangoObjectPermissionsFilter(BaseFilterBackend):
|
|||
model_cls = queryset.model
|
||||
kwargs = {
|
||||
'app_label': model_cls._meta.app_label,
|
||||
'model_name': get_model_name(model_cls)
|
||||
'model_name': model_cls._meta.model_name
|
||||
}
|
||||
permission = self.perm_format % kwargs
|
||||
if guardian.VERSION >= (1, 3):
|
||||
|
|
|
@ -8,12 +8,13 @@ to return this information in a more standardized way.
|
|||
"""
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from collections import OrderedDict
|
||||
|
||||
from django.core.exceptions import PermissionDenied
|
||||
from django.http import Http404
|
||||
from django.utils.encoding import force_text
|
||||
|
||||
from rest_framework import exceptions, serializers
|
||||
from rest_framework.compat import OrderedDict
|
||||
from rest_framework.request import clone_request
|
||||
from rest_framework.utils.field_mapping import ClassLookupDict
|
||||
|
||||
|
@ -76,7 +77,7 @@ class SimpleMetadata(BaseMetadata):
|
|||
the fields that are accepted for 'PUT' and 'POST' methods.
|
||||
"""
|
||||
actions = {}
|
||||
for method in set(['PUT', 'POST']) & set(view.allowed_methods):
|
||||
for method in {'PUT', 'POST'} & set(view.allowed_methods):
|
||||
view.request = clone_request(request, method)
|
||||
try:
|
||||
# Test global permissions
|
||||
|
|
|
@ -92,9 +92,6 @@ class DefaultContentNegotiation(BaseContentNegotiation):
|
|||
"""
|
||||
Given the incoming request, return a tokenised list of media
|
||||
type strings.
|
||||
|
||||
Allows URL style accept override. eg. "?accept=application/json"
|
||||
"""
|
||||
header = request.META.get('HTTP_ACCEPT', '*/*')
|
||||
header = request.query_params.get(self.settings.URL_ACCEPT_OVERRIDE, header)
|
||||
return [token.strip() for token in header.split(',')]
|
||||
|
|
|
@ -7,7 +7,7 @@ from __future__ import unicode_literals
|
|||
|
||||
import warnings
|
||||
from base64 import b64decode, b64encode
|
||||
from collections import namedtuple
|
||||
from collections import OrderedDict, namedtuple
|
||||
|
||||
from django.core.paginator import Paginator as DjangoPaginator
|
||||
from django.core.paginator import InvalidPage
|
||||
|
@ -16,7 +16,6 @@ from django.utils import six
|
|||
from django.utils.six.moves.urllib import parse as urlparse
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from rest_framework.compat import OrderedDict
|
||||
from rest_framework.exceptions import NotFound
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.settings import api_settings
|
||||
|
@ -80,11 +79,7 @@ def _get_displayed_page_numbers(current, final):
|
|||
|
||||
# We always include the first two pages, last two pages, and
|
||||
# two pages either side of the current page.
|
||||
included = set((
|
||||
1,
|
||||
current - 1, current, current + 1,
|
||||
final
|
||||
))
|
||||
included = {1, current - 1, current, current + 1, final}
|
||||
|
||||
# If the break would only exclude a single page number then we
|
||||
# may as well include the page number instead of the break.
|
||||
|
|
|
@ -5,8 +5,6 @@ from __future__ import unicode_literals
|
|||
|
||||
from django.http import Http404
|
||||
|
||||
from rest_framework.compat import get_model_name
|
||||
|
||||
SAFE_METHODS = ('GET', 'HEAD', 'OPTIONS')
|
||||
|
||||
|
||||
|
@ -104,7 +102,7 @@ class DjangoModelPermissions(BasePermission):
|
|||
"""
|
||||
kwargs = {
|
||||
'app_label': model_cls._meta.app_label,
|
||||
'model_name': get_model_name(model_cls)
|
||||
'model_name': model_cls._meta.model_name
|
||||
}
|
||||
return [perm % kwargs for perm in self.perms_map[method]]
|
||||
|
||||
|
@ -166,7 +164,7 @@ class DjangoObjectPermissions(DjangoModelPermissions):
|
|||
def get_required_object_permissions(self, method, model_cls):
|
||||
kwargs = {
|
||||
'app_label': model_cls._meta.app_label,
|
||||
'model_name': get_model_name(model_cls)
|
||||
'model_name': model_cls._meta.model_name
|
||||
}
|
||||
return [perm % kwargs for perm in self.perms_map[method]]
|
||||
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
# coding: utf-8
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from collections import OrderedDict
|
||||
|
||||
from django.core.exceptions import ImproperlyConfigured, ObjectDoesNotExist
|
||||
from django.core.urlresolvers import (
|
||||
NoReverseMatch, Resolver404, get_script_prefix, resolve
|
||||
|
@ -12,7 +14,6 @@ from django.utils.encoding import smart_text
|
|||
from django.utils.six.moves.urllib import parse as urlparse
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from rest_framework.compat import OrderedDict
|
||||
from rest_framework.fields import (
|
||||
Field, empty, get_attribute, is_simple_callable, iter_options
|
||||
)
|
||||
|
|
|
@ -4,7 +4,7 @@ Renderers are used to serialize a response into specific media types.
|
|||
They give us a generic way of being able to handle various media types
|
||||
on the response, such as JSON encoded data or HTML output.
|
||||
|
||||
REST framework also provides an HTML renderer the renders the browsable API.
|
||||
REST framework also provides an HTML renderer that renders the browsable API.
|
||||
"""
|
||||
from __future__ import unicode_literals
|
||||
|
||||
|
@ -420,9 +420,6 @@ class BrowsableAPIRenderer(BaseRenderer):
|
|||
if method not in view.allowed_methods:
|
||||
return # Not a valid method
|
||||
|
||||
if not api_settings.FORM_METHOD_OVERRIDE:
|
||||
return # Cannot use form overloading
|
||||
|
||||
try:
|
||||
view.check_permissions(request)
|
||||
if obj is not None:
|
||||
|
@ -530,13 +527,6 @@ class BrowsableAPIRenderer(BaseRenderer):
|
|||
instance = None
|
||||
|
||||
with override_method(view, request, method) as request:
|
||||
# If we're not using content overloading there's no point in
|
||||
# supplying a generic form, as the view won't treat the form's
|
||||
# value as the content of the request.
|
||||
if not (api_settings.FORM_CONTENT_OVERRIDE and
|
||||
api_settings.FORM_CONTENTTYPE_OVERRIDE):
|
||||
return None
|
||||
|
||||
# Check permissions
|
||||
if not self.show_form_for_method(view, method, request, instance):
|
||||
return
|
||||
|
@ -564,26 +554,20 @@ class BrowsableAPIRenderer(BaseRenderer):
|
|||
|
||||
# Generate a generic form that includes a content type field,
|
||||
# and a content field.
|
||||
content_type_field = api_settings.FORM_CONTENTTYPE_OVERRIDE
|
||||
content_field = api_settings.FORM_CONTENT_OVERRIDE
|
||||
|
||||
media_types = [parser.media_type for parser in view.parser_classes]
|
||||
choices = [(media_type, media_type) for media_type in media_types]
|
||||
initial = media_types[0]
|
||||
|
||||
# NB. http://jacobian.org/writing/dynamic-form-generation/
|
||||
class GenericContentForm(forms.Form):
|
||||
def __init__(self):
|
||||
super(GenericContentForm, self).__init__()
|
||||
|
||||
self.fields[content_type_field] = forms.ChoiceField(
|
||||
_content_type = forms.ChoiceField(
|
||||
label='Media type',
|
||||
choices=choices,
|
||||
initial=initial
|
||||
initial=initial,
|
||||
widget=forms.Select(attrs={'data-override': 'content-type'})
|
||||
)
|
||||
self.fields[content_field] = forms.CharField(
|
||||
_content = forms.CharField(
|
||||
label='Content',
|
||||
widget=forms.Textarea,
|
||||
widget=forms.Textarea(attrs={'data-override': 'content'}),
|
||||
initial=content
|
||||
)
|
||||
|
||||
|
|
|
@ -86,7 +86,7 @@ def clone_request(request, method):
|
|||
ret._full_data = request._full_data
|
||||
ret._content_type = request._content_type
|
||||
ret._stream = request._stream
|
||||
ret._method = method
|
||||
ret.method = method
|
||||
if hasattr(request, '_user'):
|
||||
ret._user = request._user
|
||||
if hasattr(request, '_auth'):
|
||||
|
@ -129,11 +129,6 @@ class Request(object):
|
|||
- authentication_classes(list/tuple). The authentications used to try
|
||||
authenticating the request's user.
|
||||
"""
|
||||
|
||||
_METHOD_PARAM = api_settings.FORM_METHOD_OVERRIDE
|
||||
_CONTENT_PARAM = api_settings.FORM_CONTENT_OVERRIDE
|
||||
_CONTENTTYPE_PARAM = api_settings.FORM_CONTENTTYPE_OVERRIDE
|
||||
|
||||
def __init__(self, request, parsers=None, authenticators=None,
|
||||
negotiator=None, parser_context=None):
|
||||
self._request = request
|
||||
|
@ -144,7 +139,6 @@ class Request(object):
|
|||
self._data = Empty
|
||||
self._files = Empty
|
||||
self._full_data = Empty
|
||||
self._method = Empty
|
||||
self._content_type = Empty
|
||||
self._stream = Empty
|
||||
|
||||
|
@ -162,30 +156,10 @@ class Request(object):
|
|||
def _default_negotiator(self):
|
||||
return api_settings.DEFAULT_CONTENT_NEGOTIATION_CLASS()
|
||||
|
||||
@property
|
||||
def method(self):
|
||||
"""
|
||||
Returns the HTTP method.
|
||||
|
||||
This allows the `method` to be overridden by using a hidden `form`
|
||||
field on a form POST request.
|
||||
"""
|
||||
if not _hasattr(self, '_method'):
|
||||
self._load_method_and_content_type()
|
||||
return self._method
|
||||
|
||||
@property
|
||||
def content_type(self):
|
||||
"""
|
||||
Returns the content type header.
|
||||
|
||||
This should be used instead of `request.META.get('HTTP_CONTENT_TYPE')`,
|
||||
as it allows the content type to be overridden by using a hidden form
|
||||
field on a form POST request.
|
||||
"""
|
||||
if not _hasattr(self, '_content_type'):
|
||||
self._load_method_and_content_type()
|
||||
return self._content_type
|
||||
meta = self._request.META
|
||||
return meta.get('CONTENT_TYPE', meta.get('HTTP_CONTENT_TYPE', ''))
|
||||
|
||||
@property
|
||||
def stream(self):
|
||||
|
@ -265,9 +239,6 @@ class Request(object):
|
|||
"""
|
||||
Parses the request content into `self.data`.
|
||||
"""
|
||||
if not _hasattr(self, '_content_type'):
|
||||
self._load_method_and_content_type()
|
||||
|
||||
if not _hasattr(self, '_data'):
|
||||
self._data, self._files = self._parse()
|
||||
if self._files:
|
||||
|
@ -276,32 +247,14 @@ class Request(object):
|
|||
else:
|
||||
self._full_data = self._data
|
||||
|
||||
def _load_method_and_content_type(self):
|
||||
"""
|
||||
Sets the method and content_type, and then check if they've
|
||||
been overridden.
|
||||
"""
|
||||
self._content_type = self.META.get('HTTP_CONTENT_TYPE',
|
||||
self.META.get('CONTENT_TYPE', ''))
|
||||
|
||||
self._perform_form_overloading()
|
||||
|
||||
if not _hasattr(self, '_method'):
|
||||
self._method = self._request.method
|
||||
|
||||
# Allow X-HTTP-METHOD-OVERRIDE header
|
||||
if 'HTTP_X_HTTP_METHOD_OVERRIDE' in self.META:
|
||||
self._method = self.META['HTTP_X_HTTP_METHOD_OVERRIDE'].upper()
|
||||
|
||||
def _load_stream(self):
|
||||
"""
|
||||
Return the content body of the request, as a stream.
|
||||
"""
|
||||
meta = self._request.META
|
||||
try:
|
||||
content_length = int(
|
||||
self.META.get(
|
||||
'CONTENT_LENGTH', self.META.get('HTTP_CONTENT_LENGTH')
|
||||
)
|
||||
meta.get('CONTENT_LENGTH', meta.get('HTTP_CONTENT_LENGTH', 0))
|
||||
)
|
||||
except (ValueError, TypeError):
|
||||
content_length = 0
|
||||
|
@ -313,50 +266,6 @@ class Request(object):
|
|||
else:
|
||||
self._stream = six.BytesIO(self.raw_post_data)
|
||||
|
||||
def _perform_form_overloading(self):
|
||||
"""
|
||||
If this is a form POST request, then we need to check if the method and
|
||||
content/content_type have been overridden by setting them in hidden
|
||||
form fields or not.
|
||||
"""
|
||||
|
||||
USE_FORM_OVERLOADING = (
|
||||
self._METHOD_PARAM or
|
||||
(self._CONTENT_PARAM and self._CONTENTTYPE_PARAM)
|
||||
)
|
||||
|
||||
# We only need to use form overloading on form POST requests.
|
||||
if (
|
||||
self._request.method != 'POST' or
|
||||
not USE_FORM_OVERLOADING or
|
||||
not is_form_media_type(self._content_type)
|
||||
):
|
||||
return
|
||||
|
||||
# At this point we're committed to parsing the request as form data.
|
||||
self._data = self._request.POST
|
||||
self._files = self._request.FILES
|
||||
self._full_data = self._data.copy()
|
||||
self._full_data.update(self._files)
|
||||
|
||||
# Method overloading - change the method and remove the param from the content.
|
||||
if (
|
||||
self._METHOD_PARAM and
|
||||
self._METHOD_PARAM in self._data
|
||||
):
|
||||
self._method = self._data[self._METHOD_PARAM].upper()
|
||||
|
||||
# Content overloading - modify the content type, and force re-parse.
|
||||
if (
|
||||
self._CONTENT_PARAM and
|
||||
self._CONTENTTYPE_PARAM and
|
||||
self._CONTENT_PARAM in self._data and
|
||||
self._CONTENTTYPE_PARAM in self._data
|
||||
):
|
||||
self._content_type = self._data[self._CONTENTTYPE_PARAM]
|
||||
self._stream = six.BytesIO(self._data[self._CONTENT_PARAM].encode(self.parser_context['encoding']))
|
||||
self._data, self._files, self._full_data = (Empty, Empty, Empty)
|
||||
|
||||
def _parse(self):
|
||||
"""
|
||||
Parse the request content, returning a two-tuple of (data, files)
|
||||
|
|
|
@ -98,7 +98,7 @@ class Response(SimpleTemplateResponse):
|
|||
state = super(Response, self).__getstate__()
|
||||
for key in (
|
||||
'accepted_renderer', 'renderer_context', 'resolver_match',
|
||||
'client', 'request', 'wsgi_request'
|
||||
'client', 'request', 'json', 'wsgi_request'
|
||||
):
|
||||
if key in state:
|
||||
del state[key]
|
||||
|
|
|
@ -22,7 +22,6 @@ def preserve_builtin_query_params(url, request=None):
|
|||
|
||||
overrides = [
|
||||
api_settings.URL_FORMAT_OVERRIDE,
|
||||
api_settings.URL_ACCEPT_OVERRIDE
|
||||
]
|
||||
|
||||
for param in overrides:
|
||||
|
|
|
@ -16,14 +16,13 @@ For example, you might have a `urls.py` that looks something like this:
|
|||
from __future__ import unicode_literals
|
||||
|
||||
import itertools
|
||||
from collections import namedtuple
|
||||
from collections import OrderedDict, namedtuple
|
||||
|
||||
from django.conf.urls import url
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
from django.core.urlresolvers import NoReverseMatch
|
||||
|
||||
from rest_framework import views
|
||||
from rest_framework.compat import OrderedDict
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.reverse import reverse
|
||||
from rest_framework.urlpatterns import format_suffix_patterns
|
||||
|
@ -175,7 +174,7 @@ class SimpleRouter(BaseRouter):
|
|||
url_path = initkwargs.pop("url_path", None) or methodname
|
||||
ret.append(Route(
|
||||
url=replace_methodname(route.url, url_path),
|
||||
mapping=dict((httpmethod, methodname) for httpmethod in httpmethods),
|
||||
mapping={httpmethod: methodname for httpmethod in httpmethods},
|
||||
name=replace_methodname(route.name, url_path),
|
||||
initkwargs=initkwargs,
|
||||
))
|
||||
|
|
|
@ -12,6 +12,8 @@ response content is handled by parsers and renderers.
|
|||
"""
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import warnings
|
||||
|
||||
from django.db import models
|
||||
from django.db.models.fields import Field as DjangoModelField
|
||||
from django.db.models.fields import FieldDoesNotExist
|
||||
|
@ -19,6 +21,7 @@ from django.utils.functional import cached_property
|
|||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from rest_framework.compat import DurationField as ModelDurationField
|
||||
from rest_framework.compat import JSONField as ModelJSONField
|
||||
from rest_framework.compat import postgres_fields, unicode_to_repr
|
||||
from rest_framework.utils import model_meta
|
||||
from rest_framework.utils.field_mapping import (
|
||||
|
@ -51,6 +54,8 @@ LIST_SERIALIZER_KWARGS = (
|
|||
'instance', 'data', 'partial', 'context', 'allow_null'
|
||||
)
|
||||
|
||||
ALL_FIELDS = '__all__'
|
||||
|
||||
|
||||
# BaseSerializer
|
||||
# --------------
|
||||
|
@ -120,10 +125,10 @@ class BaseSerializer(Field):
|
|||
}
|
||||
if allow_empty is not None:
|
||||
list_kwargs['allow_empty'] = allow_empty
|
||||
list_kwargs.update(dict([
|
||||
(key, value) for key, value in kwargs.items()
|
||||
list_kwargs.update({
|
||||
key: value for key, value in kwargs.items()
|
||||
if key in LIST_SERIALIZER_KWARGS
|
||||
]))
|
||||
})
|
||||
meta = getattr(cls, 'Meta', None)
|
||||
list_serializer_class = getattr(meta, 'list_serializer_class', ListSerializer)
|
||||
return list_serializer_class(*args, **list_kwargs)
|
||||
|
@ -166,6 +171,12 @@ class BaseSerializer(Field):
|
|||
"For example: 'serializer.save(owner=request.user)'.'"
|
||||
)
|
||||
|
||||
assert not hasattr(self, '_data'), (
|
||||
"You cannot call `.save()` after accessing `serializer.data`."
|
||||
"If you need to access data before committing to the database then "
|
||||
"inspect 'serializer.validated_data' instead. "
|
||||
)
|
||||
|
||||
validated_data = dict(
|
||||
list(self.validated_data.items()) +
|
||||
list(kwargs.items())
|
||||
|
@ -294,10 +305,10 @@ def get_validation_error_detail(exc):
|
|||
elif isinstance(exc.detail, dict):
|
||||
# If errors may be a dict we use the standard {key: list of values}.
|
||||
# Here we ensure that all the values are *lists* of errors.
|
||||
return dict([
|
||||
(key, value if isinstance(value, list) else [value])
|
||||
return {
|
||||
key: value if isinstance(value, list) else [value]
|
||||
for key, value in exc.detail.items()
|
||||
])
|
||||
}
|
||||
elif isinstance(exc.detail, list):
|
||||
# Errors raised as a list are non-field errors.
|
||||
return {
|
||||
|
@ -780,6 +791,8 @@ class ModelSerializer(Serializer):
|
|||
}
|
||||
if ModelDurationField is not None:
|
||||
serializer_field_mapping[ModelDurationField] = DurationField
|
||||
if ModelJSONField is not None:
|
||||
serializer_field_mapping[ModelJSONField] = JSONField
|
||||
serializer_related_field = PrimaryKeyRelatedField
|
||||
serializer_url_field = HyperlinkedIdentityField
|
||||
serializer_choice_field = ChoiceField
|
||||
|
@ -791,7 +804,7 @@ class ModelSerializer(Serializer):
|
|||
# you'll also need to ensure you update the `create` method on any generic
|
||||
# views, to correctly handle the 'Location' response header for
|
||||
# "HTTP 201 Created" responses.
|
||||
url_field_name = api_settings.URL_FIELD_NAME
|
||||
url_field_name = None
|
||||
|
||||
# Default `create` and `update` behavior...
|
||||
def create(self, validated_data):
|
||||
|
@ -874,6 +887,9 @@ class ModelSerializer(Serializer):
|
|||
Return the dict of field names -> field instances that should be
|
||||
used for `self.fields` when instantiating the serializer.
|
||||
"""
|
||||
if self.url_field_name is None:
|
||||
self.url_field_name = api_settings.URL_FIELD_NAME
|
||||
|
||||
assert hasattr(self, 'Meta'), (
|
||||
'Class {serializer_class} missing "Meta" attribute'.format(
|
||||
serializer_class=self.__class__.__name__
|
||||
|
@ -948,10 +964,10 @@ class ModelSerializer(Serializer):
|
|||
fields = getattr(self.Meta, 'fields', None)
|
||||
exclude = getattr(self.Meta, 'exclude', None)
|
||||
|
||||
if fields and not isinstance(fields, (list, tuple)):
|
||||
if fields and fields != ALL_FIELDS and not isinstance(fields, (list, tuple)):
|
||||
raise TypeError(
|
||||
'The `fields` option must be a list or tuple. Got %s.' %
|
||||
type(fields).__name__
|
||||
'The `fields` option must be a list or tuple or "__all__". '
|
||||
'Got %s.' % type(fields).__name__
|
||||
)
|
||||
|
||||
if exclude and not isinstance(exclude, (list, tuple)):
|
||||
|
@ -967,6 +983,20 @@ class ModelSerializer(Serializer):
|
|||
)
|
||||
)
|
||||
|
||||
if fields is None and exclude is None:
|
||||
warnings.warn(
|
||||
"Creating a ModelSerializer without either the 'fields' "
|
||||
"attribute or the 'exclude' attribute is pending deprecation "
|
||||
"since 3.3.0. Add an explicit fields = '__all__' to the "
|
||||
"{serializer_class} serializer.".format(
|
||||
serializer_class=self.__class__.__name__
|
||||
),
|
||||
PendingDeprecationWarning
|
||||
)
|
||||
|
||||
if fields == ALL_FIELDS:
|
||||
fields = None
|
||||
|
||||
if fields is not None:
|
||||
# Ensure that all declared fields have also been included in the
|
||||
# `Meta.fields` option.
|
||||
|
@ -1207,13 +1237,10 @@ class ModelSerializer(Serializer):
|
|||
|
||||
for model_field in model_fields.values():
|
||||
# Include each of the `unique_for_*` field names.
|
||||
unique_constraint_names |= set([
|
||||
model_field.unique_for_date,
|
||||
model_field.unique_for_month,
|
||||
model_field.unique_for_year
|
||||
])
|
||||
unique_constraint_names |= {model_field.unique_for_date, model_field.unique_for_month,
|
||||
model_field.unique_for_year}
|
||||
|
||||
unique_constraint_names -= set([None])
|
||||
unique_constraint_names -= {None}
|
||||
|
||||
# Include each of the `unique_together` field names,
|
||||
# so long as all the field names are included on the serializer.
|
||||
|
@ -1327,10 +1354,10 @@ class ModelSerializer(Serializer):
|
|||
# which may map onto a model field. Any dotted field name lookups
|
||||
# cannot map to a field, and must be a traversal, so we're not
|
||||
# including those.
|
||||
field_names = set([
|
||||
field_names = {
|
||||
field.source for field in self.fields.values()
|
||||
if (field.source != '*') and ('.' not in field.source)
|
||||
])
|
||||
}
|
||||
|
||||
# Note that we make sure to check `unique_together` both on the
|
||||
# base model class, but also on any parent classes.
|
||||
|
|
|
@ -26,8 +26,6 @@ from django.utils import six
|
|||
from rest_framework import ISO_8601
|
||||
from rest_framework.compat import importlib
|
||||
|
||||
USER_SETTINGS = getattr(settings, 'REST_FRAMEWORK', None)
|
||||
|
||||
DEFAULTS = {
|
||||
# Base API policies
|
||||
'DEFAULT_RENDERER_CLASSES': (
|
||||
|
@ -93,13 +91,8 @@ DEFAULTS = {
|
|||
),
|
||||
'TEST_REQUEST_DEFAULT_FORMAT': 'multipart',
|
||||
|
||||
# Browser enhancements
|
||||
'FORM_METHOD_OVERRIDE': '_method',
|
||||
'FORM_CONTENT_OVERRIDE': '_content',
|
||||
'FORM_CONTENTTYPE_OVERRIDE': '_content_type',
|
||||
'URL_ACCEPT_OVERRIDE': 'accept',
|
||||
# Hyperlink settings
|
||||
'URL_FORMAT_OVERRIDE': 'format',
|
||||
|
||||
'FORMAT_SUFFIX_KWARG': 'format',
|
||||
'URL_FIELD_NAME': 'url',
|
||||
|
||||
|
@ -188,10 +181,17 @@ class APISettings(object):
|
|||
and return the class, rather than the string literal.
|
||||
"""
|
||||
def __init__(self, user_settings=None, defaults=None, import_strings=None):
|
||||
self.user_settings = user_settings or {}
|
||||
if user_settings:
|
||||
self._user_settings = user_settings
|
||||
self.defaults = defaults or DEFAULTS
|
||||
self.import_strings = import_strings or IMPORT_STRINGS
|
||||
|
||||
@property
|
||||
def user_settings(self):
|
||||
if not hasattr(self, '_user_settings'):
|
||||
self._user_settings = getattr(settings, 'REST_FRAMEWORK', {})
|
||||
return self._user_settings
|
||||
|
||||
def __getattr__(self, attr):
|
||||
if attr not in self.defaults.keys():
|
||||
raise AttributeError("Invalid API setting: '%s'" % attr)
|
||||
|
@ -212,7 +212,7 @@ class APISettings(object):
|
|||
return val
|
||||
|
||||
|
||||
api_settings = APISettings(USER_SETTINGS, DEFAULTS, IMPORT_STRINGS)
|
||||
api_settings = APISettings(None, DEFAULTS, IMPORT_STRINGS)
|
||||
|
||||
|
||||
def reload_api_settings(*args, **kwargs):
|
||||
|
|
97
rest_framework/static/rest_framework/js/ajax-form.js
Normal file
97
rest_framework/static/rest_framework/js/ajax-form.js
Normal file
|
@ -0,0 +1,97 @@
|
|||
function replaceDocument(docString) {
|
||||
var doc = document.open("text/html");
|
||||
doc.write(docString);
|
||||
doc.close();
|
||||
}
|
||||
|
||||
|
||||
function doAjaxSubmit(e) {
|
||||
var form = $(this);
|
||||
var btn = $(this.clk);
|
||||
var method = btn.data('method') || form.data('method') || form.attr('method') || 'GET';
|
||||
method = method.toUpperCase()
|
||||
if (method === 'GET') {
|
||||
// GET requests can always use standard form submits.
|
||||
return;
|
||||
}
|
||||
|
||||
var contentType =
|
||||
form.find('input[data-override="content-type"]').val() ||
|
||||
form.find('select[data-override="content-type"] option:selected').text();
|
||||
if (method === 'POST' && !contentType) {
|
||||
// POST requests can use standard form submits, unless we have
|
||||
// overridden the content type.
|
||||
return;
|
||||
}
|
||||
|
||||
// At this point we need to make an AJAX form submission.
|
||||
e.preventDefault();
|
||||
|
||||
var url = form.attr('action');
|
||||
var data;
|
||||
if (contentType) {
|
||||
data = form.find('[data-override="content"]').val() || ''
|
||||
} else {
|
||||
contentType = form.attr('enctype') || form.attr('encoding')
|
||||
if (contentType === 'multipart/form-data') {
|
||||
if (!window.FormData) {
|
||||
alert('Your browser does not support AJAX multipart form submissions');
|
||||
return;
|
||||
}
|
||||
// Use the FormData API and allow the content type to be set automatically,
|
||||
// so it includes the boundary string.
|
||||
// See https://developer.mozilla.org/en-US/docs/Web/API/FormData/Using_FormData_Objects
|
||||
contentType = false;
|
||||
data = new FormData(form[0]);
|
||||
} else {
|
||||
contentType = 'application/x-www-form-urlencoded; charset=UTF-8'
|
||||
data = form.serialize();
|
||||
}
|
||||
}
|
||||
|
||||
var ret = $.ajax({
|
||||
url: url,
|
||||
method: method,
|
||||
data: data,
|
||||
contentType: contentType,
|
||||
processData: false,
|
||||
headers: {'Accept': 'text/html; q=1.0, */*'},
|
||||
});
|
||||
ret.always(function(data, textStatus, jqXHR) {
|
||||
if (textStatus != 'success') {
|
||||
jqXHR = data;
|
||||
}
|
||||
var responseContentType = jqXHR.getResponseHeader("content-type") || "";
|
||||
if (responseContentType.toLowerCase().indexOf('text/html') === 0) {
|
||||
replaceDocument(jqXHR.responseText);
|
||||
try {
|
||||
// Modify the location and scroll to top, as if after page load.
|
||||
history.replaceState({}, '', url);
|
||||
scroll(0,0);
|
||||
} catch(err) {
|
||||
// History API not supported, so redirect.
|
||||
window.location = url;
|
||||
}
|
||||
} else {
|
||||
// Not HTML content. We can't open this directly, so redirect.
|
||||
window.location = url;
|
||||
}
|
||||
});
|
||||
return ret;
|
||||
}
|
||||
|
||||
|
||||
function captureSubmittingElement(e) {
|
||||
var target = e.target;
|
||||
var form = this;
|
||||
form.clk = target;
|
||||
}
|
||||
|
||||
|
||||
$.fn.ajaxForm = function() {
|
||||
var options = {}
|
||||
return this
|
||||
.unbind('submit.form-plugin click.form-plugin')
|
||||
.bind('submit.form-plugin', options, doAjaxSubmit)
|
||||
.bind('click.form-plugin', options, captureSubmittingElement);
|
||||
};
|
47
rest_framework/static/rest_framework/js/csrf.js
Normal file
47
rest_framework/static/rest_framework/js/csrf.js
Normal file
|
@ -0,0 +1,47 @@
|
|||
function getCookie(name) {
|
||||
var cookieValue = null;
|
||||
if (document.cookie && document.cookie != '') {
|
||||
var cookies = document.cookie.split(';');
|
||||
for (var i = 0; i < cookies.length; i++) {
|
||||
var cookie = jQuery.trim(cookies[i]);
|
||||
// Does this cookie string begin with the name we want?
|
||||
if (cookie.substring(0, name.length + 1) == (name + '=')) {
|
||||
cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return cookieValue;
|
||||
}
|
||||
|
||||
function csrfSafeMethod(method) {
|
||||
// these HTTP methods do not require CSRF protection
|
||||
return (/^(GET|HEAD|OPTIONS|TRACE)$/.test(method));
|
||||
}
|
||||
|
||||
function sameOrigin(url) {
|
||||
// test that a given url is a same-origin URL
|
||||
// url could be relative or scheme relative or absolute
|
||||
var host = document.location.host; // host + port
|
||||
var protocol = document.location.protocol;
|
||||
var sr_origin = '//' + host;
|
||||
var origin = protocol + sr_origin;
|
||||
// Allow absolute or scheme relative URLs to same origin
|
||||
return (url == origin || url.slice(0, origin.length + 1) == origin + '/') ||
|
||||
(url == sr_origin || url.slice(0, sr_origin.length + 1) == sr_origin + '/') ||
|
||||
// or any other URL that isn't scheme relative or absolute i.e relative.
|
||||
!(/^(\/\/|http:|https:).*/.test(url));
|
||||
}
|
||||
|
||||
var csrftoken = getCookie('csrftoken');
|
||||
|
||||
$.ajaxSetup({
|
||||
beforeSend: function(xhr, settings) {
|
||||
if (!csrfSafeMethod(settings.type) && sameOrigin(settings.url)) {
|
||||
// Send the token to same-origin, relative URLs only.
|
||||
// Send the token only if the method warrants CSRF protection
|
||||
// Using the CSRFToken value acquired earlier
|
||||
xhr.setRequestHeader("X-CSRFToken", csrftoken);
|
||||
}
|
||||
}
|
||||
});
|
5
rest_framework/static/rest_framework/js/jquery-1.11.3.min.js
vendored
Normal file
5
rest_framework/static/rest_framework/js/jquery-1.11.3.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
@ -104,9 +104,7 @@
|
|||
{% endif %}
|
||||
|
||||
{% if delete_form %}
|
||||
<form class="button-form" action="{{ request.get_full_path }}" method="POST">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="{{ api_settings.FORM_METHOD_OVERRIDE }}" value="DELETE" />
|
||||
<form class="button-form" action="{{ request.get_full_path }}" data-method="DELETE">
|
||||
<button class="btn btn-danger">
|
||||
<span class="glyphicon glyphicon-remove" aria-hidden="true"></span> Delete
|
||||
</button>
|
||||
|
@ -180,7 +178,7 @@
|
|||
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">×</span></button>
|
||||
<h4 class="modal-title" id="myModalLabel">Edit</h4>
|
||||
</div>
|
||||
<form action="{{ request.get_full_path }}" method="POST" enctype="multipart/form-data" class="form-horizontal" novalidate>
|
||||
<form action="{{ request.get_full_path }}" data-method="PUT" enctype="multipart/form-data" class="form-horizontal" novalidate>
|
||||
<div class="modal-body">
|
||||
<fieldset>
|
||||
{{ put_form }}
|
||||
|
@ -188,7 +186,7 @@
|
|||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
|
||||
<button name="{{ api_settings.FORM_METHOD_OVERRIDE }}" value="PUT" type="submit" class="btn btn-primary">Save</button>
|
||||
<button type="submit" class="btn btn-primary">Save</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
@ -204,7 +202,7 @@
|
|||
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">×</span></button>
|
||||
<h4 class="modal-title" id="myModalLabel">{{ error_title }}</h4>
|
||||
</div>
|
||||
<form action="{{ request.get_full_path }}" method="POST" enctype="multipart/form-data" class="form-horizontal" novalidate>
|
||||
<form action="{{ request.get_full_path }}" data-method="{{ request.method }}" enctype="multipart/form-data" class="form-horizontal" novalidate>
|
||||
<div class="modal-body">
|
||||
<fieldset>
|
||||
{{ error_form }}
|
||||
|
@ -212,7 +210,7 @@
|
|||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
|
||||
<button name="{{ api_settings.FORM_METHOD_OVERRIDE }}" value="{{ request.method }}" type="submit" class="btn btn-primary">Save</button>
|
||||
<button type="submit" class="btn btn-primary">Save</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
@ -221,10 +219,17 @@
|
|||
{% endif %}
|
||||
|
||||
{% block script %}
|
||||
<script src="{% static "rest_framework/js/jquery-1.8.1-min.js" %}"></script>
|
||||
<script src="{% static "rest_framework/js/jquery-1.11.3.min.js" %}"></script>
|
||||
<script src="{% static "rest_framework/js/ajax-form.js" %}"></script>
|
||||
<script src="{% static "rest_framework/js/csrf.js" %}"></script>
|
||||
<script src="{% static "rest_framework/js/bootstrap.min.js" %}"></script>
|
||||
<script src="{% static "rest_framework/js/prettify-min.js" %}"></script>
|
||||
<script src="{% static "rest_framework/js/default.js" %}"></script>
|
||||
<script>
|
||||
$(document).ready(function() {
|
||||
$('form').ajaxForm();
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
</body>
|
||||
{% endblock %}
|
||||
|
|
|
@ -94,17 +94,13 @@
|
|||
{% endif %}
|
||||
|
||||
{% if options_form %}
|
||||
<form class="button-form" action="{{ request.get_full_path }}" method="POST">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="{{ api_settings.FORM_METHOD_OVERRIDE }}" value="OPTIONS" />
|
||||
<form class="button-form" action="{{ request.get_full_path }}" data-method="OPTIONS">
|
||||
<button class="btn btn-primary js-tooltip" title="Make an OPTIONS request on the {{ name }} resource">OPTIONS</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
|
||||
{% if delete_form %}
|
||||
<form class="button-form" action="{{ request.get_full_path }}" method="POST">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="{{ api_settings.FORM_METHOD_OVERRIDE }}" value="DELETE" />
|
||||
<form class="button-form" action="{{ request.get_full_path }}" data-method="DELETE">
|
||||
<button class="btn btn-danger js-tooltip" title="Make a DELETE request on the {{ name }} resource">DELETE</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
|
@ -168,7 +164,7 @@
|
|||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div {% if post_form %}class="tab-pane"{% endif %} id="post-generic-content-form">
|
||||
<div {% if raw_data_post_form %}class="tab-pane"{% endif %} id="post-generic-content-form">
|
||||
{% with form=raw_data_post_form %}
|
||||
<form action="{{ request.get_full_path }}" method="POST" class="form-horizontal">
|
||||
<fieldset>
|
||||
|
@ -200,11 +196,11 @@
|
|||
<div class="well tab-content">
|
||||
{% if put_form %}
|
||||
<div class="tab-pane" id="put-object-form">
|
||||
<form action="{{ request.get_full_path }}" method="POST" enctype="multipart/form-data" class="form-horizontal" novalidate>
|
||||
<form action="{{ request.get_full_path }}" data-method="PUT" enctype="multipart/form-data" class="form-horizontal" novalidate>
|
||||
<fieldset>
|
||||
{{ put_form }}
|
||||
<div class="form-actions">
|
||||
<button class="btn btn-primary js-tooltip" name="{{ api_settings.FORM_METHOD_OVERRIDE }}" value="PUT" title="Make a PUT request on the {{ name }} resource">PUT</button>
|
||||
<button class="btn btn-primary js-tooltip" title="Make a PUT request on the {{ name }} resource">PUT</button>
|
||||
</div>
|
||||
</fieldset>
|
||||
</form>
|
||||
|
@ -213,15 +209,15 @@
|
|||
|
||||
<div {% if put_form %}class="tab-pane"{% endif %} id="put-generic-content-form">
|
||||
{% with form=raw_data_put_or_patch_form %}
|
||||
<form action="{{ request.get_full_path }}" method="POST" class="form-horizontal">
|
||||
<form action="{{ request.get_full_path }}" data-method="PUT" class="form-horizontal">
|
||||
<fieldset>
|
||||
{% include "rest_framework/raw_data_form.html" %}
|
||||
<div class="form-actions">
|
||||
{% if raw_data_put_form %}
|
||||
<button class="btn btn-primary js-tooltip" name="{{ api_settings.FORM_METHOD_OVERRIDE }}" value="PUT" title="Make a PUT request on the {{ name }} resource">PUT</button>
|
||||
<button class="btn btn-primary js-tooltip" title="Make a PUT request on the {{ name }} resource">PUT</button>
|
||||
{% endif %}
|
||||
{% if raw_data_patch_form %}
|
||||
<button class="btn btn-primary js-tooltip" name="{{ api_settings.FORM_METHOD_OVERRIDE }}" value="PATCH" title="Make a PATCH request on the {{ name }} resource">PATCH</button>
|
||||
<button data-method="PATCH" class="btn btn-primary js-tooltip" title="Make a PATCH request on the {{ name }} resource">PATCH</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
</fieldset>
|
||||
|
@ -237,10 +233,17 @@
|
|||
</div><!-- ./wrapper -->
|
||||
|
||||
{% block script %}
|
||||
<script src="{% static "rest_framework/js/jquery-1.8.1-min.js" %}"></script>
|
||||
<script src="{% static "rest_framework/js/jquery-1.11.3.min.js" %}"></script>
|
||||
<script src="{% static "rest_framework/js/ajax-form.js" %}"></script>
|
||||
<script src="{% static "rest_framework/js/csrf.js" %}"></script>
|
||||
<script src="{% static "rest_framework/js/bootstrap.min.js" %}"></script>
|
||||
<script src="{% static "rest_framework/js/prettify-min.js" %}"></script>
|
||||
<script src="{% static "rest_framework/js/default.js" %}"></script>
|
||||
<script>
|
||||
$(document).ready(function() {
|
||||
$('form').ajaxForm();
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
</body>
|
||||
{% endblock %}
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
{% load rest_framework %}
|
||||
{% csrf_token %}
|
||||
{{ form.non_field_errors }}
|
||||
{% for field in form %}
|
||||
<div class="form-group">
|
||||
|
|
|
@ -41,8 +41,9 @@ def optional_login(request):
|
|||
except NoReverseMatch:
|
||||
return ''
|
||||
|
||||
snippet = "<li><a href='{href}?next={next}'>Log in</a></li>".format(href=login_url, next=escape(request.path))
|
||||
return snippet
|
||||
snippet = "<li><a href='{href}?next={next}'>Log in</a></li>"
|
||||
snippet = snippet.format(href=login_url, next=escape(request.path))
|
||||
return mark_safe(snippet)
|
||||
|
||||
|
||||
@register.simple_tag
|
||||
|
@ -64,8 +65,8 @@ def optional_logout(request, user):
|
|||
<li><a href='{href}?next={next}'>Log out</a></li>
|
||||
</ul>
|
||||
</li>"""
|
||||
|
||||
return snippet.format(user=user, href=logout_url, next=escape(request.path))
|
||||
snippet = snippet.format(user=escape(user), href=logout_url, next=escape(request.path))
|
||||
return mark_safe(snippet)
|
||||
|
||||
|
||||
@register.simple_tag
|
||||
|
|
|
@ -8,7 +8,6 @@ from django.core import validators
|
|||
from django.db import models
|
||||
from django.utils.text import capfirst
|
||||
|
||||
from rest_framework.compat import clean_manytomany_helptext
|
||||
from rest_framework.validators import UniqueValidator
|
||||
|
||||
NUMERIC_FIELD_TYPES = (
|
||||
|
@ -113,10 +112,19 @@ def get_field_kwargs(field_name, model_field):
|
|||
kwargs['choices'] = model_field.choices
|
||||
return kwargs
|
||||
|
||||
# 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):
|
||||
validator_kwarg = [
|
||||
validator for validator in validator_kwarg
|
||||
if not isinstance(validator, validators.DecimalValidator)
|
||||
]
|
||||
|
||||
# Ensure that max_length is passed explicitly as a keyword arg,
|
||||
# rather than as a validator.
|
||||
max_length = getattr(model_field, 'max_length', None)
|
||||
if max_length is not None and isinstance(model_field, models.CharField):
|
||||
if max_length is not None and (isinstance(model_field, models.CharField) or
|
||||
isinstance(model_field, models.TextField)):
|
||||
kwargs['max_length'] = max_length
|
||||
validator_kwarg = [
|
||||
validator for validator in validator_kwarg
|
||||
|
@ -193,7 +201,15 @@ def get_field_kwargs(field_name, model_field):
|
|||
]
|
||||
|
||||
if getattr(model_field, 'unique', False):
|
||||
validator = UniqueValidator(queryset=model_field.model._default_manager)
|
||||
unique_error_message = model_field.error_messages.get('unique', None)
|
||||
if unique_error_message:
|
||||
unique_error_message = unique_error_message % {
|
||||
'model_name': model_field.model._meta.object_name,
|
||||
'field_label': model_field.verbose_name
|
||||
}
|
||||
validator = UniqueValidator(
|
||||
queryset=model_field.model._default_manager,
|
||||
message=unique_error_message)
|
||||
validator_kwarg.append(validator)
|
||||
|
||||
if validator_kwarg:
|
||||
|
@ -222,7 +238,7 @@ def get_relation_kwargs(field_name, relation_info):
|
|||
if model_field:
|
||||
if model_field.verbose_name and needs_label(model_field, field_name):
|
||||
kwargs['label'] = capfirst(model_field.verbose_name)
|
||||
help_text = clean_manytomany_helptext(model_field.help_text)
|
||||
help_text = model_field.help_text
|
||||
if help_text:
|
||||
kwargs['help_text'] = help_text
|
||||
if not model_field.editable:
|
||||
|
|
|
@ -6,14 +6,13 @@ relationships and their associated metadata.
|
|||
Usage: `get_field_info(model)` returns a `FieldInfo` instance.
|
||||
"""
|
||||
import inspect
|
||||
from collections import namedtuple
|
||||
from collections import OrderedDict, namedtuple
|
||||
|
||||
from django.apps import apps
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
from django.db import models
|
||||
from django.utils import six
|
||||
|
||||
from rest_framework.compat import OrderedDict
|
||||
|
||||
FieldInfo = namedtuple('FieldResult', [
|
||||
'pk', # Model field instance
|
||||
'fields', # Dict of field name -> model field instance
|
||||
|
@ -45,7 +44,7 @@ def _resolve_model(obj):
|
|||
"""
|
||||
if isinstance(obj, six.string_types) and len(obj.split('.')) == 2:
|
||||
app_name, model_name = obj.split('.')
|
||||
resolved_model = models.get_model(app_name, model_name)
|
||||
resolved_model = apps.get_model(app_name, model_name)
|
||||
if resolved_model is None:
|
||||
msg = "Django did not return a model for {0}.{1}"
|
||||
raise ImproperlyConfigured(msg.format(app_name, model_name))
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
from __future__ import unicode_literals
|
||||
|
||||
import collections
|
||||
from collections import OrderedDict
|
||||
|
||||
from django.utils.encoding import force_text
|
||||
|
||||
from rest_framework.compat import OrderedDict, unicode_to_repr
|
||||
from rest_framework.compat import unicode_to_repr
|
||||
|
||||
|
||||
class ReturnDict(OrderedDict):
|
||||
|
|
|
@ -100,11 +100,11 @@ class UniqueTogetherValidator(object):
|
|||
if self.instance is not None:
|
||||
return
|
||||
|
||||
missing = dict([
|
||||
(field_name, self.missing_message)
|
||||
missing = {
|
||||
field_name: self.missing_message
|
||||
for field_name in self.fields
|
||||
if field_name not in attrs
|
||||
])
|
||||
}
|
||||
if missing:
|
||||
raise ValidationError(missing)
|
||||
|
||||
|
@ -120,10 +120,10 @@ class UniqueTogetherValidator(object):
|
|||
attrs[field_name] = getattr(self.instance, field_name)
|
||||
|
||||
# Determine the filter keyword arguments and filter the queryset.
|
||||
filter_kwargs = dict([
|
||||
(field_name, attrs[field_name])
|
||||
filter_kwargs = {
|
||||
field_name: attrs[field_name]
|
||||
for field_name in self.fields
|
||||
])
|
||||
}
|
||||
return queryset.filter(**filter_kwargs)
|
||||
|
||||
def exclude_current_instance(self, attrs, queryset):
|
||||
|
@ -184,11 +184,11 @@ class BaseUniqueForValidator(object):
|
|||
The `UniqueFor<Range>Validator` classes always force an implied
|
||||
'required' state on the fields they are applied to.
|
||||
"""
|
||||
missing = dict([
|
||||
(field_name, self.missing_message)
|
||||
missing = {
|
||||
field_name: self.missing_message
|
||||
for field_name in [self.field, self.date_field]
|
||||
if field_name not in attrs
|
||||
])
|
||||
}
|
||||
if missing:
|
||||
raise ValidationError(missing)
|
||||
|
||||
|
|
|
@ -93,7 +93,11 @@ if __name__ == "__main__":
|
|||
except ValueError:
|
||||
pass
|
||||
else:
|
||||
pytest_args = ['--cov', 'rest_framework'] + pytest_args
|
||||
pytest_args = [
|
||||
'--cov-report',
|
||||
'xml',
|
||||
'--cov',
|
||||
'rest_framework'] + pytest_args
|
||||
|
||||
if first_arg.startswith('-'):
|
||||
# `runtests.py [flags]`
|
||||
|
|
|
@ -1,11 +1,12 @@
|
|||
from __future__ import unicode_literals
|
||||
|
||||
from django.conf.urls import patterns, url
|
||||
import unittest
|
||||
|
||||
from django.conf.urls import url
|
||||
from django.db import connection, connections, transaction
|
||||
from django.http import Http404
|
||||
from django.test import TestCase, TransactionTestCase
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.utils.unittest import skipUnless
|
||||
|
||||
from rest_framework import status
|
||||
from rest_framework.exceptions import APIException
|
||||
|
@ -35,8 +36,10 @@ class APIExceptionView(APIView):
|
|||
raise APIException
|
||||
|
||||
|
||||
@skipUnless(connection.features.uses_savepoints,
|
||||
"'atomic' requires transactions and savepoints.")
|
||||
@unittest.skipUnless(
|
||||
connection.features.uses_savepoints,
|
||||
"'atomic' requires transactions and savepoints."
|
||||
)
|
||||
class DBTransactionTests(TestCase):
|
||||
def setUp(self):
|
||||
self.view = BasicView.as_view()
|
||||
|
@ -55,8 +58,10 @@ class DBTransactionTests(TestCase):
|
|||
assert BasicModel.objects.count() == 1
|
||||
|
||||
|
||||
@skipUnless(connection.features.uses_savepoints,
|
||||
"'atomic' requires transactions and savepoints.")
|
||||
@unittest.skipUnless(
|
||||
connection.features.uses_savepoints,
|
||||
"'atomic' requires transactions and savepoints."
|
||||
)
|
||||
class DBTransactionErrorTests(TestCase):
|
||||
def setUp(self):
|
||||
self.view = ErrorView.as_view()
|
||||
|
@ -83,8 +88,10 @@ class DBTransactionErrorTests(TestCase):
|
|||
assert BasicModel.objects.count() == 1
|
||||
|
||||
|
||||
@skipUnless(connection.features.uses_savepoints,
|
||||
"'atomic' requires transactions and savepoints.")
|
||||
@unittest.skipUnless(
|
||||
connection.features.uses_savepoints,
|
||||
"'atomic' requires transactions and savepoints."
|
||||
)
|
||||
class DBTransactionAPIExceptionTests(TestCase):
|
||||
def setUp(self):
|
||||
self.view = APIExceptionView.as_view()
|
||||
|
@ -113,8 +120,10 @@ class DBTransactionAPIExceptionTests(TestCase):
|
|||
assert BasicModel.objects.count() == 0
|
||||
|
||||
|
||||
@skipUnless(connection.features.uses_savepoints,
|
||||
"'atomic' requires transactions and savepoints.")
|
||||
@unittest.skipUnless(
|
||||
connection.features.uses_savepoints,
|
||||
"'atomic' requires transactions and savepoints."
|
||||
)
|
||||
class NonAtomicDBTransactionAPIExceptionTests(TransactionTestCase):
|
||||
@property
|
||||
def urls(self):
|
||||
|
@ -127,9 +136,8 @@ class NonAtomicDBTransactionAPIExceptionTests(TransactionTestCase):
|
|||
BasicModel.objects.all()
|
||||
raise Http404
|
||||
|
||||
return patterns(
|
||||
'',
|
||||
url(r'^$', NonAtomicAPIExceptionView.as_view())
|
||||
return (
|
||||
url(r'^$', NonAtomicAPIExceptionView.as_view()),
|
||||
)
|
||||
|
||||
def setUp(self):
|
||||
|
|
|
@ -1525,6 +1525,58 @@ class TestUnvalidatedDictField(FieldValues):
|
|||
field = serializers.DictField()
|
||||
|
||||
|
||||
class TestJSONField(FieldValues):
|
||||
"""
|
||||
Values for `JSONField`.
|
||||
"""
|
||||
valid_inputs = [
|
||||
({
|
||||
'a': 1,
|
||||
'b': ['some', 'list', True, 1.23],
|
||||
'3': None
|
||||
}, {
|
||||
'a': 1,
|
||||
'b': ['some', 'list', True, 1.23],
|
||||
'3': None
|
||||
}),
|
||||
]
|
||||
invalid_inputs = [
|
||||
({'a': set()}, ['Value must be valid JSON.']),
|
||||
]
|
||||
outputs = [
|
||||
({
|
||||
'a': 1,
|
||||
'b': ['some', 'list', True, 1.23],
|
||||
'3': 3
|
||||
}, {
|
||||
'a': 1,
|
||||
'b': ['some', 'list', True, 1.23],
|
||||
'3': 3
|
||||
}),
|
||||
]
|
||||
field = serializers.JSONField()
|
||||
|
||||
|
||||
class TestBinaryJSONField(FieldValues):
|
||||
"""
|
||||
Values for `JSONField` with binary=True.
|
||||
"""
|
||||
valid_inputs = [
|
||||
(b'{"a": 1, "3": null, "b": ["some", "list", true, 1.23]}', {
|
||||
'a': 1,
|
||||
'b': ['some', 'list', True, 1.23],
|
||||
'3': None
|
||||
}),
|
||||
]
|
||||
invalid_inputs = [
|
||||
('{"a": "unterminated string}', ['Value must be valid JSON.']),
|
||||
]
|
||||
outputs = [
|
||||
(['some', 'list', True, 1.23], b'["some", "list", true, 1.23]'),
|
||||
]
|
||||
field = serializers.JSONField(binary=True)
|
||||
|
||||
|
||||
# Tests for FieldField.
|
||||
# ---------------------
|
||||
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
from __future__ import unicode_literals
|
||||
|
||||
import datetime
|
||||
import unittest
|
||||
from decimal import Decimal
|
||||
|
||||
from django.conf.urls import url
|
||||
|
@ -8,7 +9,6 @@ from django.core.urlresolvers import reverse
|
|||
from django.db import models
|
||||
from django.test import TestCase
|
||||
from django.test.utils import override_settings
|
||||
from django.utils import unittest
|
||||
from django.utils.dateparse import parse_date
|
||||
from django.utils.six.moves import reload_module
|
||||
|
||||
|
|
|
@ -8,6 +8,7 @@ an appropriate set of serializer fields for each case.
|
|||
from __future__ import unicode_literals
|
||||
|
||||
import decimal
|
||||
from collections import OrderedDict
|
||||
|
||||
import django
|
||||
import pytest
|
||||
|
@ -21,7 +22,7 @@ from django.utils import six
|
|||
|
||||
from rest_framework import serializers
|
||||
from rest_framework.compat import DurationField as ModelDurationField
|
||||
from rest_framework.compat import OrderedDict, unicode_repr
|
||||
from rest_framework.compat import unicode_repr
|
||||
|
||||
|
||||
def dedent(blocktext):
|
||||
|
@ -62,7 +63,7 @@ class RegularFieldsModel(models.Model):
|
|||
positive_small_integer_field = models.PositiveSmallIntegerField()
|
||||
slug_field = models.SlugField(max_length=100)
|
||||
small_integer_field = models.SmallIntegerField()
|
||||
text_field = models.TextField()
|
||||
text_field = models.TextField(max_length=100)
|
||||
time_field = models.TimeField()
|
||||
url_field = models.URLField(max_length=100)
|
||||
custom_field = CustomField()
|
||||
|
@ -160,11 +161,12 @@ class TestRegularFieldMappings(TestCase):
|
|||
positive_small_integer_field = IntegerField()
|
||||
slug_field = SlugField(max_length=100)
|
||||
small_integer_field = IntegerField()
|
||||
text_field = CharField(style={'base_template': 'textarea.html'})
|
||||
text_field = CharField(max_length=100, style={'base_template': 'textarea.html'})
|
||||
time_field = TimeField()
|
||||
url_field = URLField(max_length=100)
|
||||
custom_field = ModelField(model_field=<tests.test_model_serializer.CustomField: custom_field>)
|
||||
""")
|
||||
|
||||
self.assertEqual(unicode_repr(TestSerializer()), expected)
|
||||
|
||||
def test_field_options(self):
|
||||
|
@ -321,6 +323,21 @@ class TestRegularFieldMappings(TestCase):
|
|||
|
||||
ExampleSerializer()
|
||||
|
||||
def test_fields_and_exclude_behavior(self):
|
||||
class ImplicitFieldsSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = RegularFieldsModel
|
||||
|
||||
class ExplicitFieldsSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = RegularFieldsModel
|
||||
fields = '__all__'
|
||||
|
||||
implicit = ImplicitFieldsSerializer()
|
||||
explicit = ExplicitFieldsSerializer()
|
||||
|
||||
assert implicit.data == explicit.data
|
||||
|
||||
|
||||
@pytest.mark.skipif(django.VERSION < (1, 8),
|
||||
reason='DurationField is only available for django1.8+')
|
||||
|
|
|
@ -1,18 +1,18 @@
|
|||
from __future__ import unicode_literals
|
||||
|
||||
import base64
|
||||
import unittest
|
||||
|
||||
from django.contrib.auth.models import Group, Permission, User
|
||||
from django.core.urlresolvers import ResolverMatch
|
||||
from django.db import models
|
||||
from django.test import TestCase
|
||||
from django.utils import unittest
|
||||
|
||||
from rest_framework import (
|
||||
HTTP_HEADER_ENCODING, authentication, generics, permissions, serializers,
|
||||
status
|
||||
)
|
||||
from rest_framework.compat import get_model_name, guardian
|
||||
from rest_framework.compat import guardian
|
||||
from rest_framework.filters import DjangoObjectPermissionsFilter
|
||||
from rest_framework.routers import DefaultRouter
|
||||
from rest_framework.test import APIRequestFactory
|
||||
|
@ -279,7 +279,7 @@ class ObjectPermissionsIntegrationTests(TestCase):
|
|||
|
||||
# give everyone model level permissions, as we are not testing those
|
||||
everyone = Group.objects.create(name='everyone')
|
||||
model_name = get_model_name(BasicPermModel)
|
||||
model_name = BasicPermModel._meta.model_name
|
||||
app_label = BasicPermModel._meta.app_label
|
||||
f = '{0}_{1}'.format
|
||||
perms = {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
from __future__ import unicode_literals
|
||||
|
||||
from django.contrib.contenttypes.generic import (
|
||||
from django.contrib.contenttypes.fields import (
|
||||
GenericForeignKey, GenericRelation
|
||||
)
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
|
|
|
@ -3,7 +3,7 @@ from __future__ import unicode_literals
|
|||
|
||||
import json
|
||||
import re
|
||||
from collections import MutableMapping
|
||||
from collections import MutableMapping, OrderedDict
|
||||
|
||||
from django.conf.urls import include, url
|
||||
from django.core.cache import cache
|
||||
|
@ -13,7 +13,6 @@ from django.utils import six
|
|||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from rest_framework import permissions, serializers, status
|
||||
from rest_framework.compat import OrderedDict
|
||||
from rest_framework.renderers import (
|
||||
BaseRenderer, BrowsableAPIRenderer, HTMLFormRenderer, JSONRenderer
|
||||
)
|
||||
|
@ -192,17 +191,6 @@ class RendererEndToEndTests(TestCase):
|
|||
self.assertEqual(resp.content, RENDERER_B_SERIALIZER(DUMMYCONTENT))
|
||||
self.assertEqual(resp.status_code, DUMMYSTATUS)
|
||||
|
||||
def test_specified_renderer_serializes_content_on_accept_query(self):
|
||||
"""The '_accept' query string should behave in the same way as the Accept header."""
|
||||
param = '?%s=%s' % (
|
||||
api_settings.URL_ACCEPT_OVERRIDE,
|
||||
RendererB.media_type
|
||||
)
|
||||
resp = self.client.get('/' + param)
|
||||
self.assertEqual(resp['Content-Type'], RendererB.media_type + '; charset=utf-8')
|
||||
self.assertEqual(resp.content, RENDERER_B_SERIALIZER(DUMMYCONTENT))
|
||||
self.assertEqual(resp.status_code, DUMMYSTATUS)
|
||||
|
||||
def test_unsatisfiable_accept_header_on_request_returns_406_status(self):
|
||||
"""If the Accept header is unsatisfiable we should return a 406 Not Acceptable response."""
|
||||
resp = self.client.get('/', HTTP_ACCEPT='foo/bar')
|
||||
|
|
|
@ -3,27 +3,20 @@ Tests for content parsing, and form-overloaded content parsing.
|
|||
"""
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import json
|
||||
from io import BytesIO
|
||||
|
||||
import django
|
||||
import pytest
|
||||
from django.conf.urls import url
|
||||
from django.contrib.auth import authenticate, login, logout
|
||||
from django.contrib.auth.models import User
|
||||
from django.contrib.sessions.middleware import SessionMiddleware
|
||||
from django.core.handlers.wsgi import WSGIRequest
|
||||
from django.test import TestCase
|
||||
from django.utils import six
|
||||
|
||||
from rest_framework import status
|
||||
from rest_framework.authentication import SessionAuthentication
|
||||
from rest_framework.parsers import (
|
||||
BaseParser, FormParser, JSONParser, MultiPartParser
|
||||
)
|
||||
from rest_framework.request import Empty, Request
|
||||
from rest_framework.parsers import BaseParser, FormParser, MultiPartParser
|
||||
from rest_framework.request import Request
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.settings import api_settings
|
||||
from rest_framework.test import APIClient, APIRequestFactory
|
||||
from rest_framework.views import APIView
|
||||
|
||||
|
@ -43,36 +36,6 @@ class PlainTextParser(BaseParser):
|
|||
return stream.read()
|
||||
|
||||
|
||||
class TestMethodOverloading(TestCase):
|
||||
def test_method(self):
|
||||
"""
|
||||
Request methods should be same as underlying request.
|
||||
"""
|
||||
request = Request(factory.get('/'))
|
||||
self.assertEqual(request.method, 'GET')
|
||||
request = Request(factory.post('/'))
|
||||
self.assertEqual(request.method, 'POST')
|
||||
|
||||
def test_overloaded_method(self):
|
||||
"""
|
||||
POST requests can be overloaded to another method by setting a
|
||||
reserved form field
|
||||
"""
|
||||
request = Request(factory.post('/', {api_settings.FORM_METHOD_OVERRIDE: 'DELETE'}))
|
||||
self.assertEqual(request.method, 'DELETE')
|
||||
|
||||
def test_x_http_method_override_header(self):
|
||||
"""
|
||||
POST requests can also be overloaded to another method by setting
|
||||
the X-HTTP-Method-Override header.
|
||||
"""
|
||||
request = Request(factory.post('/', {'foo': 'bar'}, HTTP_X_HTTP_METHOD_OVERRIDE='DELETE'))
|
||||
self.assertEqual(request.method, 'DELETE')
|
||||
|
||||
request = Request(factory.get('/', {'foo': 'bar'}, HTTP_X_HTTP_METHOD_OVERRIDE='DELETE'))
|
||||
self.assertEqual(request.method, 'DELETE')
|
||||
|
||||
|
||||
class TestContentParsing(TestCase):
|
||||
def test_standard_behaviour_determines_no_content_GET(self):
|
||||
"""
|
||||
|
@ -137,49 +100,6 @@ class TestContentParsing(TestCase):
|
|||
request.parsers = (PlainTextParser(), )
|
||||
self.assertEqual(request.data, content)
|
||||
|
||||
def test_overloaded_behaviour_allows_content_tunnelling(self):
|
||||
"""
|
||||
Ensure request.data returns content for overloaded POST request.
|
||||
"""
|
||||
json_data = {'foobar': 'qwerty'}
|
||||
content = json.dumps(json_data)
|
||||
content_type = 'application/json'
|
||||
form_data = {
|
||||
api_settings.FORM_CONTENT_OVERRIDE: content,
|
||||
api_settings.FORM_CONTENTTYPE_OVERRIDE: content_type
|
||||
}
|
||||
request = Request(factory.post('/', form_data))
|
||||
request.parsers = (JSONParser(), )
|
||||
self.assertEqual(request.data, json_data)
|
||||
|
||||
def test_form_POST_unicode(self):
|
||||
"""
|
||||
JSON POST via default web interface with unicode data
|
||||
"""
|
||||
# Note: environ and other variables here have simplified content compared to real Request
|
||||
CONTENT = b'_content_type=application%2Fjson&_content=%7B%22request%22%3A+4%2C+%22firm%22%3A+1%2C+%22text%22%3A+%22%D0%9F%D1%80%D0%B8%D0%B2%D0%B5%D1%82%21%22%7D'
|
||||
environ = {
|
||||
'REQUEST_METHOD': 'POST',
|
||||
'CONTENT_TYPE': 'application/x-www-form-urlencoded',
|
||||
'CONTENT_LENGTH': len(CONTENT),
|
||||
'wsgi.input': BytesIO(CONTENT),
|
||||
}
|
||||
wsgi_request = WSGIRequest(environ=environ)
|
||||
wsgi_request._load_post_and_files()
|
||||
parsers = (JSONParser(), FormParser(), MultiPartParser())
|
||||
parser_context = {
|
||||
'encoding': 'utf-8',
|
||||
'kwargs': {},
|
||||
'args': (),
|
||||
}
|
||||
request = Request(wsgi_request, parsers=parsers, parser_context=parser_context)
|
||||
method = request.method
|
||||
self.assertEqual(method, 'POST')
|
||||
self.assertEqual(request._content_type, 'application/json')
|
||||
self.assertEqual(request._stream.getvalue(), b'{"request": 4, "firm": 1, "text": "\xd0\x9f\xd1\x80\xd0\xb8\xd0\xb2\xd0\xb5\xd1\x82!"}')
|
||||
self.assertEqual(request._data, Empty)
|
||||
self.assertEqual(request._files, Empty)
|
||||
|
||||
|
||||
class MockView(APIView):
|
||||
authentication_classes = (SessionAuthentication,)
|
||||
|
|
|
@ -5,11 +5,11 @@ from django.test import TestCase
|
|||
from django.utils import six
|
||||
|
||||
from rest_framework import generics, routers, serializers, status, viewsets
|
||||
from rest_framework.parsers import JSONParser
|
||||
from rest_framework.renderers import (
|
||||
BaseRenderer, BrowsableAPIRenderer, JSONRenderer
|
||||
)
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.settings import api_settings
|
||||
from rest_framework.views import APIView
|
||||
from tests.models import BasicModel
|
||||
|
||||
|
@ -79,6 +79,14 @@ class MockViewSettingContentType(APIView):
|
|||
return Response(DUMMYCONTENT, status=DUMMYSTATUS, content_type='setbyview')
|
||||
|
||||
|
||||
class JSONView(APIView):
|
||||
parser_classes = (JSONParser,)
|
||||
|
||||
def post(self, request, **kwargs):
|
||||
assert request.data
|
||||
return Response(DUMMYCONTENT)
|
||||
|
||||
|
||||
class HTMLView(APIView):
|
||||
renderer_classes = (BrowsableAPIRenderer, )
|
||||
|
||||
|
@ -114,6 +122,7 @@ urlpatterns = [
|
|||
url(r'^.*\.(?P<format>.+)$', MockView.as_view(renderer_classes=[RendererA, RendererB, RendererC])),
|
||||
url(r'^$', MockView.as_view(renderer_classes=[RendererA, RendererB, RendererC])),
|
||||
url(r'^html$', HTMLView.as_view()),
|
||||
url(r'^json$', JSONView.as_view()),
|
||||
url(r'^html1$', HTMLView1.as_view()),
|
||||
url(r'^html_new_model$', HTMLNewModelView.as_view()),
|
||||
url(r'^html_new_model_viewset', include(new_model_viewset_router.urls)),
|
||||
|
@ -166,17 +175,6 @@ class RendererIntegrationTests(TestCase):
|
|||
self.assertEqual(resp.content, RENDERER_B_SERIALIZER(DUMMYCONTENT))
|
||||
self.assertEqual(resp.status_code, DUMMYSTATUS)
|
||||
|
||||
def test_specified_renderer_serializes_content_on_accept_query(self):
|
||||
"""The '_accept' query string should behave in the same way as the Accept header."""
|
||||
param = '?%s=%s' % (
|
||||
api_settings.URL_ACCEPT_OVERRIDE,
|
||||
RendererB.media_type
|
||||
)
|
||||
resp = self.client.get('/' + param)
|
||||
self.assertEqual(resp['Content-Type'], RendererB.media_type + '; charset=utf-8')
|
||||
self.assertEqual(resp.content, RENDERER_B_SERIALIZER(DUMMYCONTENT))
|
||||
self.assertEqual(resp.status_code, DUMMYSTATUS)
|
||||
|
||||
def test_specified_renderer_serializes_content_on_format_query(self):
|
||||
"""If a 'format' query is specified, the renderer with the matching
|
||||
format attribute should serialize the response."""
|
||||
|
@ -203,6 +201,25 @@ class RendererIntegrationTests(TestCase):
|
|||
self.assertEqual(resp.status_code, DUMMYSTATUS)
|
||||
|
||||
|
||||
class UnsupportedMediaTypeTests(TestCase):
|
||||
urls = 'tests.test_response'
|
||||
|
||||
def test_should_allow_posting_json(self):
|
||||
response = self.client.post('/json', data='{"test": 123}', content_type='application/json')
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_should_not_allow_posting_xml(self):
|
||||
response = self.client.post('/json', data='<test>123</test>', content_type='application/xml')
|
||||
|
||||
self.assertEqual(response.status_code, 415)
|
||||
|
||||
def test_should_not_allow_posting_a_form(self):
|
||||
response = self.client.post('/json', data={'test': 123})
|
||||
|
||||
self.assertEqual(response.status_code, 415)
|
||||
|
||||
|
||||
class Issue122Tests(TestCase):
|
||||
"""
|
||||
Tests that covers #122.
|
||||
|
@ -270,16 +287,6 @@ class Issue807Tests(TestCase):
|
|||
resp = self.client.get('/setbyview', **headers)
|
||||
self.assertEqual('setbyview', resp['Content-Type'])
|
||||
|
||||
def test_viewset_label_help_text(self):
|
||||
param = '?%s=%s' % (
|
||||
api_settings.URL_ACCEPT_OVERRIDE,
|
||||
'text/html'
|
||||
)
|
||||
resp = self.client.get('/html_new_model_viewset/' + param)
|
||||
self.assertEqual(resp['Content-Type'], 'text/html; charset=utf-8')
|
||||
# self.assertContains(resp, 'Text comes here')
|
||||
# self.assertContains(resp, 'Text description.')
|
||||
|
||||
def test_form_has_label_and_help_text(self):
|
||||
resp = self.client.get('/html_new_model')
|
||||
self.assertEqual(resp['Content-Type'], 'text/html; charset=utf-8')
|
||||
|
|
|
@ -51,6 +51,16 @@ class TestSerializer:
|
|||
with pytest.raises(AttributeError):
|
||||
serializer.data
|
||||
|
||||
def test_data_access_before_save_raises_error(self):
|
||||
def create(validated_data):
|
||||
return validated_data
|
||||
serializer = self.Serializer(data={'char': 'abc', 'integer': 123})
|
||||
serializer.create = create
|
||||
assert serializer.is_valid()
|
||||
assert serializer.data == {'char': 'abc', 'integer': 123}
|
||||
with pytest.raises(AssertionError):
|
||||
serializer.save()
|
||||
|
||||
|
||||
class TestValidateMethod:
|
||||
def test_non_field_error_validate_method(self):
|
||||
|
|
|
@ -150,16 +150,16 @@ class ResolveModelWithPatchedDjangoTests(TestCase):
|
|||
|
||||
def setUp(self):
|
||||
"""Monkeypatch get_model."""
|
||||
self.get_model = rest_framework.utils.model_meta.models.get_model
|
||||
self.get_model = rest_framework.utils.model_meta.apps.get_model
|
||||
|
||||
def get_model(app_label, model_name):
|
||||
return None
|
||||
|
||||
rest_framework.utils.model_meta.models.get_model = get_model
|
||||
rest_framework.utils.model_meta.apps.get_model = get_model
|
||||
|
||||
def tearDown(self):
|
||||
"""Revert monkeypatching."""
|
||||
rest_framework.utils.model_meta.models.get_model = self.get_model
|
||||
rest_framework.utils.model_meta.apps.get_model = self.get_model
|
||||
|
||||
def test_blows_up_if_model_does_not_resolve(self):
|
||||
with self.assertRaises(ImproperlyConfigured):
|
||||
|
|
|
@ -48,7 +48,7 @@ class TestUniquenessValidation(TestCase):
|
|||
data = {'username': 'existing'}
|
||||
serializer = UniquenessSerializer(data=data)
|
||||
assert not serializer.is_valid()
|
||||
assert serializer.errors == {'username': ['This field must be unique.']}
|
||||
assert serializer.errors == {'username': ['UniquenessModel with this username already exists.']}
|
||||
|
||||
def test_is_unique(self):
|
||||
data = {'username': 'other'}
|
||||
|
|
|
@ -74,21 +74,6 @@ class ClassBasedViewIntegrationTests(TestCase):
|
|||
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||
self.assertEqual(sanitise_json_error(response.data), expected)
|
||||
|
||||
def test_400_parse_error_tunneled_content(self):
|
||||
content = 'f00bar'
|
||||
content_type = 'application/json'
|
||||
form_data = {
|
||||
api_settings.FORM_CONTENT_OVERRIDE: content,
|
||||
api_settings.FORM_CONTENTTYPE_OVERRIDE: content_type
|
||||
}
|
||||
request = factory.post('/', form_data)
|
||||
response = self.view(request)
|
||||
expected = {
|
||||
'detail': JSON_ERROR
|
||||
}
|
||||
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||
self.assertEqual(sanitise_json_error(response.data), expected)
|
||||
|
||||
|
||||
class FunctionBasedViewIntegrationTests(TestCase):
|
||||
def setUp(self):
|
||||
|
@ -103,21 +88,6 @@ class FunctionBasedViewIntegrationTests(TestCase):
|
|||
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||
self.assertEqual(sanitise_json_error(response.data), expected)
|
||||
|
||||
def test_400_parse_error_tunneled_content(self):
|
||||
content = 'f00bar'
|
||||
content_type = 'application/json'
|
||||
form_data = {
|
||||
api_settings.FORM_CONTENT_OVERRIDE: content,
|
||||
api_settings.FORM_CONTENTTYPE_OVERRIDE: content_type
|
||||
}
|
||||
request = factory.post('/', form_data)
|
||||
response = self.view(request)
|
||||
expected = {
|
||||
'detail': JSON_ERROR
|
||||
}
|
||||
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||
self.assertEqual(sanitise_json_error(response.data), expected)
|
||||
|
||||
|
||||
class TestCustomExceptionHandler(TestCase):
|
||||
def setUp(self):
|
||||
|
|
41
tox.ini
41
tox.ini
|
@ -4,19 +4,24 @@ addopts=--tb=short
|
|||
[tox]
|
||||
envlist =
|
||||
py27-{lint,docs},
|
||||
{py26,py27,py32,py33,py34}-django{15,16},
|
||||
{py27,py32,py33,py34}-django{17,18,master}
|
||||
{py27,py32,py33,py34}-django{17,18},
|
||||
{py27,py34,py35}-django{19}
|
||||
|
||||
[testenv]
|
||||
basepython =
|
||||
py27: python2.7
|
||||
py32: python3.2
|
||||
py33: python3.3
|
||||
py34: python3.4
|
||||
py35: python3.5
|
||||
|
||||
commands = ./runtests.py --fast {posargs} --coverage
|
||||
setenv =
|
||||
PYTHONDONTWRITEBYTECODE=1
|
||||
deps =
|
||||
django15: Django==1.5.6 # Should track minimum supported
|
||||
django16: Django==1.6.3 # Should track minimum supported
|
||||
django17: Django==1.7.10 # Should track maximum supported
|
||||
django18: Django==1.8.4 # Should track maximum supported
|
||||
djangomaster: https://github.com/django/django/archive/master.tar.gz
|
||||
django17: Django==1.7.10
|
||||
django18: Django==1.8.4
|
||||
django19: https://www.djangoproject.com/download/1.9b1/tarball/
|
||||
-rrequirements/requirements-testing.txt
|
||||
-rrequirements/requirements-optionals.txt
|
||||
|
||||
|
@ -31,3 +36,25 @@ commands = mkdocs build
|
|||
deps =
|
||||
-rrequirements/requirements-testing.txt
|
||||
-rrequirements/requirements-documentation.txt
|
||||
|
||||
# Specify explicitly to exclude Django Guardian against Django 1.9
|
||||
[testenv:py27-django19]
|
||||
deps =
|
||||
https://www.djangoproject.com/download/1.9b1/tarball/
|
||||
-rrequirements/requirements-testing.txt
|
||||
markdown==2.5.2
|
||||
django-filter==0.10.0
|
||||
|
||||
[testenv:py34-django19]
|
||||
deps =
|
||||
https://www.djangoproject.com/download/1.9b1/tarball/
|
||||
-rrequirements/requirements-testing.txt
|
||||
markdown==2.5.2
|
||||
django-filter==0.10.0
|
||||
|
||||
[testenv:py35-django19]
|
||||
deps =
|
||||
https://www.djangoproject.com/download/1.9b1/tarball/
|
||||
-rrequirements/requirements-testing.txt
|
||||
markdown==2.5.2
|
||||
django-filter==0.10.0
|
||||
|
|
Loading…
Reference in New Issue
Block a user