使用Django和Django Rest Framework构建API

使用Django和DRF构建API会占用Django教程停止的位置。 在Django教程中,您构建了一个常规的Django民意调查应用程序。 我们将为类似的应用重建API。

在接下来的章节中,我们将使用授权,速率限制构建REST(ish)api,首先使用纯Django,然后使用DRF。 我们将介绍测试,持续集成,文档工具和API协作工具。

_images/cover.jpg

Chapters:

Introductions

使用Django和Django Rest Framework构建APIDjango“民意调查”教程停止开始,并引导您构建民意调查应用程序,但这次使用API​​。 您将学习Django Rest Framework的基础知识,包括序列化,视图,通用视图,视图集,测试,访问控制。 您还将学习使用swagger和raml的API文档。

Who is this book for?

如果您已完成Django“民意调查”教程,并希望学习使用DRF来构建API,那么本书非常适合您。 本书假定您对Django和Python有一些了解,如果您已经完成了“Poll”turtorial,那么您应该已经构建了这些知识。 No existing knowledge of DRF is assumed.

How to read this book?

这些章节应按顺序阅读。 如果您已经掌握了一些章节的知识,那么您可以快速浏览该章节,但我强烈建议您按顺序阅读它们,因为每章都是基于之前的章节。

Setup, Models and Admin

在本教程中,我们将介绍为基本轮询应用程序创建API的过程。 我们将使用Python 3.6.x,Django 2.0.x和Django Rest Framework 3.7.x来创建API。

首先,让我们在虚拟环境中安装所需的模块。

mkvirtualenv pollsapi
pip install Django
pip install djangorestframework

Creating a project

最早的是,为了创建一个项目,我们应该移动到我们想要存储代码的目录。 为此,请转到命令行并使用cd命令。 然后触发startproject命令。

django-admin startproject pollsapi

这个命令给了我们一个'pollsapi'指示。 该目录的内容如下所示:

manage.py

pollsapi/
    __init__.py
    settings.py
    urls.py
    wsgi.py

Database setup

我们将使用SQlite数据库,该数据库已包含在Python中。 pollsapi / settings.py文件已经具有正确的设置。

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.sqlite3',
        'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
    }
}

Now, use the migrate command which builds the needed database tables in regard to the django_pollsapi/settings.py file.

python manage.py migrate

Creating models

在创建我们的数据库模型之前,让我们创建我们的pollsapi应用程序。

python manage.py startapp polls

上面的命令导致包含不同文件的'polls'目录:

admin.py
apps.py
models.py
tests.py
views.py

进入'models.py'文件并开始编写模型。 为了创建民意调查api,我们将创建一个民意调查模型,一个Choice模型和一个Vote模型。 完成设计模型后,models.py文件应如下所示:

这些模型与您在Django介绍教程中看到的相同。

from django.db import models
from django.contrib.auth.models import User


class Poll(models.Model):
    question = models.CharField(max_length=100)
    created_by = models.ForeignKey(User, on_delete=models.CASCADE)
    pub_date = models.DateTimeField(auto_now=True)

    def __str__(self):
        return self.question


class Choice(models.Model):
    poll = models.ForeignKey(Poll, related_name='choices', on_delete=models.CASCADE)
    choice_text = models.CharField(max_length=100)

    def __str__(self):
        return self.choice_text


class Vote(models.Model):
    choice = models.ForeignKey(Choice, related_name='votes', on_delete=models.CASCADE)
    poll = models.ForeignKey(Poll, on_delete=models.CASCADE)
    voted_by = models.ForeignKey(User, on_delete=models.CASCADE)

    class Meta:
        unique_together = ("poll", "voted_by")

以上模型的设计方式使我们的API能够顺利进行。

激活模型

使用“ models.py”中的简单代码行,Django可以创建数据库模式和Python数据库访问API,该API可以访问“Poll”,“Choice”,“Vote”的对象。 要为我们的模型创建数据库表,需要将“ rest_framework”和“ pollsapi”应用添加到“ django_pollsapi / settings”文件的“ INSTALLED_APPS”中。

INSTALLED_APPS = (
...
'rest_framework',
'polls',
)

现在,运行makemigrations命令,该命令将通知Django已创建了新模型,并且需要将这些更改应用于迁移。 运行migrate命令进行实际迁移。

$ python manage.py makemigrations polls

$ python manage.py migrate

在你的polls应用中创建一个空的urls.py

urlpatterns = [
]

转到pollsapi / urls.py and include the polls urls。

from django.urls import include, re_path

urlpatterns = [
    re_path(r'^', include('polls.urls')),
]

Now you can runserver

$ python manage.py runserver

转到您选择的任何浏览器,然后点击网址http://127.0.0.1:8000

And we are in business, with a Django Congratulations page greeting us. (Though we haven’t added any API endpoints yet.)

_images/congrats.png

We will be adding API endpoints for creating and viewing polls in the next chapter.

Setting up the admin

您应该像这样在admin中注册PollChoice

from django.contrib import admin

from .models import Poll, Choice

admin.site.register(Poll)
admin.site.register(Choice)

A simple API with pure Django

