diff options
Diffstat (limited to 'rest_framework')
| -rw-r--r-- | rest_framework/__init__.py | 18 | ||||
| -rw-r--r-- | rest_framework/compat.py | 7 | ||||
| -rw-r--r-- | rest_framework/fields.py | 9 | ||||
| -rw-r--r-- | rest_framework/generics.py | 12 | ||||
| -rw-r--r-- | rest_framework/mixins.py | 10 | ||||
| -rw-r--r-- | rest_framework/renderers.py | 9 | ||||
| -rw-r--r-- | rest_framework/request.py | 11 | ||||
| -rw-r--r-- | rest_framework/response.py | 4 | ||||
| -rw-r--r-- | rest_framework/routers.py | 14 | ||||
| -rw-r--r-- | rest_framework/serializers.py | 22 | ||||
| -rw-r--r-- | rest_framework/status.py | 17 | ||||
| -rw-r--r-- | rest_framework/tests/test_fields.py | 26 | ||||
| -rw-r--r-- | rest_framework/tests/test_generics.py | 60 | ||||
| -rw-r--r-- | rest_framework/tests/test_renderers.py | 72 | ||||
| -rw-r--r-- | rest_framework/tests/test_serializer.py | 62 | ||||
| -rw-r--r-- | rest_framework/tests/test_serializer_nested.py | 35 | ||||
| -rw-r--r-- | rest_framework/tests/test_status.py | 33 | ||||
| -rw-r--r-- | rest_framework/urlpatterns.py | 2 | ||||
| -rw-r--r-- | rest_framework/utils/encoders.py | 5 |
19 files changed, 382 insertions, 46 deletions
diff --git a/rest_framework/__init__.py b/rest_framework/__init__.py index 2bd2991b..f5483b9d 100644 --- a/rest_framework/__init__.py +++ b/rest_framework/__init__.py @@ -1,6 +1,20 @@ -__version__ = '2.3.8' +""" +______ _____ _____ _____ __ _ +| ___ \ ___/ ___|_ _| / _| | | +| |_/ / |__ \ `--. | | | |_ _ __ __ _ _ __ ___ _____ _____ _ __| | __ +| /| __| `--. \ | | | _| '__/ _` | '_ ` _ \ / _ \ \ /\ / / _ \| '__| |/ / +| |\ \| |___/\__/ / | | | | | | | (_| | | | | | | __/\ V V / (_) | | | < +\_| \_\____/\____/ \_/ |_| |_| \__,_|_| |_| |_|\___| \_/\_/ \___/|_| |_|\_| +""" -VERSION = __version__ # synonym +__title__ = 'Django REST framework' +__version__ = '2.3.10' +__author__ = 'Tom Christie' +__license__ = 'BSD 2-Clause' +__copyright__ = 'Copyright 2011-2013 Tom Christie' + +# Version synonym +VERSION = __version__ # Header encoding (see RFC5987) HTTP_HEADER_ENCODING = 'iso-8859-1' diff --git a/rest_framework/compat.py b/rest_framework/compat.py index 581e29fc..05bd99e0 100644 --- a/rest_framework/compat.py +++ b/rest_framework/compat.py @@ -69,6 +69,13 @@ try: except ImportError: import urlparse +# UserDict moves in Python 3 +try: + from UserDict import UserDict + from UserDict import DictMixin +except ImportError: + from collections import UserDict + from collections import MutableMapping as DictMixin # Try to import PIL in either of the two ways it can end up installed. try: diff --git a/rest_framework/fields.py b/rest_framework/fields.py index 6c07dbb3..f1de447c 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -428,7 +428,7 @@ class BooleanField(WritableField): def field_from_native(self, data, files, field_name, into): # HTML checkboxes do not explicitly represent unchecked as `False` # we deal with that here... - if isinstance(data, QueryDict): + if isinstance(data, QueryDict) and self.default is None: self.default = False return super(BooleanField, self).field_from_native( @@ -514,6 +514,11 @@ class ChoiceField(WritableField): choices = property(_get_choices, _set_choices) + def metadata(self): + data = super(ChoiceField, self).metadata() + data['choices'] = [{'value': v, 'display_name': n} for v, n in self.choices] + return data + def validate(self, value): """ Validates that the input is in self.choices. @@ -966,7 +971,7 @@ class ImageField(FileField): return None from rest_framework.compat import Image - assert Image is not None, 'PIL must be installed for ImageField support' + assert Image is not None, 'Either Pillow or PIL must be installed for ImageField support.' # We need to get a file object for PIL. We might have a path or we might # have to read the data into memory. diff --git a/rest_framework/generics.py b/rest_framework/generics.py index 7cb80a84..fd411ad3 100644 --- a/rest_framework/generics.py +++ b/rest_framework/generics.py @@ -344,6 +344,18 @@ class GenericAPIView(views.APIView): """ pass + def pre_delete(self, obj): + """ + Placeholder method for calling before deleting an object. + """ + pass + + def post_delete(self, obj): + """ + Placeholder method for calling after saving an object. + """ + pass + def metadata(self, request): """ Return a dictionary of metadata about the view. diff --git a/rest_framework/mixins.py b/rest_framework/mixins.py index 4606c78b..43950c4b 100644 --- a/rest_framework/mixins.py +++ b/rest_framework/mixins.py @@ -6,6 +6,7 @@ which allows mixin classes to be composed in interesting ways. """ from __future__ import unicode_literals +from django.core.exceptions import ValidationError from django.http import Http404 from rest_framework import status from rest_framework.response import Response @@ -127,7 +128,12 @@ class UpdateModelMixin(object): files=request.FILES, partial=partial) if serializer.is_valid(): - self.pre_save(serializer.object) + try: + self.pre_save(serializer.object) + except ValidationError as err: + # full_clean on model instance may be called in pre_save, so we + # have to handle eventual errors. + return Response(err.message_dict, status=status.HTTP_400_BAD_REQUEST) self.object = serializer.save(**save_kwargs) self.post_save(self.object, created=created) return Response(serializer.data, status=success_status_code) @@ -186,5 +192,7 @@ class DestroyModelMixin(object): """ def destroy(self, request, *args, **kwargs): obj = self.get_object() + self.pre_delete(obj) obj.delete() + self.post_delete(obj) return Response(status=status.HTTP_204_NO_CONTENT) diff --git a/rest_framework/renderers.py b/rest_framework/renderers.py index fe4f43d4..2fdd3337 100644 --- a/rest_framework/renderers.py +++ b/rest_framework/renderers.py @@ -20,6 +20,7 @@ from rest_framework.compat import StringIO from rest_framework.compat import six from rest_framework.compat import smart_text from rest_framework.compat import yaml +from rest_framework.exceptions import ParseError from rest_framework.settings import api_settings from rest_framework.request import is_form_media_type, override_method from rest_framework.utils import encoders @@ -420,8 +421,12 @@ class BrowsableAPIRenderer(BaseRenderer): In the absence of the View having an associated form then return None. """ if request.method == method: - data = request.DATA - files = request.FILES + try: + data = request.DATA + files = request.FILES + except ParseError: + data = None + files = None else: data = None files = None diff --git a/rest_framework/request.py b/rest_framework/request.py index b883d0d4..fcea2508 100644 --- a/rest_framework/request.py +++ b/rest_framework/request.py @@ -356,7 +356,16 @@ class Request(object): if not parser: raise exceptions.UnsupportedMediaType(media_type) - parsed = parser.parse(stream, media_type, self.parser_context) + try: + parsed = parser.parse(stream, media_type, self.parser_context) + except: + # If we get an exception during parsing, fill in empty data and + # re-raise. Ensures we don't simply repeat the error when + # attempting to render the browsable renderer response, or when + # logging the request or similar. + self._data = QueryDict('', self._request._encoding) + self._files = MultiValueDict() + raise # Parser classes may return the raw data, or a # DataAndFiles object. Unpack the result as required. diff --git a/rest_framework/response.py b/rest_framework/response.py index 5877c8a3..1dc6abcf 100644 --- a/rest_framework/response.py +++ b/rest_framework/response.py @@ -61,6 +61,10 @@ class Response(SimpleTemplateResponse): assert charset, 'renderer returned unicode, and did not specify ' \ 'a charset value.' return bytes(ret.encode(charset)) + + if not ret: + del self['Content-Type'] + return ret @property diff --git a/rest_framework/routers.py b/rest_framework/routers.py index 3fee1e49..97b35c10 100644 --- a/rest_framework/routers.py +++ b/rest_framework/routers.py @@ -184,18 +184,24 @@ class SimpleRouter(BaseRouter): bound_methods[method] = action return bound_methods - def get_lookup_regex(self, viewset): + def get_lookup_regex(self, viewset, lookup_prefix=''): """ Given a viewset, return the portion of URL regex that is used to match against a single instance. + + Note that lookup_prefix is not used directly inside REST rest_framework + itself, but is required in order to nicely support nested router + implementations, such as drf-nested-routers. + + https://github.com/alanjds/drf-nested-routers """ if self.trailing_slash: - base_regex = '(?P<{lookup_field}>[^/]+)' + base_regex = '(?P<{lookup_prefix}{lookup_field}>[^/]+)' else: # Don't consume `.json` style suffixes - base_regex = '(?P<{lookup_field}>[^/.]+)' + base_regex = '(?P<{lookup_prefix}{lookup_field}>[^/.]+)' lookup_field = getattr(viewset, 'lookup_field', 'pk') - return base_regex.format(lookup_field=lookup_field) + return base_regex.format(lookup_field=lookup_field, lookup_prefix=lookup_prefix) def get_urls(self): """ diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 163abf4f..40caa1f3 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -412,7 +412,13 @@ class BaseSerializer(WritableField): # Set the serializer object if it exists obj = get_component(self.parent.object, self.source or field_name) if self.parent.object else None - obj = obj.all() if is_simple_callable(getattr(obj, 'all', None)) else obj + + # If we have a model manager or similar object then we need + # to iterate through each instance. + if (self.many and + not hasattr(obj, '__iter__') and + is_simple_callable(getattr(obj, 'all', None))): + obj = obj.all() if self.source == '*': if value: @@ -872,7 +878,7 @@ class ModelSerializer(Serializer): # Reverse fk or one-to-one relations for (obj, model) in meta.get_all_related_objects_with_model(): - field_name = obj.field.related_query_name() + field_name = obj.get_accessor_name() if field_name in attrs: related_data[field_name] = attrs.pop(field_name) @@ -896,7 +902,10 @@ class ModelSerializer(Serializer): # Update an existing instance... if instance is not None: for key, val in attrs.items(): - setattr(instance, key, val) + try: + setattr(instance, key, val) + except ValueError: + self._errors[key] = self.error_messages['required'] # ...or create a new instance else: @@ -940,11 +949,16 @@ class ModelSerializer(Serializer): del(obj._m2m_data) if getattr(obj, '_related_data', None): + related_fields = dict([ + (field.get_accessor_name(), field) + for field, model + in obj._meta.get_all_related_objects_with_model() + ]) for accessor_name, related in obj._related_data.items(): if isinstance(related, RelationsList): # Nested reverse fk relationship for related_item in related: - fk_field = obj._meta.get_field_by_name(accessor_name)[0].field.name + fk_field = related_fields[accessor_name].field.name setattr(related_item, fk_field, obj) self.save_object(related_item) diff --git a/rest_framework/status.py b/rest_framework/status.py index b9f249f9..76435371 100644 --- a/rest_framework/status.py +++ b/rest_framework/status.py @@ -6,6 +6,23 @@ And RFC 6585 - http://tools.ietf.org/html/rfc6585 """ from __future__ import unicode_literals + +def is_informational(code): + return code >= 100 and code <= 199 + +def is_success(code): + return code >= 200 and code <= 299 + +def is_redirect(code): + return code >= 300 and code <= 399 + +def is_client_error(code): + return code >= 400 and code <= 499 + +def is_server_error(code): + return code >= 500 and code <= 599 + + HTTP_100_CONTINUE = 100 HTTP_101_SWITCHING_PROTOCOLS = 101 HTTP_200_OK = 200 diff --git a/rest_framework/tests/test_fields.py b/rest_framework/tests/test_fields.py index ab2cceac..5c96bce9 100644 --- a/rest_framework/tests/test_fields.py +++ b/rest_framework/tests/test_fields.py @@ -707,20 +707,21 @@ class ChoiceFieldTests(TestCase): self.assertEqual(f.choices, models.fields.BLANK_CHOICE_DASH + SAMPLE_CHOICES) def test_invalid_choice_model(self): - s = ChoiceFieldModelSerializer(data={'choice' : 'wrong_value'}) + s = ChoiceFieldModelSerializer(data={'choice': 'wrong_value'}) self.assertFalse(s.is_valid()) self.assertEqual(s.errors, {'choice': ['Select a valid choice. wrong_value is not one of the available choices.']}) self.assertEqual(s.data['choice'], '') def test_empty_choice_model(self): """ - Test that the 'empty' value is correctly passed and used depending on the 'null' property on the model field. + Test that the 'empty' value is correctly passed and used depending on + the 'null' property on the model field. """ - s = ChoiceFieldModelSerializer(data={'choice' : ''}) + s = ChoiceFieldModelSerializer(data={'choice': ''}) self.assertTrue(s.is_valid()) self.assertEqual(s.data['choice'], '') - s = ChoiceFieldModelWithNullSerializer(data={'choice' : ''}) + s = ChoiceFieldModelWithNullSerializer(data={'choice': ''}) self.assertTrue(s.is_valid()) self.assertEqual(s.data['choice'], None) @@ -740,6 +741,23 @@ class ChoiceFieldTests(TestCase): self.assertEqual(f.from_native(''), None) self.assertEqual(f.from_native(None), None) + def test_metadata_choices(self): + """ + Make sure proper choices are included in the field's metadata. + """ + choices = [{'value': v, 'display_name': n} for v, n in SAMPLE_CHOICES] + f = serializers.ChoiceField(choices=SAMPLE_CHOICES) + self.assertEqual(f.metadata()['choices'], choices) + + def test_metadata_choices_not_required(self): + """ + Make sure proper choices are included in the field's metadata. + """ + choices = [{'value': v, 'display_name': n} + for v, n in models.fields.BLANK_CHOICE_DASH + SAMPLE_CHOICES] + f = serializers.ChoiceField(required=False, choices=SAMPLE_CHOICES) + self.assertEqual(f.metadata()['choices'], choices) + class EmailFieldTests(TestCase): """ diff --git a/rest_framework/tests/test_generics.py b/rest_framework/tests/test_generics.py index 79cd99ac..996bd5b0 100644 --- a/rest_framework/tests/test_generics.py +++ b/rest_framework/tests/test_generics.py @@ -23,6 +23,10 @@ class InstanceView(generics.RetrieveUpdateDestroyAPIView): """ model = BasicModel + def get_queryset(self): + queryset = super(InstanceView, self).get_queryset() + return queryset.exclude(text='filtered out') + class SlugSerializer(serializers.ModelSerializer): slug = serializers.Field() # read only @@ -160,10 +164,10 @@ class TestInstanceView(TestCase): """ Create 3 BasicModel intances. """ - items = ['foo', 'bar', 'baz'] + items = ['foo', 'bar', 'baz', 'filtered out'] for item in items: BasicModel(text=item).save() - self.objects = BasicModel.objects + self.objects = BasicModel.objects.exclude(text='filtered out') self.data = [ {'id': obj.id, 'text': obj.text} for obj in self.objects.all() @@ -352,6 +356,17 @@ class TestInstanceView(TestCase): updated = self.objects.get(id=1) self.assertEqual(updated.text, 'foobar') + def test_put_to_filtered_out_instance(self): + """ + PUT requests to an URL of instance which is filtered out should not be + able to create new objects. + """ + data = {'text': 'foo'} + filtered_out_pk = BasicModel.objects.filter(text='filtered out')[0].pk + request = factory.put('/{0}'.format(filtered_out_pk), data, format='json') + response = self.view(request, pk=filtered_out_pk).render() + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + def test_put_as_create_on_id_based_url(self): """ PUT requests to RetrieveUpdateDestroyAPIView should create an object @@ -508,6 +523,25 @@ class ExclusiveFilterBackend(object): return queryset.filter(text='other') +class TwoFieldModel(models.Model): + field_a = models.CharField(max_length=100) + field_b = models.CharField(max_length=100) + + +class DynamicSerializerView(generics.ListCreateAPIView): + model = TwoFieldModel + renderer_classes = (renderers.BrowsableAPIRenderer, renderers.JSONRenderer) + + def get_serializer_class(self): + if self.request.method == 'POST': + class DynamicSerializer(serializers.ModelSerializer): + class Meta: + model = TwoFieldModel + fields = ('field_b',) + return DynamicSerializer + return super(DynamicSerializerView, self).get_serializer_class() + + class TestFilterBackendAppliedToViews(TestCase): def setUp(self): @@ -564,28 +598,6 @@ class TestFilterBackendAppliedToViews(TestCase): self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.data, {'id': 1, 'text': 'foo'}) - -class TwoFieldModel(models.Model): - field_a = models.CharField(max_length=100) - field_b = models.CharField(max_length=100) - - -class DynamicSerializerView(generics.ListCreateAPIView): - model = TwoFieldModel - renderer_classes = (renderers.BrowsableAPIRenderer, renderers.JSONRenderer) - - def get_serializer_class(self): - if self.request.method == 'POST': - class DynamicSerializer(serializers.ModelSerializer): - class Meta: - model = TwoFieldModel - fields = ('field_b',) - return DynamicSerializer - return super(DynamicSerializerView, self).get_serializer_class() - - -class TestFilterBackendAppliedToViews(TestCase): - def test_dynamic_serializer_form_in_browsable_api(self): """ GET requests to ListCreateAPIView should return filtered list. diff --git a/rest_framework/tests/test_renderers.py b/rest_framework/tests/test_renderers.py index 76299a89..2ae8ae18 100644 --- a/rest_framework/tests/test_renderers.py +++ b/rest_framework/tests/test_renderers.py @@ -15,7 +15,9 @@ from rest_framework.renderers import BaseRenderer, JSONRenderer, YAMLRenderer, \ from rest_framework.parsers import YAMLParser, XMLParser from rest_framework.settings import api_settings from rest_framework.test import APIRequestFactory +from collections import MutableMapping import datetime +import json import pickle import re @@ -64,11 +66,23 @@ class MockView(APIView): class MockGETView(APIView): - def get(self, request, **kwargs): return Response({'foo': ['bar', 'baz']}) + +class MockPOSTView(APIView): + def post(self, request, **kwargs): + return Response({'foo': request.DATA}) + + +class EmptyGETView(APIView): + renderer_classes = (JSONRenderer,) + + def get(self, request, **kwargs): + return Response(status=status.HTTP_204_NO_CONTENT) + + class HTMLView(APIView): renderer_classes = (BrowsableAPIRenderer, ) @@ -88,8 +102,10 @@ urlpatterns = patterns('', url(r'^cache$', MockGETView.as_view()), url(r'^jsonp/jsonrenderer$', MockGETView.as_view(renderer_classes=[JSONRenderer, JSONPRenderer])), url(r'^jsonp/nojsonrenderer$', MockGETView.as_view(renderer_classes=[JSONPRenderer])), + url(r'^parseerror$', MockPOSTView.as_view(renderer_classes=[JSONRenderer, BrowsableAPIRenderer])), url(r'^html$', HTMLView.as_view()), url(r'^html1$', HTMLView1.as_view()), + url(r'^empty$', EmptyGETView.as_view()), url(r'^api', include('rest_framework.urls', namespace='rest_framework')) ) @@ -219,6 +235,22 @@ class RendererEndToEndTests(TestCase): self.assertEqual(resp.content, RENDERER_B_SERIALIZER(DUMMYCONTENT)) self.assertEqual(resp.status_code, DUMMYSTATUS) + def test_parse_error_renderers_browsable_api(self): + """Invalid data should still render the browsable API correctly.""" + resp = self.client.post('/parseerror', data='foobar', content_type='application/json', HTTP_ACCEPT='text/html') + self.assertEqual(resp['Content-Type'], 'text/html; charset=utf-8') + self.assertEqual(resp.status_code, status.HTTP_400_BAD_REQUEST) + + def test_204_no_content_responses_have_no_content_type_set(self): + """ + Regression test for #1196 + + https://github.com/tomchristie/django-rest-framework/issues/1196 + """ + resp = self.client.get('/empty') + self.assertEqual(resp.get('Content-Type', None), None) + self.assertEqual(resp.status_code, status.HTTP_204_NO_CONTENT) + _flat_repr = '{"foo": ["bar", "baz"]}' _indented_repr = '{\n "foo": [\n "bar",\n "baz"\n ]\n}' @@ -244,6 +276,44 @@ class JSONRendererTests(TestCase): ret = JSONRenderer().render(_('test')) self.assertEqual(ret, b'"test"') + def test_render_dict_abc_obj(self): + class Dict(MutableMapping): + def __init__(self): + self._dict = dict() + def __getitem__(self, key): + return self._dict.__getitem__(key) + def __setitem__(self, key, value): + return self._dict.__setitem__(key, value) + def __delitem__(self, key): + return self._dict.__delitem__(key) + def __iter__(self): + return self._dict.__iter__() + def __len__(self): + return self._dict.__len__() + def keys(self): + return self._dict.keys() + + x = Dict() + x['key'] = 'string value' + x[2] = 3 + ret = JSONRenderer().render(x) + data = json.loads(ret.decode('utf-8')) + self.assertEquals(data, {'key': 'string value', '2': 3}) + + def test_render_obj_with_getitem(self): + class DictLike(object): + def __init__(self): + self._dict = {} + def set(self, value): + self._dict = dict(value) + def __getitem__(self, key): + return self._dict[key] + + x = DictLike() + x.set({'a': 1, 'b': 'string'}) + with self.assertRaises(TypeError): + JSONRenderer().render(x) + def test_without_content_type_args(self): """ Test basic JSON rendering. diff --git a/rest_framework/tests/test_serializer.py b/rest_framework/tests/test_serializer.py index 1f85a474..14d1c664 100644 --- a/rest_framework/tests/test_serializer.py +++ b/rest_framework/tests/test_serializer.py @@ -558,6 +558,29 @@ class ModelValidationTests(TestCase): self.assertFalse(second_serializer.is_valid()) self.assertEqual(second_serializer.errors, {'title': ['Album with this Title already exists.']}) + def test_foreign_key_is_null_with_partial(self): + """ + Test ModelSerializer validation with partial=True + + Specifically test that a null foreign key does not pass validation + """ + album = Album(title='test') + album.save() + + class PhotoSerializer(serializers.ModelSerializer): + class Meta: + model = Photo + + photo_serializer = PhotoSerializer(data={'description': 'test', 'album': album.pk}) + self.assertTrue(photo_serializer.is_valid()) + photo = photo_serializer.save() + + # Updating only the album (foreign key) + photo_serializer = PhotoSerializer(instance=photo, data={'album': ''}, partial=True) + self.assertFalse(photo_serializer.is_valid()) + self.assertTrue('album' in photo_serializer.errors) + self.assertEqual(photo_serializer.errors['album'], photo_serializer.error_messages['required']) + def test_foreign_key_with_partial(self): """ Test ModelSerializer validation with partial=True @@ -1720,3 +1743,42 @@ class TestSerializerTransformMethods(TestCase): 'b_renamed': None, } ) + + +class DefaultTrueBooleanModel(models.Model): + cat = models.BooleanField(default=True) + dog = models.BooleanField(default=False) + + +class SerializerDefaultTrueBoolean(TestCase): + + def setUp(self): + super(SerializerDefaultTrueBoolean, self).setUp() + + class DefaultTrueBooleanSerializer(serializers.ModelSerializer): + class Meta: + model = DefaultTrueBooleanModel + fields = ('cat', 'dog') + + self.default_true_boolean_serializer = DefaultTrueBooleanSerializer + + def test_enabled_as_false(self): + serializer = self.default_true_boolean_serializer(data={'cat': False, + 'dog': False}) + self.assertEqual(serializer.is_valid(), True) + self.assertEqual(serializer.data['cat'], False) + self.assertEqual(serializer.data['dog'], False) + + def test_enabled_as_true(self): + serializer = self.default_true_boolean_serializer(data={'cat': True, + 'dog': True}) + self.assertEqual(serializer.is_valid(), True) + self.assertEqual(serializer.data['cat'], True) + self.assertEqual(serializer.data['dog'], True) + + def test_enabled_partial(self): + serializer = self.default_true_boolean_serializer(data={'cat': False}, + partial=True) + self.assertEqual(serializer.is_valid(), True) + self.assertEqual(serializer.data['cat'], False) + self.assertEqual(serializer.data['dog'], False) diff --git a/rest_framework/tests/test_serializer_nested.py b/rest_framework/tests/test_serializer_nested.py index 029f8bff..7114a060 100644 --- a/rest_framework/tests/test_serializer_nested.py +++ b/rest_framework/tests/test_serializer_nested.py @@ -6,6 +6,7 @@ Doesn't cover model serializers. from __future__ import unicode_literals from django.test import TestCase from rest_framework import serializers +from . import models class WritableNestedSerializerBasicTests(TestCase): @@ -311,3 +312,37 @@ class ForeignKeyNestedSerializerUpdateTests(TestCase): serializer = self.AlbumSerializer(instance=original, data=data) self.assertEqual(serializer.is_valid(), True) self.assertEqual(serializer.object, expected) + + +class NestedModelSerializerUpdateTests(TestCase): + def test_second_nested_level(self): + john = models.Person.objects.create(name="john") + + post = john.blogpost_set.create(title="Test blog post") + post.blogpostcomment_set.create(text="I hate this blog post") + post.blogpostcomment_set.create(text="I love this blog post") + + class BlogPostCommentSerializer(serializers.ModelSerializer): + class Meta: + model = models.BlogPostComment + + class BlogPostSerializer(serializers.ModelSerializer): + comments = BlogPostCommentSerializer(many=True, source='blogpostcomment_set') + class Meta: + model = models.BlogPost + fields = ('id', 'title', 'comments') + + class PersonSerializer(serializers.ModelSerializer): + posts = BlogPostSerializer(many=True, source='blogpost_set') + class Meta: + model = models.Person + fields = ('id', 'name', 'age', 'posts') + + serialize = PersonSerializer(instance=john) + deserialize = PersonSerializer(data=serialize.data, instance=john) + self.assertTrue(deserialize.is_valid()) + + result = deserialize.object + result.save() + self.assertEqual(result.id, john.id) + diff --git a/rest_framework/tests/test_status.py b/rest_framework/tests/test_status.py new file mode 100644 index 00000000..7b1bdae3 --- /dev/null +++ b/rest_framework/tests/test_status.py @@ -0,0 +1,33 @@ +from __future__ import unicode_literals +from django.test import TestCase +from rest_framework.status import ( + is_informational, is_success, is_redirect, is_client_error, is_server_error +) + + +class TestStatus(TestCase): + def test_status_categories(self): + self.assertFalse(is_informational(99)) + self.assertTrue(is_informational(100)) + self.assertTrue(is_informational(199)) + self.assertFalse(is_informational(200)) + + self.assertFalse(is_success(199)) + self.assertTrue(is_success(200)) + self.assertTrue(is_success(299)) + self.assertFalse(is_success(300)) + + self.assertFalse(is_redirect(299)) + self.assertTrue(is_redirect(300)) + self.assertTrue(is_redirect(399)) + self.assertFalse(is_redirect(400)) + + self.assertFalse(is_client_error(399)) + self.assertTrue(is_client_error(400)) + self.assertTrue(is_client_error(499)) + self.assertFalse(is_client_error(500)) + + self.assertFalse(is_server_error(499)) + self.assertTrue(is_server_error(500)) + self.assertTrue(is_server_error(599)) + self.assertFalse(is_server_error(600))
\ No newline at end of file diff --git a/rest_framework/urlpatterns.py b/rest_framework/urlpatterns.py index d9143bb4..0ff137b0 100644 --- a/rest_framework/urlpatterns.py +++ b/rest_framework/urlpatterns.py @@ -57,6 +57,6 @@ def format_suffix_patterns(urlpatterns, suffix_required=False, allowed=None): allowed_pattern = '(%s)' % '|'.join(allowed) suffix_pattern = r'\.(?P<%s>%s)$' % (suffix_kwarg, allowed_pattern) else: - suffix_pattern = r'\.(?P<%s>[a-z]+)$' % suffix_kwarg + suffix_pattern = r'\.(?P<%s>[a-z0-9]+)$' % suffix_kwarg return apply_suffix_patterns(urlpatterns, suffix_pattern, suffix_required) diff --git a/rest_framework/utils/encoders.py b/rest_framework/utils/encoders.py index 35ad206b..3ac920c6 100644 --- a/rest_framework/utils/encoders.py +++ b/rest_framework/utils/encoders.py @@ -44,6 +44,11 @@ class JSONEncoder(json.JSONEncoder): return str(o) elif hasattr(o, 'tolist'): return o.tolist() + elif hasattr(o, '__getitem__'): + try: + return dict(o) + except: + pass elif hasattr(o, '__iter__'): return [i for i in o] return super(JSONEncoder, self).default(o) |
