Merge remote-tracking branch 'reference/master' into bug/3354

Conflicts:
	rest_framework/compat.py
This commit is contained in:
Xavier Ordoquy 2015-10-20 14:44:44 +02:00
commit 026e114665
60 changed files with 772 additions and 718 deletions

View File

@ -5,6 +5,9 @@ sudo: false
env: env:
- TOX_ENV=py27-lint - TOX_ENV=py27-lint
- TOX_ENV=py27-docs - TOX_ENV=py27-docs
- TOX_ENV=py35-django19
- TOX_ENV=py34-django19
- TOX_ENV=py27-django19
- TOX_ENV=py34-django18 - TOX_ENV=py34-django18
- TOX_ENV=py33-django18 - TOX_ENV=py33-django18
- TOX_ENV=py32-django18 - TOX_ENV=py32-django18
@ -13,28 +16,12 @@ env:
- TOX_ENV=py33-django17 - TOX_ENV=py33-django17
- TOX_ENV=py32-django17 - TOX_ENV=py32-django17
- TOX_ENV=py27-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: matrix:
# Python 3.5 not yet available on travis, watch this to see when it is.
fast_finish: true fast_finish: true
allow_failures: allow_failures:
- env: TOX_ENV=py27-djangomaster - env: TOX_ENV=py35-django19
- env: TOX_ENV=py32-djangomaster
- env: TOX_ENV=py33-djangomaster
- env: TOX_ENV=py34-djangomaster
install: install:
- pip install tox - pip install tox
@ -44,4 +31,4 @@ script:
after_success: after_success:
- pip install codecov - pip install codecov
- codecov - codecov -e TOX_ENV

View File

@ -36,8 +36,8 @@ There is a live example API for testing purposes, [available here][sandbox].
# Requirements # Requirements
* Python (2.6.5+, 2.7, 3.2, 3.3, 3.4) * Python (2.7, 3.2, 3.3, 3.4, 3.5)
* Django (1.5.6+, 1.6.3+, 1.7, 1.8) * Django (1.7, 1.8, 1.9)
# Installation # Installation

View File

@ -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-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][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). [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 [mac]: http://tools.ietf.org/html/draft-hammer-oauth-v2-mac-token-05
[djoser]: https://github.com/sunscrapers/djoser [djoser]: https://github.com/sunscrapers/djoser
[django-rest-auth]: https://github.com/Tivix/django-rest-auth [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 [django-rest-knox]: https://github.com/James1345/django-rest-knox

View File

@ -57,7 +57,7 @@ Note that setting a `default` value implies that the field is not required. Incl
### `source` ### `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. 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): class DocumentField(DictField):
child = CharField() 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 # Miscellaneous fields

View File

@ -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 ## 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. 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.

View File

@ -287,7 +287,7 @@ Similarly if a nested representation should be a list of items, you should pass
## Writable nested representations ## 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 = CommentSerializer(data={'user': {'email': 'foobar', 'username': 'doe'}, 'content': 'baz'})
serializer.is_valid() 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 #### 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: 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 AccountSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = Account 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. 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 ## 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: For example:
@ -468,7 +469,27 @@ For example:
model = Account model = Account
fields = ('id', 'account_name', 'users', 'created') 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. 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 ## 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: 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. * `.data` - Returns the outgoing primitive representation.
* `.is_valid()` - Deserializes and validates incoming data. * `.is_valid()` - Deserializes and validates incoming data.
* `.validated_data` - Returns the validated 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. * `.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: There are four methods that can be overridden, depending on what functionality you want the serializer class to support:

View File

@ -249,47 +249,23 @@ Default:
--- ---
## Browser overrides ## Content type controls
*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'`
#### URL_FORMAT_OVERRIDE #### 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'` Default: `'format'`
@ -451,12 +427,6 @@ A string representing the key that should be used for the URL fields generated b
Default: `'url'` 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 #### 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. 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.

View File

@ -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. 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): def allow_request(self, request, view):
return random.randint(1, 10) == 1 return random.randint(1, 10) == 1

View File

@ -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`. * `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`. * `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`: 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`:

View File

@ -52,8 +52,8 @@ Some reasons you might want to use REST framework:
REST framework requires the following: REST framework requires the following:
* Python (2.6.5+, 2.7, 3.2, 3.3, 3.4) * Python (2.6.5+, 2.7, 3.2, 3.3, 3.4, 3.5)
* Django (1.5.6+, 1.6.3+, 1.7+, 1.8) * Django (1.7+, 1.8, 1.9)
The following packages are optional: The following packages are optional:

View File