在本章中,我们将使用纯Django构建API。 我们不会使用Django Rest Framework(或任何其他库)。 要开始使用 admin 添加一些Poll

The endpoints and the URLS

我们的API将有两个端点以JSON格式返回数据。

  • /polls/ GETs list of Poll
  • /polls/<id>/ GETs data of a specific Poll

Connecting urls to the views

Write two place holder view functions and connect them in your urls.py. We will finish polls_list and polls_detail shortly.

# In views.py
def polls_list(request):
    pass

def polls_detail(request, pk):
    pass


# in urls.py
from django.urls import path
from .views import polls_list, polls_detail

urlpatterns = [
    path("polls/", polls_list, name="polls_list"),
    path("polls/<int:pk>/", polls_detail, name="polls_detail")
]

Writing the views

We will now write the polls_list and polls_detail

from django.shortcuts import render, get_object_or_404
from django.http import JsonResponse

from .models import Poll

def polls_list(request):
    MAX_OBJECTS = 20
    polls = Poll.objects.all()[:MAX_OBJECTS]
    data = {"results": list(polls.values("question", "created_by__username", "pub_date"))}
    return JsonResponse(data)


def polls_detail(request, pk):
    poll = get_object_or_404(Poll, pk=pk)
    data = {"results": {
        "question": poll.question,
        "created_by": poll.created_by.username,
        "pub_date": poll.pub_date
    }}
    return JsonResponse(data)

This should be standard Django for you. polls = Poll.objects.all()[:20] gets us upto 20 Poll objects. We get a list of dictionaries using {"results": list(polls.values("question", "created_by__username", "pub_date"))} and return it with a JsonResponse. A JsonResponse is a like HttpResponse with content-type=application/json.

Similarly, polls_detail gets a specific Poll using get_object_or_404(Poll, pk=pk), and returns it wrapped in JsonResponse.

Using the API

You can now access the API using curl, wget, postman, browser or any other API consuming tools. Here us the response with curl.

$ curl http://localhost:8000/polls/

{"results": [{"pk": 1, "question": "What is the weight of an unladen swallow?", "created_by__username": "shabda", "pub_date": "2018-03-12T10:14:19.002Z"}, {"pk": 2, "question": "What do you prefer, Flask or Django?", "created_by__username": "shabda", "pub_date": "2018-03-12T10:15:55.949Z"}, {"pk": 3, "question": "What is your favorite vacation spot?", "created_by__username": "shabda", "pub_date": "2018-03-12T10:16:11.998Z"}]}

You should consider using postman or a similar tool. This is how your API looks in Postman.

_images/postman_polls_detail.png

Why do we need DRF?

(DRF = Django Rest Framework)

我们仅使用Django就可以构建API,而无需使用DRF,那么为什么需要DRF? 几乎总是需要使用API​​来执行常见任务,例如访问控制,序列化,速率限制等。

DRF提供了一套经过深思熟虑的基础组件集和方便的挂钩点,用于构建API。 在其余各章中,我们将使用DRF。

Serializing and Deserializing Data

DRF使构建Web API的过程变得简单而灵活。 With batteries included, it comes with well designed base classes which allows us to serialize and deserialize data.

Serialization and Deserialization

我们的API需要的第一件事是提供一种将模型实例序列化为表示的方法。 Serialization is the process of making a streamable representation of the data which we can transfer over the network. Deserialization is its reverse process.

Creating Serializers

Lets get started with creating serializer classes which will serialize and deserialize the model instances to json representations. Create a file named polls/serializers.py. We will use ModelSerializer which will reduce code duplication by automatically determing the set of fields and by creating implementations of the create() and update() methods.

Our polls/serializers.py looks like this.

from rest_framework import serializers

from .models import Poll, Choice, Vote


class VoteSerializer(serializers.ModelSerializer):
    class Meta:
        model = Vote
        fields = '__all__'


class ChoiceSerializer(serializers.ModelSerializer):
    votes = VoteSerializer(many=True, required=False)

    class Meta:
        model = Choice
        fields = '__all__'


class PollSerializer(serializers.ModelSerializer):
    choices = ChoiceSerializer(many=True, read_only=True, required=False)

    class Meta:
        model = Poll
        fields = '__all__'

The PollSerializer in detail

Our PollSerializer looks like this.

...

class PollSerializer(serializers.ModelSerializer):
    choices = ChoiceSerializer(many=True, read_only=True, required=False)

    class Meta:
        model = Poll
        fields = '__all__'

What have we got with this? The PollSerializer class has a number of methods,

  • A is_valid(self, ..) method which can tell if the data is sufficient and valid to create/update a model instance.
  • A save(self, ..) method, which khows how to create or update an instance.
  • A create(self, validated_data, ..) method which knows how to create an instance. This method can be overriden to customize the create behaviour.
  • A update(self, instance, validated_data, ..) method which knows how to update an instance. This method can be overriden to customize the update behaviour.

Using the PollSerializer

Let’s use the serializer to create a Poll object.

In [1]: from polls.serializers import PollSerializer

In [2]: from polls.models import Poll

In [3]: poll_serializer = PollSerializer(data={"question": "Mojito or Caipirinha?", "created_by": 1})

