From dbb435ca3165eba990d3dc2ce72c8e108e123f2f Mon Sep 17 00:00:00 2001 From: Kevin Chang Date: Wed, 12 Nov 2014 12:46:09 -0800 Subject: Added missing default style for FileField --- rest_framework/renderers.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/rest_framework/renderers.py b/rest_framework/renderers.py index b56f99db..37d3c47c 100644 --- a/rest_framework/renderers.py +++ b/rest_framework/renderers.py @@ -374,6 +374,10 @@ class HTMLFormRenderer(BaseRenderer): 'base_template': 'input.html', 'input_type': 'time' }, + serializers.FileField: { + 'base_template': 'input.html', + 'input_type': 'file' + }, serializers.BooleanField: { 'base_template': 'checkbox.html' }, -- cgit v1.2.3 From ad060aa360fa2ed33bd83cbb419d7b996a428726 Mon Sep 17 00:00:00 2001 From: Gregor Müllegger Date: Sat, 15 Nov 2014 15:23:58 +0100 Subject: More helpful error message when default `.create` fails. Closes #2013. --- rest_framework/serializers.py | 14 +++++++++++++- tests/test_model_serializer.py | 28 ++++++++++++++++++++++++++++ 2 files changed, 41 insertions(+), 1 deletion(-) diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index e7e93f38..8dafea4d 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -34,6 +34,7 @@ from rest_framework.validators import ( ) import copy import inspect +import sys import warnings # Note: We do the following so that users of the framework can use this style: @@ -593,7 +594,18 @@ class ModelSerializer(Serializer): if relation_info.to_many and (field_name in validated_attrs): many_to_many[field_name] = validated_attrs.pop(field_name) - instance = ModelClass.objects.create(**validated_attrs) + try: + instance = ModelClass.objects.create(**validated_attrs) + except TypeError as exc: + msg = ( + 'The mentioned argument might be a field on the serializer ' + 'that is not part of the model. You need to override the ' + 'create() method in your ModelSerializer subclass to support ' + 'this.') + six.reraise( + type(exc), + type(exc)(str(exc) + '. ' + msg), + sys.exc_info()[2]) # Save many-to-many relationships after the instance is created. if many_to_many: diff --git a/tests/test_model_serializer.py b/tests/test_model_serializer.py index 3aec0da0..90767dac 100644 --- a/tests/test_model_serializer.py +++ b/tests/test_model_serializer.py @@ -10,6 +10,7 @@ from django.core.validators import MaxValueValidator, MinValueValidator, MinLeng from django.db import models from django.test import TestCase from rest_framework import serializers +import pytest def dedent(blocktext): @@ -26,6 +27,10 @@ class CustomField(models.Field): pass +class OneFieldModel(models.Model): + char_field = models.CharField(max_length=100) + + class RegularFieldsModel(models.Model): """ A model class for testing regular flat fields. @@ -68,6 +73,29 @@ class FieldOptionsModel(models.Model): choices_field = models.CharField(max_length=100, choices=COLOR_CHOICES) +class TestModelSerializer(TestCase): + def test_create_method(self): + class TestSerializer(serializers.ModelSerializer): + non_model_field = serializers.CharField() + + class Meta: + model = OneFieldModel + fields = ('char_field', 'non_model_field') + + serializer = TestSerializer(data={ + 'char_field': 'foo', + 'non_model_field': 'bar', + }) + serializer.is_valid() + with pytest.raises(TypeError): + serializer.save() + + try: + serializer.save() + except TypeError as exc: + assert 'ModelSerializer' in str(exc) + + class TestRegularFieldMappings(TestCase): def test_regular_fields(self): """ -- cgit v1.2.3 From 2f03483f966c5402734b5db2f7006c788bbe04f7 Mon Sep 17 00:00:00 2001 From: Tymur Maryokhin Date: Sat, 29 Nov 2014 19:45:26 +0100 Subject: Removed unused models --- tests/models.py | 99 +-------------------------------------------------------- 1 file changed, 1 insertion(+), 98 deletions(-) diff --git a/tests/models.py b/tests/models.py index 06ec5a22..a0e0b3cc 100644 --- a/tests/models.py +++ b/tests/models.py @@ -3,35 +3,16 @@ from django.db import models from django.utils.translation import ugettext_lazy as _ -def foobar(): - return 'foobar' - - -class CustomField(models.CharField): - - def __init__(self, *args, **kwargs): - kwargs['max_length'] = 12 - super(CustomField, self).__init__(*args, **kwargs) - - class RESTFrameworkModel(models.Model): """ Base for test models that sets app_label, so they play nicely. """ + class Meta: app_label = 'tests' abstract = True -class HasPositiveIntegerAsChoice(RESTFrameworkModel): - some_choices = ((1, 'A'), (2, 'B'), (3, 'C')) - some_integer = models.PositiveIntegerField(choices=some_choices) - - -class Anchor(RESTFrameworkModel): - text = models.CharField(max_length=100, default='anchor') - - class BasicModel(RESTFrameworkModel): text = models.CharField(max_length=100, verbose_name=_("Text comes here"), help_text=_("Text description.")) @@ -41,24 +22,6 @@ class SlugBasedModel(RESTFrameworkModel): slug = models.SlugField(max_length=32) -class DefaultValueModel(RESTFrameworkModel): - text = models.CharField(default='foobar', max_length=100) - extra = models.CharField(blank=True, null=True, max_length=100) - - -class CallableDefaultValueModel(RESTFrameworkModel): - text = models.CharField(default=foobar, max_length=100) - - -class ManyToManyModel(RESTFrameworkModel): - rel = models.ManyToManyField(Anchor, help_text='Some help text.') - - -class ReadOnlyManyToManyModel(RESTFrameworkModel): - text = models.CharField(max_length=100, default='anchor') - rel = models.ManyToManyField(Anchor) - - class BaseFilterableItem(RESTFrameworkModel): text = models.CharField(max_length=100) @@ -72,72 +35,12 @@ class FilterableItem(BaseFilterableItem): # Model for regression test for #285 - class Comment(RESTFrameworkModel): email = models.EmailField() content = models.CharField(max_length=200) created = models.DateTimeField(auto_now_add=True) -class ActionItem(RESTFrameworkModel): - title = models.CharField(max_length=200) - started = models.NullBooleanField(default=False) - done = models.BooleanField(default=False) - info = CustomField(default='---', max_length=12) - - -# Models for reverse relations -class Person(RESTFrameworkModel): - name = models.CharField(max_length=10) - age = models.IntegerField(null=True, blank=True) - - @property - def info(self): - return { - 'name': self.name, - 'age': self.age, - } - - -class BlogPost(RESTFrameworkModel): - title = models.CharField(max_length=100) - writer = models.ForeignKey(Person, null=True, blank=True) - - def get_first_comment(self): - return self.blogpostcomment_set.all()[0] - - -class BlogPostComment(RESTFrameworkModel): - text = models.TextField() - blog_post = models.ForeignKey(BlogPost) - - -class Album(RESTFrameworkModel): - title = models.CharField(max_length=100, unique=True) - ref = models.CharField(max_length=10, unique=True, null=True, blank=True) - - -class Photo(RESTFrameworkModel): - description = models.TextField() - album = models.ForeignKey(Album) - - -# Model for issue #324 -class BlankFieldModel(RESTFrameworkModel): - title = models.CharField(max_length=100, blank=True, null=False, - default="title") - - -# Model for issue #380 -class OptionalRelationModel(RESTFrameworkModel): - other = models.ForeignKey('OptionalRelationModel', blank=True, null=True) - - -# Model for RegexField -class Book(RESTFrameworkModel): - isbn = models.CharField(max_length=13) - - # Models for relations tests # ManyToMany class ManyToManyTarget(RESTFrameworkModel): -- cgit v1.2.3 From dd9d40d8c01f54f1542ba728d89b8b2da584dc1f Mon Sep 17 00:00:00 2001 From: Tymur Maryokhin Date: Sat, 29 Nov 2014 20:04:50 +0100 Subject: Moved non-conflicting models --- tests/models.py | 12 ------------ tests/test_generics.py | 34 +++++++++++++++++++++++++--------- 2 files changed, 25 insertions(+), 21 deletions(-) diff --git a/tests/models.py b/tests/models.py index a0e0b3cc..456b0a0b 100644 --- a/tests/models.py +++ b/tests/models.py @@ -17,11 +17,6 @@ class BasicModel(RESTFrameworkModel): text = models.CharField(max_length=100, verbose_name=_("Text comes here"), help_text=_("Text description.")) -class SlugBasedModel(RESTFrameworkModel): - text = models.CharField(max_length=100) - slug = models.SlugField(max_length=32) - - class BaseFilterableItem(RESTFrameworkModel): text = models.CharField(max_length=100) @@ -34,13 +29,6 @@ class FilterableItem(BaseFilterableItem): date = models.DateField() -# Model for regression test for #285 -class Comment(RESTFrameworkModel): - email = models.EmailField() - content = models.CharField(max_length=200) - created = models.DateTimeField(auto_now_add=True) - - # Models for relations tests # ManyToMany class ManyToManyTarget(RESTFrameworkModel): diff --git a/tests/test_generics.py b/tests/test_generics.py index 2690fb47..b78584f0 100644 --- a/tests/test_generics.py +++ b/tests/test_generics.py @@ -6,12 +6,26 @@ from django.test import TestCase from django.utils import six from rest_framework import generics, renderers, serializers, status from rest_framework.test import APIRequestFactory -from tests.models import BasicModel, Comment, SlugBasedModel +from tests.models import BasicModel, RESTFrameworkModel from tests.models import ForeignKeySource, ForeignKeyTarget factory = APIRequestFactory() +# Models +class SlugBasedModel(RESTFrameworkModel): + text = models.CharField(max_length=100) + slug = models.SlugField(max_length=32) + + +# Model for regression test for #285 +class Comment(RESTFrameworkModel): + email = models.EmailField() + content = models.CharField(max_length=200) + created = models.DateTimeField(auto_now_add=True) + + +# Serializers class BasicSerializer(serializers.ModelSerializer): class Meta: model = BasicModel @@ -22,6 +36,15 @@ class ForeignKeySerializer(serializers.ModelSerializer): model = ForeignKeySource +class SlugSerializer(serializers.ModelSerializer): + slug = serializers.ReadOnlyField() + + class Meta: + model = SlugBasedModel + fields = ('text', 'slug') + + +# Views class RootView(generics.ListCreateAPIView): queryset = BasicModel.objects.all() serializer_class = BasicSerializer @@ -37,14 +60,6 @@ class FKInstanceView(generics.RetrieveUpdateDestroyAPIView): serializer_class = ForeignKeySerializer -class SlugSerializer(serializers.ModelSerializer): - slug = serializers.ReadOnlyField() - - class Meta: - model = SlugBasedModel - fields = ('text', 'slug') - - class SlugBasedInstanceView(InstanceView): """ A model with a slug-field. @@ -54,6 +69,7 @@ class SlugBasedInstanceView(InstanceView): lookup_field = 'slug' +# Tests class TestRootView(TestCase): def setUp(self): """ -- cgit v1.2.3 From e2ea98e8ab3192fa8d252d33cc03929fcf6ed02f Mon Sep 17 00:00:00 2001 From: Tymur Maryokhin Date: Sat, 29 Nov 2014 20:23:55 +0100 Subject: Fixed typos --- README.md | 8 ++++---- docs/index.md | 4 ++-- docs/topics/2.3-announcement.md | 6 +++--- docs/topics/release-notes.md | 4 ++-- docs/tutorial/6-viewsets-and-routers.md | 4 ++-- docs/tutorial/quickstart.md | 2 +- tests/test_generics.py | 22 +++++++++++----------- 7 files changed, 25 insertions(+), 25 deletions(-) diff --git a/README.md b/README.md index c86bb65f..31fe1e52 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ [![build-status-image]][travis] [![pypi-version]][pypi] -**Awesome web-browseable Web APIs.** +**Awesome web-browsable Web APIs.** Full documentation for the project is available at [http://www.django-rest-framework.org][docs]. @@ -19,7 +19,7 @@ Django REST framework is a powerful and flexible toolkit for building Web APIs. Some reasons you might want to use REST framework: -* The [Web browseable API][sandbox] is a huge useability win for your developers. +* The [Web browsable API][sandbox] is a huge usability win for your developers. * [Authentication policies][authentication] including [OAuth1a][oauth1-section] and [OAuth2][oauth2-section] out of the box. * [Serialization][serializers] that supports both [ORM][modelserializer-section] and [non-ORM][serializer-section] data sources. * Customizable all the way down - just use [regular function-based views][functionview-section] if you don't need the [more][generic-views] [powerful][viewsets] [features][routers]. @@ -27,7 +27,7 @@ Some reasons you might want to use REST framework: There is a live example API for testing purposes, [available here][sandbox]. -**Below**: *Screenshot from the browseable API* +**Below**: *Screenshot from the browsable API* ![Screenshot][image] @@ -86,7 +86,7 @@ router.register(r'users', UserViewSet) # Wire up our API using automatic URL routing. -# Additionally, we include login URLs for the browseable API. +# Additionally, we include login URLs for the browsable API. urlpatterns = [ url(r'^', include(router.urls)), url(r'^api-auth/', include('rest_framework.urls', namespace='rest_framework')) diff --git a/docs/index.md b/docs/index.md index b5257c73..feada2a9 100644 --- a/docs/index.md +++ b/docs/index.md @@ -31,7 +31,7 @@ Django REST framework is a powerful and flexible toolkit that makes it easy to b Some reasons you might want to use REST framework: -* The [Web browseable API][sandbox] is a huge usability win for your developers. +* The [Web browsable API][sandbox] is a huge usability win for your developers. * [Authentication policies][authentication] including [OAuth1a][oauth1-section] and [OAuth2][oauth2-section] out of the box. * [Serialization][serializers] that supports both [ORM][modelserializer-section] and [non-ORM][serializer-section] data sources. * Customizable all the way down - just use [regular function-based views][functionview-section] if you don't need the [more][generic-views] [powerful][viewsets] [features][routers]. @@ -132,7 +132,7 @@ Here's our project's root `urls.py` module: router.register(r'users', UserViewSet) # Wire up our API using automatic URL routing. - # Additionally, we include login URLs for the browseable API. + # Additionally, we include login URLs for the browsable API. urlpatterns = [ url(r'^', include(router.urls)), url(r'^api-auth/', include('rest_framework.urls', namespace='rest_framework')) diff --git a/docs/topics/2.3-announcement.md b/docs/topics/2.3-announcement.md index 9c9f3e9f..66e46865 100644 --- a/docs/topics/2.3-announcement.md +++ b/docs/topics/2.3-announcement.md @@ -35,7 +35,7 @@ As an example of just how simple REST framework APIs can now be, here's an API w # Wire up our API using automatic URL routing. - # Additionally, we include login URLs for the browseable API. + # Additionally, we include login URLs for the browsable API. urlpatterns = [ url(r'^', include(router.urls)), url(r'^api-auth/', include('rest_framework.urls', namespace='rest_framework')) @@ -207,9 +207,9 @@ The old-style signature will continue to function but will raise a `PendingDepre ## View names and descriptions -The mechanics of how the names and descriptions used in the browseable API are generated has been modified and cleaned up somewhat. +The mechanics of how the names and descriptions used in the browsable API are generated has been modified and cleaned up somewhat. -If you've been customizing this behavior, for example perhaps to use `rst` markup for the browseable API, then you'll need to take a look at the implementation to see what updates you need to make. +If you've been customizing this behavior, for example perhaps to use `rst` markup for the browsable API, then you'll need to take a look at the implementation to see what updates you need to make. Note that the relevant methods have always been private APIs, and the docstrings called them out as intended to be deprecated. diff --git a/docs/topics/release-notes.md b/docs/topics/release-notes.md index 53187589..19dfbb98 100644 --- a/docs/topics/release-notes.md +++ b/docs/topics/release-notes.md @@ -121,7 +121,7 @@ You can determine your currently installed version using `pip freeze`: * Add `UnicodeYAMLRenderer` that extends `YAMLRenderer` with unicode. * Fix `parse_header` argument convertion. * Fix mediatype detection under Python 3. -* Web browseable API now offers blank option on dropdown when the field is not required. +* Web browsable API now offers blank option on dropdown when the field is not required. * `APIException` representation improved for logging purposes. * Allow source="*" within nested serializers. * Better support for custom oauth2 provider backends. @@ -200,7 +200,7 @@ You can determine your currently installed version using `pip freeze`: * Added `MAX_PAGINATE_BY` setting and `max_paginate_by` generic view attribute. * Added `cache` attribute to throttles to allow overriding of default cache. * 'Raw data' tab in browsable API now contains pre-populated data. -* 'Raw data' and 'HTML form' tab preference in browseable API now saved between page views. +* 'Raw data' and 'HTML form' tab preference in browsable API now saved between page views. * Bugfix: `required=True` argument fixed for boolean serializer fields. * Bugfix: `client.force_authenticate(None)` should also clear session info if it exists. * Bugfix: Client sending empty string instead of file now clears `FileField`. diff --git a/docs/tutorial/6-viewsets-and-routers.md b/docs/tutorial/6-viewsets-and-routers.md index 3fad509a..816e9da6 100644 --- a/docs/tutorial/6-viewsets-and-routers.md +++ b/docs/tutorial/6-viewsets-and-routers.md @@ -112,7 +112,7 @@ Here's our re-wired `urls.py` file. router.register(r'users', views.UserViewSet) # The API URLs are now determined automatically by the router. - # Additionally, we include the login URLs for the browseable API. + # Additionally, we include the login URLs for the browsable API. urlpatterns = [ url(r'^', include(router.urls)), url(r'^api-auth/', include('rest_framework.urls', namespace='rest_framework')) @@ -130,7 +130,7 @@ That doesn't mean it's always the right approach to take. There's a similar set ## Reviewing our work -With an incredibly small amount of code, we've now got a complete pastebin Web API, which is fully web browseable, and comes complete with authentication, per-object permissions, and multiple renderer formats. +With an incredibly small amount of code, we've now got a complete pastebin Web API, which is fully web browsable, and comes complete with authentication, per-object permissions, and multiple renderer formats. We've walked through each step of the design process, and seen how if we need to customize anything we can gradually work our way down to simply using regular Django views. diff --git a/docs/tutorial/quickstart.md b/docs/tutorial/quickstart.md index 1c398c1f..3e1ce0a9 100644 --- a/docs/tutorial/quickstart.md +++ b/docs/tutorial/quickstart.md @@ -100,7 +100,7 @@ Okay, now let's wire up the API URLs. On to `tutorial/urls.py`... router.register(r'groups', views.GroupViewSet) # Wire up our API using automatic URL routing. - # Additionally, we include login URLs for the browseable API. + # Additionally, we include login URLs for the browsable API. urlpatterns = [ url(r'^', include(router.urls)), url(r'^api-auth/', include('rest_framework.urls', namespace='rest_framework')) diff --git a/tests/test_generics.py b/tests/test_generics.py index b78584f0..94023c30 100644 --- a/tests/test_generics.py +++ b/tests/test_generics.py @@ -143,13 +143,13 @@ class TestRootView(TestCase): self.assertEqual(created.text, 'foobar') -EXPECTED_QUERYS_FOR_PUT = 3 if django.VERSION < (1, 6) else 2 +EXPECTED_QUERIES_FOR_PUT = 3 if django.VERSION < (1, 6) else 2 class TestInstanceView(TestCase): def setUp(self): """ - Create 3 BasicModel intances. + Create 3 BasicModel instances. """ items = ['foo', 'bar', 'baz', 'filtered out'] for item in items: @@ -189,7 +189,7 @@ class TestInstanceView(TestCase): """ data = {'text': 'foobar'} request = factory.put('/1', data, format='json') - with self.assertNumQueries(EXPECTED_QUERYS_FOR_PUT): + with self.assertNumQueries(EXPECTED_QUERIES_FOR_PUT): response = self.view(request, pk='1').render() self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(dict(response.data), {'id': 1, 'text': 'foobar'}) @@ -203,7 +203,7 @@ class TestInstanceView(TestCase): data = {'text': 'foobar'} request = factory.patch('/1', data, format='json') - with self.assertNumQueries(EXPECTED_QUERYS_FOR_PUT): + with self.assertNumQueries(EXPECTED_QUERIES_FOR_PUT): response = self.view(request, pk=1).render() self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.data, {'id': 1, 'text': 'foobar'}) @@ -238,7 +238,7 @@ class TestInstanceView(TestCase): """ data = {'id': 999, 'text': 'foobar'} request = factory.put('/1', data, format='json') - with self.assertNumQueries(EXPECTED_QUERYS_FOR_PUT): + with self.assertNumQueries(EXPECTED_QUERIES_FOR_PUT): response = self.view(request, pk=1).render() self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.data, {'id': 1, 'text': 'foobar'}) @@ -304,9 +304,10 @@ class TestOverriddenGetObject(TestCase): Test cases for a RetrieveUpdateDestroyAPIView that does NOT use the queryset/model mechanism but instead overrides get_object() """ + def setUp(self): """ - Create 3 BasicModel intances. + Create 3 BasicModel instances. """ items = ['foo', 'bar', 'baz'] for item in items: @@ -379,11 +380,11 @@ class ClassB(models.Model): class ClassA(models.Model): name = models.CharField(max_length=255) - childs = models.ManyToManyField(ClassB, blank=True, null=True) + children = models.ManyToManyField(ClassB, blank=True, null=True) class ClassASerializer(serializers.ModelSerializer): - childs = serializers.PrimaryKeyRelatedField( + children = serializers.PrimaryKeyRelatedField( many=True, queryset=ClassB.objects.all() ) @@ -396,8 +397,8 @@ class ExampleView(generics.ListCreateAPIView): queryset = ClassA.objects.all() -class TestM2MBrowseableAPI(TestCase): - def test_m2m_in_browseable_api(self): +class TestM2MBrowsableAPI(TestCase): + def test_m2m_in_browsable_api(self): """ Test for particularly ugly regression with m2m in browsable API """ @@ -440,7 +441,6 @@ class DynamicSerializerView(generics.ListCreateAPIView): class TestFilterBackendAppliedToViews(TestCase): - def setUp(self): """ Create 3 BasicModel instances to filter on. -- cgit v1.2.3 From b9503cd603613e4ae72b7718ba70a00b1537b289 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Mon, 1 Dec 2014 10:48:45 +0000 Subject: Support Django's core ValidationError for backwards compat. Refs #2145. --- docs/index.md | 2 +- docs/topics/3.0-announcement.md | 42 +++++++++++++++++++++++++++++++++++------ rest_framework/serializers.py | 11 +++++++++++ 3 files changed, 48 insertions(+), 7 deletions(-) diff --git a/docs/index.md b/docs/index.md index 10b45584..7631be1e 100644 --- a/docs/index.md +++ b/docs/index.md @@ -11,7 +11,7 @@ **Note**: This is the documentation for the **version 3.0** of REST framework. Documentation for [version 2.4](http://tomchristie.github.io/rest-framework-2-docs/) is also available. -For more details see the [3.0 release notes](3.0-announcement). +For more details see the [3.0 release notes][3.0-announcement]. --- diff --git a/docs/topics/3.0-announcement.md b/docs/topics/3.0-announcement.md index b32fe510..72fee019 100644 --- a/docs/topics/3.0-announcement.md +++ b/docs/topics/3.0-announcement.md @@ -6,9 +6,11 @@ This release is incremental in nature. There *are* some breaking API changes, an The difference in quality of the REST framework API and implementation should make writing, maintaining and debugging your application far easier. -3.0 is the first of three releases that have been funded by our recent [Kickstarter campaign](kickstarter.com/projects/tomchristie/django-rest-framework-3). +3.0 is the first of three releases that have been funded by our recent [Kickstarter campaign][kickstarter]. -As ever, a huge thank you to our many [wonderful sponsors](sponsors). If you're looking for a Django gig, and want to work with smart community-minded folks, you should probably check out that list and see who's hiring. +As ever, a huge thank you to our many [wonderful sponsors][sponsors]. If you're looking for a Django gig, and want to work with smart community-minded folks, you should probably check out that list and see who's hiring. + +--- ## New features @@ -51,6 +53,8 @@ Instead of passing the files argument separately: The usage of `request.QUERY_PARAMS` is now pending deprecation in favor of the lowercased `request.query_params`. +--- + ## Serializers #### Single-step object creation. @@ -149,7 +153,7 @@ Previously `serializers.ValidationError` error was simply a synonym for `django. The reason behind this is that Django's `ValidationError` class is intended for use with HTML forms and its API makes using it slightly awkward with nested validation errors that can occur in serializers. -For most users this change shouldn't require any updates to your codebase, but it is worth ensuring that whenever raising validation errors you are always using the `serializers.ValidationError` exception class, and not Django's built-in exception. +For most users this change shouldn't require any updates to your codebase, but it is worth ensuring that whenever raising validation errors you should prefer using the `serializers.ValidationError` exception class, and not Django's built-in exception. We strongly recommend that you use the namespaced import style of `import serializers` and not `from serializers import ValidationError` in order to avoid any potential confusion. @@ -218,7 +222,18 @@ If you absolutely need to preserve `transform_` behavior, for exampl This change also means that we no longer use the `.full_clean()` method on model instances, but instead perform all validation explicitly on the serializer. This gives a cleaner separation, and ensures that there's no automatic validation behavior on `ModelSerializer` classes that can't also be easily replicated on regular `Serializer` classes. -It's important to note that this change also means that the model `.clean()` method will not be called as part of serializer validation, as it would be if using a `ModelForm`. Use the serializer `.validate()` method to perform a final validation step on incoming data where required. +For the most part this change should be transparent. Field validation and uniqueness checks will still be run as normal, but the implementation is a little different. + +The one difference that you do need to note is that the `.clean()` method will not be called as part of serializer validation, as it would be if using a `ModelForm`. Use the serializer `.validate()` method to perform a final validation step on incoming data where required. + +There may be some cases where you really do need to keep validation logic in the model `.clean()` method, and cannot instead separate it into the serializer `.validate()`. You can do so by explicitly instantiating a model instance in the `.validate()` method. + + def validate(self, attrs): + instance = ExampleModel(**attrs) + instance.clean() + return attrs + +Again, you really should look at properly separating the validation logic out of the model method if possible, but the above might be useful in some backwards compatibility cases, or for an easy migration path. #### Writable nested serialization. @@ -524,6 +539,8 @@ The following class is an example of a generic serializer that can handle coerci # Force anything else to its string representation. output[attribute_name] = str(attribute) +--- + ## Serializer fields #### The `Field` and `ReadOnly` field classes. @@ -721,6 +738,8 @@ REST framework also now includes explicit validator classes for validating the ` These classes are documented in the [Validators](../api-guide/validators.md) section of the documentation. +--- + ## Generic views #### Simplification of view logic. @@ -769,12 +788,16 @@ The generic views now raise `ValidationFailed` exception for invalid data. This This change means that you can now easily customize the style of error responses across your entire API, without having to modify any of the generic views. +--- + ## The metadata API Behavior for dealing with `OPTIONS` requests was previously built directly into the class based views. This has now been properly separated out into a Metadata API that allows the same pluggable style as other API policies in REST framework. This makes it far easier to use a different style for `OPTIONS` responses throughout your API, and makes it possible to create third-party metadata policies. +--- + ## Serializers as HTML forms REST framework 3.0 includes templated HTML form rendering for serializers. @@ -806,6 +829,8 @@ Similarly, to use a radio button control instead of the default `select` control This API should be considered provisional, and there may be minor alterations with the incoming 3.1 release. +--- + ## API style There are some improvements in the default style we use in our API responses. @@ -899,12 +924,16 @@ Or modify it on an individual serializer field, using the `coerce_to_string` key The default JSON renderer will return float objects for un-coerced `Decimal` instances. This allows you to easily switch between string or float representations for decimals depending on your API design needs. -## Miscellaneous notes. +--- + +## Miscellaneous notes * The serializer `ChoiceField` does not currently display nested choices, as was the case in 2.4. This will be address as part of 3.1. * Due to the new templated form rendering, the 'widget' option is no longer valid. This means there's no easy way of using third party "autocomplete" widgets for rendering select inputs that contain a large number of choices. You'll either need to use a regular select or a plain text input. We may consider addressing this in 3.1 or 3.2 if there's sufficient demand. -## What's coming next. +--- + +## What's coming next 3.0 is an incremental release, and there are several upcoming features that will build on the baseline improvements that it makes. @@ -919,5 +948,6 @@ The 3.2 release is planned to introduce an alternative admin-style interface to You can follow development on the GitHub site, where we use [milestones to indicate planning timescales](https://github.com/tomchristie/django-rest-framework/milestones). +[kickstarter]: http://kickstarter.com/projects/tomchristie/django-rest-framework-3 [sponsors]: http://www.django-rest-framework.org/topics/kickstarter-announcement/#sponsors [mixins.py]: https://github.com/tomchristie/django-rest-framework/blob/master/rest_framework/mixins.py diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index f7aa3a7d..de0d026d 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -11,6 +11,7 @@ python primitives. response content is handled by parsers and renderers. """ from django.core.exceptions import ImproperlyConfigured +from django.core.exceptions import ValidationError as DjangoValidationError from django.db import models from django.db.models.fields import FieldDoesNotExist from django.utils import six @@ -330,6 +331,14 @@ class Serializer(BaseSerializer): raise ValidationError({ api_settings.NON_FIELD_ERRORS_KEY: [exc.detail] }) + except DjangoValidationError as exc: + # Normally you should raise `serializers.ValidationError` + # inside your codebase, but we handle Django's validation + # exception class as well for simpler compat. + # Eg. Calling Model.clean() explictily inside Serializer.validate() + raise ValidationError({ + api_settings.NON_FIELD_ERRORS_KEY: list(exc.messages) + }) return value @@ -353,6 +362,8 @@ class Serializer(BaseSerializer): validated_value = validate_method(validated_value) except ValidationError as exc: errors[field.field_name] = exc.detail + except DjangoValidationError as exc: + errors[field.field_name] = list(exc.messages) except SkipField: pass else: -- cgit v1.2.3 From 22c5b863bc395a6df84ec2536849540d3e92da7a Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Mon, 1 Dec 2014 11:37:38 +0000 Subject: More descriptive docstring on ModelSerializer --- rest_framework/serializers.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index de0d026d..3783b285 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -565,6 +565,14 @@ class ModelSerializer(Serializer): * A set of default fields are automatically populated. * A set of default validators are automatically populated. * Default `.create()` and `.update()` implementations are provided. + + The process of automatically determining a set of serializer fields + based on the model fields is reasonably complex, but you almost certainly + don't need to dig into the implemention. + + If the `ModelSerializer` class *doesn't* generate the set of fields that + you need you should either declare the extra/differing fields explicitly on + the serializer class, or simply use a `Serializer` class. """ _field_mapping = ClassLookupDict({ models.AutoField: IntegerField, -- cgit v1.2.3 From 270c7acdd75e9dd0b7a600e5648b0ec364c9a510 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Mon, 1 Dec 2014 11:59:04 +0000 Subject: Minor validtors tweak --- docs/topics/3.0-announcement.md | 9 +++++---- rest_framework/serializers.py | 31 ++++++++++++++++++++++++++++--- 2 files changed, 33 insertions(+), 7 deletions(-) diff --git a/docs/topics/3.0-announcement.md b/docs/topics/3.0-announcement.md index 72fee019..281e33bd 100644 --- a/docs/topics/3.0-announcement.md +++ b/docs/topics/3.0-announcement.md @@ -727,10 +727,11 @@ The `UniqueTogetherValidator` should be applied to a serializer, and takes a `qu position = serializers.IntegerField() name = serializers.CharField(max_length=100) - default_validators = [UniqueTogetherValidator( - queryset=RaceResult.objects.all(), - fields=('category', 'position') - )] + class Meta: + validators = [UniqueTogetherValidator( + queryset=RaceResult.objects.all(), + fields=('category', 'position') + )] #### The `UniqueForDateValidator` classes. diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 3783b285..0d0a4d9a 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -601,6 +601,26 @@ class ModelSerializer(Serializer): _related_class = PrimaryKeyRelatedField def create(self, validated_attrs): + """ + We have a bit of extra checking around this in order to provide + descriptive messages when something goes wrong, but this method is + essentially just: + + return ExampleModel.objects.create(**validated_attrs) + + If there are many to many fields present on the instance then they + cannot be set until the model is instantiated, in which case the + implementation is like so: + + example_relationship = validated_attrs.pop('example_relationship') + instance = ExampleModel.objects.create(**validated_attrs) + instance.example_relationship = example_relationship + return instance + + The default implementation also does not handle nested relationships. + If you want to support writable nested relationships you'll need + to write an explicit `.create()` method. + """ # Check that the user isn't trying to handle a writable nested field. # If we don't do this explicitly they'd likely get a confusing # error at the point of calling `Model.objects.create()`. @@ -651,14 +671,19 @@ class ModelSerializer(Serializer): return instance def get_validators(self): + # If the validators have been declared explicitly then use that. + validators = getattr(getattr(self, 'Meta', None), 'validators', None) + if validators is not None: + return validators + + # Determine the default set of validators. + validators = [] + model_class = self.Meta.model field_names = set([ field.source for field in self.fields.values() if (field.source != '*') and ('.' not in field.source) ]) - validators = getattr(getattr(self, 'Meta', None), 'validators', []) - model_class = self.Meta.model - # Note that we make sure to check `unique_together` both on the # base model class, but also on any parent classes. for parent_class in [model_class] + list(model_class._meta.parents.keys()): -- cgit v1.2.3 From 1caa14dd1425812fd47f58e859d694a81915caf3 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Mon, 1 Dec 2014 12:22:21 +0000 Subject: Minor 3.0 announcment tweaks --- docs/topics/3.0-announcement.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/topics/3.0-announcement.md b/docs/topics/3.0-announcement.md index 281e33bd..4aafb3d8 100644 --- a/docs/topics/3.0-announcement.md +++ b/docs/topics/3.0-announcement.md @@ -1,8 +1,8 @@ -# REST framework 3.0 +# Django REST framework 3.0 The 3.0 release of Django REST framework is the result of almost four years of iteration and refinement. It comprehensively addresses some of the previous remaining design issues in serializers, fields and the generic views. -This release is incremental in nature. There *are* some breaking API changes, and upgrading *will* require you to read the release notes carefully, but the migration path should otherwise be relatively straightforward. +**This release is incremental in nature. There *are* some breaking API changes, and upgrading *will* require you to read the release notes carefully, but the migration path should otherwise be relatively straightforward.** The difference in quality of the REST framework API and implementation should make writing, maintaining and debugging your application far easier. -- cgit v1.2.3 From 555c450497e96bf8fed82fc76e70adf907c5c409 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Mon, 1 Dec 2014 14:46:47 +0000 Subject: Add missing 'validators.py' link. Closes #2166. --- docs/api-guide/validators.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/api-guide/validators.md b/docs/api-guide/validators.md index f087e191..8f5a8929 100644 --- a/docs/api-guide/validators.md +++ b/docs/api-guide/validators.md @@ -1,4 +1,4 @@ - +source: validators.py --- -- cgit v1.2.3 From 080fa4f5f863609f5647ce1424f13b01e9f427ad Mon Sep 17 00:00:00 2001 From: José Padilla Date: Mon, 1 Dec 2014 11:26:50 -0400 Subject: Change allow_none to allow_null --- docs/topics/3.0-announcement.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/topics/3.0-announcement.md b/docs/topics/3.0-announcement.md index 4aafb3d8..8791ad08 100644 --- a/docs/topics/3.0-announcement.md +++ b/docs/topics/3.0-announcement.md @@ -557,22 +557,22 @@ We now use the following: * `Field` is the base class for all fields. It does not include any default implementation for either serializing or deserializing data. * `ReadOnlyField` is a concrete implementation for read-only fields that simply returns the attribute value without modification. -#### The `required`, `allow_none`, `allow_blank` and `default` arguments. +#### The `required`, `allow_null`, `allow_blank` and `default` arguments. REST framework now has more explicit and clear control over validating empty values for fields. Previously the meaning of the `required=False` keyword argument was underspecified. In practice its use meant that a field could either be not included in the input, or it could be included, but be `None` or the empty string. -We now have a better separation, with separate `required`, `allow_none` and `allow_blank` arguments. +We now have a better separation, with separate `required`, `allow_null` and `allow_blank` arguments. The following set of arguments are used to control validation of empty values: * `required=False`: The value does not need to be present in the input, and will not be passed to `.create()` or `.update()` if it is not seen. * `default=`: The value does not need to be present in the input, and a default value will be passed to `.create()` or `.update()` if it is not seen. -* `allow_none=True`: `None` is a valid input. +* `allow_null=True`: `None` is a valid input. * `allow_blank=True`: `''` is valid input. For `CharField` and subclasses only. -Typically you'll want to use `required=False` if the corresponding model field has a default value, and additionally set either `allow_none=True` or `allow_blank=True` if required. +Typically you'll want to use `required=False` if the corresponding model field has a default value, and additionally set either `allow_null=True` or `allow_blank=True` if required. The `default` argument is also available and always implies that the field is not required to be in the input. It is unnecessary to use the `required` argument when a default is specified, and doing so will result in an error. -- cgit v1.2.3 From d1fe61ce94af1e942f8d1026fb84b1909c230779 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Mon, 1 Dec 2014 17:05:11 +0000 Subject: Fix requirements. Closes #2170. --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 8a698230..f284644a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1 +1 @@ -Django>=1.4.2 +Django>=1.4.11 -- cgit v1.2.3 From bc0c25df3020772124e1767895c7e7cb60d974c1 Mon Sep 17 00:00:00 2001 From: José Padilla Date: Mon, 1 Dec 2014 17:31:11 -0400 Subject: Consolidate Django and test requirements --- CONTRIBUTING.md | 17 ++++++++--------- docs/topics/contributing.md | 1 - docs/topics/third-party-resources.md | 2 +- requirements-test.txt | 15 --------------- requirements.txt | 17 +++++++++++++++++ 5 files changed, 26 insertions(+), 26 deletions(-) delete mode 100644 requirements-test.txt diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 69802995..96e55161 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -14,7 +14,7 @@ If you use REST framework, we'd love you to be vocal about your experiences with Other really great ways you can help move the community forward include helping answer questions on the [discussion group][google-group], or setting up an [email alert on StackOverflow][so-filter] so that you get notified of any new questions with the `django-rest-framework` tag. -When answering questions make sure to help future contributors find their way around by hyperlinking wherever possible to related threads and tickets, and include backlinks from those items if relevant. +When answering questions make sure to help future contributors find their way around by hyperlinking wherever possible to related threads and tickets, and include backlinks from those items if relevant. ## Code of conduct @@ -38,7 +38,7 @@ Some tips on good issue reporting: ## Triaging issues -Getting involved in triaging incoming issues is a good way to start contributing. Every single ticket that comes into the ticket tracker needs to be reviewed in order to determine what the next steps should be. Anyone can help out with this, you just need to be willing to +Getting involved in triaging incoming issues is a good way to start contributing. Every single ticket that comes into the ticket tracker needs to be reviewed in order to determine what the next steps should be. Anyone can help out with this, you just need to be willing to * Read through the ticket - does it make sense, is it missing any context that would help explain it better? * Is the ticket reported in the correct place, would it be better suited as a discussion on the discussion group? @@ -62,7 +62,6 @@ To run the tests, clone the repository, and then: virtualenv env env/bin/activate pip install -r requirements.txt - pip install -r requirements-test.txt # Run the tests ./runtests.py @@ -130,8 +129,8 @@ There are a couple of conventions you should follow when working on the document Headers should use the hash style. For example: ### Some important topic - -The underline style should not be used. **Don't do this:** + +The underline style should not be used. **Don't do this:** Some important topic ==================== @@ -141,9 +140,9 @@ The underline style should not be used. **Don't do this:** Links should always use the reference style, with the referenced hyperlinks kept at the end of the document. Here is a link to [some other thing][other-thing]. - + More text... - + [other-thing]: http://example.com/other/thing This style helps keep the documentation source consistent and readable. @@ -159,9 +158,9 @@ Linking in this style means you'll be able to click the hyperlink in your markdo If you want to draw attention to a note or warning, use a pair of enclosing lines, like so: --- - + **Note:** A useful documentation note. - + --- # Third party packages diff --git a/docs/topics/contributing.md b/docs/topics/contributing.md index 99f4fc3c..c9626ebf 100644 --- a/docs/topics/contributing.md +++ b/docs/topics/contributing.md @@ -62,7 +62,6 @@ To run the tests, clone the repository, and then: virtualenv env source env/bin/activate pip install -r requirements.txt - pip install -r requirements-test.txt # Run the tests ./runtests.py diff --git a/docs/topics/third-party-resources.md b/docs/topics/third-party-resources.md index efa0b91f..0358d614 100644 --- a/docs/topics/third-party-resources.md +++ b/docs/topics/third-party-resources.md @@ -93,7 +93,7 @@ The cookiecutter template includes a `runtests.py` which uses the `pytest` packa Before running, you'll need to install a couple test requirements. - $ pip install -r requirements-test.txt + $ pip install -r requirements.txt Once requirements installed, you can run `runtests.py`. diff --git a/requirements-test.txt b/requirements-test.txt deleted file mode 100644 index 06c8849a..00000000 --- a/requirements-test.txt +++ /dev/null @@ -1,15 +0,0 @@ -# Test requirements -pytest-django==2.6 -pytest==2.5.2 -pytest-cov==1.6 -flake8==2.2.2 - -# Optional packages -markdown>=2.1.0 -PyYAML>=3.10 -defusedxml>=0.3 -django-guardian==1.2.4 -django-filter>=0.5.4 -django-oauth-plus>=2.2.1 -oauth2>=1.5.211 -django-oauth2-provider>=0.2.4 diff --git a/requirements.txt b/requirements.txt index f284644a..f282d3ba 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1 +1,18 @@ +# Minimum Django version Django>=1.4.11 + +# Test requirements +pytest-django==2.6 +pytest==2.5.2 +pytest-cov==1.6 +flake8==2.2.2 + +# Optional packages +markdown>=2.1.0 +PyYAML>=3.10 +defusedxml>=0.3 +django-guardian==1.2.4 +django-filter>=0.5.4 +django-oauth-plus>=2.2.1 +oauth2>=1.5.211 +django-oauth2-provider>=0.2.4 -- cgit v1.2.3 From f4fc4670ca491eabd5bcdfcef382d8373dd5e380 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Tue, 2 Dec 2014 08:53:36 +0000 Subject: Promote 'many_init' to public API. Closes #2152. --- docs/api-guide/serializers.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/docs/api-guide/serializers.md b/docs/api-guide/serializers.md index 0ee80d53..4c78473e 100644 --- a/docs/api-guide/serializers.md +++ b/docs/api-guide/serializers.md @@ -689,6 +689,21 @@ Here's an example of how you might choose to implement multiple updates: It is possible that a third party package may be included alongside the 3.1 release that provides some automatic support for multiple update operations, similar to the `allow_add_remove` behavior that was present in REST framework 2. +#### Customizing ListSerializer initialization + +When a serializer with `many=True` is instantiated, we need to determine which arguments and keyword arguments should be passed to the `.__init__()` method for both the child `Serializer` class, and for the parent `ListSerializer` class. + +The default implementation is to pass all arguments to both classes, except for `validators`, and any custom keyword arguments, both of which are assumed to be intended for the child serializer class. + +Occasionally you might need to explicitly specify how the child and parent classes should be instantiated when `many=True` is passed. You can do so by using the `many_init` class method. + + @classmethod + def many_init(cls, *args, **kwargs): + # Instantiate the child serializer. + kwargs['child'] = cls() + # Instantiate the parent list serializer. + return CustomListSerializer(*args, **kwargs) + --- # BaseSerializer -- cgit v1.2.3 From 53f52765fc90472a05cbeb34760b45f735a7332c Mon Sep 17 00:00:00 2001 From: BrickXu Date: Tue, 2 Dec 2014 12:55:34 +0800 Subject: Not allow to pass an empty actions to viewset.as_view(). Refs issue #2171 --- rest_framework/viewsets.py | 6 ++++++ tests/test_viewsets.py | 35 +++++++++++++++++++++++++++++++++++ 2 files changed, 41 insertions(+) create mode 100644 tests/test_viewsets.py diff --git a/rest_framework/viewsets.py b/rest_framework/viewsets.py index 84b4bd8d..70d14695 100644 --- a/rest_framework/viewsets.py +++ b/rest_framework/viewsets.py @@ -48,6 +48,12 @@ class ViewSetMixin(object): # eg. 'List' or 'Instance'. cls.suffix = None + # actions must not be empty + if not actions: + raise TypeError("The `actions` argument must be provided when " + "calling `.as_view()` on a ViewSet. For example " + "`.as_view({'get': 'list'})`") + # sanitize keyword arguments for key in initkwargs: if key in cls.http_method_names: diff --git a/tests/test_viewsets.py b/tests/test_viewsets.py new file mode 100644 index 00000000..4d18a955 --- /dev/null +++ b/tests/test_viewsets.py @@ -0,0 +1,35 @@ +from django.test import TestCase +from rest_framework import status +from rest_framework.response import Response +from rest_framework.test import APIRequestFactory +from rest_framework.viewsets import GenericViewSet + + +factory = APIRequestFactory() + + +class BasicViewSet(GenericViewSet): + def list(self, request, *args, **kwargs): + return Response({'ACTION': 'LIST'}) + + +class InitializeViewSetsTestCase(TestCase): + def test_initialize_view_set_with_actions(self): + request = factory.get('/', '', content_type='application/json') + my_view = BasicViewSet.as_view(actions={ + 'get': 'list', + }) + + response = my_view(request) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data, {'ACTION': 'LIST'}) + + def test_initialize_view_set_with_empty_actions(self): + try: + BasicViewSet.as_view() + except TypeError as e: + self.assertEqual(str(e), "The `actions` argument must be provided " + "when calling `.as_view()` on a ViewSet. " + "For example `.as_view({'get': 'list'})`") + else: + self.fail("actions must not be empty.") -- cgit v1.2.3 From 6ac79b822325784ad145ff0ad064127750c4f7e0 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Tue, 2 Dec 2014 09:19:59 +0000 Subject: Document Field.fail(). Closes #2147. --- docs/api-guide/fields.md | 49 +++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 48 insertions(+), 1 deletion(-) diff --git a/docs/api-guide/fields.md b/docs/api-guide/fields.md index 391a52e5..aa5cc84e 100644 --- a/docs/api-guide/fields.md +++ b/docs/api-guide/fields.md @@ -453,7 +453,7 @@ If you want to create a custom field, you'll need to subclass `Field` and then o The `.to_representation()` method is called to convert the initial datatype into a primitive, serializable datatype. -The `to_internal_value()` method is called to restore a primitive datatype into its internal python representation. +The `to_internal_value()` method is called to restore a primitive datatype into its internal python representation. This method should raise a `serializer.ValidationError` if the data is invalid. Note that the `WritableField` class that was present in version 2.x no longer exists. You should subclass `Field` and override `to_internal_value()` if the field supports data input. @@ -498,6 +498,53 @@ As an example, let's create a field that can be used represent the class name of """ return obj.__class__.__name__ +#### Raising validation errors + +Our `ColorField` class above currently does not perform any data validation. +To indicate invalid data, we should raise a `serializers.ValidationError`, like so: + + def to_internal_value(self, data): + if not isinstance(data, six.text_type): + msg = 'Incorrect type. Expected a string, but got %s' + raise ValidationError(msg % type(data).__name__) + + if not re.match(r'^rgb\([0-9]+,[0-9]+,[0-9]+\)$', data): + raise ValidationError('Incorrect format. Expected `rgb(#,#,#)`.') + + data = data.strip('rgb(').rstrip(')') + red, green, blue = [int(col) for col in data.split(',')] + + if any([col > 255 or col < 0 for col in (red, green, blue)]): + raise ValidationError('Value out of range. Must be between 0 and 255.') + + return Color(red, green, blue) + +The `.fail()` method is a shortcut for raising `ValidationError` that takes a message string from the `error_messages` dictionary. For example: + + default_error_messages = { + 'incorrect_type': 'Incorrect type. Expected a string, but got {input_type}', + 'incorrect_format': 'Incorrect format. Expected `rgb(#,#,#)`.', + 'out_of_range': 'Value out of range. Must be between 0 and 255.' + } + + def to_internal_value(self, data): + if not isinstance(data, six.text_type): + msg = 'Incorrect type. Expected a string, but got %s' + self.fail('incorrect_type', input_type=type(data).__name__) + + if not re.match(r'^rgb\([0-9]+,[0-9]+,[0-9]+\)$', data): + self.fail('incorrect_format') + + data = data.strip('rgb(').rstrip(')') + red, green, blue = [int(col) for col in data.split(',')] + + if any([col > 255 or col < 0 for col in (red, green, blue)]): + self.fail('out_of_range') + + return Color(red, green, blue) + +This style keeps you error messages more cleanly separated from your code, and should be preferred. + # Third party packages The following third party packages are also available. -- cgit v1.2.3 From 79e18a2a06178e8c00dfafc1cfd062f2528ec2c1 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Tue, 2 Dec 2014 09:27:40 +0000 Subject: Raise assertion error if calling .save() on a serializer with errors. Closes #2098. --- rest_framework/serializers.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 0d0a4d9a..a4140c0f 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -127,6 +127,14 @@ class BaseSerializer(Field): (self.__class__.__module__, self.__class__.__name__) ) + assert hasattr(self, '_errors'), ( + 'You must call `.is_valid()` before calling `.save()`.' + ) + + assert not self.errors, ( + 'You cannot call `.save()` on a serializer with invalid data.' + ) + validated_data = dict( list(self.validated_data.items()) + list(kwargs.items()) -- cgit v1.2.3 From 76ac641fbd6c9d7dff5da3c551c3fd1ef7dedd2e Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Tue, 2 Dec 2014 13:04:49 +0000 Subject: Minor tweaks for helpful message on Model.objects.create() failure. --- rest_framework/serializers.py | 23 ++++++++++++++--------- tests/test_model_serializer.py | 10 +++------- 2 files changed, 17 insertions(+), 16 deletions(-) diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 143d205d..d417ca80 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -35,7 +35,6 @@ from rest_framework.validators import ( ) import copy import inspect -import sys import warnings # Note: We do the following so that users of the framework can use this style: @@ -658,14 +657,20 @@ class ModelSerializer(Serializer): instance = ModelClass.objects.create(**validated_attrs) except TypeError as exc: msg = ( - 'The mentioned argument might be a field on the serializer ' - 'that is not part of the model. You need to override the ' - 'create() method in your ModelSerializer subclass to support ' - 'this.') - six.reraise( - type(exc), - type(exc)(str(exc) + '. ' + msg), - sys.exc_info()[2]) + 'Got a `TypeError` when calling `%s.objects.create()`. ' + 'This may be because you have a writable field on the ' + 'serializer class that is not a valid argument to ' + '`%s.objects.create()`. You may need to make the field ' + 'read-only, or override the %s.create() method to handle ' + 'this correctly.\nOriginal exception text was: %s.' % + ( + ModelClass.__name__, + ModelClass.__name__, + self.__class__.__name__, + exc + ) + ) + raise TypeError(msg) # Save many-to-many relationships after the instance is created. if many_to_many: diff --git a/tests/test_model_serializer.py b/tests/test_model_serializer.py index 90767dac..1bcd58e0 100644 --- a/tests/test_model_serializer.py +++ b/tests/test_model_serializer.py @@ -10,7 +10,6 @@ from django.core.validators import MaxValueValidator, MinValueValidator, MinLeng from django.db import models from django.test import TestCase from rest_framework import serializers -import pytest def dedent(blocktext): @@ -87,13 +86,10 @@ class TestModelSerializer(TestCase): 'non_model_field': 'bar', }) serializer.is_valid() - with pytest.raises(TypeError): + with self.assertRaises(TypeError) as excinfo: serializer.save() - - try: - serializer.save() - except TypeError as exc: - assert 'ModelSerializer' in str(exc) + msginitial = 'Got a `TypeError` when calling `OneFieldModel.objects.create()`.' + assert str(excinfo.exception).startswith(msginitial) class TestRegularFieldMappings(TestCase): -- cgit v1.2.3 From e30e3f6dfc8cdb47c1048bbe497599d250d7bf75 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Tue, 2 Dec 2014 13:14:11 +0000 Subject: Update README with 3.0 info. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 31fe1e52..95d05c9b 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ Full documentation for the project is available at [http://www.django-rest-frame --- -**Note**: The incoming 3.0 version has now been merged to the `master` branch on GitHub. For the source of the currently available PyPI version, please see the `2.4.4` tag. +**Note**: We have now released Django REST framework 3.0. For older codebases you may want to refer to the version 2.4.4 [source code](https://github.com/tomchristie/django-rest-framework/tree/version-2.4.x), and [documentation](http://tomchristie.github.io/rest-framework-2-docs/). --- -- cgit v1.2.3 From 0359e9250d34e18aef2db6216f24c130a4f51fce Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Tue, 2 Dec 2014 13:52:46 +0000 Subject: FileUploadParser. Raising StopFutureHandlers removes any handlers not yet run for the active set. Closes #2109. --- rest_framework/parsers.py | 13 +++++++------ tests/test_parsers.py | 32 ++++++++++++++++++++++++++++++-- 2 files changed, 37 insertions(+), 8 deletions(-) diff --git a/rest_framework/parsers.py b/rest_framework/parsers.py index ccb82f03..d229abec 100644 --- a/rest_framework/parsers.py +++ b/rest_framework/parsers.py @@ -256,23 +256,24 @@ class FileUploadParser(BaseParser): chunks = ChunkIter(stream, chunk_size) counters = [0] * len(upload_handlers) - for handler in upload_handlers: + for index, handler in enumerate(upload_handlers): try: handler.new_file(None, filename, content_type, content_length, encoding) except StopFutureHandlers: + upload_handlers = upload_handlers[:index + 1] break for chunk in chunks: - for i, handler in enumerate(upload_handlers): + for index, handler in enumerate(upload_handlers): chunk_length = len(chunk) - chunk = handler.receive_data_chunk(chunk, counters[i]) - counters[i] += chunk_length + chunk = handler.receive_data_chunk(chunk, counters[index]) + counters[index] += chunk_length if chunk is None: break - for i, handler in enumerate(upload_handlers): - file_obj = handler.file_complete(counters[i]) + for index, handler in enumerate(upload_handlers): + file_obj = handler.file_complete(counters[index]) if file_obj: return DataAndFiles(None, {'file': file_obj}) raise ParseError("FileUpload parse error - " diff --git a/tests/test_parsers.py b/tests/test_parsers.py index 3f2672df..88eccef3 100644 --- a/tests/test_parsers.py +++ b/tests/test_parsers.py @@ -1,12 +1,13 @@ # -*- coding: utf-8 -*- from __future__ import unicode_literals -from rest_framework.compat import StringIO from django import forms from django.core.files.uploadhandler import MemoryFileUploadHandler from django.test import TestCase from django.utils import unittest from rest_framework.compat import etree +from rest_framework.compat import StringIO +from rest_framework.exceptions import ParseError from rest_framework.parsers import FormParser, FileUploadParser from rest_framework.parsers import XMLParser import datetime @@ -104,13 +105,40 @@ class TestFileUploadParser(TestCase): self.parser_context = {'request': request, 'kwargs': {}} def test_parse(self): - """ Make sure the `QueryDict` works OK """ + """ + Parse raw file upload. + """ parser = FileUploadParser() self.stream.seek(0) data_and_files = parser.parse(self.stream, None, self.parser_context) file_obj = data_and_files.files['file'] self.assertEqual(file_obj._size, 14) + def test_parse_missing_filename(self): + """ + Parse raw file upload when filename is missing. + """ + parser = FileUploadParser() + self.stream.seek(0) + self.parser_context['request'].META['HTTP_CONTENT_DISPOSITION'] = '' + with self.assertRaises(ParseError): + parser.parse(self.stream, None, self.parser_context) + + def test_parse_missing_filename_multiple_upload_handlers(self): + """ + Parse raw file upload with multiple handlers when filename is missing. + Regression test for #2109. + """ + parser = FileUploadParser() + self.stream.seek(0) + self.parser_context['request'].upload_handlers = ( + MemoryFileUploadHandler(), + MemoryFileUploadHandler() + ) + self.parser_context['request'].META['HTTP_CONTENT_DISPOSITION'] = '' + with self.assertRaises(ParseError): + parser.parse(self.stream, None, self.parser_context) + def test_get_filename(self): parser = FileUploadParser() filename = parser.get_filename(self.stream, None, self.parser_context) -- cgit v1.2.3 From 84cff98fbf72355cb5e8359aa1c9b5568c289cbf Mon Sep 17 00:00:00 2001 From: David Ray Date: Tue, 2 Dec 2014 09:46:43 -0500 Subject: fix typo --- docs/api-guide/serializers.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/api-guide/serializers.md b/docs/api-guide/serializers.md index 4c78473e..a011bb52 100644 --- a/docs/api-guide/serializers.md +++ b/docs/api-guide/serializers.md @@ -96,7 +96,7 @@ If we want to be able to return complete object instances based on the validated If your object instances correspond to Django models you'll also want to ensure that these methods save the object to the database. For example, if `Comment` was a Django model, the methods might look like this: def create(self, validated_data): - return Comment.objcts.create(**validated_data) + return Comment.objects.create(**validated_data) def update(self, instance, validated_data): instance.email = validated_data.get('email', instance.email) -- cgit v1.2.3 From 33096a1de6c20581caab36bc1af0e686d47483e7 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Tue, 2 Dec 2014 15:15:21 +0000 Subject: BindingDict inherits from collections.MutableMapping. Closes #2135. --- rest_framework/utils/serializer_helpers.py | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/rest_framework/utils/serializer_helpers.py b/rest_framework/utils/serializer_helpers.py index 92d19857..277cf649 100644 --- a/rest_framework/utils/serializer_helpers.py +++ b/rest_framework/utils/serializer_helpers.py @@ -1,3 +1,4 @@ +import collections from rest_framework.compat import OrderedDict @@ -70,7 +71,7 @@ class NestedBoundField(BoundField): return BoundField(field, value, error, prefix=self.name + '.') -class BindingDict(object): +class BindingDict(collections.MutableMapping): """ This dict-like object is used to store fields on a serializer. @@ -92,11 +93,8 @@ class BindingDict(object): def __delitem__(self, key): del self.fields[key] - def items(self): - return self.fields.items() - - def keys(self): - return self.fields.keys() + def __iter__(self): + return iter(self.fields) - def values(self): - return self.fields.values() + def __len__(self): + return len(self.fields) -- cgit v1.2.3 From 5ad22aea605f888e06186d907674669c46a611ef Mon Sep 17 00:00:00 2001 From: Matías Lang Date: Tue, 2 Dec 2014 12:23:25 -0300 Subject: Updated serializers documentation There was an error in the docs: the field extra_field_kwargs of the serializer's Meta doesn't work. The field must be extra_kwargs instead.--- docs/api-guide/serializers.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/api-guide/serializers.md b/docs/api-guide/serializers.md index a011bb52..1779c863 100644 --- a/docs/api-guide/serializers.md +++ b/docs/api-guide/serializers.md @@ -567,13 +567,13 @@ There needs to be a way of determining which views should be used for hyperlinki By default hyperlinks are expected to correspond to a view name that matches the style `'{model_name}-detail'`, and looks up the instance by a `pk` keyword argument. -You can override a URL field view name and lookup field by using either, or both of, the `view_name` and `lookup_field` options in the `extra_field_kwargs` setting, like so: +You can override a URL field view name and lookup field by using either, or both of, the `view_name` and `lookup_field` options in the `extra_kwargs` setting, like so: class AccountSerializer(serializers.HyperlinkedModelSerializer): class Meta: model = Account fields = ('account_url', 'account_name', 'users', 'created') - extra_field_kwargs = { + extra_kwargs = { 'url': {'view_name': 'accounts', 'lookup_field': 'account_name'} 'users': {'lookup_field': 'username'} } -- cgit v1.2.3 From 9397a653a4cfd6725616ea8e1b23093d3b0d59b5 Mon Sep 17 00:00:00 2001 From: eiriksm Date: Wed, 3 Dec 2014 09:22:42 +0100 Subject: Use svg version of travis build status badge --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 95d05c9b..b8957545 100644 --- a/README.md +++ b/README.md @@ -180,7 +180,7 @@ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -[build-status-image]: https://secure.travis-ci.org/tomchristie/django-rest-framework.png?branch=master +[build-status-image]: https://secure.travis-ci.org/tomchristie/django-rest-framework.svg?branch=master [travis]: http://travis-ci.org/tomchristie/django-rest-framework?branch=master [pypi-version]: https://pypip.in/version/djangorestframework/badge.svg [pypi]: https://pypi.python.org/pypi/djangorestframework -- cgit v1.2.3 From 74a9ece3dcf76372c26aaf3bdd6c48bbfbf45f99 Mon Sep 17 00:00:00 2001 From: eiriksm Date: Wed, 3 Dec 2014 13:37:56 +0100 Subject: Update build status icon on github pages page. --- docs/index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/index.md b/docs/index.md index e0ba2332..52e42fc9 100644 --- a/docs/index.md +++ b/docs/index.md @@ -4,7 +4,7 @@ - +

