diff options
| -rw-r--r-- | README.md | 1 | ||||
| -rw-r--r-- | docs/api-guide/parsers.md | 11 | ||||
| -rw-r--r-- | docs/index.md | 12 | ||||
| -rw-r--r-- | docs/topics/release-notes.md | 1 | ||||
| -rw-r--r-- | rest_framework/relations.py | 20 | ||||
| -rw-r--r-- | rest_framework/tests/relations_hyperlink.py | 9 | ||||
| -rw-r--r-- | rest_framework/tests/relations_pk.py | 7 | ||||
| -rw-r--r-- | rest_framework/tests/relations_slug.py | 162 |
8 files changed, 197 insertions, 26 deletions
@@ -295,4 +295,3 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. [markdown]: http://pypi.python.org/pypi/Markdown/ [pyyaml]: http://pypi.python.org/pypi/PyYAML [django-filter]: http://pypi.python.org/pypi/django-filter - diff --git a/docs/api-guide/parsers.md b/docs/api-guide/parsers.md index de968557..0cd01639 100644 --- a/docs/api-guide/parsers.md +++ b/docs/api-guide/parsers.md @@ -14,6 +14,16 @@ REST framework includes a number of built in Parser classes, that allow you to a The set of valid parsers for a view is always defined as a list of classes. When either `request.DATA` or `request.FILES` is accessed, REST framework will examine the `Content-Type` header on the incoming request, and determine which parser to use to parse the request content. +--- + +**Note**: When developing client applications always remember to make sure you're setting the `Content-Type` header when sending data in an HTTP request. + +If you don't set the content type, most clients will default to using `'application/x-www-form-urlencoded'`, which may not be what you wanted. + +As an example, if you are sending `json` encoded data using jQuery with the [.ajax() method][jquery-ajax], you should make sure to include the `contentType: 'application/json'` setting. + +--- + ## Setting the parsers The default set of parsers may be set globally, using the `DEFAULT_PARSER_CLASSES` setting. For example, the following settings would allow requests with `YAML` content. @@ -169,6 +179,7 @@ The following third party packages are also available. [MessagePack][messagepack] is a fast, efficient binary serialization format. [Juan Riaza][juanriaza] maintains the [djangorestframework-msgpack][djangorestframework-msgpack] package which provides MessagePack renderer and parser support for REST framework. +[jquery-ajax]: http://api.jquery.com/jQuery.ajax/ [cite]: https://groups.google.com/d/topic/django-developers/dxI4qVzrBY4/discussion [messagepack]: https://github.com/juanriaza/django-rest-framework-msgpack [juanriaza]: https://github.com/juanriaza diff --git a/docs/index.md b/docs/index.md index 497f1900..3ed11c02 100644 --- a/docs/index.md +++ b/docs/index.md @@ -132,9 +132,9 @@ Run the tests: ## Support -For support please see the [REST framework discussion group][group], or try the `#restframework` channel on `irc.freenode.net`. +For support please see the [REST framework discussion group][group], try the `#restframework` channel on `irc.freenode.net`, or raise a question on [Stack Overflow][stack-overflow], making sure to include the ['django-rest-framework'][django-rest-framework-tag] tag. -Paid support is also available from [DabApps], and can include work on REST framework core, or support with building your REST framework API. Please contact [Tom Christie][email] if you'd like to discuss commercial support options. +[Paid support is available][paid-support] from [DabApps][dabapps], and can include work on REST framework core, or support with building your REST framework API. Please [contact DabApps][contact-dabapps] if you'd like to discuss commercial support options. ## License @@ -209,5 +209,9 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. [credits]: topics/credits.md [group]: https://groups.google.com/forum/?fromgroups#!forum/django-rest-framework -[DabApps]: http://dabapps.com -[email]: mailto:tom@tomchristie.com +[stack-overflow]: http://stackoverflow.com/ +[django-rest-framework-tag]: http://stackoverflow.com/questions/tagged/django-rest-framework +[django-tag]: http://stackoverflow.com/questions/tagged/django +[paid-support]: http://dabapps.com/services/build/api-development/ +[dabapps]: http://dabapps.com +[contact-dabapps]: http://dabapps.com/contact/ diff --git a/docs/topics/release-notes.md b/docs/topics/release-notes.md index e00a5e93..bbe11fac 100644 --- a/docs/topics/release-notes.md +++ b/docs/topics/release-notes.md @@ -19,6 +19,7 @@ Major version numbers (x.0.0) are reserved for project milestones. No major poi ### Master * Support json encoding of timedelta objects. +* Bugfix: Return proper validation errors when incorrect types supplied for relational fields. * Bugfix: Support nullable FKs with `SlugRelatedField`. ### 2.1.16 diff --git a/rest_framework/relations.py b/rest_framework/relations.py index 7ded3891..af63ceaa 100644 --- a/rest_framework/relations.py +++ b/rest_framework/relations.py @@ -177,7 +177,7 @@ class PrimaryKeyRelatedField(RelatedField): default_error_messages = { 'does_not_exist': _("Invalid pk '%s' - object does not exist."), - 'invalid': _('Invalid value.'), + 'incorrect_type': _('Incorrect type. Expected pk value, received %s.'), } # TODO: Remove these field hacks... @@ -208,7 +208,8 @@ class PrimaryKeyRelatedField(RelatedField): msg = self.error_messages['does_not_exist'] % smart_unicode(data) raise ValidationError(msg) except (TypeError, ValueError): - msg = self.error_messages['invalid'] + received = type(data).__name__ + msg = self.error_messages['incorrect_type'] % received raise ValidationError(msg) def field_to_native(self, obj, field_name): @@ -235,7 +236,7 @@ class ManyPrimaryKeyRelatedField(ManyRelatedField): default_error_messages = { 'does_not_exist': _("Invalid pk '%s' - object does not exist."), - 'invalid': _('Invalid value.'), + 'incorrect_type': _('Incorrect type. Expected pk value, received %s.'), } def prepare_value(self, obj): @@ -275,7 +276,8 @@ class ManyPrimaryKeyRelatedField(ManyRelatedField): msg = self.error_messages['does_not_exist'] % smart_unicode(data) raise ValidationError(msg) except (TypeError, ValueError): - msg = self.error_messages['invalid'] + received = type(data).__name__ + msg = self.error_messages['incorrect_type'] % received raise ValidationError(msg) ### Slug relationships @@ -333,7 +335,7 @@ class HyperlinkedRelatedField(RelatedField): 'incorrect_match': _('Invalid hyperlink - Incorrect URL match'), 'configuration_error': _('Invalid hyperlink due to configuration error'), 'does_not_exist': _("Invalid hyperlink - object does not exist."), - 'invalid': _('Invalid value.'), + 'incorrect_type': _('Incorrect type. Expected url string, received %s.'), } def __init__(self, *args, **kwargs): @@ -397,8 +399,8 @@ class HyperlinkedRelatedField(RelatedField): try: http_prefix = value.startswith('http:') or value.startswith('https:') except AttributeError: - msg = self.error_messages['invalid'] - raise ValidationError(msg) + msg = self.error_messages['incorrect_type'] + raise ValidationError(msg % type(value).__name__) if http_prefix: # If needed convert absolute URLs to relative path @@ -434,8 +436,8 @@ class HyperlinkedRelatedField(RelatedField): except ObjectDoesNotExist: raise ValidationError(self.error_messages['does_not_exist']) except (TypeError, ValueError): - msg = self.error_messages['invalid'] - raise ValidationError(msg) + msg = self.error_messages['incorrect_type'] + raise ValidationError(msg % type(value).__name__) return obj diff --git a/rest_framework/tests/relations_hyperlink.py b/rest_framework/tests/relations_hyperlink.py index 7d65eae7..6d137f68 100644 --- a/rest_framework/tests/relations_hyperlink.py +++ b/rest_framework/tests/relations_hyperlink.py @@ -215,6 +215,13 @@ class HyperlinkedForeignKeyTests(TestCase): ] self.assertEquals(serializer.data, expected) + def test_foreign_key_update_incorrect_type(self): + data = {'url': '/foreignkeysource/1/', 'name': u'source-1', 'target': 2} + instance = ForeignKeySource.objects.get(pk=1) + serializer = ForeignKeySourceSerializer(instance, data=data) + self.assertFalse(serializer.is_valid()) + self.assertEquals(serializer.errors, {'target': [u'Incorrect type. Expected url string, received int.']}) + def test_reverse_foreign_key_update(self): data = {'url': '/foreignkeytarget/2/', 'name': u'target-2', 'sources': ['/foreignkeysource/1/', '/foreignkeysource/3/']} instance = ForeignKeyTarget.objects.get(pk=2) @@ -227,7 +234,7 @@ class HyperlinkedForeignKeyTests(TestCase): expected = [ {'url': '/foreignkeytarget/1/', 'name': u'target-1', 'sources': ['/foreignkeysource/1/', '/foreignkeysource/2/', '/foreignkeysource/3/']}, {'url': '/foreignkeytarget/2/', 'name': u'target-2', 'sources': []}, - ] + ] self.assertEquals(new_serializer.data, expected) serializer.save() diff --git a/rest_framework/tests/relations_pk.py b/rest_framework/tests/relations_pk.py index dd1e86b5..3391e60a 100644 --- a/rest_framework/tests/relations_pk.py +++ b/rest_framework/tests/relations_pk.py @@ -194,6 +194,13 @@ class PKForeignKeyTests(TestCase): ] self.assertEquals(serializer.data, expected) + def test_foreign_key_update_incorrect_type(self): + data = {'id': 1, 'name': u'source-1', 'target': 'foo'} + instance = ForeignKeySource.objects.get(pk=1) + serializer = ForeignKeySourceSerializer(instance, data=data) + self.assertFalse(serializer.is_valid()) + self.assertEquals(serializer.errors, {'target': [u'Incorrect type. Expected pk value, received str.']}) + def test_reverse_foreign_key_update(self): data = {'id': 2, 'name': u'target-2', 'sources': [1, 3]} instance = ForeignKeyTarget.objects.get(pk=2) diff --git a/rest_framework/tests/relations_slug.py b/rest_framework/tests/relations_slug.py index 503b61e8..37ccc75e 100644 --- a/rest_framework/tests/relations_slug.py +++ b/rest_framework/tests/relations_slug.py @@ -1,9 +1,23 @@ from django.test import TestCase from rest_framework import serializers -from rest_framework.tests.models import NullableForeignKeySource, ForeignKeyTarget +from rest_framework.tests.models import NullableForeignKeySource, ForeignKeySource, ForeignKeyTarget -class NullableSlugSourceSerializer(serializers.ModelSerializer): +class ForeignKeyTargetSerializer(serializers.ModelSerializer): + sources = serializers.ManySlugRelatedField(slug_field='name') + + class Meta: + model = ForeignKeyTarget + + +class ForeignKeySourceSerializer(serializers.ModelSerializer): + target = serializers.SlugRelatedField(slug_field='name') + + class Meta: + model = ForeignKeySource + + +class NullableForeignKeySourceSerializer(serializers.ModelSerializer): target = serializers.SlugRelatedField(slug_field='name', null=True) class Meta: @@ -11,6 +25,132 @@ class NullableSlugSourceSerializer(serializers.ModelSerializer): # TODO: M2M Tests, FKTests (Non-nulable), One2One +class PKForeignKeyTests(TestCase): + def setUp(self): + target = ForeignKeyTarget(name='target-1') + target.save() + new_target = ForeignKeyTarget(name='target-2') + new_target.save() + for idx in range(1, 4): + source = ForeignKeySource(name='source-%d' % idx, target=target) + source.save() + + def test_foreign_key_retrieve(self): + queryset = ForeignKeySource.objects.all() + serializer = ForeignKeySourceSerializer(queryset) + expected = [ + {'id': 1, 'name': u'source-1', 'target': 'target-1'}, + {'id': 2, 'name': u'source-2', 'target': 'target-1'}, + {'id': 3, 'name': u'source-3', 'target': 'target-1'} + ] + self.assertEquals(serializer.data, expected) + + def test_reverse_foreign_key_retrieve(self): + queryset = ForeignKeyTarget.objects.all() + serializer = ForeignKeyTargetSerializer(queryset) + expected = [ + {'id': 1, 'name': u'target-1', 'sources': ['source-1', 'source-2', 'source-3']}, + {'id': 2, 'name': u'target-2', 'sources': []}, + ] + self.assertEquals(serializer.data, expected) + + def test_foreign_key_update(self): + data = {'id': 1, 'name': u'source-1', 'target': 'target-2'} + instance = ForeignKeySource.objects.get(pk=1) + serializer = ForeignKeySourceSerializer(instance, data=data) + self.assertTrue(serializer.is_valid()) + self.assertEquals(serializer.data, data) + serializer.save() + + # Ensure source 1 is updated, and everything else is as expected + queryset = ForeignKeySource.objects.all() + serializer = ForeignKeySourceSerializer(queryset) + expected = [ + {'id': 1, 'name': u'source-1', 'target': 'target-2'}, + {'id': 2, 'name': u'source-2', 'target': 'target-1'}, + {'id': 3, 'name': u'source-3', 'target': 'target-1'} + ] + self.assertEquals(serializer.data, expected) + + def test_foreign_key_update_incorrect_type(self): + data = {'id': 1, 'name': u'source-1', 'target': 123} + instance = ForeignKeySource.objects.get(pk=1) + serializer = ForeignKeySourceSerializer(instance, data=data) + self.assertFalse(serializer.is_valid()) + self.assertEquals(serializer.errors, {'target': [u'Object with name=123 does not exist.']}) + + def test_reverse_foreign_key_update(self): + data = {'id': 2, 'name': u'target-2', 'sources': ['source-1', 'source-3']} + instance = ForeignKeyTarget.objects.get(pk=2) + serializer = ForeignKeyTargetSerializer(instance, data=data) + self.assertTrue(serializer.is_valid()) + # We shouldn't have saved anything to the db yet since save + # hasn't been called. + queryset = ForeignKeyTarget.objects.all() + new_serializer = ForeignKeyTargetSerializer(queryset) + expected = [ + {'id': 1, 'name': u'target-1', 'sources': ['source-1', 'source-2', 'source-3']}, + {'id': 2, 'name': u'target-2', 'sources': []}, + ] + self.assertEquals(new_serializer.data, expected) + + serializer.save() + self.assertEquals(serializer.data, data) + + # Ensure target 2 is update, and everything else is as expected + queryset = ForeignKeyTarget.objects.all() + serializer = ForeignKeyTargetSerializer(queryset) + expected = [ + {'id': 1, 'name': u'target-1', 'sources': ['source-2']}, + {'id': 2, 'name': u'target-2', 'sources': ['source-1', 'source-3']}, + ] + self.assertEquals(serializer.data, expected) + + def test_foreign_key_create(self): + data = {'id': 4, 'name': u'source-4', 'target': 'target-2'} + serializer = ForeignKeySourceSerializer(data=data) + serializer.is_valid() + self.assertTrue(serializer.is_valid()) + obj = serializer.save() + self.assertEquals(serializer.data, data) + self.assertEqual(obj.name, u'source-4') + + # Ensure source 4 is added, and everything else is as expected + queryset = ForeignKeySource.objects.all() + serializer = ForeignKeySourceSerializer(queryset) + expected = [ + {'id': 1, 'name': u'source-1', 'target': 'target-1'}, + {'id': 2, 'name': u'source-2', 'target': 'target-1'}, + {'id': 3, 'name': u'source-3', 'target': 'target-1'}, + {'id': 4, 'name': u'source-4', 'target': 'target-2'}, + ] + self.assertEquals(serializer.data, expected) + + def test_reverse_foreign_key_create(self): + data = {'id': 3, 'name': u'target-3', 'sources': ['source-1', 'source-3']} + serializer = ForeignKeyTargetSerializer(data=data) + self.assertTrue(serializer.is_valid()) + obj = serializer.save() + self.assertEquals(serializer.data, data) + self.assertEqual(obj.name, u'target-3') + + # Ensure target 3 is added, and everything else is as expected + queryset = ForeignKeyTarget.objects.all() + serializer = ForeignKeyTargetSerializer(queryset) + expected = [ + {'id': 1, 'name': u'target-1', 'sources': ['source-2']}, + {'id': 2, 'name': u'target-2', 'sources': []}, + {'id': 3, 'name': u'target-3', 'sources': ['source-1', 'source-3']}, + ] + self.assertEquals(serializer.data, expected) + + def test_foreign_key_update_with_invalid_null(self): + data = {'id': 1, 'name': u'source-1', 'target': None} + instance = ForeignKeySource.objects.get(pk=1) + serializer = ForeignKeySourceSerializer(instance, data=data) + self.assertFalse(serializer.is_valid()) + self.assertEquals(serializer.errors, {'target': [u'Value may not be null']}) + class SlugNullableForeignKeyTests(TestCase): def setUp(self): @@ -24,7 +164,7 @@ class SlugNullableForeignKeyTests(TestCase): def test_foreign_key_retrieve_with_null(self): queryset = NullableForeignKeySource.objects.all() - serializer = NullableSlugSourceSerializer(queryset) + serializer = NullableForeignKeySourceSerializer(queryset) expected = [ {'id': 1, 'name': u'source-1', 'target': 'target-1'}, {'id': 2, 'name': u'source-2', 'target': 'target-1'}, @@ -34,7 +174,7 @@ class SlugNullableForeignKeyTests(TestCase): def test_foreign_key_create_with_valid_null(self): data = {'id': 4, 'name': u'source-4', 'target': None} - serializer = NullableSlugSourceSerializer(data=data) + serializer = NullableForeignKeySourceSerializer(data=data) self.assertTrue(serializer.is_valid()) obj = serializer.save() self.assertEquals(serializer.data, data) @@ -42,7 +182,7 @@ class SlugNullableForeignKeyTests(TestCase): # Ensure source 4 is created, and everything else is as expected queryset = NullableForeignKeySource.objects.all() - serializer = NullableSlugSourceSerializer(queryset) + serializer = NullableForeignKeySourceSerializer(queryset) expected = [ {'id': 1, 'name': u'source-1', 'target': 'target-1'}, {'id': 2, 'name': u'source-2', 'target': 'target-1'}, @@ -58,7 +198,7 @@ class SlugNullableForeignKeyTests(TestCase): """ data = {'id': 4, 'name': u'source-4', 'target': ''} expected_data = {'id': 4, 'name': u'source-4', 'target': None} - serializer = NullableSlugSourceSerializer(data=data) + serializer = NullableForeignKeySourceSerializer(data=data) self.assertTrue(serializer.is_valid()) obj = serializer.save() self.assertEquals(serializer.data, expected_data) @@ -66,7 +206,7 @@ class SlugNullableForeignKeyTests(TestCase): # Ensure source 4 is created, and everything else is as expected queryset = NullableForeignKeySource.objects.all() - serializer = NullableSlugSourceSerializer(queryset) + serializer = NullableForeignKeySourceSerializer(queryset) expected = [ {'id': 1, 'name': u'source-1', 'target': 'target-1'}, {'id': 2, 'name': u'source-2', 'target': 'target-1'}, @@ -78,14 +218,14 @@ class SlugNullableForeignKeyTests(TestCase): def test_foreign_key_update_with_valid_null(self): data = {'id': 1, 'name': u'source-1', 'target': None} instance = NullableForeignKeySource.objects.get(pk=1) - serializer = NullableSlugSourceSerializer(instance, data=data) + serializer = NullableForeignKeySourceSerializer(instance, data=data) self.assertTrue(serializer.is_valid()) self.assertEquals(serializer.data, data) serializer.save() # Ensure source 1 is updated, and everything else is as expected queryset = NullableForeignKeySource.objects.all() - serializer = NullableSlugSourceSerializer(queryset) + serializer = NullableForeignKeySourceSerializer(queryset) expected = [ {'id': 1, 'name': u'source-1', 'target': None}, {'id': 2, 'name': u'source-2', 'target': 'target-1'}, @@ -101,14 +241,14 @@ class SlugNullableForeignKeyTests(TestCase): data = {'id': 1, 'name': u'source-1', 'target': ''} expected_data = {'id': 1, 'name': u'source-1', 'target': None} instance = NullableForeignKeySource.objects.get(pk=1) - serializer = NullableSlugSourceSerializer(instance, data=data) + serializer = NullableForeignKeySourceSerializer(instance, data=data) self.assertTrue(serializer.is_valid()) self.assertEquals(serializer.data, expected_data) serializer.save() # Ensure source 1 is updated, and everything else is as expected queryset = NullableForeignKeySource.objects.all() - serializer = NullableSlugSourceSerializer(queryset) + serializer = NullableForeignKeySourceSerializer(queryset) expected = [ {'id': 1, 'name': u'source-1', 'target': None}, {'id': 2, 'name': u'source-2', 'target': 'target-1'}, |