In [4]: poll_serializer.is_valid()
Out[4]: True

In [5]: poll = poll_serializer.save()

In [6]: poll.pk
Out[6]: 5

The poll.pk line tells us that the object has been commited to the DB. You can also use the serializer to update a Poll object.

In [9]: poll_serializer = PollSerializer(instance=poll, data={"question": "Mojito, Caipirinha or margarita?", "created_by": 1})

In [10]: poll_serializer.is_valid()
Out[10]: True

In [11]: poll_serializer.save()
Out[11]: <Poll: Mojito, Caipirinha or margarita?>

In [12]: Poll.objects.get(pk=5).question
Out[12]: 'Mojito, Caipirinha or margarita?'

我们可以看到在带有实例的Serializer上调用save会导致该实例被更新。 Poll.objects.get(pk = 5).question验证投票是否已更新。

在下一章中,我们将使用序列化器来编写视图。

Views and Generic Views

In this chapter, we will create views using APIVIew, and generics.ListCreateAPIView and family.

Creating Views with APIView

To start with, we will use the APIView to build the polls list and poll detail API we built in the chapter, A simple API with pure Django.

Add this to a new file polls/apiviews.py

from rest_framework.views import APIView
from rest_framework.response import Response
from django.shortcuts import get_object_or_404

from .models import Poll, Choice
from  .serializers import PollSerializer

class PollList(APIView):
    def get(self, request):
        polls = Poll.objects.all()[:20]
        data = PollSerializer(polls, many=True).data
        return Response(data)


class PollDetail(APIView):
    def get(self, request, pk):
        poll = get_object_or_404(Poll, pk=pk)
        data = PollSerializer(poll).data
        return Response(data)

And change your urls.py to

from django.urls import path

from .apiviews import PollList, PollDetail

urlpatterns = [
    path("polls/", PollList.as_view(), name="polls_list"),
    path("polls/<int:pk>/", PollDetail.as_view(), name="polls_detail")
]

DRF comes with a browsable api, so you can directly open http://localhost:8000/polls/ in the browser. It looks like this

_images/browsable-api-poll-details.png

You can now do an options request to /polls/, which gives

{
    "name": "Poll List",
    "description": "",
    "renders": [
        "application/json",
        "text/html"
    ],
    "parses": [
        "application/json",
        "application/x-www-form-urlencoded",
        "multipart/form-data"
    ]
}

This is how it looks like in postman.

_images/postman-poll-detail-options.png

Using DRF generic views to simplify code

The PollList and PollDetail get the work done, but there are bunch of common operations, we can do it in abstract away.

The generic views of Django Rest Framework help us in code reusablity. They infer the response format and allowed methods from the serializer class and base class.

Change your apiviews.py to the below code, and leave urls.py as is.

from rest_framework import generics

from .models import Poll, Choice
from .serializers import PollSerializer, ChoiceSerializer,\
    VoteSerializer


class PollList(generics.ListCreateAPIView):
    queryset = Poll.objects.all()
    serializer_class = PollSerializer


class PollDetail(generics.RetrieveDestroyAPIView):
    queryset = Poll.objects.all()
    serializer_class = PollSerializer

With this change, GET requests to /polls/ and /polls/<pk>/, continue to work as was, but we have a more data available with OPTIONS.

Do an OPTIONs request to /polls/, and you will get a response like this.

{
    "name": "Poll List",
    "description": "",
    "renders": [
        "application/json",
        "text/html"
    ],
    "parses": [
        "application/json",
        "application/x-www-form-urlencoded",
        "multipart/form-data"
    ],
    "actions": {
        "POST": {
            "id": {
                "type": "integer",
                "required": false,
                "read_only": true,
                "label": "ID"
            },
            // ...
            },
            "question": {
                "type": "string",
                "required": true,
                "read_only": false,
                "label": "Question",
                "max_length": 100
            },
            "pub_date": {
                "type": "datetime",
                "required": false,
                "read_only": true,
                "label": "Pub date"
            },
            "created_by": {
                "type": "field",
                "required": true,
                "read_only": false,
                "label": "Created by"
            }
        }
    }
}

This tells us

  • Our API now accepts POST
  • The required data fields
  • The type of each data field.

太漂亮了! This is what it looks like in Postman.

_images/postman-options-2.png

More generic views

Let us add the view to create choices and for voting. We will look more closely at this code shortly.

from rest_framework import generics

from .models import Poll, Choice
from .serializers import PollSerializer, ChoiceSerializer, VoteSerializer


class PollList(generics.ListCreateAPIView):
    queryset = Poll.objects.all()
    serializer_class = PollSerializer


class PollDetail(generics.RetrieveDestroyAPIView):
    queryset = Poll.objects.all()
    serializer_class = PollSerializer


class ChoiceList(generics.ListCreateAPIView):
    queryset = Choice.objects.all()
    serializer_class = ChoiceSerializer


class CreateVote(generics.CreateAPIView):
    serializer_class = VoteSerializer

Connect the new apiviews to urls.py.

# ...
from .apiviews import ChoiceList, CreateVote, # ...

