mirror of
https://github.com/encode/django-rest-framework.git
synced 2024-11-22 17:47:04 +03:00
Merge branch 'master' into modelserialization-charfield-with-null
This commit is contained in:
commit
3326ddc865
22
.travis.yml
22
.travis.yml
|
@ -5,12 +5,13 @@ python:
|
|||
- "2.7"
|
||||
- "3.2"
|
||||
- "3.3"
|
||||
- "3.4"
|
||||
|
||||
env:
|
||||
- DJANGO="https://www.djangoproject.com/download/1.7b2/tarball/"
|
||||
- DJANGO="django==1.6.3"
|
||||
- DJANGO="django==1.5.6"
|
||||
- DJANGO="django==1.4.11"
|
||||
- DJANGO="https://www.djangoproject.com/download/1.7.b4/tarball/"
|
||||
- DJANGO="django==1.6.5"
|
||||
- DJANGO="django==1.5.8"
|
||||
- DJANGO="django==1.4.13"
|
||||
- DJANGO="django==1.3.7"
|
||||
|
||||
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.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 [[ ${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=.
|
||||
|
||||
script:
|
||||
|
@ -32,13 +33,16 @@ script:
|
|||
matrix:
|
||||
exclude:
|
||||
- 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"
|
||||
env: DJANGO="django==1.4.11"
|
||||
env: DJANGO="django==1.4.13"
|
||||
- python: "3.2"
|
||||
env: DJANGO="django==1.3.7"
|
||||
- python: "3.3"
|
||||
env: DJANGO="django==1.4.11"
|
||||
env: DJANGO="django==1.4.13"
|
||||
- python: "3.3"
|
||||
env: DJANGO="django==1.3.7"
|
||||
|
||||
- python: "3.4"
|
||||
env: DJANGO="django==1.4.13"
|
||||
- python: "3.4"
|
||||
env: DJANGO="django==1.3.7"
|
||||
|
|
|
@ -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.
|
||||
|
||||
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 = (
|
||||
...
|
||||
|
|
|
@ -184,7 +184,9 @@ Corresponds to `django.db.models.fields.SlugField`.
|
|||
|
||||
## 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
|
||||
|
||||
|
|
|
@ -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`.
|
||||
|
||||
* `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.
|
||||
* `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.
|
||||
|
|
|
@ -179,7 +179,16 @@ The [wq.db package][wq.db] provides an advanced [Router][wq.db-router] class (an
|
|||
|
||||
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
|
||||
[drf-nested-routers]: https://github.com/alanjds/drf-nested-routers
|
||||
[wq.db]: http://wq.io/wq.db
|
||||
[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
|
|
@ -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:
|
||||
|
||||
description = serializers.TextField()
|
||||
description_html = serializers.TextField(source='description', read_only=True)
|
||||
description = serializers.CharField()
|
||||
description_html = serializers.CharField(source='description', read_only=True)
|
||||
|
||||
def transform_description_html(self, obj, value):
|
||||
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
|
||||
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.
|
||||
|
||||
|
@ -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.
|
||||
|
||||
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 Meta:
|
||||
|
|
|
@ -137,7 +137,7 @@ The `@action` and `@link` decorators can additionally take extra arguments that
|
|||
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'])
|
||||
def unset_password(self, request, pk=None):
|
||||
|
|
|
@ -40,24 +40,28 @@ You can determine your currently installed version using `pip freeze`:
|
|||
|
||||
## 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
|
||||
* Fix bad links for the `BrowsableAPIRenderer` with `YAMLRenderer`
|
||||
* Add `UnicodeYAMLRenderer` that extends `YAMLRenderer` with unicode
|
||||
* Fix `parse_header` argument convertion
|
||||
* Fix mediatype detection under Python3
|
||||
* Web browseable API now offers blank option on dropdown when the field is not required
|
||||
* `APIException` representation improved for logging purposes
|
||||
* Allow source="*" within nested serializers
|
||||
* Better support for custom oauth2 provider backends
|
||||
* Fix field validation if it's optional and has no value
|
||||
* Add `SEARCH_PARAM` and `ORDERING_PARAM`
|
||||
* Fix `APIRequestFactory` to support arguments within the url string for GET
|
||||
* Allow three transport modes for access tokens when accessing a protected resource
|
||||
* Fix `Request`'s `QueryDict` encoding
|
||||
* **Security fix**: Escape request path when it is include as part of the login and logout links in the browsable API.
|
||||
* `help_text` and `verbose_name` automatically set for related fields on `ModelSerializer`.
|
||||
* Fix nested serializers linked through a backward foreign key relation.
|
||||
* Fix bad links for the `BrowsableAPIRenderer` with `YAMLRenderer`.
|
||||
* Add `UnicodeYAMLRenderer` that extends `YAMLRenderer` with unicode.
|
||||
* Fix `parse_header` argument convertion.
|
||||
* Fix mediatype detection under Python 3.
|
||||
* Web browseable API now offers blank option on dropdown when the field is not required.
|
||||
* `APIException` representation improved for logging purposes.
|
||||
* Allow source="*" within nested serializers.
|
||||
* Better support for custom oauth2 provider backends.
|
||||
* Fix field validation if it's optional and has no value.
|
||||
* Add `SEARCH_PARAM` and `ORDERING_PARAM`.
|
||||
* 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
|
||||
|
||||
|
|
|
@ -8,7 +8,7 @@ ______ _____ _____ _____ __ _
|
|||
"""
|
||||
|
||||
__title__ = 'Django REST framework'
|
||||
__version__ = '2.3.13'
|
||||
__version__ = '2.3.14'
|
||||
__author__ = 'Tom Christie'
|
||||
__license__ = 'BSD 2-Clause'
|
||||
__copyright__ = 'Copyright 2011-2014 Tom Christie'
|
||||
|
|
|
@ -51,6 +51,7 @@ except ImportError:
|
|||
# guardian is optional
|
||||
try:
|
||||
import guardian
|
||||
import guardian.shortcuts # Fixes #1624
|
||||
except ImportError:
|
||||
guardian = None
|
||||
|
||||
|
|
|
@ -62,7 +62,7 @@ def get_component(obj, attr_name):
|
|||
|
||||
def readable_datetime_formats(formats):
|
||||
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)
|
||||
|
||||
|
||||
|
@ -154,7 +154,12 @@ class Field(object):
|
|||
def widget_html(self):
|
||||
if not self.widget:
|
||||
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):
|
||||
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):
|
||||
"""
|
||||
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.
|
||||
"""
|
||||
if obj is None:
|
||||
|
@ -505,7 +510,7 @@ class SlugField(CharField):
|
|||
|
||||
class ChoiceField(WritableField):
|
||||
type_name = 'ChoiceField'
|
||||
type_label = 'multiple choice'
|
||||
type_label = 'choice'
|
||||
form_field_class = forms.ChoiceField
|
||||
widget = widgets.Select
|
||||
default_error_messages = {
|
||||
|
@ -513,12 +518,16 @@ class ChoiceField(WritableField):
|
|||
'the available choices.'),
|
||||
}
|
||||
|
||||
def __init__(self, choices=(), *args, **kwargs):
|
||||
def __init__(self, choices=(), blank_display_value=None, *args, **kwargs):
|
||||
self.empty = kwargs.pop('empty', '')
|
||||
super(ChoiceField, self).__init__(*args, **kwargs)
|
||||
self.choices = choices
|
||||
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):
|
||||
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.
|
||||
"""
|
||||
|
||||
def __init__(self, method_name):
|
||||
def __init__(self, method_name, *args, **kwargs):
|
||||
self.method_name = method_name
|
||||
super(SerializerMethodField, self).__init__()
|
||||
super(SerializerMethodField, self).__init__(*args, **kwargs)
|
||||
|
||||
def field_to_native(self, obj, field_name):
|
||||
value = getattr(self.parent, self.method_name)(obj)
|
||||
|
|
|
@ -90,8 +90,8 @@ class GenericAPIView(views.APIView):
|
|||
'view': self
|
||||
}
|
||||
|
||||
def get_serializer(self, instance=None, data=None,
|
||||
files=None, many=False, partial=False):
|
||||
def get_serializer(self, instance=None, data=None, files=None, many=False,
|
||||
partial=False, allow_add_remove=False):
|
||||
"""
|
||||
Return the serializer instance that should be used for validating and
|
||||
deserializing input, and for serializing output.
|
||||
|
@ -99,7 +99,9 @@ class GenericAPIView(views.APIView):
|
|||
serializer_class = self.get_serializer_class()
|
||||
context = self.get_serializer_context()
|
||||
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):
|
||||
"""
|
||||
|
|
|
@ -21,6 +21,7 @@ from django.core.paginator import Page
|
|||
from django.db import models
|
||||
from django.forms import widgets
|
||||
from django.utils.datastructures import SortedDict
|
||||
from django.core.exceptions import ObjectDoesNotExist
|
||||
from rest_framework.compat import get_concrete_model, six
|
||||
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
|
||||
# serializer fields more explicit.
|
||||
|
||||
from rest_framework.relations import *
|
||||
from rest_framework.fields import *
|
||||
from rest_framework.relations import * # NOQA
|
||||
from rest_framework.fields import * # NOQA
|
||||
|
||||
|
||||
def _resolve_model(obj):
|
||||
|
@ -48,7 +49,7 @@ def _resolve_model(obj):
|
|||
String representations should have the format:
|
||||
'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('.')
|
||||
return models.get_model(app_name, model_name)
|
||||
elif inspect.isclass(obj) and issubclass(obj, models.Model):
|
||||
|
@ -344,7 +345,7 @@ class BaseSerializer(WritableField):
|
|||
|
||||
for field_name, field in self.fields.items():
|
||||
if field.read_only and obj is None:
|
||||
continue
|
||||
continue
|
||||
field.initialize(parent=self, field_name=field_name)
|
||||
key = self.get_field_key(field_name)
|
||||
value = field.field_to_native(obj, field_name)
|
||||
|
@ -758,9 +759,9 @@ class ModelSerializer(Serializer):
|
|||
field.read_only = True
|
||||
|
||||
ret[accessor_name] = field
|
||||
|
||||
|
||||
# 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
|
||||
# in the `read_only_fields` option
|
||||
|
@ -775,10 +776,10 @@ class ModelSerializer(Serializer):
|
|||
"on serializer '%s'." %
|
||||
(field_name, self.__class__.__name__))
|
||||
ret[field_name].read_only = True
|
||||
|
||||
|
||||
# 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:
|
||||
assert field_name not in self.base_fields.keys(), (
|
||||
"field '%s' on serializer '%s' specified in "
|
||||
|
@ -789,7 +790,7 @@ class ModelSerializer(Serializer):
|
|||
"Non-existant field '%s' specified in `write_only_fields` "
|
||||
"on serializer '%s'." %
|
||||
(field_name, self.__class__.__name__))
|
||||
ret[field_name].write_only = True
|
||||
ret[field_name].write_only = True
|
||||
|
||||
return ret
|
||||
|
||||
|
|
|
@ -122,7 +122,7 @@ def optional_login(request):
|
|||
except NoReverseMatch:
|
||||
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
|
||||
|
||||
|
||||
|
@ -136,7 +136,7 @@ def optional_logout(request):
|
|||
except NoReverseMatch:
|
||||
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
|
||||
|
||||
|
||||
|
|
|
@ -36,7 +36,7 @@ class APIRequestFactory(DjangoRequestFactory):
|
|||
"""
|
||||
|
||||
if not data:
|
||||
return ('', None)
|
||||
return ('', content_type)
|
||||
|
||||
assert format is None or content_type is None, (
|
||||
'You may not set both `format` and `content_type`.'
|
||||
|
|
|
@ -4,6 +4,7 @@ General serializer field tests.
|
|||
from __future__ import unicode_literals
|
||||
|
||||
import datetime
|
||||
import re
|
||||
from decimal import Decimal
|
||||
from uuid import uuid4
|
||||
from django.core import validators
|
||||
|
@ -103,6 +104,16 @@ class BasicFieldTests(TestCase):
|
|||
keys = list(field.to_native(ret).keys())
|
||||
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):
|
||||
"""
|
||||
|
@ -312,7 +323,7 @@ class DateTimeFieldTest(TestCase):
|
|||
f.from_native('04:61:59')
|
||||
except validators.ValidationError as e:
|
||||
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:
|
||||
self.fail("ValidationError was not properly raised")
|
||||
|
||||
|
@ -326,7 +337,7 @@ class DateTimeFieldTest(TestCase):
|
|||
f.from_native('04 -- 31')
|
||||
except validators.ValidationError as e:
|
||||
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:
|
||||
self.fail("ValidationError was not properly raised")
|
||||
|
||||
|
@ -706,6 +717,15 @@ class ChoiceFieldTests(TestCase):
|
|||
f = serializers.ChoiceField(required=False, choices=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):
|
||||
s = ChoiceFieldModelSerializer(data={'choice': 'wrong_value'})
|
||||
self.assertFalse(s.is_valid())
|
||||
|
|
|
@ -3,6 +3,7 @@ from django.test import TestCase
|
|||
|
||||
from rest_framework.serializers import _resolve_model
|
||||
from rest_framework.tests.models import BasicModel
|
||||
from rest_framework.compat import six
|
||||
|
||||
|
||||
class ResolveModelTests(TestCase):
|
||||
|
@ -19,6 +20,10 @@ class ResolveModelTests(TestCase):
|
|||
resolved_model = _resolve_model('tests.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):
|
||||
with self.assertRaises(ValueError):
|
||||
_resolve_model(TestCase)
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
from __future__ import unicode_literals
|
||||
|
||||
import sys
|
||||
import copy
|
||||
from django.test import TestCase
|
||||
from rest_framework import status
|
||||
|
@ -11,6 +12,11 @@ from rest_framework.views import APIView
|
|||
|
||||
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):
|
||||
def get(self, request, *args, **kwargs):
|
||||
|
@ -48,7 +54,7 @@ def sanitise_json_error(error_dict):
|
|||
of json.
|
||||
"""
|
||||
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]
|
||||
return ret
|
||||
|
||||
|
@ -61,7 +67,7 @@ class ClassBasedViewIntegrationTests(TestCase):
|
|||
request = factory.post('/', 'f00bar', content_type='application/json')
|
||||
response = self.view(request)
|
||||
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(sanitise_json_error(response.data), expected)
|
||||
|
@ -76,7 +82,7 @@ class ClassBasedViewIntegrationTests(TestCase):
|
|||
request = factory.post('/', form_data)
|
||||
response = self.view(request)
|
||||
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(sanitise_json_error(response.data), expected)
|
||||
|
@ -90,7 +96,7 @@ class FunctionBasedViewIntegrationTests(TestCase):
|
|||
request = factory.post('/', 'f00bar', content_type='application/json')
|
||||
response = self.view(request)
|
||||
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(sanitise_json_error(response.data), expected)
|
||||
|
@ -105,7 +111,7 @@ class FunctionBasedViewIntegrationTests(TestCase):
|
|||
request = factory.post('/', form_data)
|
||||
response = self.view(request)
|
||||
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(sanitise_json_error(response.data), expected)
|
||||
|
|
28
tox.ini
28
tox.ini
|
@ -1,10 +1,22 @@
|
|||
[tox]
|
||||
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]
|
||||
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]
|
||||
basepython = python3.3
|
||||
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
|
||||
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]
|
||||
basepython = python3.3
|
||||
deps = Django==1.6.3
|
||||
|
@ -66,6 +85,13 @@ deps = Django==1.6.3
|
|||
django-guardian==1.1.1
|
||||
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]
|
||||
basepython = python3.3
|
||||
deps = django==1.5.6
|
||||
|
|
Loading…
Reference in New Issue
Block a user