Herramientas de usuario

Herramientas del sitio


fw:djangorest

¡Esta es una revisión vieja del documento!


Django REST Framework

First Steps

Official page for Django REST Framework is: http://www.django-rest-framework.org/

Installation

pip install djangorestframework

Routers

To define the URL for the API REST you can use router classes, which mount a list of relationships between url and views. It can be used in the urlpatterns settings variable. They have a register( prefix, viewset), prefix is used to indicate URL patterns and viewset the concrete view.

from rest_framework import routers
 
router = routers.SimpleRouter()
router.register(r'users', UserViewSet)
router.register(r'accounts', AccountViewSet)
urlpatterns = router.urls

There are two types of predefined routers:

  • SimpleRouter (you can define if the last slash should be taken into account with the parametere trailing_slash).
  • DefaultRouter, which allow to specify the format.

They are in the package rest_framework.routers.

However, you could also add your views in this way:

urlpatterns = patterns('',
                       url(r'^invitations/$', 'core.invitations.views.invitations'),
                       )

Serializers

Serializers allow complex datatypes to be converted to native Python objects which can then be easily rendered into JSON, XML, or other formats.
Having an object like:

class Comment(object):
    def __init__(self, email, content, created=None):
        self.email = email
        self.content = content
        self.created = created or datetime.datetime.now()
 
comment = Comment(email='leila@example.com', content='foo bar')

We can declare a serializer like this:

from rest_framework import serializers
 
class CommentSerializer(serializers.Serializer):
    email = serializers.EmailField()
    content = serializers.CharField(max_length=200)
    created = serializers.DateTimeField()
 
    def restore_object(self, attrs, instance=None):
        if instance is not None:
            instance.email = attrs.get('email', instance.email)
            instance.content = attrs.get('content', instance.content)
            instance.created = attrs.get('created', instance.created)
            return instance
        return Comment(**attrs)

It's format by fields which will be serialized/deserialized. The restore_object method allows the deserializing of complex data. I we didn't define this method, the deserializing would return a dictionary.

Serializing\Deserializing

Serializing

serializer = CommentSerializer(comment, many=False)
serializer.data # {'email': u'leila@example.com', 'content': u'foo bar'....

Deserializing

Having a input text stream from a json, StringIO allows that from a string:

from StringIO import StringIO
from rest_framework.parsers import JSONParser
 
stream = StringIO(json)
data = JSONParser().parse(stream)  # it's a dict (?)
serializer = CommentSerializer(data=data)
serializer.is_valid() # True
serializer.object     # <Comment object at 0x10633b2d0>

Updating a serialized object

serializer = CommentSerializer(comment, data=data)  # Update `comment`

Or limited fields:

serializer = CommentSerializer(comment, data={'content': u'foo bar'}, partial=True)

Changing representation

For example, we have a description and a descriptionhtml fields. When serialize description we want that descriptionhtml is shown as markup. We can do it like this:

description = serializers.TextField()
descriptionhtml = serializers.TextField(source='description', read_only=True)
 
def transform_descriptionhtml(self, obj, value):
    from django.contrib.markup.templatetags.markup import markdown
    return markdown(value)

It uses the transform_<field> method.

Serializing multiple objects

queryset = Book.objects.all()
serializer = BookSerializer(queryset, many=True)
serializer.data

Deserializing multiple objects

data = [
    {'title': 'The bell jar', 'author': 'Sylvia Plath'},
    {'title': 'For whom the bell tolls', 'author': 'Ernest Hemingway'}
]
serializer = BookSerializer(data=data, many=True)

Deserializing multiple objects for update

It changes the title for 3 and 4 books:

queryset = Book.objects.all()
data = [
    {'id': 3, 'title': 'The Bell Jar'},
    {'id': 4, 'title': 'For Whom the Bell Tolls'}
]
serializer = BookSerializer(queryset, data=data, many=True)
serializer.is_valid()
serializer.save()

You may want to allow new items to be created, and missing items to be deleted. To do so, pass allow_add_remove=True to the serializer.

serializer = BookSerializer(queryset, data=data, many=True, allow_add_remove=True)
serializer.is_valid()
serializer.save() 

Changing the way to identify an object

To do it (required when perform a bulk edit) we must override the get_identity to determine the identifier. If not, it will use id field.