urlpatterns = [
    # ...
    path("choices/", ChoiceList.as_view(), name="choice_list"),
    path("vote/", CreateVote.as_view(), name="create_vote"),

]

There is a lot going on here, let us look at the attributes we need to override or set.

  • queryset:这确定了初始查询集。 可以通过视图进一步筛选,切片或排序查询集。
  • serializer_class:这将用于验证和反序列化输入以及序列化输出。

我们使用了rest_framework.generic中的三个不同的类。 The names of the classes are representative of what they do, but lets quickly look at them.

  • ListCreateAPIView: Get a list of entities, or create them. Allows GET and POST
  • RetrieveDestroyAPIView: Retrieve and inidvidual entity details, or delete the entity. Allows GET and DELETE
  • CreateAPIView: Allows creating entities, but not listing them. Allows POST.

Create some choices by POSTing to /choices/.

{
    "choice_text": "Flask",
    "poll": 2
}

The response looks like this

{
    "id": 4,
    "votes": [],
    "choice_text": "Flask",
    "poll": 2
}

You can also retrieve the Poll to by doing a GET to /polls/<pk>/. You should get something like this

{
    "id": 2,
    "choices": [
        {
            "id": 3,
            "votes": [],
            "choice_text": "Django",
            "poll": 2
        },
        {
            "id": 4,
            "votes": [],
            "choice_text": "Flask",
            "poll": 2
        }
    ],
    "question": "What do you prefer, Flask or Django?",
    "pub_date": "2018-03-12T10:15:55.949721Z",
    "created_by": 1
}

If you make a mistake while POSTing, the API will warn you. POST a json with choice_text missing to /choices/.

{
    "poll": 2
}

You will get a response like this

{
    "choice_text": [
        "This field is required."
    ]
}

Check the status code is 400 Bad Request.

Next Steps

We have working API at this point, but we can simplify our API with a better URL design and remove some code duplication using viewsets. We will be doing that in the next chapter.

More views and viewsets

A better URL structure

We have three API endpoints

  • /polls/ and /polls/<pk>/
  • /choices/
  • /vote/

They get the work done, but we can make our API more intuitive by nesting them correctly. Our redesigned urls look like this:

  • /polls/ and /polls/<pk>
  • /polls/<pk>/choices/ to GET the choices for a specific poll, and to create choices for a specific poll. (Idenitfied by the <pk>)
  • /polls/<pk>/choices/<choice_pk>/vote/ - To vote for the choice identified by <choice_pk> under poll with <pk>.

Changing the views

We will make changes to ChoiceList and CreateVote, because the /polls/ and /polls/<pk> have not changed.

from rest_framework import generics
from rest_framework.views import APIView
from rest_framework import status
from rest_framework.response import Response

from .models import Poll, Choice
from .serializers import PollSerializer, ChoiceSerializer, VoteSerializer

# ...
# PollList and PollDetail views

class ChoiceList(generics.ListCreateAPIView):
    def get_queryset(self):
        queryset = Choice.objects.filter(poll_id=self.kwargs["pk"])
        return queryset
    serializer_class = ChoiceSerializer


class CreateVote(APIView):

    def post(self, request, pk, choice_pk):
        voted_by = request.data.get("voted_by")
        data = {'choice': choice_pk, 'poll': pk, 'voted_by': voted_by}
        serializer = VoteSerializer(data=data)
        if serializer.is_valid():
            vote = serializer.save()
            return Response(serializer.data, status=status.HTTP_201_CREATED)
        else:
            return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)

And change your urls.py to a nested structure.

#...
urlpatterns = [
    path("polls/<int:pk>/choices/", ChoiceList.as_view(), name="choice_list"),
    path("polls/<int:pk>/choices/<int:choice_pk>/vote/", CreateVote.as_view(), name="create_vote"),

]

You can see the changes by doing a GET to http://localhost:8000/polls/1/choices/, which should give you.

[
    {
        "id": 1,
        "votes": [],
        "choice_text": "Flask",
        "poll": 1
    },
    {
        "id": 2,
        "votes": [
        ],
        "choice_text": "Django",
        "poll": 1
    }
]

You can vote for choices 2, of poll 1 by doing a POST to http://localhost:8000/polls/1/choices/2/vote/ with data {"voted_by": 1}.

{
    "id": 2,
    "choice": 2,
    "poll": 1,
    "voted_by": 1
}

Lets get back to ChoiceList.

# urls.py
#...
urlpatterns = [
    # ...
    path("polls/<int:pk>/choices/", ChoiceList.as_view(), name="choice_list"),
]

# apiviews.py
# ...

class ChoiceList(generics.ListCreateAPIView):
    def get_queryset(self):
        queryset = Choice.objects.filter(poll_id=self.kwargs["pk"])
        return queryset
    serializer_class = ChoiceSerializer

From the urls, we pass on pk to ChoiceList. We override the get_queryset method, to filter on choices with this poll_id, and let DRF handle the rest.

And for CreateVote,

# urls.py
#...
urlpatterns = [
    # ...
    path("polls/<int:pk>/choices/<int:choice_pk>/vote/", CreateVote.as_view(), name="polls_list"),
]

