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

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

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

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

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

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

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

View File

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

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

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

View File

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

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

View File

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

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

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

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

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)

28
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,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