Merge branch 'master' into modelserialization-charfield-with-null

This commit is contained in:
Serhiy Voyt 2014-06-27 18:10:50 +03:00
commit 3326ddc865
19 changed files with 155 additions and 66 deletions

View File

@ -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"
- DJANGO="django==1.3.7" - DJANGO="django==1.3.7"
install: install:
@ -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,13 +33,16 @@ 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.2" - python: "3.2"
env: DJANGO="django==1.3.7" env: DJANGO="django==1.3.7"
- python: "3.3" - python: "3.3"
env: DJANGO="django==1.4.11" env: DJANGO="django==1.4.13"
- python: "3.3" - python: "3.3"
env: DJANGO="django==1.3.7" env: DJANGO="django==1.3.7"
- python: "3.4"
env: DJANGO="django==1.4.13"
- python: "3.4"
env: DJANGO="django==1.3.7"

View File

@ -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 = (
... ...

View File

@ -184,7 +184,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

View File

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

View File

@ -179,7 +179,16 @@ The [wq.db package][wq.db] provides an advanced [Router][wq.db-router] class (an
app.router.register_model(MyModel) app.router.register_model(MyModel)
## DRF-extensions
The [`DRF-extensions` package][drf-extensions] provides [routers][drf-extensions-routers] for creating [nested viewsets][drf-extensions-nested-viewsets], [collection level controllers][drf-extensions-collection-level-controllers] with [customizable endpoint names][drf-extensions-customizable-endpoint-names].
[cite]: http://guides.rubyonrails.org/routing.html [cite]: http://guides.rubyonrails.org/routing.html
[drf-nested-routers]: https://github.com/alanjds/drf-nested-routers [drf-nested-routers]: https://github.com/alanjds/drf-nested-routers
[wq.db]: http://wq.io/wq.db [wq.db]: http://wq.io/wq.db
[wq.db-router]: http://wq.io/docs/app.py [wq.db-router]: http://wq.io/docs/app.py
[drf-extensions]: http://chibisov.github.io/drf-extensions/docs/
[drf-extensions-routers]: http://chibisov.github.io/drf-extensions/docs/#routers
[drf-extensions-nested-viewsets]: http://chibisov.github.io/drf-extensions/docs/#nested-routes
[drf-extensions-collection-level-controllers]: http://chibisov.github.io/drf-extensions/docs/#collection-level-controllers
[drf-extensions-customizable-endpoint-names]: http://chibisov.github.io/drf-extensions/docs/#controller-endpoint-name

View File

@ -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:

View File

@ -137,7 +137,7 @@ The `@action` and `@link` decorators can additionally take extra arguments that
def set_password(self, request, pk=None): def set_password(self, request, pk=None):
... ...
The `@action` decorator will route `POST` requests by default, but may also accept other HTTP methods, by using the `method` 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:
@action(methods=['POST', 'DELETE']) @action(methods=['POST', 'DELETE'])
def unset_password(self, request, pk=None): def unset_password(self, request, pk=None):

View File

@ -40,24 +40,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

View File

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

View File

@ -51,6 +51,7 @@ except ImportError:
# guardian is optional # guardian is optional
try: try:
import guardian import guardian
import guardian.shortcuts # Fixes #1624
except ImportError: except ImportError:
guardian = None guardian = None

View File

@ -62,7 +62,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)
@ -154,7 +154,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)
@ -182,7 +187,7 @@ class Field(object):
def field_to_native(self, obj, field_name): def field_to_native(self, obj, field_name):
""" """
Given and object and a field name, returns the value that should be Given an object and a field name, returns the value that should be
serialized for that field. serialized for that field.
""" """
if obj is None: if obj is None:
@ -505,7 +510,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 = {
@ -513,12 +518,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
@ -1022,9 +1031,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)

View File

@ -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):
""" """

View File

@ -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 get_concrete_model, six from rest_framework.compat import get_concrete_model, 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):

View File

@ -122,7 +122,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
@ -136,7 +136,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

View File

@ -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`.'

View File

@ -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())

View File

@ -3,6 +3,7 @@ from django.test import TestCase
from rest_framework.serializers import _resolve_model from rest_framework.serializers import _resolve_model
from rest_framework.tests.models import BasicModel from rest_framework.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)

View File

@ -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)

28
tox.ini
View File

@ -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,py2.7-django1.3,py2.6-django1.3 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,
py2.7-django1.3,py2.6-django1.3
[testenv] [testenv]
commands = {envpython} rest_framework/runtests/runtests.py commands = {envpython} rest_framework/runtests/runtests.py
[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
[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/
@ -30,6 +42,13 @@ deps = https://www.djangoproject.com/download/1.7b2/tarball/
django-guardian==1.1.1 django-guardian==1.1.1
Pillow==2.3.0 Pillow==2.3.0
[testenv:py3.4-django1.6]
basepython = python3.4
deps = Django==1.6.3
django-filter==0.7
defusedxml==0.3
Pillow==2.3.0
[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
@ -66,6 +85,13 @@ deps = Django==1.6.3
django-guardian==1.1.1 django-guardian==1.1.1
Pillow==2.3.0 Pillow==2.3.0
[testenv:py3.4-django1.5]
basepython = python3.4
deps = django==1.5.6
django-filter==0.7
defusedxml==0.3
Pillow==2.3.0
[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