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:
Xavier Ordoquy 2014-06-23 14:02:45 +02:00
commit 2489e38a06
25 changed files with 251 additions and 75 deletions

View File

@ -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"
install:
- 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.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,8 +33,10 @@ 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.3"
env: DJANGO="django==1.4.11"
env: DJANGO="django==1.4.13"
- python: "3.4"
env: DJANGO="django==1.4.13"

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

View File

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

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

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

View File

@ -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):
...
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'])
def unset_password(self, request, pk=None):

View File

@ -52,24 +52,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
## 2.3.x series

View File

@ -104,7 +104,7 @@ Don't forget to sync the database for the first time.
## 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 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
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.

View File

@ -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.
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.
python ./manage.py createsuperuser
python manage.py createsuperuser
## Adding endpoints for our User models

View File

@ -21,7 +21,7 @@ First of all let's refactor our `UserList` and `UserDetail` views into a single
queryset = User.objects.all()
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.
@ -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.
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',
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
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.
* Join the [REST framework discussion group][group], and help build the community.

View File

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

View File

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

View File

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

View File

@ -64,7 +64,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)
@ -156,7 +156,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)
@ -499,7 +504,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 = {
@ -507,12 +512,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
@ -1016,9 +1025,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)

View File

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

View File

@ -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 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)
@ -753,9 +754,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
@ -770,10 +771,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 "
@ -784,7 +785,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
@ -828,6 +829,15 @@ class ModelSerializer(Serializer):
if model_field.verbose_name is not None:
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)
def get_field(self, model_field):

View File

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

View File

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

View File

@ -173,6 +173,12 @@ class AnonRateThrottle(SimpleRateThrottle):
if request.user.is_authenticated():
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 % {
'scope': self.scope,
'ident': self.get_ident(request)

View File

@ -151,7 +151,8 @@ class ForeignKeySource(RESTFrameworkModel):
class NullableForeignKeySource(RESTFrameworkModel):
name = models.CharField(max_length=100)
target = models.ForeignKey(ForeignKeyTarget, null=True, blank=True,
related_name='nullable_sources')
related_name='nullable_sources',
verbose_name='Optional target object')
# OneToOne

View File

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

View File

@ -5,6 +5,7 @@ from django.test import TestCase
from rest_framework import generics, renderers, serializers, status
from rest_framework.test import APIRequestFactory
from tests.models import BasicModel, Comment, SlugBasedModel
from tests.models import ForeignKeySource, ForeignKeyTarget
from rest_framework.compat import six
factory = APIRequestFactory()
@ -28,6 +29,13 @@ class InstanceView(generics.RetrieveUpdateDestroyAPIView):
return queryset.exclude(text='filtered out')
class FKInstanceView(generics.RetrieveUpdateDestroyAPIView):
"""
FK: example description for OPTIONS.
"""
model = ForeignKeySource
class SlugSerializer(serializers.ModelSerializer):
slug = serializers.Field() # read only
@ -407,6 +415,72 @@ class TestInstanceView(TestCase):
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):
"""
Test cases for a RetrieveUpdateDestroyAPIView that does NOT use the

View File

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

View File

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

30
tox.ini
View File

@ -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
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]
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]
basepython = python3.3
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
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]
basepython = python3.3
deps = Django==1.6.3
@ -73,6 +93,14 @@ deps = Django==1.6.3
Pillow==2.3.0
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]
basepython = python3.3
deps = django==1.5.6