Django-REST入门

创建一个新环境

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
virtualenv env # 创建虚拟环境, 命名: env
source env/bin/activate # 进入虚拟环境env
pip install django
pip install djangorestframework
pip install pygments # 代码高亮插件
django-admin.py startproject tutorial
cd tutorial
python manage.py startapp snippets
# tutorial/settings.py
INSTALLED_APPS = (
...
'rest_framework',
'snippets',
)

创建一个 Model

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
from django.db import models
from pygments.lexers import get_all_lexers
from pygments.styles import get_all_styles
LEXERS = [item for item in get_all_lexers() if item[1]]
LANGUAGE_CHOICES = sorted([(item[1][0], item[0]) for item in LEXERS])
STYLE_CHOICES = sorted((item, item) for item in get_all_styles())
class Snippet(models.Model):
created = models.DateTimeField(auto_now_add=True)
title = models.CharField(max_length=100, blank=True, default=''
code = models.TextField()
linenos = models.BooleanField(default=False)
language = models.CharField(choices=LANGUAGE_CHOICES, default='python'
style = models.CharField(choices=STYLE_CHOICES, default='friendly'
class Meta:
ordering = ('created',)
python manage.py makemigrations snippets
python manage.py migrate

创建一个序列化类(serializers.py)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
from rest_framework import serializers
from snippets.models import Snippet, LANGUAGE_CHOICES, STYLE_CHOICES
class SnippetSerializer(serializers.Serializer):
pk = serializers.IntegerField(read_only=True)
title = serializers.CharField(required=False, allow_blank=True, max_length=
code = serializers.CharField(style={'base_template': 'textarea.html'
linenos = serializers.BooleanField(required=False)
language = serializers.ChoiceField(choices=LANGUAGE_CHOICES, default=
style = serializers.ChoiceField(choices=STYLE_CHOICES, default=
def create(self, validated_data):
"""
Create and return a new `Snippet` instance, given the validated data.
"""
return Snippet.objects.create(**validated_data)
def update(self, instance, validated_data):
"""
Update and return an existing `Snippet` instance, given the validated data.
"""
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

用序列化(Serializers)工作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
from snippets.models import Snippet
from snippets.serializers import SnippetSerializer
from rest_framework.renderers import JSONRenderer
from rest_framework.parsers import JSONParser
snippet = Snippet(code='foo = "bar"\n')
snippet.save()
snippet = Snippet(code='print "hello, world"\n')
snippet.save()
serializer = SnippetSerializer(snippet)
serializer.data
# {'pk': 2, 'title': u'', 'code': u'print "hello, world"\n', 'linenos': False, 'language': ...}
# Serializer -> JSON
content = JSONRenderer().render(serializer.data)
content
# '{"pk": 2, "title": "", "code": "print \\"hello, world\\"\\n", "linenos": false, "language...
# stream -> json
from django.utils.six import BytesIO
stream = BytesIO(content)
data = JSONParser().parse(stream)
# json -> serializer
serializer = SnippetSerializer(data=data)
serializer.is_valid()
# True
serializer.validated_data
# OrderedDict([('title', ''), ('code', 'print "hello, world"\n'), ('linenos', False), ('language', 'python'), ('style', 'friendly')])
serializer.save()
# <Snippet: Snippet object>

使用模型序列化ModelSerializers

1
2
3
4
class SnippetSerializer(serializers.ModelSerializer):
class Meta:
model = Snippet
fields = ('id', 'title', 'code', 'linenos', 'language', 'style')

用我们的序列化来写常规的Django视图

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
# snippets/views.py
from django.http import HttpResponse
from django.views.decorators.csrf import csrf_exempt
from rest_framework.renderers import JSONRenderer
from rest_framework.parsers import JSONParser
from snippets.models import Snippet
from snippets.serializers import SnippetSerializer
class JSONResponse(HttpResponse):
"""
An HttpResponse that renders its content into JSON.
"""
def __init__(self, data, **kwargs):
content = JSONRenderer().render(data)
kwargs['content_type'] = 'application/json'
super(JSONResponse, self).__init__(content, **kwargs)
@csrf_exempt
def snippet_detail(request, pk):
"""
Retrieve, update or delete a code snippet.
"""
try:
snippet = Snippet.objects.get(pk=pk)
except Snippet.DoesNotExist:
return HttpResponse(status=404)
if request.method == 'GET':
serializer = SnippetSerializer(snippet)
return JSONResponse(serializer.data)
elif request.method == 'PUT':
data = JSONParser().parse(request)
serializer = SnippetSerializer(snippet, data=data)
if serializer.is_valid():
serializer.save()
return JSONResponse(serializer.data)
return JSONResponse(serializer.errors, status=400)
elif request.method == 'DELETE':
snippet.delete()
return HttpResponse(status=204)

我们需要用线将这些视图连起来。 创建 snippets/urls.py 文件:

1
2
3
4
5
6
from django.conf.urls import url
from snippets import views
urlpatterns = [
url(r'^snippets/$', views.snippet_list),
url(r'^snippets/(?P<pk>[0-9]+)/$', views.snippet_detail),
]

请求与响应

REST框架介绍了一个 请求(Request) 对象, 它扩展了常规的 HttpResquest ,并且提供更灵活的请求解析。

1
2
request.POST # 只处理表单数据。 只对'POST'方法起作用。
request.data # 可以处理任意数据。 对'POST', 'PUT'和'PATCH'方法起作用。

REST 框架也介绍了 Response 对象, 它是一类用未渲染内容和内容协商来决定正确的内容类型并把它返回给客户端的 模板响应

1
return Response(data) # 根据客户端的请求来渲染成指定的内容类型。

REST框架为每个状态码(status code)提供更明确的标识符, 例如在 状态(status) 模型中的 HTTP_400_BAD_REQUEST 。 用这些标识符代替纯数字的HTTP状态码是很好的注意。

装饰API视图

  • @api_view 装饰器用在基于视图的方法上。
  • APIView 类用在基于视图的类上。 这些装饰器提供一些功能

例如去报在你的视图中接收 Request 对象, 例如在你的 Response 对象中添加上下文,这样我们就能实现内容通信。 这里装饰器也提供了一些行为, 例如在合适的时候返回 405 Method Not Allowed 响应, 例如处理任何在访问错误输入的 request.data 时出现的 解析错误(ParseError) 异常。

结合在一起

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
from rest_framework import status
from rest_framework.decorators import api_view
from rest_framework.response import Response
from snippets.models import Snippet
from snippets.serializers import SnippetSerializer
@api_view(['GET', 'PUT', 'DELETE'])
def snippet_detail(request, pk):
"""
Retrieve, update or delete a snippet instance.
"""
try:
snippet = Snippet.objects.get(pk=pk)
except Snippet.DoesNotExist:
return Response(status=status.HTTP_404_NOT_FOUND)
if request.method == 'GET':
serializer = SnippetSerializer(snippet)
return Response(serializer.data)
elif request.method == 'PUT':
serializer = SnippetSerializer(snippet, data=request.data)
if serializer.is_valid():
serializer.save()
return Response(serializer.data)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
elif request.method == 'DELETE':
snippet.delete()
return Response(status=status.HTTP_204_NO_CONTENT)

我们不再明确打印我们的对指定内容类型的请求或响应。 request.data 能够处理 json 请求, 但是它也能处理其他格式。 相似地, 虽然我们可以在响应对象中带数据, 但允许REST框架渲染响应成正确的内容类型。

在我们的链接(URLs)后添加可选格式后缀

为了利用我们的响应内容不再是单一格式的事实, 我们应该为我们的API尾部添加格式后缀。 用格式后缀给我们明确参考指定格式的URL, 这意味着我们的API能够处理像 http://example.com/api/items/4/.json 一样的链接。 在视图函数中添加一个 format 参数, 像这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
def snippet_list(request, pk, fornat=None):
pass
# urls.py
from django.conf.urls import url
from rest_framework.urlpatterns import format_suffix_patterns
from snippets import views
urlpatterns = [
url(r'^snippets/$', views.snippet_list),
url(r'^snippets/(?P<pk>[0-9]+)$', views.snippet_detail),
]
rlpatterns = format_suffix_patterns(urlpatterns)

因为API是基于客户端请求来选择响应内容的类型, 所以默认情况下, 在Web浏览器访问资源时, API返回HTML格式的资源。 这语序API返回完全可以网页浏览的HTML。 有可以网页浏览API是很好的, 这使开发和使用你的API更简单, 这也为其他想要查看和使用你的API的开发者大大降低了门槛。

基于视图的类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
# views.py
class SnippetDetail(APIView):
"""
Retrieve, update or delete a snippet instance.
"""
def get_object(self, pk):
try:
return Snippet.objects.get(pk=pk)
except Snippet.DoesNotExist:
raise Http404
def get(self, request, pk, format=None):
snippet = self.get_object(pk)
serializer = SnippetSerializer(snippet)
return Response(serializer.data)
def put(self, request, pk, format=None):
snippet = self.get_object(pk)
serializer = SnippetSerializer(snippet, data=request.data)
if serializer.is_valid():
serializer.save()
return Response(serializer.data)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
def delete(self, request, pk, format=None):
snippet = self.get_object(pk)
snippet.delete()
return Response(status=status.HTTP_204_NO_CONTENT)
# urls.py
from django.conf.urls import url
from rest_framework.urlpatterns import format_suffix_patterns
from snippets import views
urlpatterns = [
url(r'^snippets/$', views.SnippetList.as_view()),
url(r'^snippets/(?P<pk>[0-9]+)/$', views.SnippetDetail.as_view()),
]
rlpatterns = format_suffix_patterns(urlpatterns)

使用混合

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
from snippets.models import Snippet
from snippets.serializers import SnippetSerializer
from rest_framework import mixins
from rest_framework import generics
class SnippetList(mixins.ListModelMixin,
mixins.CreateModelMixin,
generics.GenericAPIView):
queryset = Snippet.objects.all()
serializer_class = SnippetSerializer
def get(self, request, *args, **kwargs):
return self.list(request, *args, **kwargs)
def post(self, request, *args, **kwargs):
return self.create(request, *args, **kwargs)
def delete(self, request, *args, **kwargs):
return self.destroy(request, *args, **kwargs)

授权与权限

我们打算对我们的 Snippet 模型类做些改变。 首先, 让我们添加几个字段。 其中一个字段将显示出哪个用户创建里snippet数据。 另一个字段将用于HTML代码高亮。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
owner = models.ForeignKey('auth.User', related_name='snippets')
highlighted = models.TextField()
#
def save(self, *args, **kwargs):
"""
Use the `pygments` library to create a highlighted HTML
representation of the code snippet.
"""
lexer = get_lexer_by_name(self.language)
linenos = self.linenos and 'table' or False
options = self.title and {'title': self.title} or {}
formatter = HtmlFormatter(style=self.style, linenos=linenos,
full=True, **options)
self.highlighted = highlight(self.code, lexer, formatter)
super(Snippet, self).save(*args, **kwargs)

既然我们已经创建了多个用户, 那么我们最好将用户添加到我们的API。 很容易创建一个新的序列。 在 serializers.py 中添加

1
2
3
4
5
6
7
8
from django.contrib.auth.models import User
class UserSerializer(serializers.ModelSerializer):
snippets = serializers.PrimaryKeyRelatedField(many=True, queryset=Snippet.objects.all())
class Meta:
model = User
fields = ('id', 'username', 'snippets')

我们需要添加在 views.py 中添加一些视图。 我们想要为用户添加只读视图, 所以我们会使用基于视图的一般类 ListAPIView 和 RetrieveAPIView 。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
from snippets.serializers import UserSerializer
from django.contrib.auth.models import User
class UserList(generics.ListAPIView):
queryset = User.objects.all()
serializer_class = UserSerializer
class UserDetail(generics.RetrieveAPIView):
queryset = User.objects.all()
serializer_class = UserSerializer
# urls.py
url(r'^users/$', views.UserList.as_view()),
url(r'^users/(?P<pk>[0-9]+)/$', views.UserDetail.as_view()),

如果我们创建snippet数据, 我们没办法将用户和snippet实例联系起来。 虽然用户不是序列表示的部分, 但是它是请求的一个属性。 我们通过重写snippet视图的 .perform_create() 方法来做到, 这个方法允许我们修改如何保存实例, 修改任何请求对象或者请求连接里的信息。 在 SnippetList 视图类中添加以下方法;

1
2
def perform_create(self, serializer):
serializer.save(owner=self.request.user)

我们序列的 create() 方法将会另外传入一个来自有效的请求数据的 ‘owner’ 字段。

更新snippet序列

既然已经将snippets和创建它们的用户联系在一起了, 那么我们需要更新对应的 SnippetSerializer 。 在 serializers.py 的序列定义(serializer definition)中添加以下字段:

1
owner = serializers.ReadOnlyField(source='owner.username')

去报你也添加 ‘owner’, 到内部类 Meta 的字段列表里。 这个字段很有趣。 source 参数控制哪个属性被用于构成一个字段, 并且能够指出序列实例的任何属性。 它也能想上面一样使用点标记(.), 这中情况下他会横贯给定的属性, 就是我们使用Django模板语言一样。 *

为视图添加权限

snippets数据已经和用户联系在一起, 我们想确保只有授权的用户可以创建、 更新和删除snippet数据。 REST框架包括许多权限类(permission classes), 我们可以使用这些权限类来现在视图的访问权限。 这种情况下, 其中我们需要 IsAuthenticatedOrReadOnly , 这个类确保授权请求有读写权限, 而没有授权的用户只有只读权限。 首先, 在视图模块中引入以下代码

1
2
3
4
from rest_framework import permissions
# SnippetList 和 SnippetDetail 视图类中
permission_classes = (permissions.IsAuthenticatedOrReadOnly, )

如果你现在用浏览器打开API, 你会发现你已经不能创建新的snippets数据。 为此,我们需要以用户身份登录。 为了使用浏览器打开API, 我们需要添加一个登录视图, 编辑URL配置(URLconf)文件 urls.py 文件。 在 urls.py 顶部添加下面import。

1
2
3
4
5
6
from django.conf.urls import include
urlpatterns += [
url(r'^api-auth/', include('rest_framework.urls',
namespace='rest_framework')),
]

现在如果你刷新浏览器页面, 你会看到右上角的’Login’链接。 如果你用之前创建的用户登录, 你就可以再次写snippets数据了。一旦你创建snippets数据, 浏览’/users/‘, 然后你会发现在每个用户的’snippets’字段, 显示的内容包括与每个用户相关的snippets主键。

视图集和路由

视图集重写当前视图。 首先, 我们要把我们的 UserList 和 UserDetail 视图重写成单个 UserViewSet 。 我们可以 UserViewSet 代替 UserList 和 UserDetail 。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
from rest_framework import viewsets
class UserViewSet(viewsets.ReadOnlyModelViewSet):
"""
This viewset automatically provides `list` and `detail` actions.
"""
queryset = User.objects.all()
serializer_class = UserSerializer
from rest_framework.decorators import detail_route
class SnippetViewSet(viewsets.ModelViewSet):
"""
This viewset automatically provides `list`, `create`, `retrieve`,
`update` and `destroy` actions.
Additionally we also provide an extra `highlight` action.
"""
queryset = Snippet.objects.all()
serializer_class = SnippetSerializer
permission_classes = (permissions.IsAuthenticatedOrReadOnly,
IsOwnerOrReadOnly,)
@detail_route(renderer_classes=[renderers.StaticHTMLRenderer])
def highlight(self, request, *args, **kwargs):
snippet = self.get_object()
return Response(snippet.highlighted)
def perform_create(self, serializer):
serializer.save(owner=self.request.user)

这次我们使用 ModelViewSet 类是为了获得完整的默认读写操作的集合。 注意:我们也用了 @detail_route 装饰器来创建自定义动作, 命名为 highlight 。 这
个装饰器用于添加任何自定义的端点, 这些端点不符合标准的 create/update/delete 方式。 使用 @detail_route 装饰器的自定义动作会响应 GET 请求。 如果我们让动作响应 POST 请求, 我们可以使用 methods 参数。 自定义动作的URL在默认情况下是依赖于方法本身。 如果你想改变url本来创建的方式, 你可以将url_path包含在装饰器关键参数中。

明确绑定viewset到URL

我们定义URLConf的时候, 处理方法只绑定了动作。 为了看看发生了什么, 我们必须从我们的视图集(ViewSets)创建一个视图集合。 在 urls.py 文件中, 我们将 ViewSet 类绑定到具体视图的集合。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
from snippets.views import SnippetViewSet, UserViewSet, api_root
from rest_framework import renderers
snippet_list = SnippetViewSet.as_view({
'get': 'list',
'post': 'create'
})
snippet_detail = SnippetViewSet.as_view({
'get': 'retrieve',
'put': 'update',
'patch': 'partial_update',
'delete': 'destroy'
})
snippet_highlight = SnippetViewSet.as_view({
'get': 'highlight'
}, renderer_classes=[renderers.StaticHTMLRenderer])
user_list = UserViewSet.as_view({
'get': 'list'
})
user_detail = UserViewSet.as_view({
'get': 'retrieve'
})

注意我们如何通过绑定http方法到每个视图需要的动作来从 ViewSet 类创建多视图。 既然我们已经绑定了我们的资源和具体视图, 我们就可以和以前一样将我们的视图注册到URL配置中。

1
2
3
4
5
6
7
8
urlpatterns = format_suffix_patterns([
url(r'^$', api_root),
url(r'^snippets/$', snippet_list, name='snippet-list'),
url(r'^snippets/(?P<pk>[0-9]+)/$', snippet_detail, name='snippet-detail'),
url(r'^snippets/(?P<pk>[0-9]+)/highlight/$', snippet_highlight, name='snippet-highlight'),
url(r'^users/$', user_list, name='user-list'),
url(r'^users/(?P<pk>[0-9]+)/$', user_detail, name='user-detail')
])

使用路由

因为我们使用 ViewSet 类而不是 View 类, 所以实际上我们不需要自己设计URL配置。 按惯例, 使用 Router 类就可以自动将资源与视图(views)、 链接(urls)联系起来。 我们需要做的只是用一个路由注册合适的视图集合。 现在, 我们把剩下的做完。 我们重写了 urls.py 文件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
from django.conf.urls import url, include
from snippets import views
from rest_framework.routers import DefaultRouter
# Create a router and register our viewsets with it.
router = DefaultRouter()
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 browsable API.
urlpatterns = [
url(r'^', include(router.urls)),
url(r'^api-auth/', include('rest_framework.urls', namespace='rest_framework'
]

用路由注册视图和提供一个urlpattern是相似的, 包括两个参数–视图的URL前缀和视图本身。 我们使用的 默认路由(DefaultRouter) 类会自动为我们创建API根视图, 所以我们就可以从我们的 views 模块删除 api_root 方法。