Merge remote-tracking branch 'reference/master' into p3k

This commit is contained in:
Xavier Ordoquy 2012-12-02 12:43:32 +01:00
commit 5fad46d7e2
17 changed files with 123 additions and 62 deletions

View File

@ -58,6 +58,29 @@ To run the tests.
# Changelog
## 2.1.6
**Date**: 23rd Nov 2012
* Bugfix: Unfix DjangoModelPermissions. (I am a doofus.)
## 2.1.5
**Date**: 23rd Nov 2012
* Bugfix: Fix DjangoModelPermissions.
## 2.1.4
**Date**: 22nd Nov 2012
* Support for partial updates with serializers.
* Added `RegexField`.
* Added `SerializerMethodField`.
* Serializer performance improvements.
* Added `obtain_token_view` to get tokens when using `TokenAuthentication`.
* Bugfix: Django 1.5 configurable user support for `TokenAuthentication`.
## 2.1.3
**Date**: 16th Nov 2012

View File

@ -116,7 +116,7 @@ When using `TokenAuthentication`, you may want to provide a mechanism for client
REST framework provides a built-in view to provide this behavior. To use it, add the `obtain_auth_token` view to your URLconf:
urlpatterns += patterns('',
url(r'^api-token-auth/', 'rest_framework.authtoken.obtain_auth_token')
url(r'^api-token-auth/', 'rest_framework.authtoken.views.obtain_auth_token')
)
Note that the URL part of the pattern can be whatever you want to use.

View File

@ -77,6 +77,10 @@ When deserializing data, we can either create a new instance, or update an exist
serializer = CommentSerializer(data=data) # Create new instance
serializer = CommentSerializer(comment, data=data) # Update `instance`
By default, serializers must be passed values for all required fields or they will throw validation errors. You can use the `partial` argument in order to allow partial updates.
serializer = CommentSerializer(comment, data={'content': u'foo bar'}, partial=True) # Update `instance` with partial data
## Validation
When deserializing data, you always need to call `is_valid()` before attempting to access the deserialized object. If any validation errors occur, the `.errors` and `.non_field_errors` properties will contain the resulting error messages.

View File