class AccountSerializer(serializers.Serializer):
    slug = serializers.CharField(max_length=100)
    created = serializers.DateTimeField()
 
    def get_identity(self, data):
        try:
            return data.get('slug', None)
        except AttributeError:
            return None

Validation

When deserializing data you always need to call is_valid() before access the object. It will test if the deserialization was ok, if not the .errors will contain which problems were found.

serializer = CommentSerializer(data={'email': 'foobar', 'content': 'baz'})
serializer.is_valid()
# False
serializer.errors
# {'email': [u'Enter a valid e-mail address.'], 'created': [u'This field is required.']}

Specify a field validation

Adding .validate_<fieldname> method to Serializer subclasses you can concrete a validation. It takes a dictionary of deserialized attributes as a first argument, and the field name in that dictionary as a second argument. It should either just return the attrs dictionary or raise a ValidationError.

def validate_title(self, attrs, source):
  value = attrs[source]
  if "django" not in value.lower():
    raise serializers.ValidationError("Blog post is not about Django")
  return attrs

Specify a object validation

Adding a method called .validate() to Serializer subclasses you can concrete an object validation. This method takes a single argument, which is the attrs dictionary. It should raise a ValidationError if necessary, or just return attrs:

def validate(self, attrs):
  if attrs['start_date'] > attrs['finish_date']:
    raise serializers.ValidationError("finish must occur after start")
  return attrs

Saving an object

To save the deserialized objects created by a serializer, call the .save() method. You can override the default save behaviour by overriding the .save_object(obj) method on the serializer class.

if serializer.is_valid():
    serializer.save()

Nested objects

We can use a Serializer class as a field of another class:

class UserSerializer(serializers.Serializer):
    email = serializers.EmailField()
    username = serializers.CharField(max_length=100)
 
class CommentSerializer(serializers.Serializer):
    user = UserSerializer()
    content = serializers.CharField(max_length=200)
    created = serializers.DateTimeField()

If the object is optional (accept None as value), we will use the required=False flag:

user = UserSerializer(required=False)  # May be an anonymous user.

If the object should be a list of items, we will use many=True flag:

edits = EditItemSerializer(many=True)

Model classes

You can work with model classes using a subclass Meta which field model should be assigned with its corresponding model class:

class AccountSerializer(serializers.ModelSerializer):
    class Meta:
        model = Account

You can specify which fields you will use:

class AccountSerializer(serializers.ModelSerializer):
    class Meta:
        model = Account
        fields = ('id', 'account_name', 'users', 'created')

In relationships you can specify which level of nested objects it will include:

class AccountSerializer(serializers.ModelSerializer):
    class Meta:
        model = Account
        fields = ('id', 'account_name', 'users', 'created')
        depth = 1

Or include read only fields with read_only_fields field, or write only fields with write_only_fields field. Even you can add more fields to your serializer class than those that have the model class.

Parsers

Parsers are classes to convert from a datatype to another. We can set default parsers for all our API REST or any of the views. There are some default parsers:

  • JSONParser
  • YAMLParser
  • XMLParser
  • FormParser
  • MultiPartParser
  • FileUploadParser

You can code your own parse inheriting from BaseParser. There also are other third party packages with their own parsers MessagePack, CamelCaseJSON

Views

Class Based Views

REST framework provides an APIView class to inherit from. It works… Giving a Request instance to handler methods.

Handler methods may return a Response object.

APIException exceptions are caught properly by the APIView class.

Requests are authenticated before dispatching it to the handler method.

Handler methods are those like .get(), put(), patch(), delete(), or .post().

The number of attributes may be set on the class.

.renderer_classes, .parser_classes, .authentication_classes, .throttle_classes, .permission_classes, and .content_negotiation_class attributes can be set into the APIView subclass.

There is a decorator which you can use it to indicate that a function will work as handler, it receives which handlers will accept as string list @api_vide(['GET', 'POST']).

There's also another decorator to use to ensure that concrete restrictions are applied: @throttle_classes Other usefull methods are also provided (.get_renderers(self), .get_parsers(self), .get_authenticators(self), .get_throttles(self), .get_permissions(self), .get_content_negotiator(self))

Methods that are called before dispatching to the handler method are .check_permissions(self, request), .check_throttles(self, request), .perform_content_negotiation(self, request, force=False).