# apiviews.py
# ...

class CreateVote(APIView):

    def post(self, request, pk, choice_pk):
        voted_by = request.data.get("voted_by")
        data = {'choice': choice_pk, 'poll': pk, 'voted_by': voted_by}
        serializer = VoteSerializer(data=data)
        if serializer.is_valid():
            vote = serializer.save()
            return Response(serializer.data, status=status.HTTP_201_CREATED)
        else:
            return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)

We pass on poll id and choice id. 我们从APIView继承它,而不是通用视图,因为我们可以合理地定制行为。 这类似于我们之前的APIView,其中我们将数据传递给序列化程序,并根据序列化程序是否有效来保存或返回错误。

Introducing Viewsets and Routers

Our urls are looking good, and we have a views with very little code duplication, but we can do better.

The /polls/ and /polls/<pk>/ urls require two view classes, with the same serializer and base queryset. We can group them into a viewset, and connect them to the urls using a router.

This is what it will look like:

# urls.py
# ...
from rest_framework.routers import DefaultRouter
from .apiviews import PollViewSet


router = DefaultRouter()
router.register('polls', PollViewSet, base_name='polls')


urlpatterns = [
    # ...
]

urlpatterns += router.urls

# apiviews.py
# ...
from rest_framework import viewsets

from .models import Poll, Choice
from .serializers import PollSerializer, ChoiceSerializer, VoteSerializer


class PollViewSet(viewsets.ModelViewSet):
    queryset = Poll.objects.all()
    serializer_class = PollSerializer

There is no change at all to the urls or to the responses. You can verify this by doing a GET to /polls/ and /polls/<pk>/.

Choosing the base class to use

We have seen 4 ways to build API views until now

  • Pure Django views
  • APIView subclasses
  • generics.* subclasses
  • viewsets.ModelViewSet

那么你应该在哪个时候使用? My rule of thumb is,

  • 当您要允许模型上的所有或大多数CRUD操作时,请使用viewsets.ModelViewSet
  • Use generics.* when you only want to allow some operations on a model
  • Use APIView when you want to completely customize the behaviour.

Next steps

In the next chapter, we will look at adding access control to our apis.

Access Control

在本章中,我们将向API添加访问控制,并添加API以创建和验证用户。

现在我们的API完全是宽容的。 任何人都可以创建,访问和删除任何内容 We want to add these access controls.

  • 必须对用户进行身份验证才能访问民意调查或民意调查列表。
  • 只有经过身份验证的用户才能创建投票。
  • 只有经过身份验证的用户才能创建选项。
  • 经过身份验证的用户只能为他们创建的民意调查创建选择。
  • 经过身份验证的用户只能删除他们创建的民意调查。
  • 只有经过身份验证的用户才能投票。 用户可以投票支持其他人的民意调查。

To enable the access control, we need to add two more APIs

  • API to create a user, we will call this endpoint /users/
  • API to verify a user and get a token to identify them, we will call this endpoint /login/

Creating a user

We will add an user serializer, which will allow creating. Add the following code to serializers.py.

# ...
from django.contrib.auth.models import User

# ...
class UserSerializer(serializers.ModelSerializer):

    class Meta:
        model = User
        fields = ('username', 'email', 'password')
        extra_kwargs = {'password': {'write_only': True}}

    def create(self, validated_data):
        user = User(
            email=validated_data['email'],
            username=validated_data['username']
        )
        user.set_password(validated_data['password'])
        user.save()
        return user

我们重写了ModelSerializer方法的create()以保存User实例。 We ensure that we set the password correctly using user.set_password, rather than setting the raw password as the hash. We also don’t want to get back the password in response which we ensure using extra_kwargs = {'password': {'write_only': True}}.

Let us also add views to the User Serializer for creating the user and connect it to the urls.py

# in apiviews.py
# ...
from .serializers import PollSerializer, ChoiceSerializer, VoteSerializer, UserSerializer

# ...
class UserCreate(generics.CreateAPIView):
    serializer_class = UserSerializer

# in urls.py
# ...
from .apiviews import PollViewSet, ChoiceList, CreateVote, UserCreate


urlpatterns = [
    # ...
    path("users/", UserCreate.as_view(), name="user_create"),
]

We can test this api by posting to /users/ with this json.

{
    "username": "nate.silver",
    "email": "nate.silver@example.com",
    "password": "FiveThirtyEight"
}

Which give back this response.

{
    "username": "nate.silver",
    "email": "nate.silver@example.com"
}

Try posting the same json, and you will get a error response (HTTP status code 400)

{
    "username": [
        "A user with that username already exists."
    ]
}

Authentication scheme setup

使用Django Rest Framework,我们可以设置一个默认的身份验证方案,该方案使用DEFAULT_AUTHENTICATION_CLASSES应用于所有视图。 我们将在本教程中使用令牌认证。 In your settings.py, add this.

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

You also need to enable rest_framework.authtoken app, so update INSTALLED_APPS in your settings.py.

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

Run python manage.py migrate to create the new tables.

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

另外,不要忘记通过覆盖全局设置来豁免UserCreate视图进行身份验证。 polls / apiviews.py中的UserCreate应如下所示。