@ -4,58 +4,36 @@
> >
> — [RESTful Web Services][cite], Leonard Richardson & Sam Ruby. > — [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... ## Browser based PUT, DELETE, etc...
REST framework supports browser-based `PUT`, `DELETE` and other methods, by The [AJAX form library][ajax-form] supports browser-based `PUT`, `DELETE` and other methods on HTML forms.
overloading `POST` requests using a hidden form field.
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="/" data-method="PUT">
<input name='foo'/>
<form action="/news-items/5" method="POST"> ...
<input type="hidden" name="_method" value="DELETE">
</form> </form>
`request.method` would return `"DELETE"`. 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.
## 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'},
...
});
## Browser based submission of non-form content ## Browser based submission of non-form content
Browser-based submission of content types other than form are supported by 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.
using form fields named `_content` and `_content_type`:
For example, given the following form: For example:
<form action="/news-items/5" method="PUT"> <form action="/">
<input type="hidden" name="_content_type" value="application/json"> <input data-override='content-type' value='application/json' type='hidden'/>
<input name="_content" value="{'count': 1}"> <textarea data-override='content'>{}</textarea>
</form> <input type="submit"/>
</form>
`request.content_type` would return `"application/json"`, and Note that prior to 3.3.0, this support was server-side rather than javascript based.
`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.
## URL based format suffixes ## 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 useful shortcut for determining which content type should be returned from
the view. the view.
This is a more concise than using the `accept` override, but it also gives This behavior is controlled using the `URL_FORMAT_OVERRIDE` setting.
you less control. (For example you can't specify any media type parameters)
## 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? ## 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. 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 [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 [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 [html5]: http://www.w3.org/TR/html5-diff/#changes-2010-06-24
[put_delete]: http://amundsen.com/examples/put-delete-forms/ [put_delete]: http://amundsen.com/examples/put-delete-forms/
[aws_elb]: https://forums.aws.amazon.com/thread.jspa?messageID=400724

View File

@ -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.x series
### 3.2.4 ### 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 [gh3361]: https://github.com/tomchristie/django-rest-framework/issues/3361
[gh3364]: https://github.com/tomchristie/django-rest-framework/issues/3364 [gh3364]: https://github.com/tomchristie/django-rest-framework/issues/3364
[gh3415]: https://github.com/tomchristie/django-rest-framework/issues/3415 [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

View File

@ -237,6 +237,7 @@ To submit new content, [open an issue][drf-create-issue] or [create a pull reque
### Misc ### 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. * [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-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. * [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]. * [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. * [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. * [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 ## 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 [django-rest-framework-braces]: https://github.com/dealertrack/django-rest-framework-braces
[dry-rest-permissions]: https://github.com/Helioscene/dry-rest-permissions [dry-rest-permissions]: https://github.com/Helioscene/dry-rest-permissions
[django-url-filter]: https://github.com/miki725/django-url-filter [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/

View File

@ -136,7 +136,7 @@
</div> <!--/.wrapper --> </div> <!--/.wrapper -->
<footer class="span12"> <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> </p>
</footer> </footer>

View File

@ -1,7 +1,7 @@
from django.contrib.auth import authenticate from django.contrib.auth import authenticate
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from rest_framework import exceptions, serializers from rest_framework import serializers
class AuthTokenSerializer(serializers.Serializer): class AuthTokenSerializer(serializers.Serializer):
@ -18,13 +18,13 @@ class AuthTokenSerializer(serializers.Serializer):
if user: if user:
if not user.is_active: if not user.is_active:
msg = _('User account is disabled.') msg = _('User account is disabled.')
raise exceptions.ValidationError(msg) raise serializers.ValidationError(msg)
else: else:
msg = _('Unable to log in with provided credentials.') msg = _('Unable to log in with provided credentials.')
raise exceptions.ValidationError(msg) raise serializers.ValidationError(msg)
else: else:
msg = _('Must include "username" and "password".') msg = _('Must include "username" and "password".')
raise exceptions.ValidationError(msg) raise serializers.ValidationError(msg)
attrs['user'] = user attrs['user'] = user
return attrs return attrs

View File

@ -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']

View File

@ -62,16 +62,6 @@ def distinct(queryset, base):
return queryset.distinct() 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. # contrib.postgres only supported from 1.8 onwards.
try: try:
from django.contrib.postgres import fields as postgres_fields from django.contrib.postgres import fields as postgres_fields
@ -79,41 +69,30 @@ except ImportError:
postgres_fields = None 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 # django-filter is optional
try: try:
import django_filters import django_filters
except ImportError: except ImportError:
django_filters = None 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 # Django-guardian is optional. Import only if guardian is in INSTALLED_APPS
# Fixes (#1712). We keep the try/except for the test suite. # Fixes (#1712). We keep the try/except for the test suite.
guardian = None guardian = None
try: try:
import guardian if 'guardian' in settings.INSTALLED_APPS:
import guardian.shortcuts # Fixes #1624 import guardian
import guardian.shortcuts # Fixes #1624
except ImportError: except ImportError:
pass 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): class CustomValidatorMessage(object):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
self.message = kwargs.pop('message', self.message) self.message = kwargs.pop('message', self.message)
@ -136,32 +115,6 @@ class MaxLengthValidator(CustomValidatorMessage, MaxLengthValidator):
pass 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 # PATCH method is not implemented by Django
if 'patch' not in View.http_method_names: if 'patch' not in View.http_method_names:
View.http_method_names = View.http_method_names + ['patch'] View.http_method_names = View.http_method_names + ['patch']

View File

@ -30,10 +30,10 @@ def _force_text_recursive(data):
return ReturnList(ret, serializer=data.serializer) return ReturnList(ret, serializer=data.serializer)
return data return data
elif isinstance(data, dict): elif isinstance(data, dict):
ret = dict([ ret = {
(key, _force_text_recursive(value)) key: _force_text_recursive(value)
for key, value in data.items() for key, value in data.items()
]) }
if isinstance(data, ReturnDict): if isinstance(data, ReturnDict):
return ReturnDict(ret, serializer=data.serializer) return ReturnDict(ret, serializer=data.serializer)
return data return data

View File

@ -5,13 +5,17 @@ import copy
import datetime import datetime
import decimal import decimal
import inspect import inspect
import json
import re import re
import uuid import uuid
from collections import OrderedDict
from django.conf import settings from django.conf import settings
from django.core.exceptions import ValidationError as DjangoValidationError from django.core.exceptions import ValidationError as DjangoValidationError
from django.core.exceptions import ObjectDoesNotExist 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 FilePathField as DjangoFilePathField
from django.forms import ImageField as DjangoImageField from django.forms import ImageField as DjangoImageField
from django.utils import six, timezone 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 import ISO_8601
from rest_framework.compat import ( from rest_framework.compat import (
EmailValidator, MaxLengthValidator, MaxValueValidator, MinLengthValidator, MaxLengthValidator, MaxValueValidator, MinLengthValidator,
MinValueValidator, OrderedDict, URLValidator, duration_string, MinValueValidator, duration_string, parse_duration, unicode_repr,
parse_duration, unicode_repr, unicode_to_repr unicode_to_repr
) )
from rest_framework.exceptions import ValidationError from rest_framework.exceptions import ValidationError
from rest_framework.settings import api_settings from rest_framework.settings import api_settings
@ -600,8 +604,8 @@ class BooleanField(Field):
} }
default_empty_html = False default_empty_html = False
initial = False initial = False
TRUE_VALUES = set(('t', 'T', 'true', 'True', 'TRUE', '1', 1, True)) TRUE_VALUES = {'t', 'T', 'true', 'True', 'TRUE', '1', 1, True}
FALSE_VALUES = set(('f', 'F', 'false', 'False', 'FALSE', '0', 0, 0.0, False)) FALSE_VALUES = {'f', 'F', 'false', 'False', 'FALSE', '0', 0, 0.0, False}
def __init__(self, **kwargs): def __init__(self, **kwargs):
assert 'allow_null' not in kwargs, '`allow_null` is not a valid option. Use `NullBooleanField` instead.' 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.') 'invalid': _('"{input}" is not a valid boolean.')
} }
initial = None initial = None
TRUE_VALUES = set(('t', 'T', 'true', 'True', 'TRUE', '1', 1, True)) TRUE_VALUES = {'t', 'T', 'true', 'True', 'TRUE', '1', 1, True}
FALSE_VALUES = set(('f', 'F', 'false', 'False', 'FALSE', '0', 0, 0.0, False)) FALSE_VALUES = {'f', 'F', 'false', 'False', 'FALSE', '0', 0, 0.0, False}
NULL_VALUES = set(('n', 'N', 'null', 'Null', 'NULL', '', None)) NULL_VALUES = {'n', 'N', 'null', 'Null', 'NULL', '', None}
def __init__(self, **kwargs): def __init__(self, **kwargs):
assert 'allow_null' not in kwargs, '`allow_null` is not a valid option.' 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. 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): def __init__(self, max_digits, decimal_places, coerce_to_string=None, max_value=None, min_value=None, **kwargs):
self.max_digits = max_digits self.max_digits = max_digits
self.decimal_places = decimal_places 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.max_value = max_value
self.min_value = min_value self.min_value = min_value
@ -970,12 +973,14 @@ class DecimalField(Field):
return value return value
def to_representation(self, 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): if not isinstance(value, decimal.Decimal):
value = decimal.Decimal(six.text_type(value).strip()) value = decimal.Decimal(six.text_type(value).strip())
quantized = self.quantize(value) quantized = self.quantize(value)
if not self.coerce_to_string: if not coerce_to_string:
return quantized return quantized
return '{0:f}'.format(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}.'), 'invalid': _('Datetime has wrong format. Use one of these formats instead: {format}.'),
'date': _('Expected a datetime but got a date.'), '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 datetime_parser = datetime.datetime.strptime
def __init__(self, format=empty, input_formats=None, default_timezone=None, *args, **kwargs): def __init__(self, format=empty, input_formats=None, default_timezone=None, *args, **kwargs):
self.format = format if format is not empty else self.format if format is not empty:
self.input_formats = input_formats if input_formats is not None else self.input_formats self.format = format
self.default_timezone = default_timezone if default_timezone is not None else self.default_timezone 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) super(DateTimeField, self).__init__(*args, **kwargs)
def enforce_timezone(self, value): 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 `None`, always return naive datetimes.
When `self.default_timezone` is not `None`, always return aware 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): field_timezone = getattr(self, 'timezone', self.default_timezone())
return timezone.make_aware(value, self.default_timezone)
elif (self.default_timezone is None) and timezone.is_aware(value): 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 timezone.make_naive(value, timezone.UTC())
return value return value
def default_timezone(self):
return timezone.get_default_timezone() if settings.USE_TZ else None
def to_internal_value(self, value): 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): if isinstance(value, datetime.date) and not isinstance(value, datetime.datetime):
self.fail('date') self.fail('date')
if isinstance(value, datetime.datetime): if isinstance(value, datetime.datetime):
return self.enforce_timezone(value) return self.enforce_timezone(value)
for format in self.input_formats: for input_format in input_formats:
if format.lower() == ISO_8601: if input_format.lower() == ISO_8601:
try: try:
parsed = parse_datetime(value) parsed = parse_datetime(value)
except (ValueError, TypeError): except (ValueError, TypeError):
@ -1037,25 +1049,27 @@ class DateTimeField(Field):
return self.enforce_timezone(parsed) return self.enforce_timezone(parsed)
else: else:
try: try:
parsed = self.datetime_parser(value, format) parsed = self.datetime_parser(value, input_format)
except (ValueError, TypeError): except (ValueError, TypeError):
pass pass
else: else:
return self.enforce_timezone(parsed) 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) self.fail('invalid', format=humanized_format)
def to_representation(self, value): 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 return value
if self.format.lower() == ISO_8601: if output_format.lower() == ISO_8601:
value = value.isoformat() value = value.isoformat()
if value.endswith('+00:00'): if value.endswith('+00:00'):
value = value[:-6] + 'Z' value = value[:-6] + 'Z'
return value return value
return value.strftime(self.format) return value.strftime(output_format)
class DateField(Field): class DateField(Field):
@ -1063,24 +1077,26 @@ class DateField(Field):
'invalid': _('Date has wrong format. Use one of these formats instead: {format}.'), 'invalid': _('Date has wrong format. Use one of these formats instead: {format}.'),
'datetime': _('Expected a date but got a datetime.'), '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 datetime_parser = datetime.datetime.strptime
def __init__(self, format=empty, input_formats=None, *args, **kwargs): def __init__(self, format=empty, input_formats=None, *args, **kwargs):
self.format = format if format is not empty else self.format if format is not empty:
self.input_formats = input_formats if input_formats is not None else self.input_formats self.format = format
if input_formats is not None:
self.input_formats = input_formats
super(DateField, self).__init__(*args, **kwargs) super(DateField, self).__init__(*args, **kwargs)
def to_internal_value(self, value): def to_internal_value(self, value):
input_formats = getattr(self, 'input_formats', api_settings.DATE_INPUT_FORMATS)
if isinstance(value, datetime.datetime): if isinstance(value, datetime.datetime):
self.fail('datetime') self.fail('datetime')
if isinstance(value, datetime.date): if isinstance(value, datetime.date):
return value return value
for format in self.input_formats: for input_format in input_formats:
if format.lower() == ISO_8601: if input_format.lower() == ISO_8601:
try: try:
parsed = parse_date(value) parsed = parse_date(value)
except (ValueError, TypeError): except (ValueError, TypeError):
@ -1090,20 +1106,22 @@ class DateField(Field):
return parsed return parsed
else: else:
try: try:
parsed = self.datetime_parser(value, format) parsed = self.datetime_parser(value, input_format)
except (ValueError, TypeError): except (ValueError, TypeError):
pass pass
else: else:
return parsed.date() 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) self.fail('invalid', format=humanized_format)
def to_representation(self, value): def to_representation(self, value):
output_format = getattr(self, 'format', api_settings.DATE_FORMAT)
if not value: if not value:
return None return None
if self.format is None: if output_format is None:
return value return value
# Applying a `DateField` to a datetime value is almost always # 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.' '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)): if (isinstance(value, str)):
value = datetime.datetime.strptime(value, '%Y-%m-%d').date() value = datetime.datetime.strptime(value, '%Y-%m-%d').date()
return value.isoformat() return value.isoformat()
return value.strftime(self.format) return value.strftime(output_format)
class TimeField(Field): class TimeField(Field):
default_error_messages = { default_error_messages = {
'invalid': _('Time has wrong format. Use one of these formats instead: {format}.'), '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 datetime_parser = datetime.datetime.strptime
def __init__(self, format=empty, input_formats=None, *args, **kwargs): def __init__(self, format=empty, input_formats=None, *args, **kwargs):
self.format = format if format is not empty else self.format if format is not empty:
self.input_formats = input_formats if input_formats is not None else self.input_formats self.format = format
if input_formats is not None:
self.input_formats = input_formats
super(TimeField, self).__init__(*args, **kwargs) super(TimeField, self).__init__(*args, **kwargs)
def to_internal_value(self, value): def to_internal_value(self, value):
input_formats = getattr(self, 'input_formats', api_settings.TIME_INPUT_FORMATS)
if isinstance(value, datetime.time): if isinstance(value, datetime.time):
return value return value
for format in self.input_formats: for input_format in input_formats:
if format.lower() == ISO_8601: if input_format.lower() == ISO_8601:
try: try:
parsed = parse_time(value) parsed = parse_time(value)
except (ValueError, TypeError): except (ValueError, TypeError):
@ -1151,17 +1171,19 @@ class TimeField(Field):
return parsed return parsed
else: else:
try: try:
parsed = self.datetime_parser(value, format) parsed = self.datetime_parser(value, input_format)
except (ValueError, TypeError): except (ValueError, TypeError):
pass pass
else: else:
return parsed.time() 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) self.fail('invalid', format=humanized_format)
def to_representation(self, value): 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 return value
# Applying a `TimeField` to a datetime value is almost always # 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.' '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.isoformat()
return value.strftime(self.format) return value.strftime(output_format)
class DurationField(Field): class DurationField(Field):
@ -1219,9 +1241,9 @@ class ChoiceField(Field):
# Map the string representation of choices to the underlying value. # Map the string representation of choices to the underlying value.
# Allows us to deal with eg. integer choices while supporting either # Allows us to deal with eg. integer choices while supporting either
# integer or string input, but still get the correct datatype out. # integer or string input, but still get the correct datatype out.
self.choice_strings_to_values = dict([ self.choice_strings_to_values = {
(six.text_type(key), key) for key in self.choices.keys() six.text_type(key): key for key in self.choices.keys()
]) }
self.allow_blank = kwargs.pop('allow_blank', False) self.allow_blank = kwargs.pop('allow_blank', False)
@ -1280,15 +1302,15 @@ class MultipleChoiceField(ChoiceField):
if not self.allow_empty and len(data) == 0: if not self.allow_empty and len(data) == 0:
self.fail('empty') self.fail('empty')
return set([ return {
super(MultipleChoiceField, self).to_internal_value(item) super(MultipleChoiceField, self).to_internal_value(item)
for item in data for item in data
]) }
def to_representation(self, value): def to_representation(self, value):
return set([ return {
self.choice_strings_to_values.get(six.text_type(item), item) for item in value self.choice_strings_to_values.get(six.text_type(item), item) for item in value
]) }
class FilePathField(ChoiceField): class FilePathField(ChoiceField):
@ -1318,12 +1340,12 @@ class FileField(Field):
'empty': _('The submitted file is empty.'), 'empty': _('The submitted file is empty.'),
'max_length': _('Ensure this filename has at most {max_length} characters (it has {length}).'), '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): def __init__(self, *args, **kwargs):
self.max_length = kwargs.pop('max_length', None) self.max_length = kwargs.pop('max_length', None)
self.allow_empty_file = kwargs.pop('allow_empty_file', False) 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) super(FileField, self).__init__(*args, **kwargs)
def to_internal_value(self, data): def to_internal_value(self, data):
@ -1344,10 +1366,12 @@ class FileField(Field):
return data return data
def to_representation(self, value): def to_representation(self, value):
use_url = getattr(self, 'use_url', api_settings.UPLOADED_FILES_USE_URL)
if not value: if not value:
return None return None
if self.use_url: if use_url:
if not getattr(value, 'url', None): if not getattr(value, 'url', None):
# If the file has not been saved it may not have a URL. # If the file has not been saved it may not have a URL.
return None return None
@ -1484,19 +1508,50 @@ class DictField(Field):
data = html.parse_html_dict(data) data = html.parse_html_dict(data)
if not isinstance(data, dict): if not isinstance(data, dict):
self.fail('not_a_dict', input_type=type(data).__name__) self.fail('not_a_dict', input_type=type(data).__name__)
return dict([ return {
(six.text_type(key), self.child.run_validation(value)) six.text_type(key): self.child.run_validation(value)
for key, value in data.items() for key, value in data.items()
]) }
def to_representation(self, value): def to_representation(self, value):
""" """
List of object instances -> List of dicts of primitive datatypes. List of object instances -> List of dicts of primitive datatypes.
""" """
return dict([ return {
(six.text_type(key), self.child.to_representation(val)) six.text_type(key): self.child.to_representation(val)
for key, val in value.items() 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... # Miscellaneous field types...

View File

@ -11,9 +11,7 @@ from django.core.exceptions import ImproperlyConfigured
from django.db import models from django.db import models
from django.utils import six from django.utils import six
from rest_framework.compat import ( from rest_framework.compat import distinct, django_filters, guardian
distinct, django_filters, get_model_name, guardian
)
from rest_framework.settings import api_settings from rest_framework.settings import api_settings
FilterSet = django_filters and django_filters.FilterSet or None FilterSet = django_filters and django_filters.FilterSet or None
@ -202,7 +200,7 @@ class DjangoObjectPermissionsFilter(BaseFilterBackend):
model_cls = queryset.model model_cls = queryset.model
kwargs = { kwargs = {
'app_label': model_cls._meta.app_label, '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 permission = self.perm_format % kwargs
if guardian.VERSION >= (1, 3): if guardian.VERSION >= (1, 3):

View File

@ -8,12 +8,13 @@ to return this information in a more standardized way.
""" """
from __future__ import unicode_literals from __future__ import unicode_literals
from collections import OrderedDict
from django.core.exceptions import PermissionDenied from django.core.exceptions import PermissionDenied
from django.http import Http404 from django.http import Http404
from django.utils.encoding import force_text from django.utils.encoding import force_text
from rest_framework import exceptions, serializers from rest_framework import exceptions, serializers
from rest_framework.compat import OrderedDict
from rest_framework.request import clone_request from rest_framework.request import clone_request
from rest_framework.utils.field_mapping import ClassLookupDict 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. the fields that are accepted for 'PUT' and 'POST' methods.
""" """
actions = {} 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) view.request = clone_request(request, method)
try: try:
# Test global permissions # Test global permissions

View File

@ -92,9 +92,6 @@ class DefaultContentNegotiation(BaseContentNegotiation):
""" """
Given the incoming request, return a tokenised list of media Given the incoming request, return a tokenised list of media
type strings. type strings.
Allows URL style accept override. eg. "?accept=application/json"
""" """
header = request.META.get('HTTP_ACCEPT', '*/*') 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(',')] return [token.strip() for token in header.split(',')]

View File

@ -7,7 +7,7 @@ from __future__ import unicode_literals
import warnings import warnings
from base64 import b64decode, b64encode 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 Paginator as DjangoPaginator
from django.core.paginator import InvalidPage 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.six.moves.urllib import parse as urlparse
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from rest_framework.compat import OrderedDict
from rest_framework.exceptions import NotFound from rest_framework.exceptions import NotFound
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework.settings import api_settings 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 # We always include the first two pages, last two pages, and
# two pages either side of the current page. # two pages either side of the current page.
included = set(( included = {1, current - 1, current, current + 1, final}
1,
current - 1, current, current + 1,
final
))
# If the break would only exclude a single page number then we # If the break would only exclude a single page number then we
# may as well include the page number instead of the break. # may as well include the page number instead of the break.

View File

@ -5,8 +5,6 @@ from __future__ import unicode_literals
from django.http import Http404 from django.http import Http404
from rest_framework.compat import get_model_name
SAFE_METHODS = ('GET', 'HEAD', 'OPTIONS') SAFE_METHODS = ('GET', 'HEAD', 'OPTIONS')
@ -104,7 +102,7 @@ class DjangoModelPermissions(BasePermission):
""" """
kwargs = { kwargs = {
'app_label': model_cls._meta.app_label, '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]] 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): def get_required_object_permissions(self, method, model_cls):
kwargs = { kwargs = {
'app_label': model_cls._meta.app_label, '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]] return [perm % kwargs for perm in self.perms_map[method]]

View File

@ -1,6 +1,8 @@
# coding: utf-8 # coding: utf-8
from __future__ import unicode_literals from __future__ import unicode_literals
from collections import OrderedDict
from django.core.exceptions import ImproperlyConfigured, ObjectDoesNotExist from django.core.exceptions import ImproperlyConfigured, ObjectDoesNotExist
from django.core.urlresolvers import ( from django.core.urlresolvers import (
NoReverseMatch, Resolver404, get_script_prefix, resolve 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.six.moves.urllib import parse as urlparse
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from rest_framework.compat import OrderedDict
from rest_framework.fields import ( from rest_framework.fields import (
Field, empty, get_attribute, is_simple_callable, iter_options Field, empty, get_attribute, is_simple_callable, iter_options
) )

View File

@ -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 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. 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 from __future__ import unicode_literals
@ -420,9 +420,6 @@ class BrowsableAPIRenderer(BaseRenderer):
if method not in view.allowed_methods: if method not in view.allowed_methods:
return # Not a valid method return # Not a valid method
if not api_settings.FORM_METHOD_OVERRIDE:
return # Cannot use form overloading
try: try:
view.check_permissions(request) view.check_permissions(request)
if obj is not None: if obj is not None:
@ -530,13 +527,6 @@ class BrowsableAPIRenderer(BaseRenderer):
instance = None instance = None
with override_method(view, request, method) as request: 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 # Check permissions
if not self.show_form_for_method(view, method, request, instance): if not self.show_form_for_method(view, method, request, instance):
return return
@ -564,28 +554,22 @@ class BrowsableAPIRenderer(BaseRenderer):
# Generate a generic form that includes a content type field, # Generate a generic form that includes a content type field,
# and a content 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] media_types = [parser.media_type for parser in view.parser_classes]
choices = [(media_type, media_type) for media_type in media_types] choices = [(media_type, media_type) for media_type in media_types]
initial = media_types[0] initial = media_types[0]
# NB. http://jacobian.org/writing/dynamic-form-generation/
class GenericContentForm(forms.Form): class GenericContentForm(forms.Form):
def __init__(self): _content_type = forms.ChoiceField(
super(GenericContentForm, self).__init__() label='Media type',
choices=choices,
self.fields[content_type_field] = forms.ChoiceField( initial=initial,
label='Media type', widget=forms.Select(attrs={'data-override': 'content-type'})
choices=choices, )
initial=initial _content = forms.CharField(
) label='Content',
self.fields[content_field] = forms.CharField( widget=forms.Textarea(attrs={'data-override': 'content'}),
label='Content', initial=content
widget=forms.Textarea, )
initial=content
)
return GenericContentForm() return GenericContentForm()

View File

@ -86,7 +86,7 @@ def clone_request(request, method):
ret._full_data = request._full_data ret._full_data = request._full_data
ret._content_type = request._content_type ret._content_type = request._content_type
ret._stream = request._stream ret._stream = request._stream
ret._method = method ret.method = method
if hasattr(request, '_user'): if hasattr(request, '_user'):
ret._user = request._user ret._user = request._user
if hasattr(request, '_auth'): if hasattr(request, '_auth'):
@ -129,11 +129,6 @@ class Request(object):
- authentication_classes(list/tuple). The authentications used to try - authentication_classes(list/tuple). The authentications used to try
authenticating the request's user. 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, def __init__(self, request, parsers=None, authenticators=None,
negotiator=None, parser_context=None): negotiator=None, parser_context=None):
self._request = request self._request = request
@ -144,7 +139,6 @@ class Request(object):
self._data = Empty self._data = Empty
self._files = Empty self._files = Empty
self._full_data = Empty self._full_data = Empty
self._method = Empty
self._content_type = Empty self._content_type = Empty
self._stream = Empty self._stream = Empty
@ -162,30 +156,10 @@ class Request(object):
def _default_negotiator(self): def _default_negotiator(self):
return api_settings.DEFAULT_CONTENT_NEGOTIATION_CLASS() 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 @property
def content_type(self): def content_type(self):
""" meta = self._request.META
Returns the content type header. return meta.get('CONTENT_TYPE', meta.get('HTTP_CONTENT_TYPE', ''))
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
@property @property
def stream(self): def stream(self):
@ -265,9 +239,6 @@ class Request(object):
""" """
Parses the request content into `self.data`. Parses the request content into `self.data`.
""" """
if not _hasattr(self, '_content_type'):
self._load_method_and_content_type()
if not _hasattr(self, '_data'): if not _hasattr(self, '_data'):
self._data, self._files = self._parse() self._data, self._files = self._parse()
if self._files: if self._files:
@ -276,32 +247,14 @@ class Request(object):
else: else:
self._full_data = self._data 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): def _load_stream(self):
""" """
Return the content body of the request, as a stream. Return the content body of the request, as a stream.
""" """
meta = self._request.META
try: try:
content_length = int( content_length = int(
self.META.get( meta.get('CONTENT_LENGTH', meta.get('HTTP_CONTENT_LENGTH', 0))
'CONTENT_LENGTH', self.META.get('HTTP_CONTENT_LENGTH')
)
) )
except (ValueError, TypeError): except (ValueError, TypeError):
content_length = 0 content_length = 0
@ -313,50 +266,6 @@ class Request(object):
else: else:
self._stream = six.BytesIO(self.raw_post_data) 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): def _parse(self):
""" """
Parse the request content, returning a two-tuple of (data, files) Parse the request content, returning a two-tuple of (data, files)

View File

@ -98,7 +98,7 @@ class Response(SimpleTemplateResponse):
state = super(Response, self).__getstate__() state = super(Response, self).__getstate__()
for key in ( for key in (
'accepted_renderer', 'renderer_context', 'resolver_match', 'accepted_renderer', 'renderer_context', 'resolver_match',
'client', 'request', 'wsgi_request' 'client', 'request', 'json', 'wsgi_request'
): ):
if key in state: if key in state:
del state[key] del state[key]

View File

@ -22,7 +22,6 @@ def preserve_builtin_query_params(url, request=None):
overrides = [ overrides = [
api_settings.URL_FORMAT_OVERRIDE, api_settings.URL_FORMAT_OVERRIDE,
api_settings.URL_ACCEPT_OVERRIDE
] ]
for param in overrides: for param in overrides:

View File

@ -16,14 +16,13 @@ For example, you might have a `urls.py` that looks something like this:
from __future__ import unicode_literals from __future__ import unicode_literals
import itertools import itertools
from collections import namedtuple from collections import OrderedDict, namedtuple
from django.conf.urls import url from django.conf.urls import url
from django.core.exceptions import ImproperlyConfigured from django.core.exceptions import ImproperlyConfigured
from django.core.urlresolvers import NoReverseMatch from django.core.urlresolvers import NoReverseMatch
from rest_framework import views from rest_framework import views
from rest_framework.compat import OrderedDict
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework.reverse import reverse from rest_framework.reverse import reverse
from rest_framework.urlpatterns import format_suffix_patterns from rest_framework.urlpatterns import format_suffix_patterns
@ -175,7 +174,7 @@ class SimpleRouter(BaseRouter):
url_path = initkwargs.pop("url_path", None) or methodname url_path = initkwargs.pop("url_path", None) or methodname
ret.append(Route( ret.append(Route(
url=replace_methodname(route.url, url_path), 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), name=replace_methodname(route.name, url_path),
initkwargs=initkwargs, initkwargs=initkwargs,
)) ))

View File

@ -12,6 +12,8 @@ response content is handled by parsers and renderers.
""" """
from __future__ import unicode_literals from __future__ import unicode_literals
import warnings
from django.db import models from django.db import models
from django.db.models.fields import Field as DjangoModelField from django.db.models.fields import Field as DjangoModelField
from django.db.models.fields import FieldDoesNotExist 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 django.utils.translation import ugettext_lazy as _
from rest_framework.compat import DurationField as ModelDurationField 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.compat import postgres_fields, unicode_to_repr
from rest_framework.utils import model_meta from rest_framework.utils import model_meta
from rest_framework.utils.field_mapping import ( from rest_framework.utils.field_mapping import (
@ -51,6 +54,8 @@ LIST_SERIALIZER_KWARGS = (
'instance', 'data', 'partial', 'context', 'allow_null' 'instance', 'data', 'partial', 'context', 'allow_null'
) )
ALL_FIELDS = '__all__'
# BaseSerializer # BaseSerializer
# -------------- # --------------
@ -120,10 +125,10 @@ class BaseSerializer(Field):
} }
if allow_empty is not None: if allow_empty is not None:
list_kwargs['allow_empty'] = allow_empty list_kwargs['allow_empty'] = allow_empty
list_kwargs.update(dict([ list_kwargs.update({
(key, value) for key, value in kwargs.items() key: value for key, value in kwargs.items()
if key in LIST_SERIALIZER_KWARGS if key in LIST_SERIALIZER_KWARGS
])) })
meta = getattr(cls, 'Meta', None) meta = getattr(cls, 'Meta', None)
list_serializer_class = getattr(meta, 'list_serializer_class', ListSerializer) list_serializer_class = getattr(meta, 'list_serializer_class', ListSerializer)
return list_serializer_class(*args, **list_kwargs) return list_serializer_class(*args, **list_kwargs)
@ -166,6 +171,12 @@ class BaseSerializer(Field):
"For example: 'serializer.save(owner=request.user)'.'" "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( validated_data = dict(
list(self.validated_data.items()) + list(self.validated_data.items()) +
list(kwargs.items()) list(kwargs.items())
@ -294,10 +305,10 @@ def get_validation_error_detail(exc):
elif isinstance(exc.detail, dict): elif isinstance(exc.detail, dict):
# If errors may be a dict we use the standard {key: list of values}. # 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. # Here we ensure that all the values are *lists* of errors.
return dict([ return {
(key, value if isinstance(value, list) else [value]) key: value if isinstance(value, list) else [value]
for key, value in exc.detail.items() for key, value in exc.detail.items()
]) }
elif isinstance(exc.detail, list): elif isinstance(exc.detail, list):
# Errors raised as a list are non-field errors. # Errors raised as a list are non-field errors.
return { return {
@ -780,6 +791,8 @@ class ModelSerializer(Serializer):
} }
if ModelDurationField is not None: if ModelDurationField is not None:
serializer_field_mapping[ModelDurationField] = DurationField serializer_field_mapping[ModelDurationField] = DurationField
if ModelJSONField is not None:
serializer_field_mapping[ModelJSONField] = JSONField
serializer_related_field = PrimaryKeyRelatedField serializer_related_field = PrimaryKeyRelatedField
serializer_url_field = HyperlinkedIdentityField serializer_url_field = HyperlinkedIdentityField
serializer_choice_field = ChoiceField 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 # you'll also need to ensure you update the `create` method on any generic
# views, to correctly handle the 'Location' response header for # views, to correctly handle the 'Location' response header for
# "HTTP 201 Created" responses. # "HTTP 201 Created" responses.
url_field_name = api_settings.URL_FIELD_NAME url_field_name = None
# Default `create` and `update` behavior... # Default `create` and `update` behavior...
def create(self, validated_data): def create(self, validated_data):
@ -874,6 +887,9 @@ class ModelSerializer(Serializer):
Return the dict of field names -> field instances that should be Return the dict of field names -> field instances that should be
used for `self.fields` when instantiating the serializer. 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'), ( assert hasattr(self, 'Meta'), (
'Class {serializer_class} missing "Meta" attribute'.format( 'Class {serializer_class} missing "Meta" attribute'.format(
serializer_class=self.__class__.__name__ serializer_class=self.__class__.__name__
@ -948,10 +964,10 @@ class ModelSerializer(Serializer):
fields = getattr(self.Meta, 'fields', None) fields = getattr(self.Meta, 'fields', None)
exclude = getattr(self.Meta, 'exclude', 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( raise TypeError(
'The `fields` option must be a list or tuple. Got %s.' % 'The `fields` option must be a list or tuple or "__all__". '
type(fields).__name__ 'Got %s.' % type(fields).__name__
) )
if exclude and not isinstance(exclude, (list, tuple)): 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: if fields is not None:
# Ensure that all declared fields have also been included in the # Ensure that all declared fields have also been included in the
# `Meta.fields` option. # `Meta.fields` option.
@ -1207,13 +1237,10 @@ class ModelSerializer(Serializer):
for model_field in model_fields.values(): for model_field in model_fields.values():
# Include each of the `unique_for_*` field names. # Include each of the `unique_for_*` field names.
unique_constraint_names |= set([ unique_constraint_names |= {model_field.unique_for_date, model_field.unique_for_month,
model_field.unique_for_date, model_field.unique_for_year}
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, # Include each of the `unique_together` field names,
# so long as all the field names are included on the serializer. # 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 # 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 # cannot map to a field, and must be a traversal, so we're not
# including those. # including those.
field_names = set([ field_names = {
field.source for field in self.fields.values() field.source for field in self.fields.values()
if (field.source != '*') and ('.' not in field.source) if (field.source != '*') and ('.' not in field.source)
]) }
# Note that we make sure to check `unique_together` both on the # Note that we make sure to check `unique_together` both on the
# base model class, but also on any parent classes. # base model class, but also on any parent classes.

View File

@ -26,8 +26,6 @@ from django.utils import six
from rest_framework import ISO_8601 from rest_framework import ISO_8601
from rest_framework.compat import importlib from rest_framework.compat import importlib
USER_SETTINGS = getattr(settings, 'REST_FRAMEWORK', None)
DEFAULTS = { DEFAULTS = {
# Base API policies # Base API policies
'DEFAULT_RENDERER_CLASSES': ( 'DEFAULT_RENDERER_CLASSES': (
@ -93,13 +91,8 @@ DEFAULTS = {
), ),
'TEST_REQUEST_DEFAULT_FORMAT': 'multipart', 'TEST_REQUEST_DEFAULT_FORMAT': 'multipart',
# Browser enhancements # Hyperlink settings
'FORM_METHOD_OVERRIDE': '_method',
'FORM_CONTENT_OVERRIDE': '_content',
'FORM_CONTENTTYPE_OVERRIDE': '_content_type',
'URL_ACCEPT_OVERRIDE': 'accept',
'URL_FORMAT_OVERRIDE': 'format', 'URL_FORMAT_OVERRIDE': 'format',
'FORMAT_SUFFIX_KWARG': 'format', 'FORMAT_SUFFIX_KWARG': 'format',
'URL_FIELD_NAME': 'url', 'URL_FIELD_NAME': 'url',
@ -188,10 +181,17 @@ class APISettings(object):
and return the class, rather than the string literal. and return the class, rather than the string literal.
""" """
def __init__(self, user_settings=None, defaults=None, import_strings=None): 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.defaults = defaults or DEFAULTS
self.import_strings = import_strings or IMPORT_STRINGS 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): def __getattr__(self, attr):
if attr not in self.defaults.keys(): if attr not in self.defaults.keys():
raise AttributeError("Invalid API setting: '%s'" % attr) raise AttributeError("Invalid API setting: '%s'" % attr)
@ -212,7 +212,7 @@ class APISettings(object):
return val return val
api_settings = APISettings(USER_SETTINGS, DEFAULTS, IMPORT_STRINGS) api_settings = APISettings(None, DEFAULTS, IMPORT_STRINGS)
def reload_api_settings(*args, **kwargs): def reload_api_settings(*args, **kwargs):

View 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);
};

View 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);
}
}
});

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -104,9 +104,7 @@
{% endif %} {% endif %}
{% if delete_form %} {% if delete_form %}
<form class="button-form" action="{{ request.get_full_path }}" method="POST"> <form class="button-form" action="{{ request.get_full_path }}" data-method="DELETE">
{% csrf_token %}
<input type="hidden" name="{{ api_settings.FORM_METHOD_OVERRIDE }}" value="DELETE" />
<button class="btn btn-danger"> <button class="btn btn-danger">
<span class="glyphicon glyphicon-remove" aria-hidden="true"></span> Delete <span class="glyphicon glyphicon-remove" aria-hidden="true"></span> Delete
</button> </button>
@ -180,7 +178,7 @@
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button> <button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button>
<h4 class="modal-title" id="myModalLabel">Edit</h4> <h4 class="modal-title" id="myModalLabel">Edit</h4>
</div> </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"> <div class="modal-body">
<fieldset> <fieldset>
{{ put_form }} {{ put_form }}
@ -188,7 +186,7 @@
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">Close</button> <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> </div>
</form> </form>
</div> </div>
@ -204,7 +202,7 @@
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button> <button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button>
<h4 class="modal-title" id="myModalLabel">{{ error_title }}</h4> <h4 class="modal-title" id="myModalLabel">{{ error_title }}</h4>
</div> </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"> <div class="modal-body">
<fieldset> <fieldset>
{{ error_form }} {{ error_form }}
@ -212,7 +210,7 @@
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">Close</button> <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> </div>
</form> </form>
</div> </div>
@ -221,10 +219,17 @@
{% endif %} {% endif %}
{% block script %} {% 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/bootstrap.min.js" %}"></script>
<script src="{% static "rest_framework/js/prettify-min.js" %}"></script> <script src="{% static "rest_framework/js/prettify-min.js" %}"></script>
<script src="{% static "rest_framework/js/default.js" %}"></script> <script src="{% static "rest_framework/js/default.js" %}"></script>
<script>
$(document).ready(function() {
$('form').ajaxForm();
});
</script>
{% endblock %} {% endblock %}
</body> </body>
{% endblock %} {% endblock %}

View File

@ -94,17 +94,13 @@
{% endif %} {% endif %}
{% if options_form %} {% if options_form %}
<form class="button-form" action="{{ request.get_full_path }}" method="POST"> <form class="button-form" action="{{ request.get_full_path }}" data-method="OPTIONS">
{% csrf_token %}
<input type="hidden" name="{{ api_settings.FORM_METHOD_OVERRIDE }}" value="OPTIONS" />
<button class="btn btn-primary js-tooltip" title="Make an OPTIONS request on the {{ name }} resource">OPTIONS</button> <button class="btn btn-primary js-tooltip" title="Make an OPTIONS request on the {{ name }} resource">OPTIONS</button>
</form> </form>
{% endif %} {% endif %}
{% if delete_form %} {% if delete_form %}
<form class="button-form" action="{{ request.get_full_path }}" method="POST"> <form class="button-form" action="{{ request.get_full_path }}" data-method="DELETE">
{% csrf_token %}
<input type="hidden" name="{{ api_settings.FORM_METHOD_OVERRIDE }}" value="DELETE" />
<button class="btn btn-danger js-tooltip" title="Make a DELETE request on the {{ name }} resource">DELETE</button> <button class="btn btn-danger js-tooltip" title="Make a DELETE request on the {{ name }} resource">DELETE</button>
</form> </form>
{% endif %} {% endif %}
@ -168,7 +164,7 @@
</div> </div>
{% endif %} {% 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 %} {% with form=raw_data_post_form %}
<form action="{{ request.get_full_path }}" method="POST" class="form-horizontal"> <form action="{{ request.get_full_path }}" method="POST" class="form-horizontal">
<fieldset> <fieldset>
@ -200,11 +196,11 @@
<div class="well tab-content"> <div class="well tab-content">
{% if put_form %} {% if put_form %}
<div class="tab-pane" id="put-object-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> <fieldset>
{{ put_form }} {{ put_form }}
<div class="form-actions"> <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> </div>
</fieldset> </fieldset>
</form> </form>
@ -213,15 +209,15 @@
<div {% if put_form %}class="tab-pane"{% endif %} id="put-generic-content-form"> <div {% if put_form %}class="tab-pane"{% endif %} id="put-generic-content-form">
{% with form=raw_data_put_or_patch_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> <fieldset>
{% include "rest_framework/raw_data_form.html" %} {% include "rest_framework/raw_data_form.html" %}
<div class="form-actions"> <div class="form-actions">
{% if raw_data_put_form %} {% 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 %} {% endif %}
{% if raw_data_patch_form %} {% 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 %} {% endif %}
</div> </div>
</fieldset> </fieldset>
@ -237,10 +233,17 @@
</div><!-- ./wrapper --> </div><!-- ./wrapper -->
{% block script %} {% 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/bootstrap.min.js" %}"></script>
<script src="{% static "rest_framework/js/prettify-min.js" %}"></script> <script src="{% static "rest_framework/js/prettify-min.js" %}"></script>
<script src="{% static "rest_framework/js/default.js" %}"></script> <script src="{% static "rest_framework/js/default.js" %}"></script>
<script>
$(document).ready(function() {
$('form').ajaxForm();
});
</script>
{% endblock %} {% endblock %}
</body> </body>
{% endblock %} {% endblock %}

View File

@ -1,5 +1,4 @@
{% load rest_framework %} {% load rest_framework %}
{% csrf_token %}
{{ form.non_field_errors }} {{ form.non_field_errors }}
{% for field in form %} {% for field in form %}
<div class="form-group"> <div class="form-group">

View File

@ -41,8 +41,9 @@ def optional_login(request):
except NoReverseMatch: except NoReverseMatch:
return '' return ''
snippet = "<li><a href='{href}?next={next}'>Log in</a></li>".format(href=login_url, next=escape(request.path)) snippet = "<li><a href='{href}?next={next}'>Log in</a></li>"
return snippet snippet = snippet.format(href=login_url, next=escape(request.path))
return mark_safe(snippet)
@register.simple_tag @register.simple_tag
@ -64,8 +65,8 @@ def optional_logout(request, user):
<li><a href='{href}?next={next}'>Log out</a></li> <li><a href='{href}?next={next}'>Log out</a></li>
</ul> </ul>
</li>""" </li>"""
snippet = snippet.format(user=escape(user), href=logout_url, next=escape(request.path))
return snippet.format(user=user, href=logout_url, next=escape(request.path)) return mark_safe(snippet)
@register.simple_tag @register.simple_tag

View File

@ -8,7 +8,6 @@ from django.core import validators
from django.db import models from django.db import models
from django.utils.text import capfirst from django.utils.text import capfirst
from rest_framework.compat import clean_manytomany_helptext
from rest_framework.validators import UniqueValidator from rest_framework.validators import UniqueValidator
NUMERIC_FIELD_TYPES = ( NUMERIC_FIELD_TYPES = (
@ -113,10 +112,19 @@ def get_field_kwargs(field_name, model_field):
kwargs['choices'] = model_field.choices kwargs['choices'] = model_field.choices
return kwargs 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, # Ensure that max_length is passed explicitly as a keyword arg,
# rather than as a validator. # rather than as a validator.
max_length = getattr(model_field, 'max_length', None) 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 kwargs['max_length'] = max_length
validator_kwarg = [ validator_kwarg = [
validator for validator in 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): 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) validator_kwarg.append(validator)
if validator_kwarg: if validator_kwarg:
@ -222,7 +238,7 @@ def get_relation_kwargs(field_name, relation_info):
if model_field: if model_field:
if model_field.verbose_name and needs_label(model_field, field_name): if model_field.verbose_name and needs_label(model_field, field_name):
kwargs['label'] = capfirst(model_field.verbose_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: if help_text:
kwargs['help_text'] = help_text kwargs['help_text'] = help_text
if not model_field.editable: if not model_field.editable:

View File

@ -6,14 +6,13 @@ relationships and their associated metadata.
Usage: `get_field_info(model)` returns a `FieldInfo` instance. Usage: `get_field_info(model)` returns a `FieldInfo` instance.
""" """
import inspect import inspect
from collections import namedtuple from collections import OrderedDict, namedtuple
from django.apps import apps
from django.core.exceptions import ImproperlyConfigured from django.core.exceptions import ImproperlyConfigured
from django.db import models from django.db import models
from django.utils import six from django.utils import six
from rest_framework.compat import OrderedDict
FieldInfo = namedtuple('FieldResult', [ FieldInfo = namedtuple('FieldResult', [
'pk', # Model field instance 'pk', # Model field instance
'fields', # Dict of field name -> 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: if isinstance(obj, six.string_types) and len(obj.split('.')) == 2:
app_name, model_name = obj.split('.') 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: if resolved_model is None:
msg = "Django did not return a model for {0}.{1}" msg = "Django did not return a model for {0}.{1}"
raise ImproperlyConfigured(msg.format(app_name, model_name)) raise ImproperlyConfigured(msg.format(app_name, model_name))

View File

@ -1,10 +1,11 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import collections import collections
from collections import OrderedDict
from django.utils.encoding import force_text 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): class ReturnDict(OrderedDict):

View File

@ -100,11 +100,11 @@ class UniqueTogetherValidator(object):
if self.instance is not None: if self.instance is not None:
return return
missing = dict([ missing = {
(field_name, self.missing_message) field_name: self.missing_message
for field_name in self.fields for field_name in self.fields
if field_name not in attrs if field_name not in attrs
]) }
if missing: if missing:
raise ValidationError(missing) raise ValidationError(missing)
@ -120,10 +120,10 @@ class UniqueTogetherValidator(object):
attrs[field_name] = getattr(self.instance, field_name) attrs[field_name] = getattr(self.instance, field_name)
# Determine the filter keyword arguments and filter the queryset. # Determine the filter keyword arguments and filter the queryset.
filter_kwargs = dict([ filter_kwargs = {
(field_name, attrs[field_name]) field_name: attrs[field_name]
for field_name in self.fields for field_name in self.fields
]) }
return queryset.filter(**filter_kwargs) return queryset.filter(**filter_kwargs)
def exclude_current_instance(self, attrs, queryset): def exclude_current_instance(self, attrs, queryset):
@ -184,11 +184,11 @@ class BaseUniqueForValidator(object):
The `UniqueFor<Range>Validator` classes always force an implied The `UniqueFor<Range>Validator` classes always force an implied
'required' state on the fields they are applied to. 'required' state on the fields they are applied to.
""" """
missing = dict([ missing = {
(field_name, self.missing_message) field_name: self.missing_message
for field_name in [self.field, self.date_field] for field_name in [self.field, self.date_field]
if field_name not in attrs if field_name not in attrs
]) }
if missing: if missing:
raise ValidationError(missing) raise ValidationError(missing)

View File

@ -93,7 +93,11 @@ if __name__ == "__main__":
except ValueError: except ValueError:
pass pass
else: else:
pytest_args = ['--cov', 'rest_framework'] + pytest_args pytest_args = [
'--cov-report',
'xml',
'--cov',
'rest_framework'] + pytest_args
if first_arg.startswith('-'): if first_arg.startswith('-'):
# `runtests.py [flags]` # `runtests.py [flags]`

View File

@ -1,11 +1,12 @@
from __future__ import unicode_literals 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.db import connection, connections, transaction
from django.http import Http404 from django.http import Http404
from django.test import TestCase, TransactionTestCase from django.test import TestCase, TransactionTestCase
from django.utils.decorators import method_decorator from django.utils.decorators import method_decorator
from django.utils.unittest import skipUnless
from rest_framework import status from rest_framework import status
from rest_framework.exceptions import APIException from rest_framework.exceptions import APIException
@ -35,8 +36,10 @@ class APIExceptionView(APIView):
raise APIException raise APIException
@skipUnless(connection.features.uses_savepoints, @unittest.skipUnless(
"'atomic' requires transactions and savepoints.") connection.features.uses_savepoints,
"'atomic' requires transactions and savepoints."
)
class DBTransactionTests(TestCase): class DBTransactionTests(TestCase):
def setUp(self): def setUp(self):
self.view = BasicView.as_view() self.view = BasicView.as_view()
@ -55,8 +58,10 @@ class DBTransactionTests(TestCase):
assert BasicModel.objects.count() == 1 assert BasicModel.objects.count() == 1
@skipUnless(connection.features.uses_savepoints, @unittest.skipUnless(
"'atomic' requires transactions and savepoints.") connection.features.uses_savepoints,
"'atomic' requires transactions and savepoints."
)
class DBTransactionErrorTests(TestCase): class DBTransactionErrorTests(TestCase):
def setUp(self): def setUp(self):
self.view = ErrorView.as_view() self.view = ErrorView.as_view()
@ -83,8 +88,10 @@ class DBTransactionErrorTests(TestCase):
assert BasicModel.objects.count() == 1 assert BasicModel.objects.count() == 1
@skipUnless(connection.features.uses_savepoints, @unittest.skipUnless(
"'atomic' requires transactions and savepoints.") connection.features.uses_savepoints,
"'atomic' requires transactions and savepoints."
)
class DBTransactionAPIExceptionTests(TestCase): class DBTransactionAPIExceptionTests(TestCase):
def setUp(self): def setUp(self):
self.view = APIExceptionView.as_view() self.view = APIExceptionView.as_view()
@ -113,8 +120,10 @@ class DBTransactionAPIExceptionTests(TestCase):
assert BasicModel.objects.count() == 0 assert BasicModel.objects.count() == 0
@skipUnless(connection.features.uses_savepoints, @unittest.skipUnless(
"'atomic' requires transactions and savepoints.") connection.features.uses_savepoints,
"'atomic' requires transactions and savepoints."
)
class NonAtomicDBTransactionAPIExceptionTests(TransactionTestCase): class NonAtomicDBTransactionAPIExceptionTests(TransactionTestCase):
@property @property
def urls(self): def urls(self):
@ -127,9 +136,8 @@ class NonAtomicDBTransactionAPIExceptionTests(TransactionTestCase):
BasicModel.objects.all() BasicModel.objects.all()
raise Http404 raise Http404
return patterns( return (
'', url(r'^$', NonAtomicAPIExceptionView.as_view()),
url(r'^$', NonAtomicAPIExceptionView.as_view())
) )
def setUp(self): def setUp(self):

View File

@ -1525,6 +1525,58 @@ class TestUnvalidatedDictField(FieldValues):
field = serializers.DictField() 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. # Tests for FieldField.
# --------------------- # ---------------------

View File

@ -1,6 +1,7 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import datetime import datetime
import unittest
from decimal import Decimal from decimal import Decimal
from django.conf.urls import url from django.conf.urls import url
@ -8,7 +9,6 @@ from django.core.urlresolvers import reverse
from django.db import models from django.db import models
from django.test import TestCase from django.test import TestCase
from django.test.utils import override_settings from django.test.utils import override_settings
from django.utils import unittest
from django.utils.dateparse import parse_date from django.utils.dateparse import parse_date
from django.utils.six.moves import reload_module from django.utils.six.moves import reload_module

View File

@ -8,6 +8,7 @@ an appropriate set of serializer fields for each case.
from __future__ import unicode_literals from __future__ import unicode_literals
import decimal import decimal
from collections import OrderedDict
import django import django
import pytest import pytest
@ -21,7 +22,7 @@ from django.utils import six
from rest_framework import serializers from rest_framework import serializers
from rest_framework.compat import DurationField as ModelDurationField 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): def dedent(blocktext):
@ -62,7 +63,7 @@ class RegularFieldsModel(models.Model):
positive_small_integer_field = models.PositiveSmallIntegerField() positive_small_integer_field = models.PositiveSmallIntegerField()
slug_field = models.SlugField(max_length=100) slug_field = models.SlugField(max_length=100)
small_integer_field = models.SmallIntegerField() small_integer_field = models.SmallIntegerField()
text_field = models.TextField() text_field = models.TextField(max_length=100)
time_field = models.TimeField() time_field = models.TimeField()
url_field = models.URLField(max_length=100) url_field = models.URLField(max_length=100)
custom_field = CustomField() custom_field = CustomField()
@ -160,11 +161,12 @@ class TestRegularFieldMappings(TestCase):
positive_small_integer_field = IntegerField() positive_small_integer_field = IntegerField()
slug_field = SlugField(max_length=100) slug_field = SlugField(max_length=100)
small_integer_field = IntegerField() 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() time_field = TimeField()
url_field = URLField(max_length=100) url_field = URLField(max_length=100)
custom_field = ModelField(model_field=<tests.test_model_serializer.CustomField: custom_field>) custom_field = ModelField(model_field=<tests.test_model_serializer.CustomField: custom_field>)
""") """)
self.assertEqual(unicode_repr(TestSerializer()), expected) self.assertEqual(unicode_repr(TestSerializer()), expected)
def test_field_options(self): def test_field_options(self):
@ -321,6 +323,21 @@ class TestRegularFieldMappings(TestCase):
ExampleSerializer() 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), @pytest.mark.skipif(django.VERSION < (1, 8),
reason='DurationField is only available for django1.8+') reason='DurationField is only available for django1.8+')

View File

@ -1,18 +1,18 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import base64 import base64
import unittest
from django.contrib.auth.models import Group, Permission, User from django.contrib.auth.models import Group, Permission, User
from django.core.urlresolvers import ResolverMatch from django.core.urlresolvers import ResolverMatch
from django.db import models from django.db import models
from django.test import TestCase from django.test import TestCase
from django.utils import unittest
from rest_framework import ( from rest_framework import (
HTTP_HEADER_ENCODING, authentication, generics, permissions, serializers, HTTP_HEADER_ENCODING, authentication, generics, permissions, serializers,
status 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.filters import DjangoObjectPermissionsFilter
from rest_framework.routers import DefaultRouter from rest_framework.routers import DefaultRouter
from rest_framework.test import APIRequestFactory from rest_framework.test import APIRequestFactory
@ -279,7 +279,7 @@ class ObjectPermissionsIntegrationTests(TestCase):
# give everyone model level permissions, as we are not testing those # give everyone model level permissions, as we are not testing those
everyone = Group.objects.create(name='everyone') everyone = Group.objects.create(name='everyone')
model_name = get_model_name(BasicPermModel) model_name = BasicPermModel._meta.model_name
app_label = BasicPermModel._meta.app_label app_label = BasicPermModel._meta.app_label
f = '{0}_{1}'.format f = '{0}_{1}'.format
perms = { perms = {

View File

@ -1,6 +1,6 @@
from __future__ import unicode_literals from __future__ import unicode_literals
from django.contrib.contenttypes.generic import ( from django.contrib.contenttypes.fields import (
GenericForeignKey, GenericRelation GenericForeignKey, GenericRelation
) )
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType

View File

@ -3,7 +3,7 @@ from __future__ import unicode_literals
import json import json
import re import re
from collections import MutableMapping from collections import MutableMapping, OrderedDict
from django.conf.urls import include, url from django.conf.urls import include, url
from django.core.cache import cache from django.core.cache import cache
@ -13,7 +13,6 @@ from django.utils import six
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from rest_framework import permissions, serializers, status from rest_framework import permissions, serializers, status
from rest_framework.compat import OrderedDict
from rest_framework.renderers import ( from rest_framework.renderers import (
BaseRenderer, BrowsableAPIRenderer, HTMLFormRenderer, JSONRenderer BaseRenderer, BrowsableAPIRenderer, HTMLFormRenderer, JSONRenderer
) )
@ -192,17 +191,6 @@ class RendererEndToEndTests(TestCase):
self.assertEqual(resp.content, RENDERER_B_SERIALIZER(DUMMYCONTENT)) self.assertEqual(resp.content, RENDERER_B_SERIALIZER(DUMMYCONTENT))
self.assertEqual(resp.status_code, DUMMYSTATUS) 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): 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.""" """If the Accept header is unsatisfiable we should return a 406 Not Acceptable response."""
resp = self.client.get('/', HTTP_ACCEPT='foo/bar') resp = self.client.get('/', HTTP_ACCEPT='foo/bar')

View File

@ -3,27 +3,20 @@ Tests for content parsing, and form-overloaded content parsing.
""" """
from __future__ import unicode_literals from __future__ import unicode_literals
import json
from io import BytesIO
import django import django
import pytest import pytest
from django.conf.urls import url from django.conf.urls import url
from django.contrib.auth import authenticate, login, logout from django.contrib.auth import authenticate, login, logout
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.contrib.sessions.middleware import SessionMiddleware from django.contrib.sessions.middleware import SessionMiddleware
from django.core.handlers.wsgi import WSGIRequest
from django.test import TestCase from django.test import TestCase
from django.utils import six from django.utils import six
from rest_framework import status from rest_framework import status
from rest_framework.authentication import SessionAuthentication from rest_framework.authentication import SessionAuthentication
from rest_framework.parsers import ( from rest_framework.parsers import BaseParser, FormParser, MultiPartParser
BaseParser, FormParser, JSONParser, MultiPartParser from rest_framework.request import Request
)
from rest_framework.request import Empty, Request
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework.settings import api_settings
from rest_framework.test import APIClient, APIRequestFactory from rest_framework.test import APIClient, APIRequestFactory
from rest_framework.views import APIView from rest_framework.views import APIView
@ -43,36 +36,6 @@ class PlainTextParser(BaseParser):
return stream.read() 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): class TestContentParsing(TestCase):
def test_standard_behaviour_determines_no_content_GET(self): def test_standard_behaviour_determines_no_content_GET(self):
""" """
@ -137,49 +100,6 @@ class TestContentParsing(TestCase):
request.parsers = (PlainTextParser(), ) request.parsers = (PlainTextParser(), )
self.assertEqual(request.data, content) 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): class MockView(APIView):
authentication_classes = (SessionAuthentication,) authentication_classes = (SessionAuthentication,)

View File

@ -5,11 +5,11 @@ from django.test import TestCase
from django.utils import six from django.utils import six
from rest_framework import generics, routers, serializers, status, viewsets from rest_framework import generics, routers, serializers, status, viewsets
from rest_framework.parsers import JSONParser
from rest_framework.renderers import ( from rest_framework.renderers import (
BaseRenderer, BrowsableAPIRenderer, JSONRenderer BaseRenderer, BrowsableAPIRenderer, JSONRenderer
) )
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework.settings import api_settings
from rest_framework.views import APIView from rest_framework.views import APIView
from tests.models import BasicModel from tests.models import BasicModel
@ -79,6 +79,14 @@ class MockViewSettingContentType(APIView):
return Response(DUMMYCONTENT, status=DUMMYSTATUS, content_type='setbyview') 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): class HTMLView(APIView):
renderer_classes = (BrowsableAPIRenderer, ) renderer_classes = (BrowsableAPIRenderer, )
@ -114,6 +122,7 @@ urlpatterns = [
url(r'^.*\.(?P<format>.+)$', MockView.as_view(renderer_classes=[RendererA, RendererB, RendererC])), url(r'^.*\.(?P<format>.+)$', MockView.as_view(renderer_classes=[RendererA, RendererB, RendererC])),
url(r'^$', 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'^html$', HTMLView.as_view()),
url(r'^json$', JSONView.as_view()),
url(r'^html1$', HTMLView1.as_view()), url(r'^html1$', HTMLView1.as_view()),
url(r'^html_new_model$', HTMLNewModelView.as_view()), url(r'^html_new_model$', HTMLNewModelView.as_view()),
url(r'^html_new_model_viewset', include(new_model_viewset_router.urls)), 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.content, RENDERER_B_SERIALIZER(DUMMYCONTENT))
self.assertEqual(resp.status_code, DUMMYSTATUS) 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): def test_specified_renderer_serializes_content_on_format_query(self):
"""If a 'format' query is specified, the renderer with the matching """If a 'format' query is specified, the renderer with the matching
format attribute should serialize the response.""" format attribute should serialize the response."""
@ -203,6 +201,25 @@ class RendererIntegrationTests(TestCase):
self.assertEqual(resp.status_code, DUMMYSTATUS) 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): class Issue122Tests(TestCase):
""" """
Tests that covers #122. Tests that covers #122.
@ -270,16 +287,6 @@ class Issue807Tests(TestCase):
resp = self.client.get('/setbyview', **headers) resp = self.client.get('/setbyview', **headers)
self.assertEqual('setbyview', resp['Content-Type']) 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): def test_form_has_label_and_help_text(self):
resp = self.client.get('/html_new_model') resp = self.client.get('/html_new_model')
self.assertEqual(resp['Content-Type'], 'text/html; charset=utf-8') self.assertEqual(resp['Content-Type'], 'text/html; charset=utf-8')

View File

@ -51,6 +51,16 @@ class TestSerializer:
with pytest.raises(AttributeError): with pytest.raises(AttributeError):
serializer.data 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: class TestValidateMethod:
def test_non_field_error_validate_method(self): def test_non_field_error_validate_method(self):

View File

@ -150,16 +150,16 @@ class ResolveModelWithPatchedDjangoTests(TestCase):
def setUp(self): def setUp(self):
"""Monkeypatch get_model.""" """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): def get_model(app_label, model_name):
return None 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): def tearDown(self):
"""Revert monkeypatching.""" """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): def test_blows_up_if_model_does_not_resolve(self):
with self.assertRaises(ImproperlyConfigured): with self.assertRaises(ImproperlyConfigured):

View File

@ -48,7 +48,7 @@ class TestUniquenessValidation(TestCase):
data = {'username': 'existing'} data = {'username': 'existing'}
serializer = UniquenessSerializer(data=data) serializer = UniquenessSerializer(data=data)
assert not serializer.is_valid() 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): def test_is_unique(self):
data = {'username': 'other'} data = {'username': 'other'}

View File

@ -74,21 +74,6 @@ class ClassBasedViewIntegrationTests(TestCase):
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertEqual(sanitise_json_error(response.data), expected) 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): class FunctionBasedViewIntegrationTests(TestCase):
def setUp(self): def setUp(self):
@ -103,21 +88,6 @@ class FunctionBasedViewIntegrationTests(TestCase):
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertEqual(sanitise_json_error(response.data), expected) 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): class TestCustomExceptionHandler(TestCase):
def setUp(self): def setUp(self):

49
tox.ini
View File

@ -4,30 +4,57 @@ addopts=--tb=short
[tox] [tox]
envlist = envlist =
py27-{lint,docs}, py27-{lint,docs},
{py26,py27,py32,py33,py34}-django{15,16}, {py27,py32,py33,py34}-django{17,18},
{py27,py32,py33,py34}-django{17,18,master} {py27,py34,py35}-django{19}
[testenv] [testenv]
basepython =
py27: python2.7
py32: python3.2
py33: python3.3
py34: python3.4
py35: python3.5
commands = ./runtests.py --fast {posargs} --coverage commands = ./runtests.py --fast {posargs} --coverage
setenv = setenv =
PYTHONDONTWRITEBYTECODE=1 PYTHONDONTWRITEBYTECODE=1
deps = deps =
django15: Django==1.5.6 # Should track minimum supported django17: Django==1.7.10
django16: Django==1.6.3 # Should track minimum supported django18: Django==1.8.4
django17: Django==1.7.10 # Should track maximum supported django19: https://www.djangoproject.com/download/1.9b1/tarball/
django18: Django==1.8.4 # Should track maximum supported -rrequirements/requirements-testing.txt
djangomaster: https://github.com/django/django/archive/master.tar.gz -rrequirements/requirements-optionals.txt
-rrequirements/requirements-testing.txt
-rrequirements/requirements-optionals.txt
[testenv:py27-lint] [testenv:py27-lint]
commands = ./runtests.py --lintonly commands = ./runtests.py --lintonly
deps = deps =
-rrequirements/requirements-codestyle.txt -rrequirements/requirements-codestyle.txt
-rrequirements/requirements-testing.txt -rrequirements/requirements-testing.txt
[testenv:py27-docs] [testenv:py27-docs]
commands = mkdocs build commands = mkdocs build
deps = deps =
-rrequirements/requirements-testing.txt -rrequirements/requirements-testing.txt
-rrequirements/requirements-documentation.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