Merge branch 'master' into version-3.1

This commit is contained in:
Tom Christie 2014-12-18 11:21:25 +00:00
commit c8d88c8c8a
22 changed files with 244 additions and 55 deletions

View File

@ -112,6 +112,8 @@ Two options are currently used in HTML form generation, `'input_type'` and `'bas
A boolean representation.
When using HTML encoded form input be aware that omitting a value will always be treated as setting a field to `False`, even if it has a `default=True` option specified. This is because HTML checkbox inputs represent the unchecked state by omitting the value, so REST framework treats omission as if it is an empty checkbox input.
Corresponds to `django.db.models.fields.BooleanField`.
**Signature:** `BooleanField()`

View File

@ -10,12 +10,24 @@ Together with [authentication] and [throttling], permissions determine whether a
Permission checks are always run at the very start of the view, before any other code is allowed to proceed. Permission checks will typically use the authentication information in the `request.user` and `request.auth` properties to determine if the incoming request should be permitted.
Permissions are used to grant or deny access different classes of users to different parts of the API.
The simplest style of permission would be to allow access to any authenticated user, and deny access to any unauthenticated user. This corresponds the `IsAuthenticated` class in REST framework.
A slightly less strict style of permission would be to allow full access to authenticated users, but allow read-only access to unauthenticated users. This corresponds to the `IsAuthenticatedOrReadOnly` class in REST framework.
## How permissions are determined
Permissions in REST framework are always defined as a list of permission classes.
Before running the main body of the view each permission in the list is checked.
If any permission check fails an `exceptions.PermissionDenied` exception will be raised, and the main body of the view will not run.
If any permission check fails an `exceptions.PermissionDenied` or `exceptions.NotAuthenticated` exception will be raised, and the main body of the view will not run.
When the permissions checks fail either a "403 Forbidden" or a "401 Unauthorized" response will be returned, according to the following rules:
* The request was successfully authenticated, but permission was denied. *— An HTTP 403 Forbidden response will be returned.*
* The request was not successfully authenticated, and the highest priority authentication class *does not* use `WWW-Authenticate` headers. *— An HTTP 403 Forbidden response will be returned.*
* The request was not successfully authenticated, and the highest priority authentication class *does* use `WWW-Authenticate` headers. *— An HTTP 401 Unauthorized response, with an appropriate `WWW-Authenticate` header will be returned.*
## Object level permissions

View File

@ -397,7 +397,7 @@ We could define a custom field that could be used to serialize tagged instances,
return 'Note: ' + value.text
raise Exception('Unexpected type of tagged object')
If you need the target of the relationship to have a nested representation, you can use the required serializers inside the `.to_native()` method:
If you need the target of the relationship to have a nested representation, you can use the required serializers inside the `.to_representation()` method:
def to_representation(self, value):
"""

View File

@ -22,11 +22,13 @@ The serializers in REST framework work very similarly to Django's `Form` and `Mo
Let's start by creating a simple object we can use for example purposes:
from datetime import datetime
class Comment(object):
def __init__(self, email, content, created=None):
self.email = email
self.content = content
self.created = created or datetime.datetime.now()
self.created = created or datetime.now()
comment = Comment(email='leila@example.com', content='foo bar')
@ -61,10 +63,10 @@ At this point we've translated the model instance into Python native datatypes.
Deserialization is similar. First we parse a stream into Python native datatypes...
from StringIO import StringIO
from django.utils.six import BytesIO
from rest_framework.parsers import JSONParser
stream = StringIO(json)
stream = BytesIO(json)
data = JSONParser().parse(stream)
...then we restore those native datatypes into a dictionary of validated data.
@ -240,6 +242,12 @@ Serializer classes can also include reusable validators that are applied to the
For more information see the [validators documentation](validators.md).
## Accessing the initial data and instance
When passing an initial object or queryset to a serializer instance, the object will be made available as `.instance`. If no initial object is passed then the `.instance` attribute will be `None`.
When passing data to a serializer instance, the unmodified data will be made available as `.initial_data`. If the data keyword argument is not passed then the `.initial_data` attribute will not exist.
## Partial updates
By default, serializers must be passed values for all required fields or they will raise validation errors. You can use the `partial` argument in order to allow partial updates.

