1, 序列化 Serialization
創建一個新環境
在做其他事之前,我們會用virtualenv創建一個新的虛擬環境。這將確保我們的包配置與我們正在工作的其他項目完全隔離。
virtualenv env # 創建虛擬環境,命名: env source env/bin/activate # 進入虛擬環境env
既然我們已經在虛擬環境中,那么我們就可以安裝我們依賴的包了。
pip install django pip install djangorestframework pip install pygments # 代碼高亮插件
開始
首先,我們來創建一個新項目。
cd ~ django-admin.py startproject tutorial cd tutorial
輸完以上命令,我們就可以創建一個應用,我們將會用他來創建簡單的Web API。
python manage.py startapp snippets
我們會添加一個新的snippets
應用和rest_framework
應用到INSTALLED_APPS
。讓我們編輯tutorial/settings.py
文件:
INSTALLED_APPS = ( ... 'rest_framework', 'snippets.apps.SnippetsConfig', )
Ok, 我們准備下一步。
創建一個 Model
為了實現本教程的目的,我們將創建一個簡單的Snippet
模型,這個模型用來保存snippets
代碼。開始編輯snippets/models.py
文件。
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', max_length=100) style = models.CharField(choices=STYLE_CHOICES, default='friendly', max_length=100) class Meta: ordering = ('created',)
我們也需要為我們的snippet模型創建一個初始遷移(initial migration),然后第一次同步數據庫。
python manage.py makemigrations snippets
python manage.py migrate
創建一個序列化類(Serializer class)
着手我們的Web API,首先要做的是,提供一種將我們的snippet
實例序列化/反序列化成例如json
這樣的表述形式。我們可以通過聲明序列來完成,這些序列與Django
的表單(forms)
工作相似。在snippets
目錄創建一個新文件serializers.py
,添加下列代碼。
from rest_framework import serializers from snippets.models import Snippet, LANGUAGE_CHOICES, STYLE_CHOICES class SnippetSerializer(serializers.Serializer): id = serializers.IntegerField(read_only=True) title = serializers.CharField(required=False, allow_blank=True, max_length=100) code = serializers.CharField(style={'base_template': 'textarea.html'}) linenos = serializers.BooleanField(required=False) language = serializers.ChoiceField(choices=LANGUAGE_CHOICES, default='python') style = serializers.ChoiceField(choices=STYLE_CHOICES, default='friendly') 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
序列化類(serializer class)的第一部分定義了一些需要被序列化/反序列化字段。create()
和update()
方法定義了在調用serializer.save()
時成熟的實例是如何被創建和修改的。 序列化類(serializer class)與Django的表單類(Form class)非常相似,包括對各種字段有相似的確認標志(flag),例如required
,max_length
和default
。 在某些情況下,這些字段標志也能控制序列應該怎么表現,例如在將序列渲染成HTML時。{'base_template': 'textarea.html}'
標志相當於對Django表單(Form)類使用widget=widgets.Textarea
。這對控制API的顯示尤其有用,以后的教程將會看到。 事實上,以后我們可以通過使用ModelSerializer
類來節約我們的時間,但是現在為了讓我們序列化定義更清晰,我們用Serializer類。
用序列化(Serializers)工作
在我們深入之前,我們需要熟練使用新的序列化列(Serializer class)。然我們開始使用Django命令行吧。
python manage.py shell
Okay,讓我們寫一些snippets代碼來使序列化工作。
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()
現在我們已經有了一些snippet實例。讓我們看看如何將其中一個實例序列化。
注: Model -> Serializer
serializer = SnippetSerializer(snippet) serializer.data # {'id': 2, 'title': u'', 'code': u'print "hello, world"\n', 'linenos': False, 'language': u'python', 'style': u'friendly'}
現在,我們已經將模型實例(model instance)轉化成Python原生數據類型。為了完成實例化過程,我們要將數據渲染成json。
注: Serializer -> JSON
content = JSONRenderer().render(serializer.data) content # '{"id": 2, "title": "", "code": "print \\"hello, world\\"\\n", "linenos": false, "language": "python", "style": "friendly"}'
反序列化也一樣。首先,我們需要將流(stream)解析成Python原生數據類型...
注: stream -> json
from django.utils.six import BytesIO stream = BytesIO(content) data = JSONParser().parse(stream)
...然后我們要將Python原生數據類型恢復成正常的對象實例。
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>
可以看到,API和表單(forms)是多么相似啊。當我們用我們的序列寫視圖的時候,相似性會相當明顯。 除了將模型實例(model instance)
序列化外,我們也能序列化查詢集(querysets)
,只需要添加一個序列化參數many=True
。
serializer = SnippetSerializer(Snippet.objects.all(), many=True) serializer.data # [OrderedDict([('id', 1), ('title', u''), ('code', u'foo = "bar"\n'), ('linenos', False), ('language', 'python'), ('style', 'friendly')]), OrderedDict([('id', 2), ('title', u''), ('code', u'print "hello, world"\n'), ('linenos', False), ('language', 'python'), ('style', 'friendly')]), OrderedDict([('id', 3), ('title', u''), ('code', u'print "hello, world"'), ('linenos', False), ('language', 'python'), ('style', 'friendly')])]
使用模型序列化ModelSerializers
我們的SnippetSerializer
類復制了包含Snippet
模型在內的很多信息。如果我們能簡化我們的代碼,那就更好了。 以Django
提供表單(Form)
類和模型表單(ModelForm)
類相同的方式,REST
框架包括了實例化(Serializer)
類和模型實例化(ModelSerializer)
類。 我們來看看用ModelSerializer
類創建的序列。再次打開snippets/serializers.py
文件,用下面的代碼重寫SnippetSerializer
類。
class SnippetSerializer(serializers.ModelSerializer): class Meta: model = Snippet fields = ('id', 'title', 'code', 'linenos', 'language', 'style')
序列一個非常棒的屬性就是,你能夠通過打印序列實例的結構(representation)
查看它的所有字段。輸入python manage.py shell
打開命令行,然后嘗試以下代碼:
from snippets.serializers import SnippetSerializer serializer = SnippetSerializer() print(repr(serializer)) # SnippetSerializer(): # id = IntegerField(label='ID', read_only=True) # title = CharField(allow_blank=True, max_length=100, required=False) # code = CharField(style={'base_template': 'textarea.html'}) # linenos = BooleanField(required=False) # language = ChoiceField(choices=[('Clipper', 'FoxPro'), ('Cucumber', 'Gherkin'), ('RobotFramework', 'RobotFramework'), ('abap', 'ABAP'), ('ada', 'Ada')... # style = ChoiceField(choices=[('autumn', 'autumn'), ('borland', 'borland'), ('bw', 'bw'), ('colorful', 'colorful')...
記住,ModelSerializer
類並沒有做什么有魔力的事情,它們僅僅是一個創建序列化類的快捷方式。
- 一個自動決定的字段集合。
- 簡單的默認
create()
和update()
方法的實現。
用我們的序列化來寫常規的Django視圖
讓我們看看,使用我們新的序列化類,我們怎么寫一些API視圖。此刻,我們不會使用REST框架的其他特性,僅僅像寫常規Django視圖一樣。 通過創建HttpResponse
的一個子類來開始,其中,我們可以用這個子類來渲染任何我們返回的json
數據。 編輯snippets/views.py
文件,添加以下代碼。
from django.http import HttpResponse, JsonResponse 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
我們的根API將是一個支持列出所有存在的snippets
的視圖,或者創建一個新的snippet
對象。
@csrf_exempt def snippet_list(request): """ List all code snippets, or create a new snippet. """ if request.method == 'GET': snippets = Snippet.objects.all() serializer = SnippetSerializer(snippets, many=True) return JsonResponse(serializer.data, safe=False) elif request.method == 'POST': data = JSONParser().parse(request) serializer = SnippetSerializer(data=data) if serializer.is_valid(): serializer.save() return JsonResponse(serializer.data, status=201) return JsonResponse(serializer.errors, status=400)
注意,因為我們希望可以從沒有CSRF token
的客戶端POST
數據到這個視圖,我們需要標記這個視圖為csrf_exempt
。通常,你並不想這么做,並且事實上REST框架視圖更實用的做法不是這樣的,但是目前來說,這足以到達我們的目的。 我們也需要一個與單個snippet
對象相應的視圖,並且我們使用這個視圖來讀取、更新或者刪除這個snippet
對象。
@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文件:
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), ]
我們也需要在根url配置文件tutorial/urls.py中添加我們的snippet應用URL。
from django.conf.urls import url, include urlpatterns = [ url(r'^', include('snippets.urls')), ]
有一些當時我們沒有正確處理的邊緣事件是沒有價值的。如果我們發送不正確的json數據,或者如果我們制造了一個視圖沒有寫處理的方法(method),那么我們會得到500“服務器錯誤”的響應。當然,現在也會出現這個問題。
測試我們Web API的第一次努力
現在我們開始創建一個測試服務器來服務我們的snippets應用。 退出命令行......
quit()
...然后啟動Django開發服務器。
python manage.py runserver Validating models... 0 errors found Django version 1.11, using settings 'tutorial.settings' Development server is running at http://127.0.0.1:8000/ Quit the server with CONTROL-C.
我們可以在另一個終端測試服務器。 我們可以用curl和httpie來測試我們的API。Httpie是一個面向用戶的非常友好的http客戶端,它是用Python寫的。讓我們來安裝它。 你可以通過pip來安裝httpie:
pip install httpie
最后,我們來獲取一個包含所有snippets的列表:
http http://127.0.0.1:8000/snippets/ HTTP/1.1 200 OK ... [ { "id": 1, "title": "", "code": "foo = \"bar\"\n", "linenos": false, "language": "python", "style": "friendly" }, { "id": 2, "title": "", "code": "print \"hello, world\"\n", "linenos": false, "language": "python", "style": "friendly" } ]
或者我們可以通過id來獲取指定的snippet:
http http://127.0.0.1:8000/snippets/2/ HTTP/1.1 200 OK ... { "id": 2, "title": "", "code": "print \"hello, world\"\n", "linenos": false, "language": "python", "style": "friendly" }
相似地,你可以通過在瀏覽器中訪問這些鏈接來獲得相同的json數據。
我們現在在哪
到目前為止,我們做的都很好,我們已經獲得一個序列化API,這和Django的表單API非常相似,並且我們寫好了一些常用的Django視圖。 現在,我們的API視圖除了服務於json外,不會做任何其他特別的東西,並且有一些錯誤我們仍然需要清理,但是它是一個可用的Web API。 我們將會在本教程的第二部分改善這里東西。
2, 請求與響應
從這開始,我們將接觸REST框架的核心。讓我們來介紹一系列必要的搭建模塊。
請求對象
REST框架介紹了一個請求(Request)
對象,它擴展了常規的HttpResquest
,並且,提供更靈活的請求解析。請求(Request)
對象的核心功能是request.data
屬性,這個屬性與request.POST
相似,但是它對Web APIs
更加有用。
request.POST # 只處理表單數據。只對'POST'方法起作用。 request.data # 可以處理任意數據。對'POST','PUT'和'PATCH'方法起作用。
響應對象
REST 框架也介紹了Response
對象,它是一類用未渲染內容和內容協商來決定正確的內容類型並把它返回給客戶端的模板響應(TemplateResponse)
。
return Response(data) # 根據客戶端的請求來渲染成指定的內容類型。
狀態碼
總是在你的視圖中用數字的HTTP狀態碼會更加容易理解,並且如果你用其他錯誤代碼表示錯誤,就不太容易注意到了。REST框架為每個狀態碼`(status code)`提供更明確的標識符,例如在狀態`(status)`模型中的`HTTP_400_BAD_REQUEST`。用這些標識符代替純數字的HTTP狀態碼是很好的注意。
裝飾API視圖
REST框架提供兩個裝飾器,你可以用它們來寫API視圖。
- 1
@api_view
裝飾器用在基於視圖的方法上。 - 2
APIView
類用在基於視圖的類上。 這些裝飾器提供一些功能,例如確保在你的視圖中接收Request對象,例如在你的Response對象中添加上下文,這樣我們就能實現內容通信。 這里裝飾器也提供了一些行為,例如在合適的時候返回405 Method Not Allowed
響應,例如處理任何在訪問錯誤輸入的request.data
時出現的解析錯誤(ParseError)
異常。
結合在一起
好了,讓我們開始用這些新的組件寫一些視圖。 我們不再需要在我們的視圖(views.py)
中使用JSONResponse
類,所有現在把它刪掉。一旦我們這樣做了,我們就能很快重建我們的視圖。
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', 'POST']) def snippet_list(request): """ List all code snippets, or create a new snippet. """ if request.method == 'GET': snippets = Snippet.objects.all() serializer = SnippetSerializer(snippets, many=True) return Response(serializer.data) elif request.method == 'POST': serializer = SnippetSerializer(data=request.data) if serializer.is_valid(): serializer.save() return Response(serializer.data, status=status.HTTP_201_CREATED) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
我們的實例視圖是我們之前例子的改良版。簡明了很多,並且目前的代碼和我們使用Forms API很相似。我們也用有意義的狀態碼標識符。 在views.py
模塊中,有一個獨立的snippet視圖。
@api_view(['GET', 'PUT', 'DELETE']) def snippet_detail(request, pk): """ Retrieve, update or delete a code snippet. """ 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)
這對我們來說應該非常熟悉,因為它與常規的Django視圖沒有什么區別。 注意,我們不再明確打印我們的對指定內容類型的請求或響應。request.data
能夠處理json
請求,但是它也能處理其他格式。相似地,雖然我們可以在響應對象中帶數據,但允許REST框架渲染響應成正確的內容類型。
在我們的鏈接(URLs)后添加可選格式后綴
為了利用我們的響應內容不再是單一格式的事實,我們應該為我們的API尾部添加格式后綴。用格式后綴給我們明確參考指定格式的URL,這意味着我們的API能夠處理像http://example.com/api/items/4/.json
一樣的鏈接。 在視圖函數中添加一個format
參數,像這樣:
def snippet_list(request, format=None):
和
def snippet_detail(request, pk, format=None):
現在可以很快更新urls.py文件,在已經存在的URL中添加一個格式后綴模式(format_suffix_patterns)
。
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), ] urlpatterns = format_suffix_patterns(urlpatterns)
我們不必添加額外的URL模式,但是它給我們簡單、清楚的方式渲染除特定的格式。
看看吧
和教程第一部分一樣,我們要開始從命令行測試API。雖然我們能在發送無效的請求時更妥當處理錯誤,但是現在一切都做的夠好了。 我們能想之前一樣獲取所有的snippets列表。
http http://127.0.0.1:8000/snippets/ HTTP/1.1 200 OK ... [ { "id": 1, "title": "", "code": "foo = \"bar\"\n", "linenos": false, "language": "python", "style": "friendly" }, { "id": 2, "title": "", "code": "print \"hello, world\"\n", "linenos": false, "language": "python", "style": "friendly" } ]
我們能控制我們返回的響應格式,或者使用Accept
響應頭。
http http://127.0.0.1:8000/snippets/ Accept:application/json # Request JSON http http://127.0.0.1:8000/snippets/ Accept:text/html # Request HTML
或者在URL后添加格式后綴:
http http://127.0.0.1:8000/snippets.json # JSON 后綴 http http://127.0.0.1:8000/snippets.api # 瀏覽用的 API 后綴
同樣地,我們可以控制我們發送的請求格式,用Content-Type
請求頭。
# POST using form data http --form POST http://127.0.0.1:8000/snippets/ code="print 123" { "id": 3, "title": "", "code": "print 123", "linenos": false, "language": "python", "style": "friendly" } # POST using JSON http --json POST http://127.0.0.1:8000/snippets/ code="print 456" { "id": 4, "title": "", "code": "print 456", "linenos": false, "language": "python", "style": "friendly" }
你也可以從瀏覽器打開API,通過訪問http://127.0.0.1:8000/snippets/。
Browsability
因為API是基於客戶端請求來選擇響應內容的類型,所以默認情況下,在Web瀏覽器訪問資源時,API返回HTML格式的資源。這語序API返回完全可以網頁瀏覽的HTML。 有可以網頁瀏覽API是很好的,這使開發和使用你的API更簡單,這也為其他想要查看和使用你的API的開發者大大降低了門檻。 關於可瀏覽API的特性和如何自定義可瀏覽API,請見可瀏覽API話題。
接下來要干什么?
在教程的第三部分,我們基於視圖用類,並且看看普通的視圖我們如何減少代碼。
3, 基於視圖的類(class based view)
除了可以用基於視圖的函數(function based view)寫我們的API,我們也可以用基於視圖的類。正如我們所見,這是一個非常有利的模式,允許我們重用同樣的功能,並幫助我們使代碼緊湊。
用基於視圖的類重寫我們的API
我們將會想重寫一個基於視圖的類一樣重寫根視圖。這包括重構views.py
文件。
from snippets.models import Snippet from snippets.serializers import SnippetSerializer from django.http import Http404 from rest_framework.views import APIView from rest_framework.response import Response from rest_framework import status class SnippetList(APIView): """ List all snippets, or create a new snippet. """ def get(self, request, format=None): snippets = Snippet.objects.all() serializer = SnippetSerializer(snippets, many=True) return Response(serializer.data) def post(self, request, format=None): serializer = SnippetSerializer(data=request.data) if serializer.is_valid(): serializer.save() return Response(serializer.data, status=status.HTTP_201_CREATED) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
到目前為止,一切都很好。這和之前的情況很相似,但是我們已經很好地通過不同的HTTP方法區分。現在我們也需要在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()), ] urlpatterns = format_suffix_patterns(urlpatterns)
好了,我們做完了。如果你啟用開發服務器,那么一切都和之前一樣。
使用混合(mixins)
使用基於視圖的類最大的一個好處是,它允許我們快速創建可復用的行為。我們一直使用的create/retrieve/update/delete
操作將和我們創建的任何后端模型API視圖非常相似。這些普遍的行為是通過REST框架的混合類(mixin classes)
實現的。 讓我們看看如何通過混合類(mixin classes)
組建視圖。下面是我們的views.py
模型。
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)
我們會花一會兒准確測試這里發生了什么。我們使用GenericAPIView
加上ListMOdelMixin
和CreatteModelMixin
創建我們的視圖。 基類提供核心功能,混合類提供.list()
和.create()
動作。然后我們合適的動作綁定明確的get
和post
方法。到目前為止,東西已經足夠簡單。
class SnippetDetail(mixins.RetrieveModelMixin, mixins.UpdateModelMixin, mixins.DestroyModelMixin, generics.GenericAPIView): queryset = Snippet.objects.all() serializer_class = SnippetSerializer def get(self, request, *args, **kwargs): return self.retrieve(request, *args, **kwargs) def put(self, request, *args, **kwargs): return self.update(request, *args, **kwargs) def delete(self, request, *args, **kwargs): return self.destroy(request, *args, **kwargs)
太像了。我們用GenericAPIView
類提供核心功能,添加混合(mixin)
,來提供.retrieve()
,.update()
和.destroy()
動作。
使用基於視圖的一般類(generic class)
盡管我們已經使用混合類(mixin classes)以比之前更少的代碼重寫了視圖,但是我們可以進一步深入。REST框架提供一個已經混入一般視圖的集合,我們能用他們來整理我們的views.py
模塊。
from snippets.models import Snippet from snippets.serializers import SnippetSerializer from rest_framework import generics class SnippetList(generics.ListCreateAPIView): queryset = Snippet.objects.all() serializer_class = SnippetSerializer class SnippetDetail(generics.RetrieveUpdateDestroyAPIView): queryset = Snippet.objects.all() serializer_class = SnippetSerializer
哇,如此簡潔。我們的代碼看起來是如此簡潔、地道的Django。 接下來我們要學習本教程的第四部分,在第四部分我們會為我們的API處理授權(authentication)和權限(permissions)。
4, 授權(Authentication)與權限(Permissions)
當前,我們的API沒有限制誰能編輯或刪除snippets代碼。我們想要一些更高級的行為以確保:
- snippets數據總是與創建者聯系在一起。
- 只有授權用戶才能創建snippets。
- 只有snippet的創建者才能更新或者刪除它。
- 沒有授權的請求應該只有只讀權限。
在我們的模型中添加信息
我們打算對我們的Snippet
模型類做些改變。首先,讓我們添加幾個字段。其中一個字段將顯示出哪個用戶創建里snippet
數據。另一個字段將用於HTML代碼高亮。 將下面兩個字段添加到Snippet
模型中,在snippets/models.py
中。
owner = models.ForeignKey('auth.User', related_name='snippets', on_delete=models.CASCADE) highlighted = models.TextField()
我們也需要確保當模型保存以后,我們可以看到高亮的字段。為此我們用pygments
代碼高亮庫來形成高亮字段。 我們需要一些額外的包:
from pygments.lexers import get_lexer_by_name from pygments.formatters.html import HtmlFormatter from pygments import highlight
然后給我們的模型類添加.save()
方法:
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)
然后,我們需要更細我們的數據庫表。為此,正常情況下,我們會創建數據庫遷移(database migration),但是就本教程來說,我們只需要刪除原來的數據庫,然后重新創建即可。
rm -f db.sqlite3 rm -r snippets/migrations python manage.py makemigrations snippets python manage.py migrate
你可能也想要創建不同的用戶來測試API。最快的方式就是用createsuperuser
命令。
python manage.py createsuperuser
為我們的用戶模型添加端點
既然我們已經創建了多個用戶,那么我們最好將用戶添加到我們的API。很容易創建一個新的序列。在serializers.py
中添加;
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')
因為'snippets'
在用戶模型中是一個相反的關系,默認情況下在使用ModelSerializer
類時我們不會包括,所以我們需要手動為用戶序列添加這個字段。 我們需要添加在views.py
中添加一些視圖。我們想要為用戶添加只讀視圖,所以我們會使用基於視圖的一般類ListAPIView
和RetrieveAPIView
。
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
確保文件中引入了UserSerializer
類。
from snippets.serializers import UserSerializer
最后,我們需要通過修改URL配置,將這些視圖添加進API。添加以下urls.py
中。
url(r'^users/$', views.UserList.as_view()), url(r'^users/(?P<pk>[0-9]+)/$', views.UserDetail.as_view()),
將用戶和Snippets連接起來
現在,如果我們創建snippet
數據,我們沒辦法將用戶和snippet
實例聯系起來。雖然用戶不是序列表示的部分,但是它是請求的一個屬性。 我們通過重寫snippet
視圖的.perform_create()
方法來做到,這個方法允許我們修改如何保存實例,修改任何請求對象或者請求連接里的信息。 在SnippetList
視圖類中添加以下方法;
def perform_create(self, serializer): serializer.save(owner=self.request.user)
現在,我們序列的create()
方法將會另外傳入一個來自有效的請求數據的'owner'
字段。
更新我們的序列
既然已經將snippets
和創建它們的用戶聯系在一起了,那么我們需要更新對應的SnippetSerializer
。在serializers.py
的序列定義(serializer definition)
中添加以下字段:
owner = serializers.ReadOnlyField(source='owner.username')
注意;確保你將'owner'字段添加到內部類Meta的字段列表里。 這個字段很有趣。source參數控制哪個屬性被用於構成一個字段,並且能夠指出序列實例的任何屬性。它也能像上面一樣使用點標記(.),這種情況下他會橫貫給定的屬性,就是我們使用Django模板語言一樣。 我們添加的字段是隱式ReadOnly類,與其他類相反,如CharField,BooleanField,隱式ReadOnlyField總是只讀的,用於序列化表示,但在數據非序列化時不能用於更新實例。這里我們也可以用CharField(read_only=True)。
為視圖添加需要的權限
snippets
數據已經和用戶聯系在一起,我們想確保只有授權的用戶可以創建、更新和刪除snippet
數據。 REST框架包括許多權限類(permission classes)
,我們可以使用這些權限類來現在視圖的訪問權限。這種情況下,其中我們需要IsAuthenticatedOrReadOnly
,這個類確保授權請求有讀寫權限,而沒有授權的用戶只有只讀權限。 首先,在視圖模塊中引入以下代碼:
from rest_framework import permissions
接下來,將以下屬性添加到SnippetList
和SnippetDetail
的視圖類中。
permission_classes = (permissions.IsAuthenticatedOrReadOnly,)
為可瀏覽API(Browsable API)添加登錄
如果你打開瀏覽器並瀏覽API,你將會發現你無法再創建新的snippets code,如果想要有創建的權限,需要登錄。 我們可以添加一個登錄視圖,通過編輯我們的根路由urls.py文件。 將下列包導入到文件上方:
from django.conf.urls import include
在文件的末尾,將login和logout的路由配好。
urlpatterns += [ url(r'^api-auth/', include('rest_framework.urls')), ]
url
樣式的r'^api-auth/'
部分實際上可以是任何你想要的URL
。唯一的限制就是include
的鏈接必須使用'rest_framework'
名字空間。現在如果你刷新瀏覽器頁面,你會看到右上角的'Login'鏈接。如果你用之前創建的用戶登錄,你就可以再次寫snippets數據了。 一旦你創建snippets數據,瀏覽'/users/',然后你會發現在每個用戶的'snippets'字段,顯示的內容包括與每個用戶相關的snippets主鍵。
對象等級權限
雖然我們真的想任何人都和一看見snippets
數據,但也要確保只有創建snippet
的用戶可以修改或刪除他的snippet
。 為此,我們需要創建自定義權限。 在snippets app
中,創建一個新文件permissions.py
。
from rest_framework import permissions class IsOwnerOrReadOnly(permissions.BasePermission): """ Custom permission to only allow owners of an object to edit it. """ def has_object_permission(self, request, view, obj): # Read permissions are allowed to any request, # so we'll always allow GET, HEAD or OPTIONS requests. if request.method in permissions.SAFE_METHODS: return True # Write permissions are only allowed to the owner of the snippet. return obj.owner == request.user
然后編輯SnippetDetail
視圖類中的permission_classes
屬性,添加自定義權限。
permission_classes = (permissions.IsAuthenticatedOrReadOnly,
IsOwnerOrReadOnly,)
確保引入了IsOwnerOrReadOnly
類。
from snippets.permissions import IsOwnerOrReadOnly
現在,如果你再次打開瀏覽器,你會發現只有你登入,你才能刪除(DELETE)
或更新(PUT)
屬於你的snippet
數據。
授權API
因為我們的API有一系列權限,所以如果我們想編輯任何snippets
,我們需要授權我們的請求。我們現在還沒有任何授權類(authenticaions classes)
,所以默認情況下只有SessionAuthentication
和BasicAuthentication
。 當我們通過Web瀏覽器與API交互時,我們可以登錄,然后瀏覽器會話(session)
將會提供必須的請求授權。 如果我們通過程序與API交互,我們需要為每個請求提供明確的授權證明。 如果我們在沒有授權的情況下創建一個snippet
,那么我們會得到下面的錯誤:
http POST http://127.0.0.1:8000/snippets/ code="print 123" { "detail": "Authentication credentials were not provided." }
為了請求成功,我們需要包含用戶名和密碼。
http -a admin:password123 POST http://127.0.0.1:8000/snippets/ code="print 789" { "id": 1, "owner": "admin", "title": "foo", "code": "print 789", "linenos": false, "language": "python", "style": "friendly" }
總結
現在我們已經在我們的Web API上,為我們的系統用戶和snippet的創建者,添加了很多權限和端點。 在第五部分,我們將會看怎么我們可以通過為我們的高亮snippets創建HTML端點來將所有東西聯系在一起,然后在系統內用超鏈接將我們的API聯系起來。
5, 關系(Relationships)與超鏈接API(Hyperlinked APIs)
現在,用主鍵代表我們API之間的關系。在這部分教程,我們會用超鏈接改善API之間的關系。
為我們的API根創建一個端點
現在,我們已經為'snippets'
和'users'
設置了端點,但是我們沒有為我們的API設置單獨的入口點。因此,我們會一個基於方法的常規視圖和@api_view
裝飾器來創建一個入口點。在你的snippets/views.py
中添加:
from rest_framework.decorators import api_view from rest_framework.response import Response from rest_framework.reverse import reverse @api_view(['GET']) def api_root(request, format=None): return Response({ 'users': reverse('user-list', request=request, format=format), 'snippets': reverse('snippet-list', request=request, format=format) })
我們會注意到兩件事:第一,我們用了REST框架的reverse
方法為了返回高質量的URL;第二,URL格式是方便的名字標識符,我們會在之后會在snippets/urls.py
中聲明。
創建一個高亮的snippets端點
另一件明顯的事就是,我們的API缺乏代碼高亮端點。 和我們所有的API端點不一樣,我們不想用JSON,而只是想用HTML顯示。REST框架提供兩種HTML渲染樣式,一種是用模板渲染處理HTML,另一種是用預渲染HTML。第二種是我們想要用的方式。 在創建代碼時,我們需要考慮的是,高亮視圖在我們使用的普通視圖中是不存在的。我們不會返回一個對象實例,而是對象實例的一個屬性。 我們會是使用基類代表實例,並創建我們自己的.get()
方法,而不是用普通的視圖。在你的snippets/views.py
添加:
from rest_framework import renderers from rest_framework.response import Response class SnippetHighlight(generics.GenericAPIView): queryset = Snippet.objects.all() renderer_classes = (renderers.StaticHTMLRenderer,) def get(self, request, *args, **kwargs): snippet = self.get_object() return Response(snippet.highlighted)
通常,我們需要添加新的視圖到我們的URL配置。然后,在snippest/urls.py
中添加一個鏈接:
url(r'^$', views.api_root),
然后,為高亮snippet添加一個url樣式:
url(r'^snippets/(?P<pk>[0-9]+)/highlight/$', views.SnippetHighlight.as_view()),
為我們的API添加超鏈接
處理好實體之間的關系是Web API設計中極具挑戰性的方面之一。代表一種關系可以有很多種方式:
- 使用主鍵。
- 在實體之間使用超鏈接。
- 在相關的實體上使用獨一無二的slug。
- 使用相關的實體的默認字符串。
- 在父表述使用嵌套的實體。
- 一些自定義的表述。 REST框架支持以上所有方式,都能適應正向或者反向關系,或者就行使用一般的外鍵一樣使用自定義的管理方式。 這種情況下,我們想要在實體之間使用超鏈接方式。為了達到目的,我們需要修改我們的序列
(serializers)
,以拓展HyperlinkedModelSerializer
,不是使用已經存在的ModelSerializer
。 以下是HyperlinkedModelSerializer
不同於ModelSerializer
的地方: HyperlinkedModelSerializer
默認不包括pk字段。- 它只包括一個url字段,使用
HyperlinkedIndentityField
。 - 關系使用
HyperlinkedRelatedField
,而不是PrimaryKeyRelatedField
。 我們能使用超鏈接快速重寫現存的序列。在snippets/serializers.py
中添加:
class SnippetSerializer(serializers.HyperlinkedModelSerializer): owner = serializers.ReadOnlyField(source='owner.username') highlight = serializers.HyperlinkedIdentityField(view_name='snippet-highlight', format='html') class Meta: model = Snippet fields = ('url', 'id', 'highlight', 'owner', 'title', 'code', 'linenos', 'language', 'style') class UserSerializer(serializers.HyperlinkedModelSerializer): snippets = serializers.HyperlinkedRelatedField(many=True, view_name='snippet-detail', read_only=True) class Meta: model = User fields = ('url', 'id', 'username', 'snippets')
注意,我們已經添加了一個新字段highlight
。這個字段類型是和url一樣的,只是它指向snippet-highlighturl
模式,而不是snippet-detailurl
模式。 因為我們已經包含了格式后綴的URL,如.json
,所以我們也需要在highlight字段指明,任何格式后綴超鏈接應該用.html
后綴。
確保我們的URL模式是有名字的
如果我們想要超鏈接的API,那么我們要保證我們給URL起了名字。讓我們看看我們需要命名哪個鏈接。
- 我們API根指向
user-list
和snippet-list
。 - 我們的
snippet
序列包括一個指向snippet-highlight
的字段。 - 我們的用戶血烈包括一個指向
snippet-detail
的字段。 - 我們的snippet和用戶序列包括url字段,這個字段默認指向
'{model_name}-detail'
,這種情況下,它是snippet-detail
和user-detail
。 在將那些名字加入我們的URL配置(URLconf)
后,我們的snippets/urls.py
應該是下面的樣子:
from django.conf.urls import url, include from rest_framework.urlpatterns import format_suffix_patterns from snippets import views # API endpoints urlpatterns = format_suffix_patterns([ url(r'^$', views.api_root), url(r'^snippets/$', views.SnippetList.as_view(), name='snippet-list'), url(r'^snippets/(?P<pk>[0-9]+)/$', views.SnippetDetail.as_view(), name='snippet-detail'), url(r'^snippets/(?P<pk>[0-9]+)/highlight/$', views.SnippetHighlight.as_view(), name='snippet-highlight'), url(r'^users/$', views.UserList.as_view(), name='user-list'), url(r'^users/(?P<pk>[0-9]+)/$', views.UserDetail.as_view(), name='user-detail') ])
添加分頁
用戶和snippet的列表視圖會返回很多實例,所以我們想要給這些結果分頁,分頁后允許API客戶端訪問每個單頁。 我們可以用分頁改變默認的列表風格,只要稍微修改tutorial/settings.py
文件。添加下面設置:
REST_FRAMEWORK = { 'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination', 'PAGE_SIZE': 10 }
注意:REST框架的分頁設置(settings)是一個單獨的字典,叫'REST_FRAMEWORK',它可以幫我們區分項目中的其他配置。 如果我們需要,我們可以自定義分頁樣式,但是現在我們只是用默認的。
瀏覽API
如果我們打開瀏覽器訪問API,那么你會發現你可以通過下面的鏈接使用API。 你也可以看見snippet實例的高亮(highlight)鏈接,這些鏈接會返回高亮HTML代碼。 在本教程的第六部分,我們會用ViewSets
和Routers
來減少我們API的代碼量。
視圖集(ViewSets)和路由(Routers)
REST框架包括對ViewSets
的簡短描述,這可以讓開發者把精力集中在構建狀態和交互的API模型,而且它可以基於一般規范自動構建URL。 ViewSet
類幾乎和View
類一樣,除了他們提供像read
或update
的操作,而不是像get
和put
的方法。 目前,一個ViewSet
類只綁定一個方法的集合,當它初始化一個視圖的集合時,一般使用為你處理復雜的URL定義的Router
類。
使用視圖集(ViewSets)重構
讓我們來用視圖集重寫當前視圖。 首先,我們要把我們的UserList
和UserDetail
視圖重寫成單個UserViewSet
。我們可以UserViewSet
代替UserList
和UserDetail
。
from rest_framework import viewsets class UserViewSet(viewsets.ReadOnlyModelViewSet): """ This viewset automatically provides `list` and `detail` actions. """ queryset = User.objects.all() serializer_class = UserSerializer
這里我們使用ReadOnlyModelViewSet
類自動提供默認的'只讀'操作。黨我們使用常規視圖的時候,我們仍然需要設置准確設置queryset
和serializer_class
屬性,但是我們不在需要為兩個分開的類提供同樣的信息。 接下來,我們將用SnippetHighlight
視圖類來代替SnippetList
和SnippetDetail
。我們可以用一個類代替之前的三個類。
from rest_framework.decorators import detail_route from rest_framework.response import Response 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
包含在裝飾器關鍵參數中。
明確綁定視圖集到URL
我們定義URLConf
的時候,處理方法只綁定了動作。為了看看發生了什么,我們必須從我們的視圖集(ViewSets)
創建一個視圖集合。 在urls.py
文件中,我們將ViewSet
類綁定到具體視圖的集合。
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配置中。
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
文件。
from django.conf.urls import url, include from rest_framework.routers import DefaultRouter from snippets import views # 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. urlpatterns = [ url(r'^', include(router.urls)) ]
用路由注冊視圖和提供一個urlpattern
是相似的,包括兩個參數--視圖的URL前綴和視圖本身。 我們使用的默認路由(DefaultRouter)
類會自動為我們創建API根視圖,所以我們就可以從我們的views
模塊刪除api_root
方法。
views和viewsets的比較
使用視圖集(viewsets)真的很有用。它保證URL規范存在你的API中,讓你寫最少的代碼,允許你把注意力集中在你的API提供的交互和表現上而不需要特定的URL配置。 這並不意味着這樣做總是正確的。在使用基於類的視圖代替基於函數的視圖時,我們總會發現views
與viewsets
有相似的地方。使用視圖集(viewsets)
沒有比你自己的視圖更清晰。
回顧
難以置信,用這么少的代碼,我們已經完成了一個Web API,它是完全可瀏覽的,擁有完整的授權(authentication)
、每個對象權限(per-object permissions)
和多重渲染格式(multiple renderer formats)
。 我們已經經歷了設計過程的每一步,看到了如果我們只是使用常規的Django視圖自定義任何東西。