class UserCreate(generics.CreateAPIView):
    authentication_classes = ()
    permission_classes = ()
    serializer_class = UserSerializer

注意authentication_classes = ()permission_classes = ()从全局认证方案中豁免UserCreate

我们希望确保在UserCreate视图中创建用户时创建令牌,因此我们更新UserSerializer Change your serializers.py like this

from rest_framework.authtoken.models import Token

class UserSerializer(serializers.ModelSerializer):

    class Meta:
        model = User
        fields = ('username', 'email', 'password')
        extra_kwargs = {'password': {'write_only': True}}

    def create(self, validated_data):
        user = User(
            email=validated_data['email'],
            username=validated_data['username']
        )
        user.set_password(validated_data['password'])
        user.save()
        Token.objects.create(user=user)
        return user

The login API

Since we have added rest_framework.authentication.TokenAuthentication, we will need to set a header like this Authorization: Token c2a84953f47288ac1943a3f389a6034e395ad940 to auhenticate. We need an API where a user can give their username and password, and get a token back.

We will not be adding a serializer, because we never save a token using this API.

Add a view and connect it to urls.

# in apiviews.py
# ...
from django.contrib.auth import authenticate

class LoginView(APIView):
    permission_classes = ()

    def post(self, request,):
        username = request.data.get("username")
        password = request.data.get("password")
        user = authenticate(username=username, password=password)
        if user:
            return Response({"token": user.auth_token.key})
        else:
            return Response({"error": "Wrong Credentials"}, status=status.HTTP_400_BAD_REQUEST)


# in urls.py
# ...

from .apiviews import PollViewSet, ChoiceList, CreateVote, UserCreate, LoginView



urlpatterns = [
    path("login/", LoginView.as_view(), name="login"),
    # ...
]

WARNING: You have to create a user using the /user/ endpoint before logging in using the /login/ endpoint. Using a previously existing user will result in a “User has no auth_token” error because we have not created a token for them. You can create tokens for them manually by using the django shell $ python manage.py shell.

>>> from django.contrib.auth.models import User
>>> from rest_framework.authtoken.models import Token
>>> user = User.objects.get(pk=pk_of_user_without_token)
>>> Token.objects.create(user=user)
<Token: e2b9fa2d4ae27fe1fdcf17b6e37711334d07e167>

使用正确的用户名和密码进行POST,您将得到这样的响应。

{
    "token": "c300998d0e2d1b8b4ed9215589df4497de12000c"
}

POST with a incorrect username and password, and you will get a response like this, with a HTTP status of 400.

{
    "error": "Wrong Credentials"
}

Another way to create this login endpoint is using obtain_auth_token method provide by DRF

# in urls.py
# ...
from rest_framework.authtoken import views

urlpatterns = [
    path("login/", views.obtain_auth_token, name="login"),
    # ...
]

细粒度访问控制

尝试访问没有任何标头的/ polls / API。 You will get an error with a http status code of HTTP 401 Unauthorized like this.

{
    "detail": "Authentication credentials were not provided."
}

Add an authorization header Authorization: Token <your token>, and you can access the API.

From now onwards we will use a HTTP header like this, Authorization: Token <your token> in all further requests.

We have two remaining things we need to enforce.

  • Authenticated users can create choices only for polls they have created.
  • Authenticated users can delete only polls they have created.

We will do that by overriding PollViewSet.destroy and ChoiceList.post.

在这两种情况下,我们都会针对预期用户检查request.user,如果不匹配则引发PermissionDenied错误。

You can check this by doing a DELETE on someone elses Poll. You will get an error with HTTP 403 Forbidden and response.

{
    "detail": "You can not delete this poll."
}

Similarly, trying to create choice for someone else’s Poll will get an error with HTTP 403 Forbidden and response

{
    "detail": "You can not create choice for this poll."
}

Next steps:

In the next chapter we will look at adding tests for our API and serializers. We will also look at how to use flake8 and run our tests in a CI environment.

Testing and Continuous Integeration

In this chapter we will add test to our API.

DRF provides a few important classes which makes testing APIs simpler. We will be using these classes later in the chapter in our tests.

  • APIRequestFactory: This is similar to Django’s RequestFactory. It allows you to create requests with any http method, which you can then pass on to any view method and compare responses.
  • APIClient: similar to Django’s Client. You can GET or POST a URL, and test responses.
  • APITestCase: similar to Django’s TestCase. Most of your tests will subclass this.

Now lets us write test cases to our polls application.

Creating Test Requests

Django’s ‘Requestfactory’ has the capability to create request instances which allow us in testing view functions individually. Django Rest Framework has a class called ‘APIRequestFactory’ which extends the standard Django’s ‘RequestFactory’. This class contains almost all the http verbs like .get(), .post(), .put(), .patch() et all.

Syntax for Post request:

factory = APIRequestFactory()
request = factory.post(uri, post data)

Lets add a test for the polls list.

from rest_framework.test import APITestCase
from rest_framework.test import APIRequestFactory

from polls import apiviews


