From 6cce0681a9da20f46f57aa2c19796b4266c3d505 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 5 Dec 2014 09:44:35 +0000 Subject: [PATCH] Update documentation --- api-guide/fields/index.html | 45 +++++++++++++++++++++- api-guide/serializers/index.html | 21 ++++++++-- index.html | 6 +-- topics/2.3-announcement/index.html | 6 +-- topics/contributing/index.html | 1 - topics/release-notes/index.html | 4 +- topics/third-party-resources/index.html | 2 +- tutorial/1-serialization/index.html | 24 ++++++------ tutorial/6-viewsets-and-routers/index.html | 4 +- tutorial/quickstart/index.html | 5 +-- 10 files changed, 87 insertions(+), 31 deletions(-) diff --git a/api-guide/fields/index.html b/api-guide/fields/index.html index 9b4ad3e30..46fa5f687 100644 --- a/api-guide/fields/index.html +++ b/api-guide/fields/index.html @@ -851,7 +851,7 @@ class UserSerializer(serializers.ModelSerializer):

Custom fields

If you want to create a custom field, you'll need to subclass Field and then override either one or both of the .to_representation() and .to_internal_value() methods. These two methods are used to convert between the initial datatype, and a primitive, serializable datatype. Primitive datatypes will typically be any of a number, string, boolean, date/time/datetime or None. They may also be any list or dictionary like object that only contains other primitive objects. Other types might be supported, depending on the renderer that you are using.

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.

Examples

Let's look at an example of serializing a class that represents an RGB color value:

@@ -890,6 +890,49 @@ class ColorField(serializers.Field): """ 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.

DRF Compound Fields

diff --git a/api-guide/serializers/index.html b/api-guide/serializers/index.html index cbb4f32f1..75c3f4541 100644 --- a/api-guide/serializers/index.html +++ b/api-guide/serializers/index.html @@ -461,6 +461,10 @@ Customizing multiple update +
  • + Customizing ListSerializer initialization +
  • + @@ -617,7 +621,7 @@ serializer.validated_data

    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)
    @@ -982,12 +986,12 @@ AccountSerializer():
     

    How hyperlinked views are determined

    There needs to be a way of determining which views should be used for hyperlinking to model instances.

    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'}
             }
    @@ -1084,6 +1088,17 @@ class BookSerializer(serializers.Serializer):
             list_serializer_class = BookListSerializer
     

    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

    BaseSerializer class that can be used to easily support alternative serialization and deserialization styles.

    diff --git a/index.html b/index.html index 3eac76477..c6da0ba90 100644 --- a/index.html +++ b/index.html @@ -457,7 +457,7 @@ - +


    @@ -480,7 +480,7 @@

    Django REST framework is a powerful and flexible toolkit that makes it easy to build Web APIs.

    Some reasons you might want to use REST framework:

      -
    • The Web browseable API is a huge usability win for your developers.
    • +
    • The Web browsable API is a huge usability win for your developers.
    • Authentication policies including OAuth1a and OAuth2 out of the box.
    • Serialization that supports both ORM and non-ORM data sources.
    • Customizable all the way down - just use regular function-based views if you don't need the more powerful features.
    • @@ -564,7 +564,7 @@ router = routers.DefaultRouter() 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/topics/2.3-announcement/index.html b/topics/2.3-announcement/index.html index 84a82cb68..6c11420e9 100644 --- a/topics/2.3-announcement/index.html +++ b/topics/2.3-announcement/index.html @@ -464,7 +464,7 @@ router.register(r'groups', 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')) @@ -586,8 +586,8 @@ urlpatterns = [

      In the unlikely event that you're providing a custom serializer class, and implementing these methods you should note the new call signature for both methods is now (self, model_field, related_model, to_many). For reverse relationships model_field will be None.

      The old-style signature will continue to function but will raise a PendingDeprecationWarning.

      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.

      -

      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.

      +

      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 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.


      Other notes

      diff --git a/topics/contributing/index.html b/topics/contributing/index.html index a8ec0e2b1..1f556d232 100644 --- a/topics/contributing/index.html +++ b/topics/contributing/index.html @@ -467,7 +467,6 @@ 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/topics/release-notes/index.html b/topics/release-notes/index.html index 6534fa051..e3f9c67e5 100644 --- a/topics/release-notes/index.html +++ b/topics/release-notes/index.html @@ -498,7 +498,7 @@
    • 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.
    • @@ -571,7 +571,7 @@
    • 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/topics/third-party-resources/index.html b/topics/third-party-resources/index.html index 529a7b5aa..6f0e1e49e 100644 --- a/topics/third-party-resources/index.html +++ b/topics/third-party-resources/index.html @@ -437,7 +437,7 @@ You probably want to also tag the version now:

      Tests

      The cookiecutter template includes a runtests.py which uses the pytest package as a test runner.

      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.

      $ ./runtests.py
      diff --git a/tutorial/1-serialization/index.html b/tutorial/1-serialization/index.html
      index dda47f721..9b5ffb350 100644
      --- a/tutorial/1-serialization/index.html
      +++ b/tutorial/1-serialization/index.html
      @@ -483,7 +483,7 @@ from snippets.models import Snippet, LANGUAGE_CHOICES, STYLE_CHOICES
       
       class SnippetSerializer(serializers.Serializer):
           pk = serializers.IntegerField(read_only=True)
      -    title = serializers.CharField(required=False,
      +    title = serializers.CharField(required=False, allow_blank=True
                                         max_length=100)
           code = serializers.CharField(style={'type': 'textarea'})
           linenos = serializers.BooleanField(required=False)
      @@ -492,21 +492,21 @@ class SnippetSerializer(serializers.Serializer):
           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
       
      @@ -552,7 +552,7 @@ data = JSONParser().parse(stream)
      serializer = SnippetSerializer(data=data)
       serializer.is_valid()
       # True
      -serializer.object
      +serializer.save()
       # <Snippet: Snippet object>
       

      Notice how similar the API is to working with forms. The similarity should become even more apparent when we start writing views that use our serializer.

      @@ -574,7 +574,7 @@ Open the file snippets/serializers.py again, and edit the Sni

      One nice property that serializers have is that you can inspect all the fields in a serializer instance, by printing it's representation. Open the Django shell with python manange.py shell, then try the following:

      >>> from snippets.serializers import SnippetSerializer
       >>> serializer = SnippetSerializer()
      ->>> print repr(serializer)  # In python 3 use `print(repr(serializer))`
      +>>> print(repr(serializer))
       SnippetSerializer():
           id = IntegerField(label='ID', read_only=True)
           title = CharField(allow_blank=True, max_length=100, required=False)
      @@ -657,7 +657,7 @@ def snippet_detail(request, pk):
               return HttpResponse(status=204)
       

      Finally we need to wire these views up. Create the snippets/urls.py file:

      -
      from django.conf.urls import patterns, url
      +
      from django.conf.urls import url
       from snippets import views
       
       urlpatterns = [
      diff --git a/tutorial/6-viewsets-and-routers/index.html b/tutorial/6-viewsets-and-routers/index.html
      index 0740d4963..39d535911 100644
      --- a/tutorial/6-viewsets-and-routers/index.html
      +++ b/tutorial/6-viewsets-and-routers/index.html
      @@ -480,7 +480,7 @@ router.register(r'snippets', views.SnippetViewSet)
       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'))
      @@ -492,7 +492,7 @@ urlpatterns = [
       

      Using viewsets can be a really useful abstraction. It helps ensure that URL conventions will be consistent across your API, minimizes the amount of code you need to write, and allows you to concentrate on the interactions and representations your API provides rather than the specifics of the URL conf.

      That doesn't mean it's always the right approach to take. There's a similar set of trade-offs to consider as when using class-based views instead of function based views. Using viewsets is less explicit than building your views individually.

      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.

      You can review the final tutorial code on GitHub, or try out a live example in the sandbox.

      Onwards and upwards

      diff --git a/tutorial/quickstart/index.html b/tutorial/quickstart/index.html index 762c9aeb6..f291901c9 100644 --- a/tutorial/quickstart/index.html +++ b/tutorial/quickstart/index.html @@ -403,10 +403,9 @@ pip install django pip install djangorestframework # Set up a new project with a single application -django-admin.py startproject tutorial +django-admin.py startproject tutorial . cd tutorial django-admin.py startapp quickstart -cd ..

      Now sync your database for the first time:

      python manage.py migrate
      @@ -470,7 +469,7 @@ router.register(r'users', views.UserViewSet)
       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'))