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

View File

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

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-framework-social-oauth2
[Django-rest-framework-social-oauth2][django-rest-framework-social-oauth2] library provides an easy way to integrate social plugins (facebook, twitter, google, etc.) to your authentication system and an easy oauth2 setup. With this library, you will be able to authenticate users based on external tokens (e.g. facebook access token), convert these tokens to "in-house" oauth2 tokens and use and generate oauth2 tokens to authenticate your users.
## django-rest-knox
[Django-rest-knox][django-rest-knox] library provides models and views to handle token based authentication in a more secure and extensible way than the built-in TokenAuthentication scheme - with Single Page Applications and Mobile clients in mind. It provides per-client tokens, and views to generate them when provided some other authentication (usually basic authentication), to delete the token (providing a server enforced logout) and to delete all tokens (logs out all clients that a user is logged into).
@ -404,4 +408,5 @@ HTTP Signature (currently a [IETF draft][http-signature-ietf-draft]) provides a
[mac]: http://tools.ietf.org/html/draft-hammer-oauth-v2-mac-token-05
[djoser]: https://github.com/sunscrapers/djoser
[django-rest-auth]: https://github.com/Tivix/django-rest-auth
[django-rest-framework-social-oauth2]: https://github.com/PhilipGarnero/django-rest-framework-social-oauth2
[django-rest-knox]: https://github.com/James1345/django-rest-knox

View File

@ -57,7 +57,7 @@ Note that setting a `default` value implies that the field is not required. Incl
### `source`
The name of the attribute that will be used to populate the field. May be a method that only takes a `self` argument, such as `URLField('get_absolute_url')`, or may use dotted notation to traverse attributes, such as `EmailField(source='user.email')`.
The name of the attribute that will be used to populate the field. May be a method that only takes a `self` argument, such as `URLField(source='get_absolute_url')`, or may use dotted notation to traverse attributes, such as `EmailField(source='user.email')`.
The value `source='*'` has a special meaning, and is used to indicate that the entire object should be passed through to the field. This can be useful for creating nested representations, or for fields which require access to the complete object in order to determine the output representation.
@ -459,6 +459,14 @@ You can also use the declarative style, as with `ListField`. For example:
class DocumentField(DictField):
child = CharField()
## JSONField
A field class that validates that the incoming data structure consists of valid JSON primitives. In its alternate binary mode, it will represent and validate JSON-encoded binary strings.
**Signature**: `JSONField(binary)`
- `binary` - If set to `True` then the field will output and validate a JSON encoded string, rather that a primitive data structure. Defaults to `False`.
---
# Miscellaneous fields

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
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
When dealing with nested representations that support deserializing the data, an errors with nested objects will be nested under the field name of the nested object.
When dealing with nested representations that support deserializing the data, any errors with nested objects will be nested under the field name of the nested object.
serializer = CommentSerializer(data={'user': {'email': 'foobar', 'username': 'doe'}, 'content': 'baz'})
serializer.is_valid()
@ -356,7 +356,7 @@ It is possible that a third party package, providing automatic support some kind
#### Handling saving related instances in model manager classes
An alternative to saving multiple related instances in the serializer is to write custom model manager classes handle creating the correct instances.
An alternative to saving multiple related instances in the serializer is to write custom model manager classes that handle creating the correct instances.
For example, suppose we wanted to ensure that `User` instances and `Profile` instances are always created together as a pair. We might write a custom manager class that looks something like this:
@ -438,6 +438,7 @@ Declaring a `ModelSerializer` looks like this:
class AccountSerializer(serializers.ModelSerializer):
class Meta:
model = Account
fields = ('id', 'account_name', 'users', 'created')
By default, all the model fields on the class will be mapped to a corresponding serializer fields.
@ -459,7 +460,7 @@ To do so, open the Django shell, using `python manage.py shell`, then import the
## Specifying which fields to include
If you only want a subset of the default fields to be used in a model serializer, you can do so using `fields` or `exclude` options, just as you would with a `ModelForm`.
If you only want a subset of the default fields to be used in a model serializer, you can do so using `fields` or `exclude` options, just as you would with a `ModelForm`. It is strongly recommended that you explicitly set all fields that should be serialized using the `fields` attribute. This will make it less likely to result in unintentionally exposing data when your models change.
For example:
@ -468,7 +469,27 @@ For example:
model = Account
fields = ('id', 'account_name', 'users', 'created')
The names in the `fields` option will normally map to model fields on the model class.
You can also set the `fields` attribute to the special value `'__all__'` to indicate that all fields in the model should be used.
For example:
class AccountSerializer(serializers.ModelSerializer):
class Meta:
model = Account
fields = '__all__'
You can set the `exclude` attribute to a list of fields to be excluded from the serializer.
For example:
class AccountSerializer(serializers.ModelSerializer):
class Meta:
model = Account
exclude = ('users',)
In the example above, if the `Account` model had 3 fields `account_name`, `users`, and `created`, this will result in the fields `account_name` and `created` to be serialized.
The names in the `fields` and `exclude` attributes will normally map to model fields on the model class.
Alternatively names in the `fields` options can map to properties or methods which take no arguments that exist on the model class.
@ -530,7 +551,7 @@ Please review the [Validators Documentation](/api-guide/validators/) for details
## Additional keyword arguments
There is also a shortcut allowing you to specify arbitrary additional keyword arguments on fields, using the `extra_kwargs` option. Similarly to `read_only_fields` this means you do not need to explicitly declare the field on the serializer.
There is also a shortcut allowing you to specify arbitrary additional keyword arguments on fields, using the `extra_kwargs` option. As in the case of `read_only_fields`, this means you do not need to explicitly declare the field on the serializer.
This option is a dictionary, mapping field names to a dictionary of keyword arguments. For example:
@ -811,7 +832,7 @@ This class implements the same basic API as the `Serializer` class:
* `.data` - Returns the outgoing primitive representation.
* `.is_valid()` - Deserializes and validates incoming data.
* `.validated_data` - Returns the validated incoming data.
* `.errors` - Returns an errors during validation.
* `.errors` - Returns any errors during validation.
* `.save()` - Persists the validated data into an object instance.
There are four methods that can be overridden, depending on what functionality you want the serializer class to support:

View File

@ -249,47 +249,23 @@ Default:
---
## Browser overrides
*The following settings provide URL or form-based overrides of the default browser behavior.*
#### FORM_METHOD_OVERRIDE
The name of a form field that may be used to override the HTTP method of the form.
If the value of this setting is `None` then form method overloading will be disabled.
Default: `'_method'`
#### FORM_CONTENT_OVERRIDE
The name of a form field that may be used to override the content of the form payload. Must be used together with `FORM_CONTENTTYPE_OVERRIDE`.
If either setting is `None` then form content overloading will be disabled.
Default: `'_content'`
#### FORM_CONTENTTYPE_OVERRIDE
The name of a form field that may be used to override the content type of the form payload. Must be used together with `FORM_CONTENT_OVERRIDE`.
If either setting is `None` then form content overloading will be disabled.
Default: `'_content_type'`
#### URL_ACCEPT_OVERRIDE
The name of a URL parameter that may be used to override the HTTP `Accept` header.
If the value of this setting is `None` then URL accept overloading will be disabled.
Default: `'accept'`
## Content type controls
#### URL_FORMAT_OVERRIDE
The name of a URL parameter that may be used to override the default `Accept` header based content negotiation.
The name of a URL parameter that may be used to override the default content negotiation `Accept` header behavior, by using a `format=…` query parameter in the request URL.
If the value of this setting is `None` then URL format overloading will be disabled.
For example: `http://example.com/organizations/?format=csv`
If the value of this setting is `None` then URL format overrides will be disabled.
Default: `'format'`
#### FORMAT_SUFFIX_KWARG
The name of a parameter in the URL conf that may be used to provide a format suffix. This setting is applied when using `format_suffix_patterns` to include suffixed URL patterns.
For example: `http://example.com/organizations.csv/`
Default: `'format'`
@ -451,12 +427,6 @@ A string representing the key that should be used for the URL fields generated b
Default: `'url'`
#### FORMAT_SUFFIX_KWARG
The name of a parameter in the URL conf that may be used to provide a format suffix.
Default: `'format'`
#### NUM_PROXIES
An integer of 0 or more, that may be used to specify the number of application proxies that the API runs behind. This allows throttling to more accurately identify client IP addresses. If set to `None` then less strict IP matching will be used by the throttle classes.

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.
class RandomRateThrottle(throttles.BaseThrottle):
class RandomRateThrottle(throttling.BaseThrottle):
def allow_request(self, request, view):
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`.
* `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`:

View File

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

View File

@ -4,58 +4,36 @@
>
> — [RESTful Web Services][cite], Leonard Richardson & Sam Ruby.
In order to allow the browsable API to function, there are a couple of browser enhancements that REST framework needs to provide.
As of version 3.3.0 onwards these are enabled with javascript, using the [ajax-form][ajax-form] library.
## Browser based PUT, DELETE, etc...
REST framework supports browser-based `PUT`, `DELETE` and other methods, by
overloading `POST` requests using a hidden form field.
The [AJAX form library][ajax-form] supports browser-based `PUT`, `DELETE` and other methods on HTML forms.
Note that this is the same strategy as is used in [Ruby on Rails][rails].
After including the library, use the `data-method` attribute on the form, like so:
For example, given the following form:
<form action="/news-items/5" method="POST">
<input type="hidden" name="_method" value="DELETE">
<form action="/" data-method="PUT">
<input name='foo'/>
...
</form>
`request.method` would return `"DELETE"`.
## HTTP header based method overriding
REST framework also supports method overriding via the semi-standard `X-HTTP-Method-Override` header. This can be useful if you are working with non-form content such as JSON and are working with an older web server and/or hosting provider that doesn't recognise particular HTTP methods such as `PATCH`. For example [Amazon Web Services ELB][aws_elb].
To use it, make a `POST` request, setting the `X-HTTP-Method-Override` header.
For example, making a `PATCH` request via `POST` in jQuery:
$.ajax({
url: '/myresource/',
method: 'POST',
headers: {'X-HTTP-Method-Override': 'PATCH'},
...
});
Note that prior to 3.3.0, this support was server-side rather than javascript based. The method overloading style (as used in [Ruby on Rails][rails]) is no longer supported due to subtle issues that it introduces in request parsing.
## Browser based submission of non-form content
Browser-based submission of content types other than form are supported by
using form fields named `_content` and `_content_type`:
Browser-based submission of content types such as JSON are supported by the [AJAX form library][ajax-form], using form fields with `data-override='content-type'` and `data-override='content'` attributes.
For example, given the following form:
For example:
<form action="/news-items/5" method="PUT">
<input type="hidden" name="_content_type" value="application/json">
<input name="_content" value="{'count': 1}">
<form action="/">
<input data-override='content-type' value='application/json' type='hidden'/>
<textarea data-override='content'>{}</textarea>
<input type="submit"/>
</form>
`request.content_type` would return `"application/json"`, and
`request.stream` would return `"{'count': 1}"`
## URL based accept headers
REST framework can take `?accept=application/json` style URL parameters,
which allow the `Accept` header to be overridden.
This can be useful for testing the API from a web browser, where you don't
have any control over what is sent in the `Accept` header.
Note that prior to 3.3.0, this support was server-side rather than javascript based.
## URL based format suffixes
@ -63,8 +41,37 @@ REST framework can take `?format=json` style URL parameters, which can be a
useful shortcut for determining which content type should be returned from
the view.
This is a more concise than using the `accept` override, but it also gives
you less control. (For example you can't specify any media type parameters)
This behavior is controlled using the `URL_FORMAT_OVERRIDE` setting.
## HTTP header based method overriding
Prior to version 3.3.0 the semi extension header `X-HTTP-Method-Override` was supported for overriding the request method. This behavior is no longer in core, but can be adding if needed using middleware.
For example:
METHOD_OVERRIDE_HEADER = 'HTTP_X_HTTP_METHOD_OVERRIDE'
class MethodOverrideMiddleware(object):
def process_view(self, request, callback, callback_args, callback_kwargs):
if request.method != 'POST':
return
if METHOD_OVERRIDE_HEADER not in request.META:
return
request.method = request.META[METHOD_OVERRIDE_HEADER]
## URL based accept headers
Until version 3.3.0 REST framework included built-in support for `?accept=application/json` style URL parameters, which would allow the `Accept` header to be overridden.
Since the introduction of the content negotiation API this behavior is no longer included in core, but may be added using a custom content negotiation class, if needed.
For example:
class AcceptQueryParamOverride()
def get_accept_list(self, request):
header = request.META.get('HTTP_ACCEPT', '*/*')
header = request.query_params.get('_accept', header)
return [token.strip() for token in header.split(',')]
## Doesn't HTML5 support PUT and DELETE forms?
@ -74,7 +81,7 @@ was later [dropped from the spec][html5]. There remains
as well as how to support content types other than form-encoded data.
[cite]: http://www.amazon.com/Restful-Web-Services-Leonard-Richardson/dp/0596529260
[ajax-form]: https://github.com/tomchristie/ajax-form
[rails]: http://guides.rubyonrails.org/form_helpers.html#how-do-forms-with-put-or-delete-methods-work
[html5]: http://www.w3.org/TR/html5-diff/#changes-2010-06-24
[put_delete]: http://amundsen.com/examples/put-delete-forms/
[aws_elb]: https://forums.aws.amazon.com/thread.jspa?messageID=400724

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.4
@ -533,3 +541,7 @@ For older release notes, [please see the version 2.x documentation][old-release-
[gh3361]: https://github.com/tomchristie/django-rest-framework/issues/3361
[gh3364]: https://github.com/tomchristie/django-rest-framework/issues/3364
[gh3415]: https://github.com/tomchristie/django-rest-framework/issues/3415
<!-- 3.3.0 -->
[gh3421]: https://github.com/tomchristie/django-rest-framework/pulls/3421
[gh3429]: https://github.com/tomchristie/django-rest-framework/pull/3429

View File

@ -237,6 +237,7 @@ To submit new content, [open an issue][drf-create-issue] or [create a pull reque
### Misc
* [cookiecutter-django-rest][cookiecutter-django-rest] - A cookiecutter template that takes care of the setup and configuration so you can focus on making your REST apis awesome.
* [djangorestrelationalhyperlink][djangorestrelationalhyperlink] - A hyperlinked serialiser that can can be used to alter relationships via hyperlinks, but otherwise like a hyperlink model serializer.
* [django-rest-swagger][django-rest-swagger] - An API documentation generator for Swagger UI.
* [django-rest-framework-proxy][django-rest-framework-proxy] - Proxy to redirect incoming request to another API server.
@ -246,6 +247,7 @@ To submit new content, [open an issue][drf-create-issue] or [create a pull reque
* [django-versatileimagefield][django-versatileimagefield] - Provides a drop-in replacement for Django's stock `ImageField` that makes it easy to serve images in multiple sizes/renditions from a single field. For DRF-specific implementation docs, [click here][django-versatileimagefield-drf-docs].
* [drf-tracking][drf-tracking] - Utilities to track requests to DRF API views.
* [django-rest-framework-braces][django-rest-framework-braces] - Collection of utilities for working with Django Rest Framework. The most notable ones are [FormSerializer](https://django-rest-framework-braces.readthedocs.org/en/latest/overview.html#formserializer) and [SerializerForm](https://django-rest-framework-braces.readthedocs.org/en/latest/overview.html#serializerform), which are adapters between DRF serializers and Django forms.
* [drf-haystack][drf-haystack] - Haystack search for Django Rest Framework
## Other Resources
@ -345,3 +347,5 @@ To submit new content, [open an issue][drf-create-issue] or [create a pull reque
[django-rest-framework-braces]: https://github.com/dealertrack/django-rest-framework-braces
[dry-rest-permissions]: https://github.com/Helioscene/dry-rest-permissions
[django-url-filter]: https://github.com/miki725/django-url-filter
[cookiecutter-django-rest]: https://github.com/agconti/cookiecutter-django-rest
[drf-haystack]: http://drf-haystack.readthedocs.org/en/latest/

View File

@ -136,7 +136,7 @@
</div> <!--/.wrapper -->
<footer class="span12">
<p>Documentation built with <a href="http://www.mkdocs.org/">MkDocs</a>.</a>
<p>Documentation built with <a href="http://www.mkdocs.org/">MkDocs</a>.
</p>
</footer>

View File

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

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()
# OrderedDict only available in Python 2.7.
# This will always be the case in Django 1.7 and above, as these versions
# no longer support Python 2.6.
# For Django <= 1.6 and Python 2.6 fall back to SortedDict.
try:
from collections import OrderedDict
except ImportError:
from django.utils.datastructures import SortedDict as OrderedDict
# contrib.postgres only supported from 1.8 onwards.
try:
from django.contrib.postgres import fields as postgres_fields
@ -79,41 +69,30 @@ except ImportError:
postgres_fields = None
# JSONField is only supported from 1.9 onwards
try:
from django.contrib.postgres.fields import JSONField
except ImportError:
JSONField = None
# django-filter is optional
try:
import django_filters
except ImportError:
django_filters = None
if django.VERSION >= (1, 6):
def clean_manytomany_helptext(text):
return text
else:
# Up to version 1.5 many to many fields automatically suffix
# the `help_text` attribute with hardcoded text.
def clean_manytomany_helptext(text):
if text.endswith(' Hold down "Control", or "Command" on a Mac, to select more than one.'):
text = text[:-69]
return text
# Django-guardian is optional. Import only if guardian is in INSTALLED_APPS
# Fixes (#1712). We keep the try/except for the test suite.
guardian = None
try:
if 'guardian' in settings.INSTALLED_APPS:
import guardian
import guardian.shortcuts # Fixes #1624
except ImportError:
pass
def get_model_name(model_cls):
try:
return model_cls._meta.model_name
except AttributeError:
# < 1.6 used module_name instead of model_name
return model_cls._meta.module_name
class CustomValidatorMessage(object):
def __init__(self, *args, **kwargs):
self.message = kwargs.pop('message', self.message)
@ -136,32 +115,6 @@ class MaxLengthValidator(CustomValidatorMessage, MaxLengthValidator):
pass
# URLValidator only accepts `message` in 1.6+
if django.VERSION >= (1, 6):
from django.core.validators import URLValidator
else:
from django.core.validators import URLValidator as DjangoURLValidator
class URLValidator(DjangoURLValidator):
def __init__(self, *args, **kwargs):
self.message = kwargs.pop('message', self.message)
super(URLValidator, self).__init__(*args, **kwargs)
# EmailValidator requires explicit regex prior to 1.6+
if django.VERSION >= (1, 6):
from django.core.validators import EmailValidator
else:
from django.core.validators import EmailValidator as DjangoEmailValidator
from django.core.validators import email_re
class EmailValidator(DjangoEmailValidator):
def __init__(self, *args, **kwargs):
super(EmailValidator, self).__init__(email_re, *args, **kwargs)
# PATCH method is not implemented by Django
if 'patch' not in View.http_method_names:
View.http_method_names = View.http_method_names + ['patch']

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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
on the response, such as JSON encoded data or HTML output.
REST framework also provides an HTML renderer the renders the browsable API.
REST framework also provides an HTML renderer that renders the browsable API.
"""
from __future__ import unicode_literals
@ -420,9 +420,6 @@ class BrowsableAPIRenderer(BaseRenderer):
if method not in view.allowed_methods:
return # Not a valid method
if not api_settings.FORM_METHOD_OVERRIDE:
return # Cannot use form overloading
try:
view.check_permissions(request)
if obj is not None:
@ -530,13 +527,6 @@ class BrowsableAPIRenderer(BaseRenderer):
instance = None
with override_method(view, request, method) as request:
# If we're not using content overloading there's no point in
# supplying a generic form, as the view won't treat the form's
# value as the content of the request.
if not (api_settings.FORM_CONTENT_OVERRIDE and
api_settings.FORM_CONTENTTYPE_OVERRIDE):
return None
# Check permissions
if not self.show_form_for_method(view, method, request, instance):
return
@ -564,26 +554,20 @@ class BrowsableAPIRenderer(BaseRenderer):
# Generate a generic form that includes a content type field,
# and a content field.
content_type_field = api_settings.FORM_CONTENTTYPE_OVERRIDE
content_field = api_settings.FORM_CONTENT_OVERRIDE
media_types = [parser.media_type for parser in view.parser_classes]
choices = [(media_type, media_type) for media_type in media_types]
initial = media_types[0]
# NB. http://jacobian.org/writing/dynamic-form-generation/
class GenericContentForm(forms.Form):
def __init__(self):
super(GenericContentForm, self).__init__()
self.fields[content_type_field] = forms.ChoiceField(
_content_type = forms.ChoiceField(
label='Media type',
choices=choices,
initial=initial
initial=initial,
widget=forms.Select(attrs={'data-override': 'content-type'})
)
self.fields[content_field] = forms.CharField(
_content = forms.CharField(
label='Content',
widget=forms.Textarea,
widget=forms.Textarea(attrs={'data-override': 'content'}),
initial=content
)

View File

@ -86,7 +86,7 @@ def clone_request(request, method):
ret._full_data = request._full_data
ret._content_type = request._content_type
ret._stream = request._stream
ret._method = method
ret.method = method
if hasattr(request, '_user'):
ret._user = request._user
if hasattr(request, '_auth'):
@ -129,11 +129,6 @@ class Request(object):
- authentication_classes(list/tuple). The authentications used to try
authenticating the request's user.
"""
_METHOD_PARAM = api_settings.FORM_METHOD_OVERRIDE
_CONTENT_PARAM = api_settings.FORM_CONTENT_OVERRIDE
_CONTENTTYPE_PARAM = api_settings.FORM_CONTENTTYPE_OVERRIDE
def __init__(self, request, parsers=None, authenticators=None,
negotiator=None, parser_context=None):
self._request = request
@ -144,7 +139,6 @@ class Request(object):
self._data = Empty
self._files = Empty
self._full_data = Empty
self._method = Empty
self._content_type = Empty
self._stream = Empty
@ -162,30 +156,10 @@ class Request(object):
def _default_negotiator(self):
return api_settings.DEFAULT_CONTENT_NEGOTIATION_CLASS()
@property
def method(self):
"""
Returns the HTTP method.
This allows the `method` to be overridden by using a hidden `form`
field on a form POST request.
"""
if not _hasattr(self, '_method'):
self._load_method_and_content_type()
return self._method
@property
def content_type(self):
"""
Returns the content type header.
This should be used instead of `request.META.get('HTTP_CONTENT_TYPE')`,
as it allows the content type to be overridden by using a hidden form
field on a form POST request.
"""
if not _hasattr(self, '_content_type'):
self._load_method_and_content_type()
return self._content_type
meta = self._request.META
return meta.get('CONTENT_TYPE', meta.get('HTTP_CONTENT_TYPE', ''))
@property
def stream(self):
@ -265,9 +239,6 @@ class Request(object):
"""
Parses the request content into `self.data`.
"""
if not _hasattr(self, '_content_type'):
self._load_method_and_content_type()
if not _hasattr(self, '_data'):
self._data, self._files = self._parse()
if self._files:
@ -276,32 +247,14 @@ class Request(object):
else:
self._full_data = self._data
def _load_method_and_content_type(self):
"""
Sets the method and content_type, and then check if they've
been overridden.
"""
self._content_type = self.META.get('HTTP_CONTENT_TYPE',
self.META.get('CONTENT_TYPE', ''))
self._perform_form_overloading()
if not _hasattr(self, '_method'):
self._method = self._request.method
# Allow X-HTTP-METHOD-OVERRIDE header
if 'HTTP_X_HTTP_METHOD_OVERRIDE' in self.META:
self._method = self.META['HTTP_X_HTTP_METHOD_OVERRIDE'].upper()
def _load_stream(self):
"""
Return the content body of the request, as a stream.
"""
meta = self._request.META
try:
content_length = int(
self.META.get(
'CONTENT_LENGTH', self.META.get('HTTP_CONTENT_LENGTH')
)
meta.get('CONTENT_LENGTH', meta.get('HTTP_CONTENT_LENGTH', 0))
)
except (ValueError, TypeError):
content_length = 0
@ -313,50 +266,6 @@ class Request(object):
else:
self._stream = six.BytesIO(self.raw_post_data)
def _perform_form_overloading(self):
"""
If this is a form POST request, then we need to check if the method and
content/content_type have been overridden by setting them in hidden
form fields or not.
"""
USE_FORM_OVERLOADING = (
self._METHOD_PARAM or
(self._CONTENT_PARAM and self._CONTENTTYPE_PARAM)
)
# We only need to use form overloading on form POST requests.
if (
self._request.method != 'POST' or
not USE_FORM_OVERLOADING or
not is_form_media_type(self._content_type)
):
return
# At this point we're committed to parsing the request as form data.
self._data = self._request.POST
self._files = self._request.FILES
self._full_data = self._data.copy()
self._full_data.update(self._files)
# Method overloading - change the method and remove the param from the content.
if (
self._METHOD_PARAM and
self._METHOD_PARAM in self._data
):
self._method = self._data[self._METHOD_PARAM].upper()
# Content overloading - modify the content type, and force re-parse.
if (
self._CONTENT_PARAM and
self._CONTENTTYPE_PARAM and
self._CONTENT_PARAM in self._data and
self._CONTENTTYPE_PARAM in self._data
):
self._content_type = self._data[self._CONTENTTYPE_PARAM]
self._stream = six.BytesIO(self._data[self._CONTENT_PARAM].encode(self.parser_context['encoding']))
self._data, self._files, self._full_data = (Empty, Empty, Empty)
def _parse(self):
"""
Parse the request content, returning a two-tuple of (data, files)

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

@ -8,7 +8,6 @@ from django.core import validators
from django.db import models
from django.utils.text import capfirst
from rest_framework.compat import clean_manytomany_helptext
from rest_framework.validators import UniqueValidator
NUMERIC_FIELD_TYPES = (
@ -113,10 +112,19 @@ def get_field_kwargs(field_name, model_field):
kwargs['choices'] = model_field.choices
return kwargs
# Our decimal validation is handled in the field code, not validator code.
# (In Django 1.9+ this differs from previous style)
if isinstance(model_field, models.DecimalField):
validator_kwarg = [
validator for validator in validator_kwarg
if not isinstance(validator, validators.DecimalValidator)
]
# Ensure that max_length is passed explicitly as a keyword arg,
# rather than as a validator.
max_length = getattr(model_field, 'max_length', None)
if max_length is not None and isinstance(model_field, models.CharField):
if max_length is not None and (isinstance(model_field, models.CharField) or
isinstance(model_field, models.TextField)):
kwargs['max_length'] = max_length
validator_kwarg = [
validator for validator in validator_kwarg
@ -193,7 +201,15 @@ def get_field_kwargs(field_name, model_field):
]
if getattr(model_field, 'unique', False):
validator = UniqueValidator(queryset=model_field.model._default_manager)
unique_error_message = model_field.error_messages.get('unique', None)
if unique_error_message:
unique_error_message = unique_error_message % {
'model_name': model_field.model._meta.object_name,
'field_label': model_field.verbose_name
}
validator = UniqueValidator(
queryset=model_field.model._default_manager,
message=unique_error_message)
validator_kwarg.append(validator)
if validator_kwarg:
@ -222,7 +238,7 @@ def get_relation_kwargs(field_name, relation_info):
if model_field:
if model_field.verbose_name and needs_label(model_field, field_name):
kwargs['label'] = capfirst(model_field.verbose_name)
help_text = clean_manytomany_helptext(model_field.help_text)
help_text = model_field.help_text
if help_text:
kwargs['help_text'] = help_text
if not model_field.editable:

View File

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

View File

@ -1,10 +1,11 @@
from __future__ import unicode_literals
import collections
from collections import OrderedDict
from django.utils.encoding import force_text
from rest_framework.compat import OrderedDict, unicode_to_repr
from rest_framework.compat import unicode_to_repr
class ReturnDict(OrderedDict):

View File

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

View File

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

View File

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

View File

@ -1525,6 +1525,58 @@ class TestUnvalidatedDictField(FieldValues):
field = serializers.DictField()
class TestJSONField(FieldValues):
"""
Values for `JSONField`.
"""
valid_inputs = [
({
'a': 1,
'b': ['some', 'list', True, 1.23],
'3': None
}, {
'a': 1,
'b': ['some', 'list', True, 1.23],
'3': None
}),
]
invalid_inputs = [
({'a': set()}, ['Value must be valid JSON.']),
]
outputs = [
({
'a': 1,
'b': ['some', 'list', True, 1.23],
'3': 3
}, {
'a': 1,
'b': ['some', 'list', True, 1.23],
'3': 3
}),
]
field = serializers.JSONField()
class TestBinaryJSONField(FieldValues):
"""
Values for `JSONField` with binary=True.
"""
valid_inputs = [
(b'{"a": 1, "3": null, "b": ["some", "list", true, 1.23]}', {
'a': 1,
'b': ['some', 'list', True, 1.23],
'3': None
}),
]
invalid_inputs = [
('{"a": "unterminated string}', ['Value must be valid JSON.']),
]
outputs = [
(['some', 'list', True, 1.23], b'["some", "list", true, 1.23]'),
]
field = serializers.JSONField(binary=True)
# Tests for FieldField.
# ---------------------

View File

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

View File

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

View File

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

View File

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

View File

@ -3,7 +3,7 @@ from __future__ import unicode_literals
import json
import re
from collections import MutableMapping
from collections import MutableMapping, OrderedDict
from django.conf.urls import include, url
from django.core.cache import cache
@ -13,7 +13,6 @@ from django.utils import six
from django.utils.translation import ugettext_lazy as _
from rest_framework import permissions, serializers, status
from rest_framework.compat import OrderedDict
from rest_framework.renderers import (
BaseRenderer, BrowsableAPIRenderer, HTMLFormRenderer, JSONRenderer
)
@ -192,17 +191,6 @@ class RendererEndToEndTests(TestCase):
self.assertEqual(resp.content, RENDERER_B_SERIALIZER(DUMMYCONTENT))
self.assertEqual(resp.status_code, DUMMYSTATUS)
def test_specified_renderer_serializes_content_on_accept_query(self):
"""The '_accept' query string should behave in the same way as the Accept header."""
param = '?%s=%s' % (
api_settings.URL_ACCEPT_OVERRIDE,
RendererB.media_type
)
resp = self.client.get('/' + param)
self.assertEqual(resp['Content-Type'], RendererB.media_type + '; charset=utf-8')
self.assertEqual(resp.content, RENDERER_B_SERIALIZER(DUMMYCONTENT))
self.assertEqual(resp.status_code, DUMMYSTATUS)
def test_unsatisfiable_accept_header_on_request_returns_406_status(self):
"""If the Accept header is unsatisfiable we should return a 406 Not Acceptable response."""
resp = self.client.get('/', HTTP_ACCEPT='foo/bar')

View File

@ -3,27 +3,20 @@ Tests for content parsing, and form-overloaded content parsing.
"""
from __future__ import unicode_literals
import json
from io import BytesIO
import django
import pytest
from django.conf.urls import url
from django.contrib.auth import authenticate, login, logout
from django.contrib.auth.models import User
from django.contrib.sessions.middleware import SessionMiddleware
from django.core.handlers.wsgi import WSGIRequest
from django.test import TestCase
from django.utils import six
from rest_framework import status
from rest_framework.authentication import SessionAuthentication
from rest_framework.parsers import (
BaseParser, FormParser, JSONParser, MultiPartParser
)
from rest_framework.request import Empty, Request
from rest_framework.parsers import BaseParser, FormParser, MultiPartParser
from rest_framework.request import Request
from rest_framework.response import Response
from rest_framework.settings import api_settings
from rest_framework.test import APIClient, APIRequestFactory
from rest_framework.views import APIView
@ -43,36 +36,6 @@ class PlainTextParser(BaseParser):
return stream.read()
class TestMethodOverloading(TestCase):
def test_method(self):
"""
Request methods should be same as underlying request.
"""
request = Request(factory.get('/'))
self.assertEqual(request.method, 'GET')
request = Request(factory.post('/'))
self.assertEqual(request.method, 'POST')
def test_overloaded_method(self):
"""
POST requests can be overloaded to another method by setting a
reserved form field
"""
request = Request(factory.post('/', {api_settings.FORM_METHOD_OVERRIDE: 'DELETE'}))
self.assertEqual(request.method, 'DELETE')
def test_x_http_method_override_header(self):
"""
POST requests can also be overloaded to another method by setting
the X-HTTP-Method-Override header.
"""
request = Request(factory.post('/', {'foo': 'bar'}, HTTP_X_HTTP_METHOD_OVERRIDE='DELETE'))
self.assertEqual(request.method, 'DELETE')
request = Request(factory.get('/', {'foo': 'bar'}, HTTP_X_HTTP_METHOD_OVERRIDE='DELETE'))
self.assertEqual(request.method, 'DELETE')
class TestContentParsing(TestCase):
def test_standard_behaviour_determines_no_content_GET(self):
"""
@ -137,49 +100,6 @@ class TestContentParsing(TestCase):
request.parsers = (PlainTextParser(), )
self.assertEqual(request.data, content)
def test_overloaded_behaviour_allows_content_tunnelling(self):
"""
Ensure request.data returns content for overloaded POST request.
"""
json_data = {'foobar': 'qwerty'}
content = json.dumps(json_data)
content_type = 'application/json'
form_data = {
api_settings.FORM_CONTENT_OVERRIDE: content,
api_settings.FORM_CONTENTTYPE_OVERRIDE: content_type
}
request = Request(factory.post('/', form_data))
request.parsers = (JSONParser(), )
self.assertEqual(request.data, json_data)
def test_form_POST_unicode(self):
"""
JSON POST via default web interface with unicode data
"""
# Note: environ and other variables here have simplified content compared to real Request
CONTENT = b'_content_type=application%2Fjson&_content=%7B%22request%22%3A+4%2C+%22firm%22%3A+1%2C+%22text%22%3A+%22%D0%9F%D1%80%D0%B8%D0%B2%D0%B5%D1%82%21%22%7D'
environ = {
'REQUEST_METHOD': 'POST',
'CONTENT_TYPE': 'application/x-www-form-urlencoded',
'CONTENT_LENGTH': len(CONTENT),
'wsgi.input': BytesIO(CONTENT),
}
wsgi_request = WSGIRequest(environ=environ)
wsgi_request._load_post_and_files()
parsers = (JSONParser(), FormParser(), MultiPartParser())
parser_context = {
'encoding': 'utf-8',
'kwargs': {},
'args': (),
}
request = Request(wsgi_request, parsers=parsers, parser_context=parser_context)
method = request.method
self.assertEqual(method, 'POST')
self.assertEqual(request._content_type, 'application/json')
self.assertEqual(request._stream.getvalue(), b'{"request": 4, "firm": 1, "text": "\xd0\x9f\xd1\x80\xd0\xb8\xd0\xb2\xd0\xb5\xd1\x82!"}')
self.assertEqual(request._data, Empty)
self.assertEqual(request._files, Empty)
class MockView(APIView):
authentication_classes = (SessionAuthentication,)

View File

@ -5,11 +5,11 @@ from django.test import TestCase
from django.utils import six
from rest_framework import generics, routers, serializers, status, viewsets
from rest_framework.parsers import JSONParser
from rest_framework.renderers import (
BaseRenderer, BrowsableAPIRenderer, JSONRenderer
)
from rest_framework.response import Response
from rest_framework.settings import api_settings
from rest_framework.views import APIView
from tests.models import BasicModel
@ -79,6 +79,14 @@ class MockViewSettingContentType(APIView):
return Response(DUMMYCONTENT, status=DUMMYSTATUS, content_type='setbyview')
class JSONView(APIView):
parser_classes = (JSONParser,)
def post(self, request, **kwargs):
assert request.data
return Response(DUMMYCONTENT)
class HTMLView(APIView):
renderer_classes = (BrowsableAPIRenderer, )
@ -114,6 +122,7 @@ urlpatterns = [
url(r'^.*\.(?P<format>.+)$', MockView.as_view(renderer_classes=[RendererA, RendererB, RendererC])),
url(r'^$', MockView.as_view(renderer_classes=[RendererA, RendererB, RendererC])),
url(r'^html$', HTMLView.as_view()),
url(r'^json$', JSONView.as_view()),
url(r'^html1$', HTMLView1.as_view()),
url(r'^html_new_model$', HTMLNewModelView.as_view()),
url(r'^html_new_model_viewset', include(new_model_viewset_router.urls)),
@ -166,17 +175,6 @@ class RendererIntegrationTests(TestCase):
self.assertEqual(resp.content, RENDERER_B_SERIALIZER(DUMMYCONTENT))
self.assertEqual(resp.status_code, DUMMYSTATUS)
def test_specified_renderer_serializes_content_on_accept_query(self):
"""The '_accept' query string should behave in the same way as the Accept header."""
param = '?%s=%s' % (
api_settings.URL_ACCEPT_OVERRIDE,
RendererB.media_type
)
resp = self.client.get('/' + param)
self.assertEqual(resp['Content-Type'], RendererB.media_type + '; charset=utf-8')
self.assertEqual(resp.content, RENDERER_B_SERIALIZER(DUMMYCONTENT))
self.assertEqual(resp.status_code, DUMMYSTATUS)
def test_specified_renderer_serializes_content_on_format_query(self):
"""If a 'format' query is specified, the renderer with the matching
format attribute should serialize the response."""
@ -203,6 +201,25 @@ class RendererIntegrationTests(TestCase):
self.assertEqual(resp.status_code, DUMMYSTATUS)
class UnsupportedMediaTypeTests(TestCase):
urls = 'tests.test_response'
def test_should_allow_posting_json(self):
response = self.client.post('/json', data='{"test": 123}', content_type='application/json')
self.assertEqual(response.status_code, 200)
def test_should_not_allow_posting_xml(self):
response = self.client.post('/json', data='<test>123</test>', content_type='application/xml')
self.assertEqual(response.status_code, 415)
def test_should_not_allow_posting_a_form(self):
response = self.client.post('/json', data={'test': 123})
self.assertEqual(response.status_code, 415)
class Issue122Tests(TestCase):
"""
Tests that covers #122.
@ -270,16 +287,6 @@ class Issue807Tests(TestCase):
resp = self.client.get('/setbyview', **headers)
self.assertEqual('setbyview', resp['Content-Type'])
def test_viewset_label_help_text(self):
param = '?%s=%s' % (
api_settings.URL_ACCEPT_OVERRIDE,
'text/html'
)
resp = self.client.get('/html_new_model_viewset/' + param)
self.assertEqual(resp['Content-Type'], 'text/html; charset=utf-8')
# self.assertContains(resp, 'Text comes here')
# self.assertContains(resp, 'Text description.')
def test_form_has_label_and_help_text(self):
resp = self.client.get('/html_new_model')
self.assertEqual(resp['Content-Type'], 'text/html; charset=utf-8')

View File

@ -51,6 +51,16 @@ class TestSerializer:
with pytest.raises(AttributeError):
serializer.data
def test_data_access_before_save_raises_error(self):
def create(validated_data):
return validated_data
serializer = self.Serializer(data={'char': 'abc', 'integer': 123})
serializer.create = create
assert serializer.is_valid()
assert serializer.data == {'char': 'abc', 'integer': 123}
with pytest.raises(AssertionError):
serializer.save()
class TestValidateMethod:
def test_non_field_error_validate_method(self):

View File

@ -150,16 +150,16 @@ class ResolveModelWithPatchedDjangoTests(TestCase):
def setUp(self):
"""Monkeypatch get_model."""
self.get_model = rest_framework.utils.model_meta.models.get_model
self.get_model = rest_framework.utils.model_meta.apps.get_model
def get_model(app_label, model_name):
return None
rest_framework.utils.model_meta.models.get_model = get_model
rest_framework.utils.model_meta.apps.get_model = get_model
def tearDown(self):
"""Revert monkeypatching."""
rest_framework.utils.model_meta.models.get_model = self.get_model
rest_framework.utils.model_meta.apps.get_model = self.get_model
def test_blows_up_if_model_does_not_resolve(self):
with self.assertRaises(ImproperlyConfigured):

View File

@ -48,7 +48,7 @@ class TestUniquenessValidation(TestCase):
data = {'username': 'existing'}
serializer = UniquenessSerializer(data=data)
assert not serializer.is_valid()
assert serializer.errors == {'username': ['This field must be unique.']}
assert serializer.errors == {'username': ['UniquenessModel with this username already exists.']}
def test_is_unique(self):
data = {'username': 'other'}

View File

@ -74,21 +74,6 @@ class ClassBasedViewIntegrationTests(TestCase):
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertEqual(sanitise_json_error(response.data), expected)
def test_400_parse_error_tunneled_content(self):
content = 'f00bar'
content_type = 'application/json'
form_data = {
api_settings.FORM_CONTENT_OVERRIDE: content,
api_settings.FORM_CONTENTTYPE_OVERRIDE: content_type
}
request = factory.post('/', form_data)
response = self.view(request)
expected = {
'detail': JSON_ERROR
}
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertEqual(sanitise_json_error(response.data), expected)
class FunctionBasedViewIntegrationTests(TestCase):
def setUp(self):
@ -103,21 +88,6 @@ class FunctionBasedViewIntegrationTests(TestCase):
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertEqual(sanitise_json_error(response.data), expected)
def test_400_parse_error_tunneled_content(self):
content = 'f00bar'
content_type = 'application/json'
form_data = {
api_settings.FORM_CONTENT_OVERRIDE: content,
api_settings.FORM_CONTENTTYPE_OVERRIDE: content_type
}
request = factory.post('/', form_data)
response = self.view(request)
expected = {
'detail': JSON_ERROR
}
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertEqual(sanitise_json_error(response.data), expected)
class TestCustomExceptionHandler(TestCase):
def setUp(self):

41
tox.ini
View File

@ -4,19 +4,24 @@ addopts=--tb=short
[tox]
envlist =
py27-{lint,docs},
{py26,py27,py32,py33,py34}-django{15,16},
{py27,py32,py33,py34}-django{17,18,master}
{py27,py32,py33,py34}-django{17,18},
{py27,py34,py35}-django{19}
[testenv]
basepython =
py27: python2.7
py32: python3.2
py33: python3.3
py34: python3.4
py35: python3.5
commands = ./runtests.py --fast {posargs} --coverage
setenv =
PYTHONDONTWRITEBYTECODE=1
deps =
django15: Django==1.5.6 # Should track minimum supported
django16: Django==1.6.3 # Should track minimum supported
django17: Django==1.7.10 # Should track maximum supported
django18: Django==1.8.4 # Should track maximum supported
djangomaster: https://github.com/django/django/archive/master.tar.gz
django17: Django==1.7.10
django18: Django==1.8.4
django19: https://www.djangoproject.com/download/1.9b1/tarball/
-rrequirements/requirements-testing.txt
-rrequirements/requirements-optionals.txt
@ -31,3 +36,25 @@ commands = mkdocs build
deps =
-rrequirements/requirements-testing.txt
-rrequirements/requirements-documentation.txt
# Specify explicitly to exclude Django Guardian against Django 1.9
[testenv:py27-django19]
deps =
https://www.djangoproject.com/download/1.9b1/tarball/
-rrequirements/requirements-testing.txt
markdown==2.5.2
django-filter==0.10.0
[testenv:py34-django19]
deps =
https://www.djangoproject.com/download/1.9b1/tarball/
-rrequirements/requirements-testing.txt
markdown==2.5.2
django-filter==0.10.0
[testenv:py35-django19]
deps =
https://www.djangoproject.com/download/1.9b1/tarball/
-rrequirements/requirements-testing.txt
markdown==2.5.2
django-filter==0.10.0