class TestPoll(APITestCase):
    def setUp(self):
        self.factory = APIRequestFactory()
        self.view = apiviews.PollViewSet.as_view({'get': 'list'})
        self.uri = '/polls/'

    def test_list(self):
        request = self.factory.get(self.uri)
        response = self.view(request)
        self.assertEqual(response.status_code, 200,
                         'Expected Response Code 200, received {0} instead.'
                         .format(response.status_code))

In the above lines of code, we are trying to access the PollList view. We are asserting that the HTTP response code is 200.

Now run the test command.

python manage.py test

And it will display the below message.

Creating test database for alias 'default'...
System check identified no issues (0 silenced).
F
======================================================================
FAIL: test_list (polls.tests.TestPoll)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/Users/shabda/repos/building-api-django/pollsapi/polls/tests.py", line 19, in test_list
    .format(response.status_code))
AssertionError: 401 != 200 : Expected Response Code 200, received 401 instead.

----------------------------------------------------------------------
Ran 1 test in 0.002s

FAILED (failures=1)
Destroying test database for alias 'default'...

Ouch! Our test failed. This happened because the view is not accessable without authentication. So we need to create a user and test the view after getting authenticated.

Testing APIs with authentication

To test apis with authentication, a test user needs to be created so that we can make requests in context of that user. Let’s create a test user. Change your tests to

from django.contrib.auth import get_user_model
from rest_framework.authtoken.models import Token
# ...

class TestPoll(APITestCase):
    def setUp(self):
        # ...
        self.user = self.setup_user()
        self.token = Token.objects.create(user=self.user)
        self.token.save()

    @staticmethod
    def setup_user():
        User = get_user_model()
        return User.objects.create_user(
            'test',
            email='testuser@test.com',
            password='test'
        )

    def test_list(self):
        request = self.factory.get(self.uri,
            HTTP_AUTHORIZATION='Token {}'.format(self.token.key))
        request.user = self.user
        response = self.view(request)
        self.assertEqual(response.status_code, 200,
                         'Expected Response Code 200, received {0} instead.'
                         .format(response.status_code))

Now run the test command.

python manage.py test

You should get this response

Creating test database for alias 'default'...
System check identified no issues (0 silenced).
.
----------------------------------------------------------------------
Ran 1 test in 0.119s

OK
Destroying test database for alias 'default'...

Using APIClient

The same test can be written using APIClient. It has get, .post and family. Unlike creating requests first, with APIClient you can GET ot POST to a url directly and get a response.

Add a test like this:

from rest_framework.test import APIClient

# ...


class TestPoll(APITestCase):
    def setUp(self):
        self.client = APIClient()
        # ...

    # ...
    def test_list2(self):
        response = self.client.get(self.uri)
        self.assertEqual(response.status_code, 200,
                         'Expected Response Code 200, received {0} instead.'
                         .format(response.status_code))

Let us test it now.

python manage.py test polls.tests.TestPoll


Creating test database for alias 'default'...
System check identified no issues (0 silenced).
F
======================================================================
FAIL: test_list2 (polls.tests.TestPoll)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/Users/shabda/repos/building-api-django/pollsapi/polls/tests.py", line 37, in test_list2
    .format(response.status_code))
AssertionError: 401 != 200 : Expected Response Code 200, received 401 instead.

----------------------------------------------------------------------
Ran 1 test in 0.136s

FAILED (failures=1)
Destroying test database for alias 'default'...

We are seeing the same failure we saw in the test with APIRequestFactory. You can login a APIClient by calling APIClient.login. Lets update the test.

class TestPoll(APITestCase):
    # ...

    def test_list2(self):
        self.client.login(username="test", password="test")
        response = self.client.get(self.uri)
        self.assertEqual(response.status_code, 200,
                         'Expected Response Code 200, received {0} instead.'
                         .format(response.status_code))
python manage.py test polls.tests.TestPoll
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
.
----------------------------------------------------------------------
Ran 1 test in 0.260s

OK
Destroying test database for alias 'default'...

Voilà! The test passed successfully.

.post and create

我们现在知道如何测试我们的GET API。 We can use the the APIClient with .post method this time.

Let us try creating a new poll by sending the ‘question’, and ‘created_by’ parameters which are needs in the POST method. The test function looks as follows.

class TestPoll(APITestCase):

    # ...
    def test_create(self):
        self.client.login(username="test", password="test")
        params = {
            "question": "How are you?",
            "created_by": 1
            }
        response = self.client.post(self.uri, params)
        self.assertEqual(response.status_code, 201,
                         'Expected Response Code 201, received {0} instead.'
                         .format(response.status_code))

We are asserting that the the http code is 201 if the test passes succesfully. Lets run the tests.

python manage.py test polls.tests.TestPoll.test_create

Creating test database for alias 'default'...
System check identified no issues (0 silenced).
.
----------------------------------------------------------------------
Ran 1 test in 0.267s

OK
Destroying test database for alias 'default'...

Time to celebrate with the API :)

Continuous integration with CircleCI

We have the tests, but we also want it to run on every commit. If you are using Github, CircleCI provides a very well in integrated service to run your tests. We will use Circleci. v2

