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 # 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 ## 2.1.3
**Date**: 16th Nov 2012 **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: 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('', 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. 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(data=data) # Create new instance
serializer = CommentSerializer(comment, data=data) # Update `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 ## 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. 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: For example:
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import authentication, permissions
class ListUsers(APIView): class ListUsers(APIView):
""" """
View to list all users in the system. 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] * Eugene Mechanism - [mechanism]
* Jonas Liljestrand - [jonlil] * Jonas Liljestrand - [jonlil]
* Justin Davis - [irrelative] * 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. Many thanks to everyone who's contributed to the project.
@ -163,3 +168,8 @@ To contact the author directly:
[mechanism]: https://github.com/mechanism [mechanism]: https://github.com/mechanism
[jonlil]: https://github.com/jonlil [jonlil]: https://github.com/jonlil
[irrelative]: https://github.com/irrelative [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]. > — 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 `RegexField`.
* Added `SerializerMethodField`. * Added `SerializerMethodField`.
* Serializer performance improvements. * 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 import status
from rest_framework.decorators import api_view from rest_framework.decorators import api_view
from rest_framework.response import Response from rest_framework.response import Response
from snippet.models import Snippet from snippets.models import Snippet
from snippet.serializers import SnippetSerializer from snippets.serializers import SnippetSerializer
@api_view(['GET', 'POST']) @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 django.conf.urls import patterns, url
from rest_framework.urlpatterns import format_suffix_patterns from rest_framework.urlpatterns import format_suffix_patterns
urlpatterns = patterns('snippet.views', urlpatterns = patterns('snippets.views',
url(r'^snippets/$', 'snippet_list'), url(r'^snippets/$', 'snippet_list'),
url(r'^snippets/(?P<pk>[0-9]+)$', 'snippet_detail') 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. 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 snippets.models import Snippet
from snippet.serializers import SnippetSerializer from snippets.serializers import SnippetSerializer
from django.http import Http404 from django.http import Http404
from rest_framework.views import APIView from rest_framework.views import APIView
from rest_framework.response import Response 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 django.conf.urls import patterns, url
from rest_framework.urlpatterns import format_suffix_patterns from rest_framework.urlpatterns import format_suffix_patterns
from snippetpost import views from snippets import views
urlpatterns = patterns('', urlpatterns = patterns('',
url(r'^snippets/$', views.SnippetList.as_view()), 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. Let's take a look at how we can compose our views by using the mixin classes.
from snippet.models import Snippet from snippets.models import Snippet
from snippet.serializers import SnippetSerializer from snippets.serializers import SnippetSerializer
from rest_framework import mixins from rest_framework import mixins
from rest_framework import generics 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. 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 snippets.models import Snippet
from snippet.serializers import SnippetSerializer from snippets.serializers import SnippetSerializer
from rest_framework import generics from rest_framework import generics

View File

@ -1,3 +1,3 @@
markdown>=2.1.0 markdown>=2.1.0
PyYAML>=3.10 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 VERSION = __version__ # synonym

View File

@ -18,7 +18,7 @@ class ObtainAuthToken(APIView):
if serializer.is_valid(): if serializer.is_valid():
token, created = Token.objects.get_or_create(user=serializer.object['user']) token, created = Token.objects.get_or_create(user=serializer.object['user'])
return Response({'token': token.key}) 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() 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.exceptions import ObjectDoesNotExist, ValidationError
from django.core.urlresolvers import resolve, get_script_prefix from django.core.urlresolvers import resolve, get_script_prefix
from django.conf import settings from django.conf import settings
from django import forms
from django.forms import widgets from django.forms import widgets
from django.forms.models import ModelChoiceIterator from django.forms.models import ModelChoiceIterator
from django.utils.encoding import is_protected_type from django.utils.encoding import is_protected_type
@ -45,6 +46,7 @@ class Field(object):
empty = '' empty = ''
type_name = None type_name = None
_use_files = None _use_files = None
form_field_class = forms.CharField
def __init__(self, source=None): def __init__(self, source=None):
self.parent = None self.parent = None
@ -64,6 +66,8 @@ class Field(object):
self.parent = parent self.parent = parent
self.root = parent.root or parent self.root = parent.root or parent
self.context = self.root.context self.context = self.root.context
if self.root.partial:
self.required = False
def field_from_native(self, data, files, field_name, into): def field_from_native(self, data, files, field_name, into):
""" """
@ -402,6 +406,7 @@ class PrimaryKeyRelatedField(RelatedField):
Represents a to-one relationship as a pk value. Represents a to-one relationship as a pk value.
""" """
default_read_only = False default_read_only = False
form_field_class = forms.ChoiceField
# TODO: Remove these field hacks... # TODO: Remove these field hacks...
def prepare_value(self, obj): def prepare_value(self, obj):
@ -448,6 +453,7 @@ class ManyPrimaryKeyRelatedField(ManyRelatedField):
Represents a to-many relationship as a pk value. Represents a to-many relationship as a pk value.
""" """
default_read_only = False default_read_only = False
form_field_class = forms.MultipleChoiceField
def prepare_value(self, obj): def prepare_value(self, obj):
return self.to_native(obj.pk) return self.to_native(obj.pk)
@ -491,6 +497,7 @@ class ManyPrimaryKeyRelatedField(ManyRelatedField):
class SlugRelatedField(RelatedField): class SlugRelatedField(RelatedField):
default_read_only = False default_read_only = False
form_field_class = forms.ChoiceField
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
self.slug_field = kwargs.pop('slug_field', None) self.slug_field = kwargs.pop('slug_field', None)
@ -512,7 +519,7 @@ class SlugRelatedField(RelatedField):
class ManySlugRelatedField(ManyRelatedMixin, SlugRelatedField): class ManySlugRelatedField(ManyRelatedMixin, SlugRelatedField):
pass form_field_class = forms.MultipleChoiceField
### Hyperlinked relationships ### Hyperlinked relationships
@ -525,6 +532,7 @@ class HyperlinkedRelatedField(RelatedField):
slug_field = 'slug' slug_field = 'slug'
slug_url_kwarg = None # Defaults to same as `slug_field` unless overridden slug_url_kwarg = None # Defaults to same as `slug_field` unless overridden
default_read_only = False default_read_only = False
form_field_class = forms.ChoiceField
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
try: try:
@ -624,7 +632,7 @@ class ManyHyperlinkedRelatedField(ManyRelatedMixin, HyperlinkedRelatedField):
""" """
Represents a to-many relationship, using hyperlinking. Represents a to-many relationship, using hyperlinking.
""" """
pass form_field_class = forms.MultipleChoiceField
class HyperlinkedIdentityField(Field): class HyperlinkedIdentityField(Field):
@ -682,6 +690,7 @@ class HyperlinkedIdentityField(Field):
class BooleanField(WritableField): class BooleanField(WritableField):
type_name = 'BooleanField' type_name = 'BooleanField'
form_field_class = forms.BooleanField
widget = widgets.CheckboxInput widget = widgets.CheckboxInput
default_error_messages = { default_error_messages = {
'invalid': _("'%s' value must be either True or False."), 'invalid': _("'%s' value must be either True or False."),
@ -703,6 +712,7 @@ class BooleanField(WritableField):
class CharField(WritableField): class CharField(WritableField):
type_name = 'CharField' type_name = 'CharField'
form_field_class = forms.CharField
def __init__(self, max_length=None, min_length=None, *args, **kwargs): def __init__(self, max_length=None, min_length=None, *args, **kwargs):
self.max_length, self.min_length = max_length, min_length self.max_length, self.min_length = max_length, min_length
@ -747,6 +757,7 @@ class SlugField(CharField):
class ChoiceField(WritableField): class ChoiceField(WritableField):
type_name = 'ChoiceField' type_name = 'ChoiceField'
form_field_class = forms.ChoiceField
widget = widgets.Select widget = widgets.Select
default_error_messages = { default_error_messages = {
'invalid_choice': _('Select a valid choice. %(value)s is not one of the available choices.'), '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): class EmailField(CharField):
type_name = 'EmailField' type_name = 'EmailField'
form_field_class = forms.EmailField
default_error_messages = { default_error_messages = {
'invalid': _('Enter a valid e-mail address.'), 'invalid': _('Enter a valid e-mail address.'),
@ -843,6 +855,8 @@ class RegexField(CharField):
class DateField(WritableField): class DateField(WritableField):
type_name = 'DateField' type_name = 'DateField'
widget = widgets.DateInput
form_field_class = forms.DateField
default_error_messages = { default_error_messages = {
'invalid': _("'%s' value has an invalid date format. It must be " 'invalid': _("'%s' value has an invalid date format. It must be "
@ -880,6 +894,8 @@ class DateField(WritableField):
class DateTimeField(WritableField): class DateTimeField(WritableField):
type_name = 'DateTimeField' type_name = 'DateTimeField'
widget = widgets.DateTimeInput
form_field_class = forms.DateTimeField
default_error_messages = { default_error_messages = {
'invalid': _("'%s' value has an invalid format. It must be in " 'invalid': _("'%s' value has an invalid format. It must be in "
@ -934,6 +950,7 @@ class DateTimeField(WritableField):
class IntegerField(WritableField): class IntegerField(WritableField):
type_name = 'IntegerField' type_name = 'IntegerField'
form_field_class = forms.IntegerField
default_error_messages = { default_error_messages = {
'invalid': _('Enter a whole number.'), 'invalid': _('Enter a whole number.'),
@ -963,6 +980,7 @@ class IntegerField(WritableField):
class FloatField(WritableField): class FloatField(WritableField):
type_name = 'FloatField' type_name = 'FloatField'
form_field_class = forms.FloatField
default_error_messages = { default_error_messages = {
'invalid': _("'%s' value must be a float."), 'invalid': _("'%s' value must be a float."),
@ -982,6 +1000,7 @@ class FloatField(WritableField):
class FileField(WritableField): class FileField(WritableField):
_use_files = True _use_files = True
type_name = 'FileField' type_name = 'FileField'
form_field_class = forms.FileField
widget = widgets.FileInput widget = widgets.FileInput
default_error_messages = { default_error_messages = {
@ -1024,6 +1043,7 @@ class FileField(WritableField):
class ImageField(FileField): class ImageField(FileField):
_use_files = True _use_files = True
form_field_class = forms.ImageField
default_error_messages = { default_error_messages = {
'invalid_image': _("Upload a valid image. The file you uploaded was either not an image or a corrupted image."), '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 import exceptions
from rest_framework.settings import api_settings 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 order_by_precedence, media_type_matches
from rest_framework.utils.mediatypes import _MediaType
class BaseContentNegotiation(object): class BaseContentNegotiation(object):
@ -48,7 +49,8 @@ class DefaultContentNegotiation(BaseContentNegotiation):
for media_type in media_type_set: for media_type in media_type_set:
if media_type_matches(renderer.media_type, media_type): if media_type_matches(renderer.media_type, media_type):
# Return the most specific media type as accepted. # 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 '*/*' # Eg client requests '*/*'
# Accepted media type is 'application/json' # Accepted media type is 'application/json'
return renderer, renderer.media_type return renderer, renderer.media_type

View File

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

View File

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

View File

@ -117,6 +117,18 @@ class BasicTests(TestCase):
self.assertTrue(serializer.object is expected) self.assertTrue(serializer.object is expected)
self.assertEquals(serializer.data['sub_comment'], 'And Merry Christmas!') 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): def test_model_fields_as_expected(self):
""" """
Make sure that the fields returned are the same as defined Make sure that the fields returned are the same as defined