There are other useful methods to override which are called before or after calling handler methods:

  • .initial(self, request, *args, **kwargs)
  • .handle_exception(self, exc), any exception thrown by the handler method will be passed to this method.
  • .initialize_request(self, request, *args, **kwargs)
  • .finalize_response(self, request, response, *args, **kwargs)
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import authentication, permissions
 
class ListUsers(APIView):
    authentication_classes = (authentication.TokenAuthentication,)
    permission_classes = (permissions.IsAdminUser,)
 
    def get(self, request, format=None):
        usernames = [user.username for user in User.objects.all()]
        return Response(usernames)

Generic views

They are pre-built views that allow to compose reusable behaviour.

To use them in URLconf you would include:

url(r'^/users/', ListCreateAPIView.as_view(model=User), name='user-list')

Examples

from django.contrib.auth.models import User
from myapp.serializers import UserSerializer
from rest_framework import generics
from rest_framework.permissions import IsAdminUser
 
class UserList(generics.ListCreateAPIView):
    queryset = User.objects.all()
    serializer_class = UserSerializer
    permission_classes = (IsAdminUser,)
    paginate_by = 100
 
# ... or...
class UserList(generics.ListCreateAPIView):
    queryset = User.objects.all()
    serializer_class = UserSerializer
    permission_classes = (IsAdminUser,)
 
    def get_paginate_by(self):
        # Use smaller pagination for HTML representations.
        if self.request.accepted_renderer.format == 'html':
            return 20
        return 100

Basic attributes

  • queryset, all the elements from this view. You must either set this attribute, or override the get_queryset() method.
  • serializer_class, the serializer class that should be used for validating and deserializing input, and for serializing output. You must either set this attribute, or override the get_serializer_class() method.
  • lookup_field, the model field that should be used to for performing object lookup of individual model instances. Defaults to pk.
  • lookup_url_kwarg, the URL keyword argument that should be used for object lookup.
  • model, this shortcut may be used instead of setting either (or both) of the queryset/serializer_class attributes, although using the explicit style is generally preferred. If used instead of serializer_class, then then DEFAULT_MODEL_SERIALIZER_CLASS setting will determine the base serializer class.

Pagination & filtering

  • paginate_by
  • paginate_by_param
  • pagination_serializer_class
  • page_kwarg
  • filter_backends

Base methods

  • get_queryset(self) returns all the elements from the type that use this view.