We can configure our application to use Circle CI by adding a file named .circleci/config.yml which is a YAML(a human-readable data serialization format) text file. It automatically detects when a commit has been made and pushed to a Github repository that is using CircleCI, and each time this happens, it will try to build the project and runs tests. The build failure or success is notified to the developer.

Setting up CircleCI

  • Sign-in: To get started with Circle CI we can sign-in with our github account on circleci.com.
  • Activate Github webhook: Once the Signup process gets completed we need to enable the service hook in the github profile page.
  • Add .circle/config.yml: We should add the yml file to the project.

Writing circle configuration file

In order for circle CI to build our project we need to tell the system a little bit about it. we will be needed to add a file named .circleci/config.yml to the root of our repository. We also need to create a pollsapi/requirements.txt to define our dependencies.

Add this to your pollsapi/requirements.txt

Django==2.0.3
djangorestframework==3.7.7

And then add this to .circleci/config.yml

version: 2
jobs:
  build:
    docker:
      # specify the version you desire here
      - image: circleci/python:3.6.1


    working_directory: ~/repo

    steps:
      - checkout

      # Download and cache dependencies
      - restore_cache:
          keys:
          - v1-dependencies-{{ checksum "pollsapi/requirements.txt" }}
          # fallback to using the latest cache if no exact match is found
          - v1-dependencies-

      - run:
          name: install dependencies
          command: |
            python3 -m venv venv
            . venv/bin/activate
            pip install -r pollsapi/requirements.txt

      - save_cache:
          paths:
            - ./venv
          key: v1-dependencies-{{ checksum "requirements.txt" }}

      - run:
          name: run tests
          command: |
            . venv/bin/activate
            cd pollsapi
            python manage.py test

      - store_artifacts:
          path: test-reports
          destination: test-reports

Below are the important keywords that are used in writting circleci config.yml file.

  • image: Defines the base image including the language and version to use
  • run: It specifies a command which will be run to setup environent and run tests. pip install -r pollsapi/requirements.txt sets up the environment and pip install -r pollsapi/requirements.txt

If everything passed successfully, you should see a green checkmark

_images/circleci.png

Congratulations, you have tests running in a CI environment.

From now onwards whenever we push our code to our repository a new build will be created for it and the tests will run.

We are at the end of the first part of our book. You can read the appendix, which tell about some documentation tools and api consumption tools. Go forward and build some amazing apps and apis.

Appendix

Testing and Using API with Postman

In this chapter, we’ll learn how to use the Postman app for testing our APIs.

Postman can be installed from the Postman site. It is a versatile tool for working with APIs.

In this books, you will be creating and using APIs. We’ll see how we can make use of Postman for this.

Making HTTP request

Postman is pretty intutive, but the image below should make the app easy to understand.

_images/postman.png

There are 4 key elements in making an HTTP request.

  1. URL:
    This specifies to which URL we need to make a request for. In other terms where our API endpoint resides.
  2. Method:
    Each API endpoint has a method which serves it’s purpose. The methods for eg., can be GET for retrieving some data, POST for creating or updating, DELETE for deleting a record.
  3. Headers:
    Headers provide required information about the request or the response or about the object sent in the body. Some times we use authentication headers too, in order to access the API endpoint.
  4. Body:
    The request body is where we send the object. The object which may be required for the service.

Response

Response is available in the bottom section, usually in a JSON format, but may also vary depending up on the API service.

Collections

We can save all the relative API endpoints to collections. In our example, we can save all our polls related endpoints as a collection or all the users related endpoints as another collection. This way all the APIs are organized.

Authentication

Postman also supports few authentication mechanisms like Basic Auth, Digest Auth and Oauth1. This allows us to use these authentication methods for the APIs.

Documenting APIs (with Swagger and more)

In this chapter we will see how to document our API.

As you build your API, you would need to document the API to collaborate with other people. In most companies and teams, the developer using the API is different from the one building them. API documentation and collaboration tools, become even more important in such an environment.

Swagger is a tool used to understand the capabilities of the service without access to source code, documentation, or through network traffic inspection. In simple terms, with swagger you can see what all API end points are available for a web application. You can use swagger for testing the requests and responses of the API endpoints.

DRF comes with its own tool, coreapi, for documenting and interacting with the API.

We will use both coreapi and swagger to document our API.

Adding swagger documentation

Install django-rest-swagger

pip install django-rest-swagger

Update your settings.py

INSTALLED_APPS = [
    # ...
    'polls',
    'rest_framework_swagger',
]

Add swagger to your urls.

from rest_framework_swagger.views import get_swagger_view

schema_view = get_swagger_view(title='Polls API')

# ...
urlpatterns = [
    # ...
    path(r'swagger-docs/', schema_view),
]

Navigate to /swagger-docs/. And your swagger docs are ready in all their glory.

_images/swagger.png

Using coreapi for documentation

Install coreapi

pip install coreapi

Add coreapi urls to your urls.

from rest_framework.documentation import include_docs_urls
# ...

urlpatterns = [
    # ...
    path(r'docs/', include_docs_urls(title='Polls API')),
]

And your coreapi docs are ready in all their glory.

_images/coreapi.png