mirror of
https://github.com/encode/django-rest-framework.git
synced 2025-01-23 15:54:16 +03:00
Merge remote-tracking branch 'origin/master' into 2.4.0
Conflicts: .travis.yml docs/api-guide/viewsets.md rest_framework/serializers.py rest_framework/throttling.py tests/test_generics.py tests/test_serializers.py tox.ini
This commit is contained in:
commit
2489e38a06
19
.travis.yml
19
.travis.yml
|
@ -5,12 +5,13 @@ python:
|
||||||
- "2.7"
|
- "2.7"
|
||||||
- "3.2"
|
- "3.2"
|
||||||
- "3.3"
|
- "3.3"
|
||||||
|
- "3.4"
|
||||||
|
|
||||||
env:
|
env:
|
||||||
- DJANGO="https://www.djangoproject.com/download/1.7b2/tarball/"
|
- DJANGO="https://www.djangoproject.com/download/1.7.b4/tarball/"
|
||||||
- DJANGO="django==1.6.3"
|
- DJANGO="django==1.6.5"
|
||||||
- DJANGO="django==1.5.6"
|
- DJANGO="django==1.5.8"
|
||||||
- DJANGO="django==1.4.11"
|
- DJANGO="django==1.4.13"
|
||||||
|
|
||||||
install:
|
install:
|
||||||
- pip install $DJANGO
|
- pip install $DJANGO
|
||||||
|
@ -23,7 +24,7 @@ install:
|
||||||
- "if [[ ${DJANGO::11} == 'django==1.3' ]]; then pip install django-filter==0.5.4; fi"
|
- "if [[ ${DJANGO::11} == 'django==1.3' ]]; then pip install django-filter==0.5.4; fi"
|
||||||
- "if [[ ${DJANGO::11} != 'django==1.3' ]]; then pip install django-filter==0.7; fi"
|
- "if [[ ${DJANGO::11} != 'django==1.3' ]]; then pip install django-filter==0.7; fi"
|
||||||
- "if [[ ${TRAVIS_PYTHON_VERSION::1} == '3' ]]; then pip install -e git+https://github.com/linovia/django-guardian.git@feature/django_1_7#egg=django-guardian-1.2.0; fi"
|
- "if [[ ${TRAVIS_PYTHON_VERSION::1} == '3' ]]; then pip install -e git+https://github.com/linovia/django-guardian.git@feature/django_1_7#egg=django-guardian-1.2.0; fi"
|
||||||
- "if [[ ${DJANGO} == 'https://www.djangoproject.com/download/1.7b2/tarball/' ]]; then pip install -e git+https://github.com/linovia/django-guardian.git@feature/django_1_7#egg=django-guardian-1.2.0; fi"
|
- "if [[ ${DJANGO} == 'https://www.djangoproject.com/download/1.7.b4/tarball/' ]]; then pip install -e git+https://github.com/linovia/django-guardian.git@feature/django_1_7#egg=django-guardian-1.2.0; fi"
|
||||||
- export PYTHONPATH=.
|
- export PYTHONPATH=.
|
||||||
|
|
||||||
script:
|
script:
|
||||||
|
@ -32,8 +33,10 @@ script:
|
||||||
matrix:
|
matrix:
|
||||||
exclude:
|
exclude:
|
||||||
- python: "2.6"
|
- python: "2.6"
|
||||||
env: DJANGO="https://www.djangoproject.com/download/1.7b2/tarball/"
|
env: DJANGO="https://www.djangoproject.com/download/1.7.b4/tarball/"
|
||||||
- python: "3.2"
|
- python: "3.2"
|
||||||
env: DJANGO="django==1.4.11"
|
env: DJANGO="django==1.4.13"
|
||||||
- python: "3.3"
|
- python: "3.3"
|
||||||
env: DJANGO="django==1.4.11"
|
env: DJANGO="django==1.4.13"
|
||||||
|
- python: "3.4"
|
||||||
|
env: DJANGO="django==1.4.13"
|
||||||
|
|
|
@ -119,7 +119,7 @@ Unauthenticated responses that are denied permission will result in an `HTTP 401
|
||||||
|
|
||||||
This authentication scheme uses a simple token-based HTTP Authentication scheme. Token authentication is appropriate for client-server setups, such as native desktop and mobile clients.
|
This authentication scheme uses a simple token-based HTTP Authentication scheme. Token authentication is appropriate for client-server setups, such as native desktop and mobile clients.
|
||||||
|
|
||||||
To use the `TokenAuthentication` scheme, include `rest_framework.authtoken` in your `INSTALLED_APPS` setting:
|
To use the `TokenAuthentication` scheme you'll need to [configure the authentication classes](#setting-the-authentication-scheme) to include `TokenAuthentication`, and additionally include `rest_framework.authtoken` in your `INSTALLED_APPS` setting:
|
||||||
|
|
||||||
INSTALLED_APPS = (
|
INSTALLED_APPS = (
|
||||||
...
|
...
|
||||||
|
|
|
@ -185,7 +185,9 @@ Corresponds to `django.db.models.fields.SlugField`.
|
||||||
|
|
||||||
## ChoiceField
|
## ChoiceField
|
||||||
|
|
||||||
A field that can accept a value out of a limited set of choices.
|
A field that can accept a value out of a limited set of choices. Optionally takes a `blank_display_value` parameter that customizes the display value of an empty choice.
|
||||||
|
|
||||||
|
**Signature:** `ChoiceField(choices=(), blank_display_value=None)`
|
||||||
|
|
||||||
## EmailField
|
## EmailField
|
||||||
|
|
||||||
|
|
|
@ -187,7 +187,7 @@ Remember that the `pre_save()` method is not called by `GenericAPIView` itself,
|
||||||
You won't typically need to override the following methods, although you might need to call into them if you're writing custom views using `GenericAPIView`.
|
You won't typically need to override the following methods, although you might need to call into them if you're writing custom views using `GenericAPIView`.
|
||||||
|
|
||||||
* `get_serializer_context(self)` - Returns a dictionary containing any extra context that should be supplied to the serializer. Defaults to including `'request'`, `'view'` and `'format'` keys.
|
* `get_serializer_context(self)` - Returns a dictionary containing any extra context that should be supplied to the serializer. Defaults to including `'request'`, `'view'` and `'format'` keys.
|
||||||
* `get_serializer(self, instance=None, data=None, files=None, many=False, partial=False)` - Returns a serializer instance.
|
* `get_serializer(self, instance=None, data=None, files=None, many=False, partial=False, allow_add_remove=False)` - Returns a serializer instance.
|
||||||
* `get_pagination_serializer(self, page)` - Returns a serializer instance to use with paginated data.
|
* `get_pagination_serializer(self, page)` - Returns a serializer instance to use with paginated data.
|
||||||
* `paginate_queryset(self, queryset)` - Paginate a queryset if required, either returning a page object, or `None` if pagination is not configured for this view.
|
* `paginate_queryset(self, queryset)` - Paginate a queryset if required, either returning a page object, or `None` if pagination is not configured for this view.
|
||||||
* `filter_queryset(self, queryset)` - Given a queryset, filter it with whichever filter backends are in use, returning a new queryset.
|
* `filter_queryset(self, queryset)` - Given a queryset, filter it with whichever filter backends are in use, returning a new queryset.
|
||||||
|
|
|
@ -73,8 +73,8 @@ Sometimes when serializing objects, you may not want to represent everything exa
|
||||||
|
|
||||||
If you need to customize the serialized value of a particular field, you can do this by creating a `transform_<fieldname>` method. For example if you needed to render some markdown from a text field:
|
If you need to customize the serialized value of a particular field, you can do this by creating a `transform_<fieldname>` method. For example if you needed to render some markdown from a text field:
|
||||||
|
|
||||||
description = serializers.TextField()
|
description = serializers.CharField()
|
||||||
description_html = serializers.TextField(source='description', read_only=True)
|
description_html = serializers.CharField(source='description', read_only=True)
|
||||||
|
|
||||||
def transform_description_html(self, obj, value):
|
def transform_description_html(self, obj, value):
|
||||||
from django.contrib.markup.templatetags.markup import markdown
|
from django.contrib.markup.templatetags.markup import markdown
|
||||||
|
@ -464,7 +464,7 @@ For more specific requirements such as specifying a different lookup for each fi
|
||||||
model = Account
|
model = Account
|
||||||
fields = ('url', 'account_name', 'users', 'created')
|
fields = ('url', 'account_name', 'users', 'created')
|
||||||
|
|
||||||
## Overiding the URL field behavior
|
## Overriding the URL field behavior
|
||||||
|
|
||||||
The name of the URL field defaults to 'url'. You can override this globally, by using the `URL_FIELD_NAME` setting.
|
The name of the URL field defaults to 'url'. You can override this globally, by using the `URL_FIELD_NAME` setting.
|
||||||
|
|
||||||
|
@ -478,7 +478,7 @@ You can also override this on a per-serializer basis by using the `url_field_nam
|
||||||
|
|
||||||
**Note**: The generic view implementations normally generate a `Location` header in response to successful `POST` requests. Serializers using `url_field_name` option will not have this header automatically included by the view. If you need to do so you will ned to also override the view's `get_success_headers()` method.
|
**Note**: The generic view implementations normally generate a `Location` header in response to successful `POST` requests. Serializers using `url_field_name` option will not have this header automatically included by the view. If you need to do so you will ned to also override the view's `get_success_headers()` method.
|
||||||
|
|
||||||
You can also overide the URL field's view name and lookup field without overriding the field explicitly, by using the `view_name` and `lookup_field` options, like so:
|
You can also override the URL field's view name and lookup field without overriding the field explicitly, by using the `view_name` and `lookup_field` options, like so:
|
||||||
|
|
||||||
class AccountSerializer(serializers.HyperlinkedModelSerializer):
|
class AccountSerializer(serializers.HyperlinkedModelSerializer):
|
||||||
class Meta:
|
class Meta:
|
||||||
|
|
|
@ -146,7 +146,7 @@ The decorators can additionally take extra arguments that will be set for the ro
|
||||||
def set_password(self, request, pk=None):
|
def set_password(self, request, pk=None):
|
||||||
...
|
...
|
||||||
|
|
||||||
By default, the decorators will route `GET` requests, but may also accept other HTTP methods, by using the `methods` argument. For example:
|
The `@action` decorator will route `POST` requests by default, but may also accept other HTTP methods, by using the `methods` argument. For example:
|
||||||
|
|
||||||
@detail_route(methods=['post', 'delete'])
|
@detail_route(methods=['post', 'delete'])
|
||||||
def unset_password(self, request, pk=None):
|
def unset_password(self, request, pk=None):
|
||||||
|
|
|
@ -52,24 +52,28 @@ You can determine your currently installed version using `pip freeze`:
|
||||||
|
|
||||||
## 2.3.x series
|
## 2.3.x series
|
||||||
|
|
||||||
### 2.3.x
|
### 2.3.14
|
||||||
|
|
||||||
**Date**: April 2014
|
**Date**: 12th June 2014
|
||||||
|
|
||||||
* Fix nested serializers linked through a backward foreign key relation
|
* **Security fix**: Escape request path when it is include as part of the login and logout links in the browsable API.
|
||||||
* Fix bad links for the `BrowsableAPIRenderer` with `YAMLRenderer`
|
* `help_text` and `verbose_name` automatically set for related fields on `ModelSerializer`.
|
||||||
* Add `UnicodeYAMLRenderer` that extends `YAMLRenderer` with unicode
|
* Fix nested serializers linked through a backward foreign key relation.
|
||||||
* Fix `parse_header` argument convertion
|
* Fix bad links for the `BrowsableAPIRenderer` with `YAMLRenderer`.
|
||||||
* Fix mediatype detection under Python3
|
* Add `UnicodeYAMLRenderer` that extends `YAMLRenderer` with unicode.
|
||||||
* Web browseable API now offers blank option on dropdown when the field is not required
|
* Fix `parse_header` argument convertion.
|
||||||
* `APIException` representation improved for logging purposes
|
* Fix mediatype detection under Python 3.
|
||||||
* Allow source="*" within nested serializers
|
* Web browseable API now offers blank option on dropdown when the field is not required.
|
||||||
* Better support for custom oauth2 provider backends
|
* `APIException` representation improved for logging purposes.
|
||||||
* Fix field validation if it's optional and has no value
|
* Allow source="*" within nested serializers.
|
||||||
* Add `SEARCH_PARAM` and `ORDERING_PARAM`
|
* Better support for custom oauth2 provider backends.
|
||||||
* Fix `APIRequestFactory` to support arguments within the url string for GET
|
* Fix field validation if it's optional and has no value.
|
||||||
* Allow three transport modes for access tokens when accessing a protected resource
|
* Add `SEARCH_PARAM` and `ORDERING_PARAM`.
|
||||||
* Fix `Request`'s `QueryDict` encoding
|
* Fix `APIRequestFactory` to support arguments within the url string for GET.
|
||||||
|
* Allow three transport modes for access tokens when accessing a protected resource.
|
||||||
|
* Fix `QueryDict` encoding on request objects.
|
||||||
|
* Ensure throttle keys do not contain spaces, as those are invalid if using `memcached`.
|
||||||
|
* Support `blank_display_value` on `ChoiceField`.
|
||||||
|
|
||||||
### 2.3.13
|
### 2.3.13
|
||||||
## 2.3.x series
|
## 2.3.x series
|
||||||
|
|
|
@ -104,7 +104,7 @@ Don't forget to sync the database for the first time.
|
||||||
|
|
||||||
## Creating a Serializer class
|
## Creating a Serializer class
|
||||||
|
|
||||||
The first thing we need to get started on our Web API is provide a way of serializing and deserializing the snippet instances into representations such as `json`. We can do this by declaring serializers that work very similar to Django's forms. Create a file in the `snippets` directory named `serializers.py` and add the following.
|
The first thing we need to get started on our Web API is to provide a way of serializing and deserializing the snippet instances into representations such as `json`. We can do this by declaring serializers that work very similar to Django's forms. Create a file in the `snippets` directory named `serializers.py` and add the following.
|
||||||
|
|
||||||
from django.forms import widgets
|
from django.forms import widgets
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
@ -143,7 +143,7 @@ The first thing we need to get started on our Web API is provide a way of serial
|
||||||
# Create new instance
|
# Create new instance
|
||||||
return Snippet(**attrs)
|
return Snippet(**attrs)
|
||||||
|
|
||||||
The first part of serializer class defines the fields that get serialized/deserialized. The `restore_object` method defines how fully fledged instances get created when deserializing data.
|
The first part of the serializer class defines the fields that get serialized/deserialized. The `restore_object` method defines how fully fledged instances get created when deserializing data.
|
||||||
|
|
||||||
Notice that we can also use various attributes that would typically be used on form fields, such as `widget=widgets.Textarea`. These can be used to control how the serializer should render when displayed as an HTML form. This is particularly useful for controlling how the browsable API should be displayed, as we'll see later in the tutorial.
|
Notice that we can also use various attributes that would typically be used on form fields, such as `widget=widgets.Textarea`. These can be used to control how the serializer should render when displayed as an HTML form. This is particularly useful for controlling how the browsable API should be displayed, as we'll see later in the tutorial.
|
||||||
|
|
||||||
|
|
|
@ -44,11 +44,11 @@ When that's all done we'll need to update our database tables.
|
||||||
Normally we'd create a database migration in order to do that, but for the purposes of this tutorial, let's just delete the database and start again.
|
Normally we'd create a database migration in order to do that, but for the purposes of this tutorial, let's just delete the database and start again.
|
||||||
|
|
||||||
rm tmp.db
|
rm tmp.db
|
||||||
python ./manage.py syncdb
|
python manage.py syncdb
|
||||||
|
|
||||||
You might also want to create a few different users, to use for testing the API. The quickest way to do this will be with the `createsuperuser` command.
|
You might also want to create a few different users, to use for testing the API. The quickest way to do this will be with the `createsuperuser` command.
|
||||||
|
|
||||||
python ./manage.py createsuperuser
|
python manage.py createsuperuser
|
||||||
|
|
||||||
## Adding endpoints for our User models
|
## Adding endpoints for our User models
|
||||||
|
|
||||||
|
|
|
@ -21,7 +21,7 @@ First of all let's refactor our `UserList` and `UserDetail` views into a single
|
||||||
queryset = User.objects.all()
|
queryset = User.objects.all()
|
||||||
serializer_class = UserSerializer
|
serializer_class = UserSerializer
|
||||||
|
|
||||||
Here we've used `ReadOnlyModelViewSet` class to automatically provide the default 'read-only' operations. We're still setting the `queryset` and `serializer_class` attributes exactly as we did when we were using regular views, but we no longer need to provide the same information to two separate classes.
|
Here we've used the `ReadOnlyModelViewSet` class to automatically provide the default 'read-only' operations. We're still setting the `queryset` and `serializer_class` attributes exactly as we did when we were using regular views, but we no longer need to provide the same information to two separate classes.
|
||||||
|
|
||||||
Next we're going to replace the `SnippetList`, `SnippetDetail` and `SnippetHighlight` view classes. We can remove the three views, and again replace them with a single class.
|
Next we're going to replace the `SnippetList`, `SnippetDetail` and `SnippetHighlight` view classes. We can remove the three views, and again replace them with a single class.
|
||||||
|
|
||||||
|
@ -85,7 +85,7 @@ In the `urls.py` file we bind our `ViewSet` classes into a set of concrete views
|
||||||
|
|
||||||
Notice how we're creating multiple views from each `ViewSet` class, by binding the http methods to the required action for each view.
|
Notice how we're creating multiple views from each `ViewSet` class, by binding the http methods to the required action for each view.
|
||||||
|
|
||||||
Now that we've bound our resources into concrete views, that we can register the views with the URL conf as usual.
|
Now that we've bound our resources into concrete views, we can register the views with the URL conf as usual.
|
||||||
|
|
||||||
urlpatterns = format_suffix_patterns(patterns('snippets.views',
|
urlpatterns = format_suffix_patterns(patterns('snippets.views',
|
||||||
url(r'^$', 'api_root'),
|
url(r'^$', 'api_root'),
|
||||||
|
@ -138,7 +138,7 @@ You can review the final [tutorial code][repo] on GitHub, or try out a live exam
|
||||||
|
|
||||||
## Onwards and upwards
|
## Onwards and upwards
|
||||||
|
|
||||||
We've reached the end of our tutorial. If you want to get more involved in the REST framework project, here's a few places you can start:
|
We've reached the end of our tutorial. If you want to get more involved in the REST framework project, here are a few places you can start:
|
||||||
|
|
||||||
* Contribute on [GitHub][github] by reviewing and submitting issues, and making pull requests.
|
* Contribute on [GitHub][github] by reviewing and submitting issues, and making pull requests.
|
||||||
* Join the [REST framework discussion group][group], and help build the community.
|
* Join the [REST framework discussion group][group], and help build the community.
|
||||||
|
|
|
@ -8,7 +8,7 @@ ______ _____ _____ _____ __ _
|
||||||
"""
|
"""
|
||||||
|
|
||||||
__title__ = 'Django REST framework'
|
__title__ = 'Django REST framework'
|
||||||
__version__ = '2.3.13'
|
__version__ = '2.3.14'
|
||||||
__author__ = 'Tom Christie'
|
__author__ = 'Tom Christie'
|
||||||
__license__ = 'BSD 2-Clause'
|
__license__ = 'BSD 2-Clause'
|
||||||
__copyright__ = 'Copyright 2011-2014 Tom Christie'
|
__copyright__ = 'Copyright 2011-2014 Tom Christie'
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
from django.contrib.auth import authenticate
|
from django.contrib.auth import authenticate
|
||||||
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
|
||||||
|
|
||||||
|
@ -15,10 +17,13 @@ class AuthTokenSerializer(serializers.Serializer):
|
||||||
|
|
||||||
if user:
|
if user:
|
||||||
if not user.is_active:
|
if not user.is_active:
|
||||||
raise serializers.ValidationError('User account is disabled.')
|
msg = _('User account is disabled.')
|
||||||
|
raise serializers.ValidationError(msg)
|
||||||
attrs['user'] = user
|
attrs['user'] = user
|
||||||
return attrs
|
return attrs
|
||||||
else:
|
else:
|
||||||
raise serializers.ValidationError('Unable to login with provided credentials.')
|
msg = _('Unable to login with provided credentials.')
|
||||||
|
raise serializers.ValidationError(msg)
|
||||||
else:
|
else:
|
||||||
raise serializers.ValidationError('Must include "username" and "password"')
|
msg = _('Must include "username" and "password"')
|
||||||
|
raise serializers.ValidationError(msg)
|
||||||
|
|
|
@ -47,6 +47,7 @@ except ImportError:
|
||||||
# django-guardian is optional
|
# django-guardian is optional
|
||||||
try:
|
try:
|
||||||
import guardian
|
import guardian
|
||||||
|
import guardian.shortcuts # Fixes #1624
|
||||||
except ImportError:
|
except ImportError:
|
||||||
guardian = None
|
guardian = None
|
||||||
|
|
||||||
|
|
|
@ -64,7 +64,7 @@ def get_component(obj, attr_name):
|
||||||
|
|
||||||
def readable_datetime_formats(formats):
|
def readable_datetime_formats(formats):
|
||||||
format = ', '.join(formats).replace(ISO_8601,
|
format = ', '.join(formats).replace(ISO_8601,
|
||||||
'YYYY-MM-DDThh:mm[:ss[.uuuuuu]][+HHMM|-HHMM|Z]')
|
'YYYY-MM-DDThh:mm[:ss[.uuuuuu]][+HH:MM|-HH:MM|Z]')
|
||||||
return humanize_strptime(format)
|
return humanize_strptime(format)
|
||||||
|
|
||||||
|
|
||||||
|
@ -156,7 +156,12 @@ class Field(object):
|
||||||
def widget_html(self):
|
def widget_html(self):
|
||||||
if not self.widget:
|
if not self.widget:
|
||||||
return ''
|
return ''
|
||||||
return self.widget.render(self._name, self._value)
|
|
||||||
|
attrs = {}
|
||||||
|
if 'id' not in self.widget.attrs:
|
||||||
|
attrs['id'] = self._name
|
||||||
|
|
||||||
|
return self.widget.render(self._name, self._value, attrs=attrs)
|
||||||
|
|
||||||
def label_tag(self):
|
def label_tag(self):
|
||||||
return '<label for="%s">%s:</label>' % (self._name, self.label)
|
return '<label for="%s">%s:</label>' % (self._name, self.label)
|
||||||
|
@ -499,7 +504,7 @@ class SlugField(CharField):
|
||||||
|
|
||||||
class ChoiceField(WritableField):
|
class ChoiceField(WritableField):
|
||||||
type_name = 'ChoiceField'
|
type_name = 'ChoiceField'
|
||||||
type_label = 'multiple choice'
|
type_label = 'choice'
|
||||||
form_field_class = forms.ChoiceField
|
form_field_class = forms.ChoiceField
|
||||||
widget = widgets.Select
|
widget = widgets.Select
|
||||||
default_error_messages = {
|
default_error_messages = {
|
||||||
|
@ -507,12 +512,16 @@ class ChoiceField(WritableField):
|
||||||
'the available choices.'),
|
'the available choices.'),
|
||||||
}
|
}
|
||||||
|
|
||||||
def __init__(self, choices=(), *args, **kwargs):
|
def __init__(self, choices=(), blank_display_value=None, *args, **kwargs):
|
||||||
self.empty = kwargs.pop('empty', '')
|
self.empty = kwargs.pop('empty', '')
|
||||||
super(ChoiceField, self).__init__(*args, **kwargs)
|
super(ChoiceField, self).__init__(*args, **kwargs)
|
||||||
self.choices = choices
|
self.choices = choices
|
||||||
if not self.required:
|
if not self.required:
|
||||||
self.choices = BLANK_CHOICE_DASH + self.choices
|
if blank_display_value is None:
|
||||||
|
blank_choice = BLANK_CHOICE_DASH
|
||||||
|
else:
|
||||||
|
blank_choice = [('', blank_display_value)]
|
||||||
|
self.choices = blank_choice + self.choices
|
||||||
|
|
||||||
def _get_choices(self):
|
def _get_choices(self):
|
||||||
return self._choices
|
return self._choices
|
||||||
|
@ -1016,9 +1025,9 @@ class SerializerMethodField(Field):
|
||||||
A field that gets its value by calling a method on the serializer it's attached to.
|
A field that gets its value by calling a method on the serializer it's attached to.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, method_name):
|
def __init__(self, method_name, *args, **kwargs):
|
||||||
self.method_name = method_name
|
self.method_name = method_name
|
||||||
super(SerializerMethodField, self).__init__()
|
super(SerializerMethodField, self).__init__(*args, **kwargs)
|
||||||
|
|
||||||
def field_to_native(self, obj, field_name):
|
def field_to_native(self, obj, field_name):
|
||||||
value = getattr(self.parent, self.method_name)(obj)
|
value = getattr(self.parent, self.method_name)(obj)
|
||||||
|
|
|
@ -90,8 +90,8 @@ class GenericAPIView(views.APIView):
|
||||||
'view': self
|
'view': self
|
||||||
}
|
}
|
||||||
|
|
||||||
def get_serializer(self, instance=None, data=None,
|
def get_serializer(self, instance=None, data=None, files=None, many=False,
|
||||||
files=None, many=False, partial=False):
|
partial=False, allow_add_remove=False):
|
||||||
"""
|
"""
|
||||||
Return the serializer instance that should be used for validating and
|
Return the serializer instance that should be used for validating and
|
||||||
deserializing input, and for serializing output.
|
deserializing input, and for serializing output.
|
||||||
|
@ -99,7 +99,9 @@ class GenericAPIView(views.APIView):
|
||||||
serializer_class = self.get_serializer_class()
|
serializer_class = self.get_serializer_class()
|
||||||
context = self.get_serializer_context()
|
context = self.get_serializer_context()
|
||||||
return serializer_class(instance, data=data, files=files,
|
return serializer_class(instance, data=data, files=files,
|
||||||
many=many, partial=partial, context=context)
|
many=many, partial=partial,
|
||||||
|
allow_add_remove=allow_add_remove,
|
||||||
|
context=context)
|
||||||
|
|
||||||
def get_pagination_serializer(self, page):
|
def get_pagination_serializer(self, page):
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -21,6 +21,7 @@ from django.core.paginator import Page
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.forms import widgets
|
from django.forms import widgets
|
||||||
from django.utils.datastructures import SortedDict
|
from django.utils.datastructures import SortedDict
|
||||||
|
from django.core.exceptions import ObjectDoesNotExist
|
||||||
from rest_framework.compat import six
|
from rest_framework.compat import six
|
||||||
from rest_framework.settings import api_settings
|
from rest_framework.settings import api_settings
|
||||||
|
|
||||||
|
@ -32,8 +33,8 @@ from rest_framework.settings import api_settings
|
||||||
# This helps keep the separation between model fields, form fields, and
|
# This helps keep the separation between model fields, form fields, and
|
||||||
# serializer fields more explicit.
|
# serializer fields more explicit.
|
||||||
|
|
||||||
from rest_framework.relations import *
|
from rest_framework.relations import * # NOQA
|
||||||
from rest_framework.fields import *
|
from rest_framework.fields import * # NOQA
|
||||||
|
|
||||||
|
|
||||||
def _resolve_model(obj):
|
def _resolve_model(obj):
|
||||||
|
@ -48,7 +49,7 @@ def _resolve_model(obj):
|
||||||
String representations should have the format:
|
String representations should have the format:
|
||||||
'appname.ModelName'
|
'appname.ModelName'
|
||||||
"""
|
"""
|
||||||
if type(obj) == str and len(obj.split('.')) == 2:
|
if isinstance(obj, six.string_types) and len(obj.split('.')) == 2:
|
||||||
app_name, model_name = obj.split('.')
|
app_name, model_name = obj.split('.')
|
||||||
return models.get_model(app_name, model_name)
|
return models.get_model(app_name, model_name)
|
||||||
elif inspect.isclass(obj) and issubclass(obj, models.Model):
|
elif inspect.isclass(obj) and issubclass(obj, models.Model):
|
||||||
|
@ -344,7 +345,7 @@ class BaseSerializer(WritableField):
|
||||||
|
|
||||||
for field_name, field in self.fields.items():
|
for field_name, field in self.fields.items():
|
||||||
if field.read_only and obj is None:
|
if field.read_only and obj is None:
|
||||||
continue
|
continue
|
||||||
field.initialize(parent=self, field_name=field_name)
|
field.initialize(parent=self, field_name=field_name)
|
||||||
key = self.get_field_key(field_name)
|
key = self.get_field_key(field_name)
|
||||||
value = field.field_to_native(obj, field_name)
|
value = field.field_to_native(obj, field_name)
|
||||||
|
@ -753,9 +754,9 @@ class ModelSerializer(Serializer):
|
||||||
field.read_only = True
|
field.read_only = True
|
||||||
|
|
||||||
ret[accessor_name] = field
|
ret[accessor_name] = field
|
||||||
|
|
||||||
# Ensure that 'read_only_fields' is an iterable
|
# Ensure that 'read_only_fields' is an iterable
|
||||||
assert isinstance(self.opts.read_only_fields, (list, tuple)), '`read_only_fields` must be a list or tuple'
|
assert isinstance(self.opts.read_only_fields, (list, tuple)), '`read_only_fields` must be a list or tuple'
|
||||||
|
|
||||||
# Add the `read_only` flag to any fields that have been specified
|
# Add the `read_only` flag to any fields that have been specified
|
||||||
# in the `read_only_fields` option
|
# in the `read_only_fields` option
|
||||||
|
@ -770,10 +771,10 @@ class ModelSerializer(Serializer):
|
||||||
"on serializer '%s'." %
|
"on serializer '%s'." %
|
||||||
(field_name, self.__class__.__name__))
|
(field_name, self.__class__.__name__))
|
||||||
ret[field_name].read_only = True
|
ret[field_name].read_only = True
|
||||||
|
|
||||||
# Ensure that 'write_only_fields' is an iterable
|
# Ensure that 'write_only_fields' is an iterable
|
||||||
assert isinstance(self.opts.write_only_fields, (list, tuple)), '`write_only_fields` must be a list or tuple'
|
assert isinstance(self.opts.write_only_fields, (list, tuple)), '`write_only_fields` must be a list or tuple'
|
||||||
|
|
||||||
for field_name in self.opts.write_only_fields:
|
for field_name in self.opts.write_only_fields:
|
||||||
assert field_name not in self.base_fields.keys(), (
|
assert field_name not in self.base_fields.keys(), (
|
||||||
"field '%s' on serializer '%s' specified in "
|
"field '%s' on serializer '%s' specified in "
|
||||||
|
@ -784,7 +785,7 @@ class ModelSerializer(Serializer):
|
||||||
"Non-existant field '%s' specified in `write_only_fields` "
|
"Non-existant field '%s' specified in `write_only_fields` "
|
||||||
"on serializer '%s'." %
|
"on serializer '%s'." %
|
||||||
(field_name, self.__class__.__name__))
|
(field_name, self.__class__.__name__))
|
||||||
ret[field_name].write_only = True
|
ret[field_name].write_only = True
|
||||||
|
|
||||||
return ret
|
return ret
|
||||||
|
|
||||||
|
@ -828,6 +829,15 @@ class ModelSerializer(Serializer):
|
||||||
if model_field.verbose_name is not None:
|
if model_field.verbose_name is not None:
|
||||||
kwargs['label'] = model_field.verbose_name
|
kwargs['label'] = model_field.verbose_name
|
||||||
|
|
||||||
|
if not model_field.editable:
|
||||||
|
kwargs['read_only'] = True
|
||||||
|
|
||||||
|
if model_field.verbose_name is not None:
|
||||||
|
kwargs['label'] = model_field.verbose_name
|
||||||
|
|
||||||
|
if model_field.help_text is not None:
|
||||||
|
kwargs['help_text'] = model_field.help_text
|
||||||
|
|
||||||
return PrimaryKeyRelatedField(**kwargs)
|
return PrimaryKeyRelatedField(**kwargs)
|
||||||
|
|
||||||
def get_field(self, model_field):
|
def get_field(self, model_field):
|
||||||
|
|
|
@ -40,7 +40,7 @@ def optional_login(request):
|
||||||
except NoReverseMatch:
|
except NoReverseMatch:
|
||||||
return ''
|
return ''
|
||||||
|
|
||||||
snippet = "<a href='%s?next=%s'>Log in</a>" % (login_url, request.path)
|
snippet = "<a href='%s?next=%s'>Log in</a>" % (login_url, escape(request.path))
|
||||||
return snippet
|
return snippet
|
||||||
|
|
||||||
|
|
||||||
|
@ -54,7 +54,7 @@ def optional_logout(request):
|
||||||
except NoReverseMatch:
|
except NoReverseMatch:
|
||||||
return ''
|
return ''
|
||||||
|
|
||||||
snippet = "<a href='%s?next=%s'>Log out</a>" % (logout_url, request.path)
|
snippet = "<a href='%s?next=%s'>Log out</a>" % (logout_url, escape(request.path))
|
||||||
return snippet
|
return snippet
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -36,7 +36,7 @@ class APIRequestFactory(DjangoRequestFactory):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if not data:
|
if not data:
|
||||||
return ('', None)
|
return ('', content_type)
|
||||||
|
|
||||||
assert format is None or content_type is None, (
|
assert format is None or content_type is None, (
|
||||||
'You may not set both `format` and `content_type`.'
|
'You may not set both `format` and `content_type`.'
|
||||||
|
|
|
@ -173,6 +173,12 @@ class AnonRateThrottle(SimpleRateThrottle):
|
||||||
if request.user.is_authenticated():
|
if request.user.is_authenticated():
|
||||||
return None # Only throttle unauthenticated requests.
|
return None # Only throttle unauthenticated requests.
|
||||||
|
|
||||||
|
ident = request.META.get('HTTP_X_FORWARDED_FOR')
|
||||||
|
if ident is None:
|
||||||
|
ident = request.META.get('REMOTE_ADDR')
|
||||||
|
else:
|
||||||
|
ident = ''.join(ident.split())
|
||||||
|
|
||||||
return self.cache_format % {
|
return self.cache_format % {
|
||||||
'scope': self.scope,
|
'scope': self.scope,
|
||||||
'ident': self.get_ident(request)
|
'ident': self.get_ident(request)
|
||||||
|
|
|
@ -151,7 +151,8 @@ class ForeignKeySource(RESTFrameworkModel):
|
||||||
class NullableForeignKeySource(RESTFrameworkModel):
|
class NullableForeignKeySource(RESTFrameworkModel):
|
||||||
name = models.CharField(max_length=100)
|
name = models.CharField(max_length=100)
|
||||||
target = models.ForeignKey(ForeignKeyTarget, null=True, blank=True,
|
target = models.ForeignKey(ForeignKeyTarget, null=True, blank=True,
|
||||||
related_name='nullable_sources')
|
related_name='nullable_sources',
|
||||||
|
verbose_name='Optional target object')
|
||||||
|
|
||||||
|
|
||||||
# OneToOne
|
# OneToOne
|
||||||
|
|
|
@ -4,6 +4,7 @@ General serializer field tests.
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
import datetime
|
import datetime
|
||||||
|
import re
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
from django.core import validators
|
from django.core import validators
|
||||||
|
@ -103,6 +104,16 @@ class BasicFieldTests(TestCase):
|
||||||
keys = list(field.to_native(ret).keys())
|
keys = list(field.to_native(ret).keys())
|
||||||
self.assertEqual(keys, ['c', 'b', 'a', 'z'])
|
self.assertEqual(keys, ['c', 'b', 'a', 'z'])
|
||||||
|
|
||||||
|
def test_widget_html_attributes(self):
|
||||||
|
"""
|
||||||
|
Make sure widget_html() renders the correct attributes
|
||||||
|
"""
|
||||||
|
r = re.compile('(\S+)=["\']?((?:.(?!["\']?\s+(?:\S+)=|[>"\']))+.)["\']?')
|
||||||
|
form = TimeFieldModelSerializer().data
|
||||||
|
attributes = r.findall(form.fields['clock'].widget_html())
|
||||||
|
self.assertIn(('name', 'clock'), attributes)
|
||||||
|
self.assertIn(('id', 'clock'), attributes)
|
||||||
|
|
||||||
|
|
||||||
class DateFieldTest(TestCase):
|
class DateFieldTest(TestCase):
|
||||||
"""
|
"""
|
||||||
|
@ -312,7 +323,7 @@ class DateTimeFieldTest(TestCase):
|
||||||
f.from_native('04:61:59')
|
f.from_native('04:61:59')
|
||||||
except validators.ValidationError as e:
|
except validators.ValidationError as e:
|
||||||
self.assertEqual(e.messages, ["Datetime has wrong format. Use one of these formats instead: "
|
self.assertEqual(e.messages, ["Datetime has wrong format. Use one of these formats instead: "
|
||||||
"YYYY-MM-DDThh:mm[:ss[.uuuuuu]][+HHMM|-HHMM|Z]"])
|
"YYYY-MM-DDThh:mm[:ss[.uuuuuu]][+HH:MM|-HH:MM|Z]"])
|
||||||
else:
|
else:
|
||||||
self.fail("ValidationError was not properly raised")
|
self.fail("ValidationError was not properly raised")
|
||||||
|
|
||||||
|
@ -326,7 +337,7 @@ class DateTimeFieldTest(TestCase):
|
||||||
f.from_native('04 -- 31')
|
f.from_native('04 -- 31')
|
||||||
except validators.ValidationError as e:
|
except validators.ValidationError as e:
|
||||||
self.assertEqual(e.messages, ["Datetime has wrong format. Use one of these formats instead: "
|
self.assertEqual(e.messages, ["Datetime has wrong format. Use one of these formats instead: "
|
||||||
"YYYY-MM-DDThh:mm[:ss[.uuuuuu]][+HHMM|-HHMM|Z]"])
|
"YYYY-MM-DDThh:mm[:ss[.uuuuuu]][+HH:MM|-HH:MM|Z]"])
|
||||||
else:
|
else:
|
||||||
self.fail("ValidationError was not properly raised")
|
self.fail("ValidationError was not properly raised")
|
||||||
|
|
||||||
|
@ -706,6 +717,15 @@ class ChoiceFieldTests(TestCase):
|
||||||
f = serializers.ChoiceField(required=False, choices=SAMPLE_CHOICES)
|
f = serializers.ChoiceField(required=False, choices=SAMPLE_CHOICES)
|
||||||
self.assertEqual(f.choices, models.fields.BLANK_CHOICE_DASH + SAMPLE_CHOICES)
|
self.assertEqual(f.choices, models.fields.BLANK_CHOICE_DASH + SAMPLE_CHOICES)
|
||||||
|
|
||||||
|
def test_blank_choice_display(self):
|
||||||
|
blank = 'No Preference'
|
||||||
|
f = serializers.ChoiceField(
|
||||||
|
required=False,
|
||||||
|
choices=SAMPLE_CHOICES,
|
||||||
|
blank_display_value=blank,
|
||||||
|
)
|
||||||
|
self.assertEqual(f.choices, [('', blank)] + SAMPLE_CHOICES)
|
||||||
|
|
||||||
def test_invalid_choice_model(self):
|
def test_invalid_choice_model(self):
|
||||||
s = ChoiceFieldModelSerializer(data={'choice': 'wrong_value'})
|
s = ChoiceFieldModelSerializer(data={'choice': 'wrong_value'})
|
||||||
self.assertFalse(s.is_valid())
|
self.assertFalse(s.is_valid())
|
||||||
|
|
|
@ -5,6 +5,7 @@ from django.test import TestCase
|
||||||
from rest_framework import generics, renderers, serializers, status
|
from rest_framework import generics, renderers, serializers, status
|
||||||
from rest_framework.test import APIRequestFactory
|
from rest_framework.test import APIRequestFactory
|
||||||
from tests.models import BasicModel, Comment, SlugBasedModel
|
from tests.models import BasicModel, Comment, SlugBasedModel
|
||||||
|
from tests.models import ForeignKeySource, ForeignKeyTarget
|
||||||
from rest_framework.compat import six
|
from rest_framework.compat import six
|
||||||
|
|
||||||
factory = APIRequestFactory()
|
factory = APIRequestFactory()
|
||||||
|
@ -28,6 +29,13 @@ class InstanceView(generics.RetrieveUpdateDestroyAPIView):
|
||||||
return queryset.exclude(text='filtered out')
|
return queryset.exclude(text='filtered out')
|
||||||
|
|
||||||
|
|
||||||
|
class FKInstanceView(generics.RetrieveUpdateDestroyAPIView):
|
||||||
|
"""
|
||||||
|
FK: example description for OPTIONS.
|
||||||
|
"""
|
||||||
|
model = ForeignKeySource
|
||||||
|
|
||||||
|
|
||||||
class SlugSerializer(serializers.ModelSerializer):
|
class SlugSerializer(serializers.ModelSerializer):
|
||||||
slug = serializers.Field() # read only
|
slug = serializers.Field() # read only
|
||||||
|
|
||||||
|
@ -407,6 +415,72 @@ class TestInstanceView(TestCase):
|
||||||
self.assertFalse(self.objects.filter(id=999).exists())
|
self.assertFalse(self.objects.filter(id=999).exists())
|
||||||
|
|
||||||
|
|
||||||
|
class TestFKInstanceView(TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
"""
|
||||||
|
Create 3 BasicModel instances.
|
||||||
|
"""
|
||||||
|
items = ['foo', 'bar', 'baz']
|
||||||
|
for item in items:
|
||||||
|
t = ForeignKeyTarget(name=item)
|
||||||
|
t.save()
|
||||||
|
ForeignKeySource(name='source_' + item, target=t).save()
|
||||||
|
|
||||||
|
self.objects = ForeignKeySource.objects
|
||||||
|
self.data = [
|
||||||
|
{'id': obj.id, 'name': obj.name}
|
||||||
|
for obj in self.objects.all()
|
||||||
|
]
|
||||||
|
self.view = FKInstanceView.as_view()
|
||||||
|
|
||||||
|
def test_options_root_view(self):
|
||||||
|
"""
|
||||||
|
OPTIONS requests to ListCreateAPIView should return metadata
|
||||||
|
"""
|
||||||
|
request = factory.options('/999')
|
||||||
|
with self.assertNumQueries(1):
|
||||||
|
response = self.view(request, pk=999).render()
|
||||||
|
expected = {
|
||||||
|
'name': 'Fk Instance',
|
||||||
|
'description': 'FK: example description for OPTIONS.',
|
||||||
|
'renders': [
|
||||||
|
'application/json',
|
||||||
|
'text/html'
|
||||||
|
],
|
||||||
|
'parses': [
|
||||||
|
'application/json',
|
||||||
|
'application/x-www-form-urlencoded',
|
||||||
|
'multipart/form-data'
|
||||||
|
],
|
||||||
|
'actions': {
|
||||||
|
'PUT': {
|
||||||
|
'id': {
|
||||||
|
'type': 'integer',
|
||||||
|
'required': False,
|
||||||
|
'read_only': True,
|
||||||
|
'label': 'ID'
|
||||||
|
},
|
||||||
|
'name': {
|
||||||
|
'type': 'string',
|
||||||
|
'required': True,
|
||||||
|
'read_only': False,
|
||||||
|
'label': 'name',
|
||||||
|
'max_length': 100
|
||||||
|
},
|
||||||
|
'target': {
|
||||||
|
'type': 'field',
|
||||||
|
'required': True,
|
||||||
|
'read_only': False,
|
||||||
|
'label': 'Target',
|
||||||
|
'help_text': 'Target'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
|
self.assertEqual(response.data, expected)
|
||||||
|
|
||||||
|
|
||||||
class TestOverriddenGetObject(TestCase):
|
class TestOverriddenGetObject(TestCase):
|
||||||
"""
|
"""
|
||||||
Test cases for a RetrieveUpdateDestroyAPIView that does NOT use the
|
Test cases for a RetrieveUpdateDestroyAPIView that does NOT use the
|
||||||
|
|
|
@ -3,6 +3,7 @@ from django.test import TestCase
|
||||||
|
|
||||||
from rest_framework.serializers import _resolve_model
|
from rest_framework.serializers import _resolve_model
|
||||||
from tests.models import BasicModel
|
from tests.models import BasicModel
|
||||||
|
from rest_framework.compat import six
|
||||||
|
|
||||||
|
|
||||||
class ResolveModelTests(TestCase):
|
class ResolveModelTests(TestCase):
|
||||||
|
@ -19,6 +20,10 @@ class ResolveModelTests(TestCase):
|
||||||
resolved_model = _resolve_model('tests.BasicModel')
|
resolved_model = _resolve_model('tests.BasicModel')
|
||||||
self.assertEqual(resolved_model, BasicModel)
|
self.assertEqual(resolved_model, BasicModel)
|
||||||
|
|
||||||
|
def test_resolve_unicode_representation(self):
|
||||||
|
resolved_model = _resolve_model(six.text_type('tests.BasicModel'))
|
||||||
|
self.assertEqual(resolved_model, BasicModel)
|
||||||
|
|
||||||
def test_resolve_non_django_model(self):
|
def test_resolve_non_django_model(self):
|
||||||
with self.assertRaises(ValueError):
|
with self.assertRaises(ValueError):
|
||||||
_resolve_model(TestCase)
|
_resolve_model(TestCase)
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
import sys
|
||||||
import copy
|
import copy
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
|
@ -11,6 +12,11 @@ from rest_framework.views import APIView
|
||||||
|
|
||||||
factory = APIRequestFactory()
|
factory = APIRequestFactory()
|
||||||
|
|
||||||
|
if sys.version_info[:2] >= (3, 4):
|
||||||
|
JSON_ERROR = 'JSON parse error - Expecting value:'
|
||||||
|
else:
|
||||||
|
JSON_ERROR = 'JSON parse error - No JSON object could be decoded'
|
||||||
|
|
||||||
|
|
||||||
class BasicView(APIView):
|
class BasicView(APIView):
|
||||||
def get(self, request, *args, **kwargs):
|
def get(self, request, *args, **kwargs):
|
||||||
|
@ -48,7 +54,7 @@ def sanitise_json_error(error_dict):
|
||||||
of json.
|
of json.
|
||||||
"""
|
"""
|
||||||
ret = copy.copy(error_dict)
|
ret = copy.copy(error_dict)
|
||||||
chop = len('JSON parse error - No JSON object could be decoded')
|
chop = len(JSON_ERROR)
|
||||||
ret['detail'] = ret['detail'][:chop]
|
ret['detail'] = ret['detail'][:chop]
|
||||||
return ret
|
return ret
|
||||||
|
|
||||||
|
@ -61,7 +67,7 @@ class ClassBasedViewIntegrationTests(TestCase):
|
||||||
request = factory.post('/', 'f00bar', content_type='application/json')
|
request = factory.post('/', 'f00bar', content_type='application/json')
|
||||||
response = self.view(request)
|
response = self.view(request)
|
||||||
expected = {
|
expected = {
|
||||||
'detail': 'JSON parse error - No JSON object could be decoded'
|
'detail': JSON_ERROR
|
||||||
}
|
}
|
||||||
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||||
self.assertEqual(sanitise_json_error(response.data), expected)
|
self.assertEqual(sanitise_json_error(response.data), expected)
|
||||||
|
@ -76,7 +82,7 @@ class ClassBasedViewIntegrationTests(TestCase):
|
||||||
request = factory.post('/', form_data)
|
request = factory.post('/', form_data)
|
||||||
response = self.view(request)
|
response = self.view(request)
|
||||||
expected = {
|
expected = {
|
||||||
'detail': 'JSON parse error - No JSON object could be decoded'
|
'detail': JSON_ERROR
|
||||||
}
|
}
|
||||||
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||||
self.assertEqual(sanitise_json_error(response.data), expected)
|
self.assertEqual(sanitise_json_error(response.data), expected)
|
||||||
|
@ -90,7 +96,7 @@ class FunctionBasedViewIntegrationTests(TestCase):
|
||||||
request = factory.post('/', 'f00bar', content_type='application/json')
|
request = factory.post('/', 'f00bar', content_type='application/json')
|
||||||
response = self.view(request)
|
response = self.view(request)
|
||||||
expected = {
|
expected = {
|
||||||
'detail': 'JSON parse error - No JSON object could be decoded'
|
'detail': JSON_ERROR
|
||||||
}
|
}
|
||||||
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||||
self.assertEqual(sanitise_json_error(response.data), expected)
|
self.assertEqual(sanitise_json_error(response.data), expected)
|
||||||
|
@ -105,7 +111,7 @@ class FunctionBasedViewIntegrationTests(TestCase):
|
||||||
request = factory.post('/', form_data)
|
request = factory.post('/', form_data)
|
||||||
response = self.view(request)
|
response = self.view(request)
|
||||||
expected = {
|
expected = {
|
||||||
'detail': 'JSON parse error - No JSON object could be decoded'
|
'detail': JSON_ERROR
|
||||||
}
|
}
|
||||||
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||||
self.assertEqual(sanitise_json_error(response.data), expected)
|
self.assertEqual(sanitise_json_error(response.data), expected)
|
||||||
|
|
30
tox.ini
30
tox.ini
|
@ -1,10 +1,22 @@
|
||||||
[tox]
|
[tox]
|
||||||
downloadcache = {toxworkdir}/cache/
|
downloadcache = {toxworkdir}/cache/
|
||||||
envlist = py3.3-django1.7,py3.2-django1.7,py2.7-django1.7,py3.3-django1.6,py3.2-django1.6,py2.7-django1.6,py2.6-django1.6,py3.3-django1.5,py3.2-django1.5,py2.7-django1.5,py2.6-django1.5,py2.7-django1.4,py2.6-django1.4
|
envlist =
|
||||||
|
py3.4-django1.7,py3.3-django1.7,py3.2-django1.7,py2.7-django1.7,
|
||||||
|
py3.4-django1.6,py3.3-django1.6,py3.2-django1.6,py2.7-django1.6,py2.6-django1.6,
|
||||||
|
py3.4-django1.5,py3.3-django1.5,py3.2-django1.5,py2.7-django1.5,py2.6-django1.5,
|
||||||
|
py2.7-django1.4,py2.6-django1.4,
|
||||||
|
|
||||||
[testenv]
|
[testenv]
|
||||||
commands = py.test -q
|
commands = py.test -q
|
||||||
|
|
||||||
|
[testenv:py3.4-django1.7]
|
||||||
|
basepython = python3.4
|
||||||
|
deps = https://www.djangoproject.com/download/1.7b2/tarball/
|
||||||
|
django-filter==0.7
|
||||||
|
defusedxml==0.3
|
||||||
|
Pillow==2.3.0
|
||||||
|
pytest-django==2.6.1
|
||||||
|
|
||||||
[testenv:py3.3-django1.7]
|
[testenv:py3.3-django1.7]
|
||||||
basepython = python3.3
|
basepython = python3.3
|
||||||
deps = https://www.djangoproject.com/download/1.7b2/tarball/
|
deps = https://www.djangoproject.com/download/1.7b2/tarball/
|
||||||
|
@ -33,6 +45,14 @@ deps = https://www.djangoproject.com/download/1.7b2/tarball/
|
||||||
Pillow==2.3.0
|
Pillow==2.3.0
|
||||||
pytest-django==2.6.1
|
pytest-django==2.6.1
|
||||||
|
|
||||||
|
[testenv:py3.4-django1.6]
|
||||||
|
basepython = python3.4
|
||||||
|
deps = Django==1.6.3
|
||||||
|
django-filter==0.7
|
||||||
|
defusedxml==0.3
|
||||||
|
Pillow==2.3.0
|
||||||
|
pytest-django==2.6.1
|
||||||
|
|
||||||
[testenv:py3.3-django1.6]
|
[testenv:py3.3-django1.6]
|
||||||
basepython = python3.3
|
basepython = python3.3
|
||||||
deps = Django==1.6.3
|
deps = Django==1.6.3
|
||||||
|
@ -73,6 +93,14 @@ deps = Django==1.6.3
|
||||||
Pillow==2.3.0
|
Pillow==2.3.0
|
||||||
pytest-django==2.6.1
|
pytest-django==2.6.1
|
||||||
|
|
||||||
|
[testenv:py3.4-django1.5]
|
||||||
|
basepython = python3.4
|
||||||
|
deps = django==1.5.6
|
||||||
|
django-filter==0.7
|
||||||
|
defusedxml==0.3
|
||||||
|
Pillow==2.3.0
|
||||||
|
pytest-django==2.6.1
|
||||||
|
|
||||||
[testenv:py3.3-django1.5]
|
[testenv:py3.3-django1.5]
|
||||||
basepython = python3.3
|
basepython = python3.3
|
||||||
deps = django==1.5.6
|
deps = django==1.5.6
|
||||||
|
|
Loading…
Reference in New Issue
Block a user