--- -- cgit v1.2.3 From f2dd05a6e661525908fe5ec99b52b5274b04a198 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Wed, 3 Dec 2014 22:43:40 +0000 Subject: Improved nested update test in update(). Closes #2194. --- rest_framework/serializers.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index d417ca80..b1175b5b 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -681,8 +681,8 @@ class ModelSerializer(Serializer): def update(self, instance, validated_attrs): assert not any( - isinstance(field, BaseSerializer) and not field.read_only - for field in self.fields.values() + isinstance(field, BaseSerializer) and (key in validated_attrs) + for key, field in self.fields.values() ), ( 'The `.update()` method does not suport nested writable fields ' 'by default. Write an explicit `.update()` method for serializer ' -- cgit v1.2.3 From e1d98f77563abf49c4b19dcfb95f263515ae4087 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Wed, 3 Dec 2014 22:45:44 +0000 Subject: Improve nested update and create testing. --- rest_framework/serializers.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index b1175b5b..c7f04b40 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -633,8 +633,8 @@ class ModelSerializer(Serializer): # If we don't do this explicitly they'd likely get a confusing # error at the point of calling `Model.objects.create()`. assert not any( - isinstance(field, BaseSerializer) and not field.read_only - for field in self.fields.values() + isinstance(field, BaseSerializer) and (key in validated_attrs) + for key, field in self.fields.items() ), ( 'The `.create()` method does not suport nested writable fields ' 'by default. Write an explicit `.create()` method for serializer ' @@ -682,7 +682,7 @@ class ModelSerializer(Serializer): def update(self, instance, validated_attrs): assert not any( isinstance(field, BaseSerializer) and (key in validated_attrs) - for key, field in self.fields.values() + for key, field in self.fields.items() ), ( 'The `.update()` method does not suport nested writable fields ' 'by default. Write an explicit `.update()` method for serializer ' -- cgit v1.2.3 From ab25d706c78627dfd582fe9d142ada510c4d6d90 Mon Sep 17 00:00:00 2001 From: Martin Tschammer Date: Wed, 3 Dec 2014 23:52:35 +0100 Subject: Renamed validated_attrs to validated_data to be more in line with other similar code. --- docs/tutorial/1-serialization.md | 16 ++++++++-------- rest_framework/serializers.py | 20 ++++++++++---------- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/docs/tutorial/1-serialization.md b/docs/tutorial/1-serialization.md index a3c19858..52c75d2c 100644 --- a/docs/tutorial/1-serialization.md +++ b/docs/tutorial/1-serialization.md @@ -110,21 +110,21 @@ The first thing we need to get started on our Web API is to provide a way of ser style = serializers.ChoiceField(choices=STYLE_CHOICES, default='friendly') - def create(self, validated_attrs): + def create(self, validated_data): """ Create and return a new `Snippet` instance, given the validated data. """ - return Snippet.objects.create(**validated_attrs) + return Snippet.objects.create(**validated_data) - def update(self, instance, validated_attrs): + def update(self, instance, validated_data): """ Update and return an existing `Snippet` instance, given the validated data. """ - instance.title = validated_attrs.get('title', instance.title) - instance.code = validated_attrs.get('code', instance.code) - instance.linenos = validated_attrs.get('linenos', instance.linenos) - instance.language = validated_attrs.get('language', instance.language) - instance.style = validated_attrs.get('style', instance.style) + instance.title = validated_data.get('title', instance.title) + instance.code = validated_data.get('code', instance.code) + instance.linenos = validated_data.get('linenos', instance.linenos) + instance.language = validated_data.get('language', instance.language) + instance.style = validated_data.get('style', instance.style) instance.save() return instance diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index d417ca80..a289b021 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -608,20 +608,20 @@ class ModelSerializer(Serializer): }) _related_class = PrimaryKeyRelatedField - def create(self, validated_attrs): + def create(self, validated_data): """ We have a bit of extra checking around this in order to provide descriptive messages when something goes wrong, but this method is essentially just: - return ExampleModel.objects.create(**validated_attrs) + return ExampleModel.objects.create(**validated_data) If there are many to many fields present on the instance then they cannot be set until the model is instantiated, in which case the implementation is like so: - example_relationship = validated_attrs.pop('example_relationship') - instance = ExampleModel.objects.create(**validated_attrs) + example_relationship = validated_data.pop('example_relationship') + instance = ExampleModel.objects.create(**validated_data) instance.example_relationship = example_relationship return instance @@ -644,17 +644,17 @@ class ModelSerializer(Serializer): ModelClass = self.Meta.model - # Remove many-to-many relationships from validated_attrs. + # Remove many-to-many relationships from validated_data. # They are not valid arguments to the default `.create()` method, # as they require that the instance has already been saved. info = model_meta.get_field_info(ModelClass) many_to_many = {} for field_name, relation_info in info.relations.items(): - if relation_info.to_many and (field_name in validated_attrs): - many_to_many[field_name] = validated_attrs.pop(field_name) + if relation_info.to_many and (field_name in validated_data): + many_to_many[field_name] = validated_data.pop(field_name) try: - instance = ModelClass.objects.create(**validated_attrs) + instance = ModelClass.objects.create(**validated_data) except TypeError as exc: msg = ( 'Got a `TypeError` when calling `%s.objects.create()`. ' @@ -679,7 +679,7 @@ class ModelSerializer(Serializer): return instance - def update(self, instance, validated_attrs): + def update(self, instance, validated_data): assert not any( isinstance(field, BaseSerializer) and not field.read_only for field in self.fields.values() @@ -690,7 +690,7 @@ class ModelSerializer(Serializer): (self.__class__.__module__, self.__class__.__name__) ) - for attr, value in validated_attrs.items(): + for attr, value in validated_data.items(): setattr(instance, attr, value) instance.save() return instance -- cgit v1.2.3