@ -19,6 +19,10 @@ Using the `APIView` class is pretty much the same as using a regular `View` clas
For example:
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import authentication, permissions
class ListUsers(APIView):
"""
View to list all users in the system.

View File

@ -64,6 +64,11 @@ The following people have helped make REST framework great.
* Eugene Mechanism - [mechanism]
* Jonas Liljestrand - [jonlil]
* Justin Davis - [irrelative]
* Dustin Bachrach - [dbachrach]
* Mark Shirley - [maspwr]
* Olivier Aubert - [oaubert]
* Yuri Prezument - [yprez]
* Fabian Buechler - [fabianbuechler]
Many thanks to everyone who's contributed to the project.
@ -163,3 +168,8 @@ To contact the author directly:
[mechanism]: https://github.com/mechanism
[jonlil]: https://github.com/jonlil
[irrelative]: https://github.com/irrelative
[dbachrach]: https://github.com/dbachrach
[maspwr]: https://github.com/maspwr
[oaubert]: https://github.com/oaubert
[yprez]: https://github.com/yprez
[fabianbuechler]: https://github.com/fabianbuechler

View File

@ -4,8 +4,23 @@
>
> — Eric S. Raymond, [The Cathedral and the Bazaar][cite].
## Master
## 2.1.6
**Date**: 23rd Nov 2012
* Bugfix: Unfix DjangoModelPermissions. (I am a doofus.)
## 2.1.5
**Date**: 23rd Nov 2012
* Bugfix: Fix DjangoModelPermissions.
## 2.1.4
**Date**: 22nd Nov 2012
* Support for partial updates with serializers.
* Added `RegexField`.
* Added `SerializerMethodField`.
* Serializer performance improvements.

View File

@ -41,8 +41,8 @@ We don't need our `JSONResponse` class anymore, so go ahead and delete that. On
from rest_framework import status
from rest_framework.decorators import api_view
from rest_framework.response import Response
from snippet.models import Snippet
from snippet.serializers import SnippetSerializer
from snippets.models import Snippet
from snippets.serializers import SnippetSerializer
@api_view(['GET', 'POST'])
@ -113,7 +113,7 @@ Now update the `urls.py` file slightly, to append a set of `format_suffix_patter
from django.conf.urls import patterns, url
from rest_framework.urlpatterns import format_suffix_patterns
urlpatterns = patterns('snippet.views',
urlpatterns = patterns('snippets.views',
url(r'^snippets/$', 'snippet_list'),
url(r'^snippets/(?P<pk>[0-9]+)$', 'snippet_detail')
)

View File

@ -6,8 +6,8 @@ We can also write our API views using class based views, rather than function ba
We'll start by rewriting the root view as a class based view. All this involves is a little bit of refactoring.
from snippet.models import Snippet
from snippet.serializers import SnippetSerializer
from snippets.models import Snippet
from snippets.serializers import SnippetSerializer
from django.http import Http404
from rest_framework.views import APIView
from rest_framework.response import Response
@ -66,7 +66,7 @@ We'll also need to refactor our URLconf slightly now we're using class based vie
from django.conf.urls import patterns, url
from rest_framework.urlpatterns import format_suffix_patterns
from snippetpost import views
from snippets import views
urlpatterns = patterns('',
url(r'^snippets/$', views.SnippetList.as_view()),
@ -85,8 +85,8 @@ The create/retrieve/update/delete operations that we've been using so far are go
Let's take a look at how we can compose our views by using the mixin classes.
from snippet.models import Snippet
from snippet.serializers import SnippetSerializer
from snippets.models import Snippet
from snippets.serializers import SnippetSerializer
from rest_framework import mixins
from rest_framework import generics
@ -128,8 +128,8 @@ Pretty similar. This time we're using the `SingleObjectBaseView` class to provi
Using the mixin classes we've rewritten the views to use slightly less code than before, but we can go one step further. REST framework provides a set of already mixed-in generic views that we can use.
from snippet.models import Snippet
from snippet.serializers import SnippetSerializer
from snippets.models import Snippet
from snippets.serializers import SnippetSerializer
from rest_framework import generics

View File

@ -1,3 +1,3 @@
markdown>=2.1.0
PyYAML>=3.10
-e git+https://github.com/alex/django-filter.git@0e4b3d703b31574922ab86fc78a86164aad0c1d0#egg=django-filter
django-filter>=0.5.4

View File

@ -1,3 +1,3 @@
__version__ = '2.1.3'
__version__ = '2.1.6'
VERSION = __version__ # synonym

View File

@ -18,7 +18,7 @@ class ObtainAuthToken(APIView):
if serializer.is_valid():
token, created = Token.objects.get_or_create(user=serializer.object['user'])
return Response({'token': token.key})
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
return Response(serializer.errors, status=status.HTTP_401_UNAUTHORIZED)
obtain_auth_token = ObtainAuthToken.as_view()

View File

@ -12,6 +12,7 @@ from django.core import validators
from django.core.exceptions import ObjectDoesNotExist, ValidationError
from django.core.urlresolvers import resolve, get_script_prefix
from django.conf import settings
from django import forms
from django.forms import widgets
from django.forms.models import ModelChoiceIterator
from django.utils.encoding import is_protected_type
@ -45,6 +46,7 @@ class Field(object):
empty = ''
type_name = None
_use_files = None
form_field_class = forms.CharField
def __init__(self, source=None):
self.parent = None
@ -64,6 +66,8 @@ class Field(object):
self.parent = parent
self.root = parent.root or parent
self.context = self.root.context
if self.root.partial:
self.required = False
def field_from_native(self, data, files, field_name, into):
"""
@ -231,7 +235,7 @@ class ModelField(WritableField):
getattr(self.model_field, 'min_length', None))
self.max_length = kwargs.pop('max_length',
getattr(self.model_field, 'max_length', None))
super(ModelField, self).__init__(*args, **kwargs)
if self.min_length is not None:
@ -402,6 +406,7 @@ class PrimaryKeyRelatedField(RelatedField):
Represents a to-one relationship as a pk value.
"""
default_read_only = False
form_field_class = forms.ChoiceField
# TODO: Remove these field hacks...
def prepare_value(self, obj):
@ -448,6 +453,7 @@ class ManyPrimaryKeyRelatedField(ManyRelatedField):
Represents a to-many relationship as a pk value.
"""
default_read_only = False
form_field_class = forms.MultipleChoiceField
def prepare_value(self, obj):
return self.to_native(obj.pk)
@ -491,6 +497,7 @@ class ManyPrimaryKeyRelatedField(ManyRelatedField):
class SlugRelatedField(RelatedField):
default_read_only = False
form_field_class = forms.ChoiceField
def __init__(self, *args, **kwargs):
self.slug_field = kwargs.pop('slug_field', None)
@ -512,7 +519,7 @@ class SlugRelatedField(RelatedField):
class ManySlugRelatedField(ManyRelatedMixin, SlugRelatedField):
pass
form_field_class = forms.MultipleChoiceField
### Hyperlinked relationships
@ -525,6 +532,7 @@ class HyperlinkedRelatedField(RelatedField):
slug_field = 'slug'
slug_url_kwarg = None # Defaults to same as `slug_field` unless overridden
default_read_only = False
form_field_class = forms.ChoiceField
def __init__(self, *args, **kwargs):
try:
@ -624,7 +632,7 @@ class ManyHyperlinkedRelatedField(ManyRelatedMixin, HyperlinkedRelatedField):
"""
Represents a to-many relationship, using hyperlinking.
"""
pass
form_field_class = forms.MultipleChoiceField
class HyperlinkedIdentityField(Field):
@ -682,6 +690,7 @@ class HyperlinkedIdentityField(Field):
class BooleanField(WritableField):
type_name = 'BooleanField'
form_field_class = forms.BooleanField
widget = widgets.CheckboxInput
default_error_messages = {
'invalid': _("'%s' value must be either True or False."),
@ -703,6 +712,7 @@ class BooleanField(WritableField):
class CharField(WritableField):
type_name = 'CharField'
form_field_class = forms.CharField
def __init__(self, max_length=None, min_length=None, *args, **kwargs):
self.max_length, self.min_length = max_length, min_length
@ -747,6 +757,7 @@ class SlugField(CharField):
class ChoiceField(WritableField):
type_name = 'ChoiceField'
form_field_class = forms.ChoiceField
widget = widgets.Select
default_error_messages = {
'invalid_choice': _('Select a valid choice. %(value)s is not one of the available choices.'),
@ -793,6 +804,7 @@ class ChoiceField(WritableField):
class EmailField(CharField):
type_name = 'EmailField'
form_field_class = forms.EmailField
default_error_messages = {
'invalid': _('Enter a valid e-mail address.'),
@ -843,6 +855,8 @@ class RegexField(CharField):
class DateField(WritableField):
type_name = 'DateField'
widget = widgets.DateInput
form_field_class = forms.DateField
default_error_messages = {
'invalid': _("'%s' value has an invalid date format. It must be "
@ -880,6 +894,8 @@ class DateField(WritableField):
class DateTimeField(WritableField):
type_name = 'DateTimeField'
widget = widgets.DateTimeInput
form_field_class = forms.DateTimeField
default_error_messages = {
'invalid': _("'%s' value has an invalid format. It must be in "
@ -934,6 +950,7 @@ class DateTimeField(WritableField):
class IntegerField(WritableField):
type_name = 'IntegerField'
form_field_class = forms.IntegerField
default_error_messages = {
'invalid': _('Enter a whole number.'),
@ -963,6 +980,7 @@ class IntegerField(WritableField):
class FloatField(WritableField):
type_name = 'FloatField'
form_field_class = forms.FloatField
default_error_messages = {
'invalid': _("'%s' value must be a float."),
@ -982,6 +1000,7 @@ class FloatField(WritableField):
class FileField(WritableField):
_use_files = True
type_name = 'FileField'
form_field_class = forms.FileField
widget = widgets.FileInput
default_error_messages = {
@ -1024,6 +1043,7 @@ class FileField(WritableField):
class ImageField(FileField):
_use_files = True
form_field_class = forms.ImageField
default_error_messages = {
'invalid_image': _("Upload a valid image. The file you uploaded was either not an image or a corrupted image."),

View File

@ -2,6 +2,7 @@ from django.http import Http404
from rest_framework import exceptions
from rest_framework.settings import api_settings
from rest_framework.utils.mediatypes import order_by_precedence, media_type_matches
from rest_framework.utils.mediatypes import _MediaType
class BaseContentNegotiation(object):
@ -48,7 +49,8 @@ class DefaultContentNegotiation(BaseContentNegotiation):
for media_type in media_type_set:
if media_type_matches(renderer.media_type, media_type):
# Return the most specific media type as accepted.
if len(renderer.media_type) > len(media_type):
if (_MediaType(renderer.media_type).precedence >
_MediaType(media_type).precedence):
# Eg client requests '*/*'
# Accepted media type is 'application/json'
return renderer, renderer.media_type

View File

@ -308,26 +308,6 @@ class BrowsableAPIRenderer(BaseRenderer):
return True
def serializer_to_form_fields(self, serializer):
field_mapping = {
serializers.FloatField: forms.FloatField,
serializers.IntegerField: forms.IntegerField,
serializers.DateTimeField: forms.DateTimeField,
serializers.DateField: forms.DateField,
serializers.EmailField: forms.EmailField,
serializers.RegexField: forms.RegexField,
serializers.CharField: forms.CharField,
serializers.ChoiceField: forms.ChoiceField,
serializers.BooleanField: forms.BooleanField,
serializers.PrimaryKeyRelatedField: forms.ChoiceField,
serializers.ManyPrimaryKeyRelatedField: forms.MultipleChoiceField,
serializers.SlugRelatedField: forms.ChoiceField,
serializers.ManySlugRelatedField: forms.MultipleChoiceField,
serializers.HyperlinkedRelatedField: forms.ChoiceField,
serializers.ManyHyperlinkedRelatedField: forms.MultipleChoiceField,
serializers.FileField: forms.FileField,
serializers.ImageField: forms.ImageField,
}
fields = {}
for k, v in serializer.get_fields().items():
if getattr(v, 'read_only', True):
@ -351,13 +331,7 @@ class BrowsableAPIRenderer(BaseRenderer):
kwargs['label'] = k
try:
fields[k] = field_mapping[v.__class__](**kwargs)
except KeyError:
if getattr(v, 'choices', None) is not None:
fields[k] = forms.ChoiceField(**kwargs)
else:
fields[k] = forms.CharField(**kwargs)
fields[k] = v.form_field_class(**kwargs)
return fields
def get_form(self, view, method, request):

View File

@ -62,7 +62,7 @@ def _get_declared_fields(bases, attrs):
# If this class is subclassing another Serializer, add that Serializer's
# fields. Note that we loop over the bases in *reverse*. This is necessary
# in order to the correct order of fields.
# in order to maintain the correct order of fields.
for base in bases[::-1]:
if hasattr(base, 'base_fields'):
fields = list(base.base_fields.items()) + fields
@ -93,19 +93,19 @@ class BaseSerializer(Field):
_options_class = SerializerOptions
_dict_class = SortedDictWithMetadata # Set to unsorted dict for backwards compatibility with unsorted implementations.
def __init__(self, instance=None, data=None, files=None, context=None, **kwargs):
def __init__(self, instance=None, data=None, files=None, context=None, partial=False, **kwargs):
super(BaseSerializer, self).__init__(**kwargs)
self.opts = self._options_class(self.Meta)
self.fields = copy.deepcopy(self.base_fields)
self.parent = None
self.root = None
self.partial = partial
self.context = context or {}
self.init_data = data
self.init_files = files
self.object = instance
self.default_fields = self.get_default_fields()
self.fields = self.get_fields()
self._data = None
self._files = None
@ -130,13 +130,15 @@ class BaseSerializer(Field):
ret = SortedDict()
# Get the explicitly declared fields
for key, field in self.fields.items():
base_fields = copy.deepcopy(self.base_fields)
for key, field in base_fields.items():
ret[key] = field
# Set up the field
field.initialize(parent=self, field_name=key)
# Add in the default fields
for key, val in self.default_fields.items():
default_fields = self.get_default_fields()
for key, val in default_fields.items():
if key not in ret:
ret[key] = val
@ -183,8 +185,7 @@ class BaseSerializer(Field):
ret = self._dict_class()
ret.fields = {}
fields = self.get_fields()
for field_name, field in fields.items():
for field_name, field in self.fields.items():
key = self.get_field_key(field_name)
value = field.field_to_native(obj, field_name)
ret[key] = value
@ -196,9 +197,8 @@ class BaseSerializer(Field):
Core of deserialization, together with `restore_object`.
Converts a dictionary of data into a dictionary of deserialized fields.
"""
fields = self.get_fields()
reverted_data = {}
for field_name, field in fields.items():
for field_name, field in self.fields.items():
try:
field.field_from_native(data, files, field_name, reverted_data)
except ValidationError as err:
@ -210,10 +210,7 @@ class BaseSerializer(Field):
"""
Run `validate_<fieldname>()` and `validate()` methods on the serializer
"""
# TODO: refactor this so we're not determining the fields again
fields = self.get_fields()
for field_name, field in fields.items():
for field_name, field in self.fields.items():
try:
validate_method = getattr(self, 'validate_%s' % field_name, None)
if validate_method:

View File

@ -167,14 +167,14 @@ class TokenAuthTests(TestCase):
client = Client(enforce_csrf_checks=True)
response = client.post('/auth-token/login/',
json.dumps({'username': self.username, 'password': "badpass"}), 'application/json')
self.assertEqual(response.status_code, 400)
self.assertEqual(response.status_code, 401)
def test_token_login_json_missing_fields(self):
"""Ensure token login view using JSON POST fails if missing fields."""
client = Client(enforce_csrf_checks=True)
response = client.post('/auth-token/login/',
json.dumps({'username': self.username}), 'application/json')
self.assertEqual(response.status_code, 400)
self.assertEqual(response.status_code, 401)
def test_token_login_form(self):
"""Ensure token login view using form POST works."""

View File

@ -117,6 +117,18 @@ class BasicTests(TestCase):
self.assertTrue(serializer.object is expected)
self.assertEquals(serializer.data['sub_comment'], 'And Merry Christmas!')
def test_partial_update(self):
msg = 'Merry New Year!'
partial_data = {'content': msg}
serializer = CommentSerializer(self.comment, data=partial_data)
self.assertEquals(serializer.is_valid(), False)
serializer = CommentSerializer(self.comment, data=partial_data, partial=True)
expected = self.comment
self.assertEqual(serializer.is_valid(), True)
self.assertEquals(serializer.object, expected)
self.assertTrue(serializer.object is expected)
self.assertEquals(serializer.data['content'], msg)
def test_model_fields_as_expected(self):
"""
Make sure that the fields returned are the same as defined