View File

@ -40,9 +40,21 @@ You can determine your currently installed version using `pip freeze`:
## 3.0.x series
### 3.0.2
**Date**: [17th December 2014][3.0.2-milestone].
* Ensure `request.user` is made available to response middleware. ([#2155][gh2155])
* `Client.logout()` also cancels any existing `force_authenticate`. ([#2218][gh2218], [#2259][gh2259])
* Extra assertions and better checks to preventing incorrect serializer API use. ([#2228][gh2228], [#2234][gh2234], [#2262][gh2262], [#2263][gh2263], [#2266][gh2266], [#2267][gh2267], [#2289][gh2289], [#2291][gh2291])
* Fixed `min_length` message for `CharField`. ([#2255][gh2255])
* Fix `UnicodeDecodeError`, which can occur on serializer `repr`. ([#2270][gh2270], [#2279][gh2279])
* Fix empty HTML values when a default is provided. ([#2280][gh2280], [#2294][gh2294])
* Fix `SlugRelatedField` raising `UnicodeEncodeError` when used as a multiple choice input. ([#2290][gh2290])
### 3.0.1
**Date**: [December 2014][3.0.1-milestone].
**Date**: [11th December 2014][3.0.1-milestone].
* More helpful error message when the default Serializer `create()` fails. ([#2013][gh2013])
* Raise error when attempting to save serializer if data is not valid. ([#2098][gh2098])
@ -665,9 +677,11 @@ For older release notes, [please see the GitHub repo](old-release-notes).
[ticket-582]: https://github.com/tomchristie/django-rest-framework/issues/582
[rfc-6266]: http://tools.ietf.org/html/rfc6266#section-4.3
[old-release-notes]: https://github.com/tomchristie/django-rest-framework/blob/2.4.4/docs/topics/release-notes.md#04x-series
[3.0.1-milestone]: https://github.com/tomchristie/django-rest-framework/issues?q=milestone%3A%223.0.1+Release%22
[3.0.2-milestone]: https://github.com/tomchristie/django-rest-framework/issues?q=milestone%3A%223.0.2+Release%22
<!-- 3.0.1 -->
[gh2013]: https://github.com/tomchristie/django-rest-framework/issues/2013
[gh2098]: https://github.com/tomchristie/django-rest-framework/issues/2098
[gh2109]: https://github.com/tomchristie/django-rest-framework/issues/2109
@ -697,3 +711,21 @@ For older release notes, [please see the GitHub repo](old-release-notes).
[gh2242]: https://github.com/tomchristie/django-rest-framework/issues/2242
[gh2243]: https://github.com/tomchristie/django-rest-framework/issues/2243
[gh2244]: https://github.com/tomchristie/django-rest-framework/issues/2244
<!-- 3.0.2 -->
[gh2155]: https://github.com/tomchristie/django-rest-framework/issues/2155
[gh2218]: https://github.com/tomchristie/django-rest-framework/issues/2218
[gh2228]: https://github.com/tomchristie/django-rest-framework/issues/2228
[gh2234]: https://github.com/tomchristie/django-rest-framework/issues/2234
[gh2255]: https://github.com/tomchristie/django-rest-framework/issues/2255
[gh2259]: https://github.com/tomchristie/django-rest-framework/issues/2259
[gh2262]: https://github.com/tomchristie/django-rest-framework/issues/2262
[gh2263]: https://github.com/tomchristie/django-rest-framework/issues/2263
[gh2266]: https://github.com/tomchristie/django-rest-framework/issues/2266
[gh2267]: https://github.com/tomchristie/django-rest-framework/issues/2267
[gh2270]: https://github.com/tomchristie/django-rest-framework/issues/2270
[gh2279]: https://github.com/tomchristie/django-rest-framework/issues/2279
[gh2280]: https://github.com/tomchristie/django-rest-framework/issues/2280
[gh2289]: https://github.com/tomchristie/django-rest-framework/issues/2289
[gh2290]: https://github.com/tomchristie/django-rest-framework/issues/2290
[gh2291]: https://github.com/tomchristie/django-rest-framework/issues/2291
[gh2294]: https://github.com/tomchristie/django-rest-framework/issues/2294

View File

@ -161,9 +161,7 @@ At this point we've translated the model instance into Python native datatypes.
Deserialization is similar. First we parse a stream into Python native datatypes...
# This import will use either `StringIO.StringIO` or `io.BytesIO`
# as appropriate, depending on if we're running Python 2 or Python 3.
from rest_framework.compat import BytesIO
from django.utils.six import BytesIO
stream = BytesIO(content)
data = JSONParser().parse(stream)
@ -200,7 +198,7 @@ Open the file `snippets/serializers.py` again, and edit the `SnippetSerializer`
model = Snippet
fields = ('id', 'title', 'code', 'linenos', 'language', 'style')
One nice property that serializers have is that you can inspect all the fields in a serializer instance, by printing it's representation. Open the Django shell with `python manange.py shell`, then try the following:
One nice property that serializers have is that you can inspect all the fields in a serializer instance, by printing it's representation. Open the Django shell with `python manage.py shell`, then try the following:
>>> from snippets.serializers import SnippetSerializer
>>> serializer = SnippetSerializer()

View File

@ -206,7 +206,7 @@ If we try to create a snippet without authenticating, we'll get an error:
We can make a successful request by including the username and password of one of the users we created earlier.
http POST -a tom:password http://127.0.0.1:8000/snippets/ code="print 789"
http -a tom:password POST http://127.0.0.1:8000/snippets/ code="print 789"
{
"id": 5,

View File

@ -44,8 +44,8 @@ Next we're going to replace the `SnippetList`, `SnippetDetail` and `SnippetHighl
snippet = self.get_object()
return Response(snippet.highlighted)
def pre_save(self, obj):
obj.owner = self.request.user
def perform_create(self, serializer):
serializer.save(owner=self.request.user)
This time we've used the `ModelViewSet` class in order to get the complete set of default read and write operations.

View File

@ -19,7 +19,7 @@ Create a new Django project named `tutorial`, then start a new app called `quick
pip install djangorestframework
# Set up a new project with a single application
django-admin.py startproject tutorial .
django-admin.py startproject tutorial . # Note the trailing '.' character
cd tutorial
django-admin.py startapp quickstart
cd ..

View File

@ -8,7 +8,7 @@ ______ _____ _____ _____ __
"""
__title__ = 'Django REST framework'
__version__ = '3.0.1'
__version__ = '3.0.2'
__author__ = 'Tom Christie'
__license__ = 'BSD 2-Clause'
__copyright__ = 'Copyright 2011-2014 Tom Christie'

View File

@ -5,8 +5,8 @@ In addition Django's built in 403 and 404 exceptions are handled.
(`django.http.Http404` and `django.core.exceptions.PermissionDenied`)
"""
from __future__ import unicode_literals
from django.utils import six
from django.utils.encoding import force_text
from django.utils.translation import ugettext_lazy as _
from django.utils.translation import ungettext_lazy
from rest_framework import status
@ -66,7 +66,7 @@ class ValidationError(APIException):
self.detail = _force_text_recursive(detail)
def __str__(self):
return str(self.detail)
return six.text_type(self.detail)
class ParseError(APIException):

View File

@ -184,8 +184,11 @@ class Field(object):
self.style = {} if style is None else style
self.allow_null = allow_null
if allow_null and self.default_empty_html is empty:
self.default_empty_html = None
if self.default_empty_html is not empty:
if not required:
self.default_empty_html = empty
elif default is not empty:
self.default_empty_html = default
if validators is not None:
self.validators = validators[:]
@ -557,6 +560,11 @@ class CharField(Field):
message = self.error_messages['min_length'].format(min_length=min_length)
self.validators.append(MinLengthValidator(min_length, message=message))
if self.allow_null and (not self.allow_blank) and (self.default is empty):
# HTML input cannot represent `None` values, so we need to
# forcibly coerce empty HTML values to `None` if `allow_null=True`.
self.default_empty_html = None
def run_validation(self, data=empty):
# Test for the empty string here so that it does not get validated,
# and so that subclasses do not need to handle it explicitly

View File

@ -79,16 +79,14 @@ class GenericAPIView(views.APIView):
'view': self
}
def get_serializer(self, instance=None, data=None, many=False, partial=False):
def get_serializer(self, *args, **kwargs):
"""
Return the serializer instance that should be used for validating and
deserializing input, and for serializing output.
"""
serializer_class = self.get_serializer_class()
context = self.get_serializer_context()
return serializer_class(
instance, data=data, many=many, partial=partial, context=context
)
kwargs['context'] = self.get_serializer_context()
return serializer_class(*args, **kwargs)
def get_pagination_serializer(self, page):
"""

View File

@ -1,13 +1,15 @@
from django.utils.encoding import smart_text
from rest_framework.fields import get_attribute, empty, Field
from rest_framework.reverse import reverse
from rest_framework.utils import html
# coding: utf-8
from __future__ import unicode_literals
from django.core.exceptions import ObjectDoesNotExist, ImproperlyConfigured
from django.core.urlresolvers import resolve, get_script_prefix, NoReverseMatch, Resolver404
from django.db.models.query import QuerySet
from django.utils import six
from django.utils.encoding import smart_text
from django.utils.six.moves.urllib import parse as urlparse
from django.utils.translation import ugettext_lazy as _
from rest_framework.fields import get_attribute, empty, Field
from rest_framework.reverse import reverse
from rest_framework.utils import html
class PKOnlyObject(object):
@ -103,8 +105,8 @@ class RelatedField(Field):
def choices(self):
return dict([
(
str(self.to_representation(item)),
str(item)
six.text_type(self.to_representation(item)),
six.text_type(item)
)
for item in self.queryset.all()
])
@ -364,8 +366,8 @@ class ManyRelatedField(Field):
]
return dict([
(
str(item_representation),
str(item) + ' - ' + str(item_representation)
six.text_type(item_representation),
six.text_type(item) + ' - ' + six.text_type(item_representation)
)
for item, item_representation in items_and_representations
])

View File

@ -435,12 +435,12 @@ class BrowsableAPIRenderer(BaseRenderer):
# serializer instance, rather than dynamically creating a new one.
if request.method == method and serializer is not None:
try:
data = request.data
kwargs = {'data': request.data}
except ParseError:
data = None
kwargs = {}
existing_serializer = serializer
else:
data = None
kwargs = {}
existing_serializer = None
with override_method(view, request, method) as request:
@ -460,11 +460,13 @@ class BrowsableAPIRenderer(BaseRenderer):
serializer = existing_serializer
else:
if method in ('PUT', 'PATCH'):
serializer = view.get_serializer(instance=instance, data=data)
serializer = view.get_serializer(instance=instance, **kwargs)
else:
serializer = view.get_serializer(data=data)
if data is not None:
serializer.is_valid()
serializer = view.get_serializer(**kwargs)
if hasattr(serializer, 'initial_data'):
serializer.is_valid()
form_renderer = self.form_renderer_class()
return form_renderer.render(
serializer.data,

View File

@ -277,8 +277,12 @@ class Request(object):
Sets the user on the current request. This is necessary to maintain
compatibility with django.contrib.auth where the user property is
set in the login and logout functions.
Note that we also set the user on Django's underlying `HttpRequest`
instance, ensuring that it is available to any middleware in the stack.
"""
self._user = value
self._request.user = value
@property
def auth(self):
@ -297,6 +301,7 @@ class Request(object):
request, such as an authentication token.
"""
self._auth = value
self._request.auth = value
@property
def successful_authenticator(self):
@ -456,7 +461,7 @@ class Request(object):
if user_auth_tuple is not None:
self._authenticator = authenticator
self._user, self._auth = user_auth_tuple
self.user, self.auth = user_auth_tuple
return
self._not_authenticated()
@ -471,14 +476,14 @@ class Request(object):
self._authenticator = None
if api_settings.UNAUTHENTICATED_USER:
self._user = api_settings.UNAUTHENTICATED_USER()
self.user = api_settings.UNAUTHENTICATED_USER()
else:
self._user = None
self.user = None
if api_settings.UNAUTHENTICATED_TOKEN:
self._auth = api_settings.UNAUTHENTICATED_TOKEN()
self.auth = api_settings.UNAUTHENTICATED_TOKEN()
else:
self._auth = None
self.auth = None
def __getattr__(self, attr):
"""

View File

@ -58,11 +58,31 @@ class BaseSerializer(Field):
"""
The BaseSerializer class provides a minimal class which may be used
for writing custom serializer implementations.
Note that we strongly restrict the ordering of operations/properties
that may be used on the serializer in order to enforce correct usage.
In particular, if a `data=` argument is passed then:
.is_valid() - Available.
.initial_data - Available.
.validated_data - Only available after calling `is_valid()`
.errors - Only available after calling `is_valid()`
.data - Only available after calling `is_valid()`
If a `data=` argument is not passed then:
.is_valid() - Not available.
.initial_data - Not available.
.validated_data - Not available.
.errors - Not available.
.data - Available.
"""
def __init__(self, instance=None, data=None, **kwargs):
def __init__(self, instance=None, data=empty, **kwargs):
self.instance = instance
self._initial_data = data
if data is not empty:
self.initial_data = data
self.partial = kwargs.pop('partial', False)
self._context = kwargs.pop('context', {})
kwargs.pop('many', None)
@ -156,9 +176,14 @@ class BaseSerializer(Field):
(self.__class__.__module__, self.__class__.__name__)
)
assert hasattr(self, 'initial_data'), (
'Cannot call `.is_valid()` as no `data=` keyword argument was'
'passed when instantiating the serializer instance.'
)
if not hasattr(self, '_validated_data'):
try:
self._validated_data = self.run_validation(self._initial_data)
self._validated_data = self.run_validation(self.initial_data)
except ValidationError as exc:
self._validated_data = {}
self._errors = exc.detail
@ -172,6 +197,16 @@ class BaseSerializer(Field):
@property
def data(self):
if hasattr(self, 'initial_data') and not hasattr(self, '_validated_data'):
msg = (
'When a serializer is passed a `data` keyword argument you '
'must call `.is_valid()` before attempting to access the '
'serialized `.data` representation.\n'
'You should either call `.is_valid()` first, '
'or access `.initial_data` instead.'
)
raise AssertionError(msg)
if not hasattr(self, '_data'):
if self.instance is not None and not getattr(self, '_errors', None):
self._data = self.to_representation(self.instance)
@ -295,11 +330,11 @@ class Serializer(BaseSerializer):
return getattr(getattr(self, 'Meta', None), 'validators', [])
def get_initial(self):
if self._initial_data is not None:
if hasattr(self, 'initial_data'):
return OrderedDict([
(field_name, field.get_value(self._initial_data))
(field_name, field.get_value(self.initial_data))
for field_name, field in self.fields.items()
if field.get_value(self._initial_data) is not empty
if field.get_value(self.initial_data) is not empty
and not field.read_only
])
@ -447,8 +482,8 @@ class ListSerializer(BaseSerializer):
self.child.bind(field_name='', parent=self)
def get_initial(self):
if self._initial_data is not None:
return self.to_representation(self._initial_data)
if hasattr(self, 'initial_data'):
return self.to_representation(self.initial_data)
return []
def get_value(self, dictionary):

View File

@ -59,6 +59,9 @@ version = get_version('rest_framework')
if sys.argv[-1] == 'publish':
if os.system("pip freeze | grep wheel"):
print("wheel not installed.\nUse `pip install wheel`.\nExiting.")
sys.exit()
os.system("python setup.py sdist upload")
os.system("python setup.py bdist_wheel upload")
print("You probably want to also tag the version now:")

View File

@ -22,7 +22,7 @@ class TestSimpleBoundField:
amount = serializers.IntegerField()
serializer = ExampleSerializer(data={'text': 'abc', 'amount': 123})
assert serializer.is_valid()
assert serializer['text'].value == 'abc'
assert serializer['text'].errors is None
assert serializer['text'].name == 'text'

View File

@ -215,6 +215,48 @@ class TestBooleanHTMLInput:
assert serializer.validated_data == {'archived': False}
class MockHTMLDict(dict):
"""
This class mocks up a dictionary like object, that behaves
as if it was returned for multipart or urlencoded data.
"""
getlist = None
class TestCharHTMLInput:
def test_empty_html_checkbox(self):
class TestSerializer(serializers.Serializer):
message = serializers.CharField(default='happy')
serializer = TestSerializer(data=MockHTMLDict())
assert serializer.is_valid()
assert serializer.validated_data == {'message': 'happy'}
def test_empty_html_checkbox_allow_null(self):
class TestSerializer(serializers.Serializer):
message = serializers.CharField(allow_null=True)
serializer = TestSerializer(data=MockHTMLDict())
assert serializer.is_valid()
assert serializer.validated_data == {'message': None}
def test_empty_html_checkbox_allow_null_allow_blank(self):
class TestSerializer(serializers.Serializer):
message = serializers.CharField(allow_null=True, allow_blank=True)
serializer = TestSerializer(data=MockHTMLDict({}))
assert serializer.is_valid()
assert serializer.validated_data == {'message': ''}
def test_empty_html_required_false(self):
class TestSerializer(serializers.Serializer):
message = serializers.CharField(required=False)
serializer = TestSerializer(data=MockHTMLDict())
assert serializer.is_valid()
assert serializer.validated_data == {}
class TestCreateOnlyDefault:
def setup(self):
default = serializers.CreateOnlyDefault('2001-01-01')

37
tests/test_middleware.py Normal file
View File

@ -0,0 +1,37 @@
from django.conf.urls import patterns, url
from django.contrib.auth.models import User
from rest_framework.authentication import TokenAuthentication
from rest_framework.authtoken.models import Token
from rest_framework.test import APITestCase
from rest_framework.views import APIView
urlpatterns = patterns(
'',
url(r'^$', APIView.as_view(authentication_classes=(TokenAuthentication,))),
)
class MyMiddleware(object):
def process_response(self, request, response):
assert hasattr(request, 'user'), '`user` is not set on request'
assert request.user.is_authenticated(), '`user` is not authenticated'
return response
class TestMiddleware(APITestCase):
urls = 'tests.test_middleware'
def test_middleware_can_access_user_when_processing_response(self):
user = User.objects.create_user('john', 'john@example.com', 'password')
key = 'abcd1234'
Token.objects.create(key=key, user=user)
with self.settings(
MIDDLEWARE_CLASSES=('tests.test_middleware.MyMiddleware',)
):
auth = 'Token ' + key
self.client.get('/', HTTP_AUTHORIZATION=auth)

View File

@ -224,7 +224,8 @@ class TestUserSetter(TestCase):
def setUp(self):
# Pass request object through session middleware so session is
# available to login and logout functions
self.request = Request(factory.get('/'))
self.wrapped_request = factory.get('/')
self.request = Request(self.wrapped_request)
SessionMiddleware().process_request(self.request)
User.objects.create_user('ringo', 'starr@thebeatles.com', 'yellow')
@ -244,6 +245,10 @@ class TestUserSetter(TestCase):
logout(self.request)
self.assertTrue(self.request.user.is_anonymous())
def test_logged_in_user_is_set_on_wrapped_request(self):
login(self.request, self.user)
self.assertEqual(self.wrapped_request.user, self.user)
class TestAuthSetter(TestCase):