def get_queryset(self):
    user = self.request.user
    return user.accounts.all()
  • get_object(self) an object instance that should be used for details view (using the lookup_field to filter:
def get_object(self):
    queryset = self.get_queryset()
    filter = {}
    for field in self.multiple_lookup_fields:
        filter[field] = self.kwargs[field]
 
    obj = get_object_or_404(queryset, **filter)
    self.check_object_permissions(self.request, obj)
    return obj
  • get_filter_backends(self)
  • get_serializer_class(self)
  • get_paginate_by(self)

Save and deltion hooks

  • pre_save(self, obj)
  • post_save(self, obj, created=False)
  • pre_delete(self, obj)
  • post_delete(self, obj)
def pre_save(self, obj):
    obj.owner = self.request.user

Mixins

They are classes which provide the basic actions rather than define get() and post() methods.

  • ListModelMixin, to list a queryset.
  • CreateModelMixin, to create a new model instance.
  • RetrieveModelMixin, to return a model instance.
  • UpdateModelMixin, tu update a model instance.
  • DestroyModelMixin, to destroy.

You can create custom mixins.

Concrete views

When using generic views you can use this preconf APIView's:

post get (single) get (collection) put patch delete
CreateAPIView x
ListAPIView x
RetrieveAPIView x
DestroyAPIView x
UpdateAPIView x x
ListCreateAPIView x x
RetrieveUpdateAPIView x x x
RetrieveDestroyAPIView x x
RetrieveUpdateDestroyAPIView x x x x

They are in the package rest_framework.generics.

Authentication and permissions

Authentication

request.user will be set to an instance of User class. request.auth is set with addictional authentication information like authentication token. If the user is not authenticated the request.user is set as AnonymousUser instance and request.auth as None. This behaviour can be modified changing UNAUTHENTICATED_USER and UNAUTHENTICATED_TOKEN settings.

You can set the authentication classes (authentication schemes) globally in DEFAULT_AUTHENTICATION setting, for example:

REST_FRAMEWORK = {
    'DEFAULT_AUTHENTICATION_CLASSES': (
        'rest_framework.authentication.BasicAuthentication',
        'rest_framework.authentication.SessionAuthentication',
    )
}

… Or in the APIView:

class ExampleView(APIView):
    authentication_classes = (SessionAuthentication, BasicAuthentication)
    permission_classes = (IsAuthenticated,)
 
    def get(self, request, format=None):
        content = {
            'user': unicode(request.user),  # `django.contrib.auth.User` instance.
            'auth': unicode(request.auth),  # None
        }
        return Response(content)
 
@api_view(['GET'])
@authentication_classes((SessionAuthentication, BasicAuthentication))
@permission_classes((IsAuthenticated,))
def example_view(request, format=None):
    content = {
        'user': unicode(request.user),  # `django.contrib.auth.User` instance.
        'auth': unicode(request.auth),  # None
    }
    return Response(content)

There are two responses to non authentication requests: HTTP 401 Unauthorized and HTTP 403 Permission Denied. The kind of response that will be used depends on the authentication scheme.

Authentication modes

Basic Authentication

This authentication scheme uses HTTP Basic Authentication, signed against a user's username and password, and is generally only appropriate for testing.

Session Authentication

Session authentication is appropriate for AJAX clients that are running in the same session context as your website.

Token Authentication

To use the TokenAuthentication scheme, include rest_framework.authtoken in your INSTALLED_APPS setting:

INSTALLED_APPS = (
    ...
    'rest_framework.authtoken'
)

You'll also need to create tokens for your users.

from rest_framework.authtoken.models import Token
token = Token.objects.create(user=...)
print token.key

For clients to authenticate, the token key should be included in the Authorization HTTP header. The key should be prefixed by the string literal “Token”, with whitespace separating the two strings. For example:

Authorization: Token 9944b09199c62bcf9418ad846dd0e4bbdfc6ee4b

The curl command line tool may be useful for testing token authenticated APIs. For example:

curl -X GET http://127.0.0.1:8000/api/example/ -H 'Authorization: Token 9944b09199c62bcf9418ad846dd0e4bbdfc6ee4b'

If successfully authenticated, TokenAuthentication provides the request.auth as a rest_framework.authtoken.models.BasicToken instance.

To create a token for all the users that already exist in your DB you'll do:

for user in User.objects.all():
    Token.objects.get_or_create(user=user)

If you want every user to have an automatically generated Token, you can simply catch the User's post_save signal (it should be in models.py or in any code imported by Django startup):

@receiver(post_save, sender=get_user_model())
def create_auth_token(sender, instance=None, created=False, **kwargs):
    if created:
        Token.objects.create(user=instance)

You will need to provide a token after given the username and password. It's already developed using the obtain_auth_token view:

urlpatterns += patterns('',
    url(r'^api-token-auth/', 'rest_framework.authtoken.views.obtain_auth_token')
)

It will return a JSON response when valid username and password fields are POSTed to this view. Note that this does not use the default renderer and parser classes. If you needed another version of this behaviour you should override the ObtainAuthToken view class, and using it in your url conf instead.

{ 'token' : '9944b09199c62bcf9418ad846dd0e4bbdfc6ee4b' }

Others

Custom Authentication

To implement a custom authentication scheme, subclass BaseAuthentication and override the .authenticate(self, request) method. The method should return a two-tuple of (user, auth) if authentication succeeds, or None otherwise. You also may want to raise an AuthenticationFailed exception from the .authenticate() method.

class ExampleAuthentication(authentication.BaseAuthentication):
    def authenticate(self, request):
        username = request.META.get('X_USERNAME')
        if not username:
            return None
        try:
            user = User.objects.get(username=username)
        except User.DoesNotExist:
            raise exceptions.AuthenticationFailed('No such user')
        return (user, None)

Permissions

Permissions are defined as a list of permission classes. When a view is called a permission list classes is checked, if any of them fails an exceptions.PermissionDenied exception will be raised.
To check permissions in an Object level .get_object() will be called in the view. Then you'll need to explicitly call the .check_object_permissions(request, obj) method on the view at the point at which you've retrieved the object. This will either raise a PermissionDenied or NotAuthenticated exception, or simply return if the view has the appropriate permissions.

def get_object(self):
    obj = get_object_or_404(self.get_queryset())
    self.check_object_permissions(self.request, obj)
    return obj

Setting the permisson policy

It may be set globally, using the DEFAULT_PERMISSION_CLASSES setting.

REST_FRAMEWORK = {
    'DEFAULT_PERMISSION_CLASSES': (
        'rest_framework.permissions.IsAuthenticated',
    )
}

If not specified, this setting defaults to allowing unrestricted access:

'DEFAULT_PERMISSION_CLASSES': (
   'rest_framework.permissions.AllowAny',
)

You can also set the authentication policy on a per-view, or per-viewset basis, using the APIView class based views:

class ExampleView(APIView):
    permission_classes = (IsAuthenticated,)
 
    def get(self, request, format=None):
        content = {
            'status': 'request was permitted'
        }
        return Response(content)

Or, if you're using the @api_view decorator with function based views.

@api_view('GET')
@permission_classes((IsAuthenticated, ))
def example_view(request, format=None):
    content = {
        'status': 'request was permitted'
    }
    return Response(content)

Permission classes

  • AllowAny, will allow unrestricted access, regardless of if the request was authenticated or unauthenticated.
  • IsAuthenticated, will deny permission to any unauthenticated user, and allow permission otherwise.
  • IsAdminUser, will deny permission to any user, unless user.is_staff is True.

To implement a custom permission class, override BasePermission and implement either, or both, of the methods: .has_permission(self, request, view), .has_object_permission(self, request, view, obj). The methods should return True if the request should be granted access, and False otherwise.

Testing

The APIRequestFactory class supports the same methods as Django RequestFactory class. So methods like .get(), .post(), .put(), .patch(), .delete(), .head() and .options() are available to use like that…

from rest_framework.test import APIRequestFactory
 
factory = APIRequestFactory()
request = factory.post('/notes/', {'title': 'new idea'}, format='json')

In methods like post, put and patch you can specify the request format using the format parameter. Also you could specify the content-type and send raw data:

request = factory.post('/notes/', json.dumps({'title': 'new idea'}), content_type='application/json')

You can force the user authentication if you want authomatically athenticate the request.

All the mentioned classes are in the rest_framework.test package.

Test cases

There are some classes to perform the tests:

  • APISimpleTestCase
  • APITransactionTestCase
  • APITestCase
  • APILiveServerTestCase
from django.core.urlresolvers import reverse
from rest_framework import status
from rest_framework.test import APITestCase
 
class AccountTests(APITestCase):
    def test_create_account(self):
        url = reverse('account-list')
        data = {'name': 'DabApps'}
        response = self.client.post(url, data, format='json')
        self.assertEqual(response.status_code, status.HTTP_201_CREATED)
        self.assertEqual(response.data, data)

You can test using the response.data which is preferred over parsing the response.content:

# Better this...
response = self.client.get('/users/4/')
self.assertEqual(response.data, {'id': 4, 'username': 'lauren'})
# ... than this:
response = self.client.get('/users/4/')
self.assertEqual(json.loads(response.content), {'id': 4, 'username': 'lauren'})

If you are testing views, in order to access response.content, you'll first need to render the response.

view = UserDetail.as_view()
request = factory.get('/users/4')
response = view(request, pk='4')
response.render()  # Cannot access `response.content` without this.
self.assertEqual(response.content, '{"username": "lauren", "id": 4}')

Notes

  • You could use the APIClient to make calls to the API. It has the same methods as APIRequestFactory (get(), post()…) and adds others like .login(), .credentials()

Notes

On using MongoEngine

As MongoEngine documents are not Django model classes so they are not compatible inside the Django REST Framework. However you can use it if you define some things:

  • You must define your serializer class. Here you should mark which properties are requiered.
  • Views should use get_queryset() method instead queryset attribute:
class InvitationsView(ListCreateAPIView):
    serializer_class = InvitationRequestSerializer
 
    def get_queryset(self):
        return InvitationRequest.objects
  • The urlpatterns variable should be defined as follows (not with a router):
urlpatterns = patterns('',
                       url(r'^invitations/$', InvitationsView.as_view(), name='invitations'),
                       )
fw/djangorest.1401708103.txt.gz · Última modificación: 2020/05/09 09:24 (editor externo)