Django-REST-framework使用技巧(二)

1.请求和响应

从现在开始,我们将真正开始接触REST framework的核心。下面我们介绍几个基本的模块。

请求对象(request objects)

REST framework引入了一个扩展常规HTTPRequestRequest对象,并提供了更灵活的请求解析。request对象的核心功能是request.data属性,它与request.POST类似,但对于使用Web API更加有用。

request.POST #只处理表单数据。 只适用于'POST'方法

request.data #处理任意数据。使用与'POST', 'PUT'和'PATCH'方法

响应对象(response object)

REST framework还引入了一个Response对象,该对象是一种获取未渲染内容的TemplateResponse类型,并使用内容协商来确定正确内容类型返回给客户端。

reture  Response(data) # 渲染成客户端请求的内容类型

状态码

在你的视图中使用数字HTTP状态码,并不是总利于阅读,如果写错代码,很容易被忽略。REST framework为每个状态码提供更明确的标识符,例如Status模块中HTTP_400_BAD_REQUEST

包装API视图(Wrapping API views)

REST framework提供了两种编写API视图的封装。

  • 用于基于函数视图的@api_view装饰器
  • 用于基于类视图的APIView类

这些视图封装提供了一些功能,例如确保你的视图能够接收Request实例,并将上下文添加到Response对象,使得内容协商可以正常的运行。

视图封装还内置了一些行为。例如在适当的时候返回405 Method Not Allowed响应,并处理访问错误的输入request.data时候出发任何ParaseError异常。

组合视图

我们不需要再view.py中JSONResponse类所以删掉,然后我们可以重构我们的视图。

# quickstart/view.py 

from django.http import HttpResponse,JsonResponse
from django.views.decorators.csrf import csrf_exempt
from rest_framework.parsers import JSONParser
from quickstart.models import Snippet
from quickstart.serializers import SnippetSerializer
from rest_framework.decorators import api_view #新增导入
from rest_framework.response import Response #新增导入
from rest_framework.status import HTTP_201_CREATED, HTTP_400_BAD_REQUEST, HTTP_404_NOT_FOUND,HTTP_204_NO_CONTENT  #新增导入

# @csrf_exempt # 删除
@api_view(['GET', 'POST']) #  新增
def snippet_list(request):
    # 列出所有代码 snippet, 或者创建一个新的snippet

    if request.method == 'GET':
        snippets = Snippet.objects.all()
        serializer = SnippetSerializer(snippets, many=True)
        # return JsonResponse(serializer.data, safe=False) # 删除
        return Response(serializer.data) # 新增
    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 Response(serializer.data, status=HTTP_201_CREATED) # 新增

    # return JsonResponse(serializer.errors, status=400) # 删除
    return Response(serializer.errors, status=HTTP_400_BAD_REQUEST) # 新增

我们的实例视图比前面的示例有所改进。它稍微简洁一点,现在的代码与我们使用 Forms API 时非常相似。我们还使用了指定的状态码,这使得响应更加明显。

# @csrf_exempt # 删除
@api_view(['GET', 'PUT', 'DELETE']) #新增
def snippet_detail(request, pk):
    # 获取、更新或者删除一个代码 snippet

    try:
        snippet = Snippet.objects.get(pk=pk)
    except Snippet.DoesNotExist:
        # return HttpResponse(status=404) # 删除
        return Response(status=HTTP_404_NOT_FOUND)  #新增

    if request.method == 'GET':
        serializer = SnippetSerializer(snippet)
        # return JsonResponse(serializer.data) # 删除
        return Response(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 Response(serializer.data)  #新增
        # return JsonResponse(serializer.errors, status=400) # 删除
        return Response(serializer.errors, status=HTTP_400_BAD_REQUEST)  #新增

    elif request.method == 'DELETE':
        snippet.delete()
        # return HttpResponse(status=204) # 删除
        return Response(status=HTTP_204_NO_CONTENT)  #新增

这对我们来说应该都是非常熟悉的 - 它和正常 Django 视图并没有什么不同。

注意,我们不再显式地将请求或响应绑定到给定的内容类型。request.data 可以处理传入的 json 请求,但它也可以处理其他格式。同样,我们返回带有数据的响应对象,但允许 REST framework 将响应渲染成正确的内容类型。

为我们的网址添加可选的格式后缀

为了利用我们的响应不再被硬链接到单个内容类型的事实。让我们将格式后缀的之处添加我们的API端点。使用格式后缀,欧文提供了明确的只想给定的格式URL,这意味着我们的API可以处理一些URLs,类似这样的格式:http://example.con/api/items/4.json.
像下面这样在这两个视图中添加一个format关键字参数。

def snippet_list(request, format=None):
def snippet_detail(request,pk, format=None):

现在更新quickstart/urls.py 文件,在现在的urls基础上追加一组format_suffix_patterns

# quickstart/urls.py

from django.conf.urls import url
from .views import snippet_list, snippet_detail
from rest_framework.urlpatterns import format_suffix_patterns #新增

urlpatterns = [
    url('^quickstart/$', snippet_list),
    url('^quickstart/(?P<pk>[0-9]+)/$', snippet_detail),
]

urlpatterns = format_suffix_patterns(urlpatterns) #新增

我们不一定需要添加额外的url模式。但他给我们一个简单、清晰的方式来引用特定的格式。

测试API

启动服务 python manage.py runserver

在浏览器中输入 http://127.0.0.1:8000/quickstart/,结果如下

在浏览器输入http://127.0.0.1:8000/quickstart.api,结果如下

在浏览器输入http://127.0.0.1:8000/quickstart.json,结果如下

可视化

由于API是基于客户端发起请求的选择来响应内容的格式,因此当接收到来之浏览器的请求是,会默认以HTML的格式来描述数据,这允许API返回网页完全可浏览的HTML。
有关可浏览的API功能以及如何对其进行定制更多的信息,可参考可浏览的API主题。

# 2. 基于类的视图 我们也可以使用基于**类的视图**来编写API视图,而不是基于**函数**的视图。正如我们看到的,这个模式足够强大,可以让我们重用通用功能,并帮助我们保持代码[DRY](https://en.wikipedia.org/wiki/Don't_repeat_yourself)(Don't repeat yourself)

使用基于类的视图我们重写我们的API

我们首先将基于类的视图重写根视图。所有者写都涉及对view.py的修改。

# quickstart/view.py

class SnippetList(APIView):
    # 列出所有的代码snippet,或者创建一个新的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=HTTP_201_CREATED)
        return Response(serializer.errors, status=HTTP_400_BAD_REQUEST)

这个看起来和以前的非常相似,但是我们在不同的HTTP方法之间有更好的分离,还需要更新view.py中的示例视图。

# quickstart/view.py

class SnippetDetail(APIView):
    # 获取、更新或者删除一个代码 snippet

    def get_object(self, pk):

        try:
            return Snippet.objects.get(pk=pk)
        except Snippet.DoesNotExist:
            raise HTTP_404_NOT_FOUND

    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=HTTP_400_BAD_REQUEST)
    
    def delete(self, request, pk, format=None):
        snippet = self.get_object(pk)
        snippet.delete()
        return Response(status=HTTP_204_NO_CONTENT)

看起来,它仍然非常类似基于函数的视图

使用基于列的视图,我们现在还需要重构quickstart/urls.py文件内容:

from django.conf.urls import url
from rest_framework.urlpatterns import format_suffix_patterns
from .views import SnippetList, SnippetDetail

urlpatterns = [
    url('^quickstart/$', SnippetList.as_view()),
    url('^quickstart/(?P<pk>[0-9]+)/$', SnippetDetail.as_view()),
]

urlpatterns = format_suffix_patterns(urlpatterns)

重启服务,如果你没有搬错砖,一切应该和以前一样的。

使用混合(mixins)

使用基于类的视图的优势之一,就是我们可以很容易撰写可重复的行为。
到目前为止,我们使用创建、获取、更新、删除操作和我们创建的任何基于模型的API视图非常相似。这些常见的行为是在REST framework的mixin类中实现的。

下面我们具体实践如何通过mixin类编写视图。在quickstart/view.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构建视图,并且使用了ListModelMixinCreateModelMixin.
基类提供核心功能,而mixin类提供.list().create()操作.让后我们可以明确的吧getpost绑定到适当的操作.

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类来提供核心功能,并添加mixins来提供.retrieve().update().destroy()操作。

从新运行服务,你会发现没有任何变化的。如果有,那就再运行一次。

使用通用的基于类的视图

我们使用mixin类,使用了比以前更少的代码重写了视图,但是我们可以更进一步。REST framework已经提供了一套已经混合的通用的视图。我们可以更加简化view.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

刷新浏览器,你会发现没任何变化,如果有,老规矩:你搬错砖了

# 3.认证和权限 目前,我们的api对谁都是开放的,谁都可以添加或者删除代码,没有任何隐私和限制,我们需要更高行为来确保:
  • snippet代码始终与创建者关联
  • 只有经过身份验证的用户才可以创建snippet
  • 只有snippet的创建这个才可以更新或者删除它
  • 未经身份验证的请求应具有只读的访问权限

将信息添加到我们的模型里面

我们对quickstart中的模块要再修改一下,首先要添加几个字段。其中一个字段将用于表示创建snippet代码关联的用户。
另外一个字段将用于存储高亮显示的,HTML内容表示的代码。
添加字段到quickstart/model.py文件中的Snippet模型中:

from django.db import models
from pygments.lexers import get_all_lexers
from pygments.styles import get_all_styles


# 提取出'pyment' 支持的所有语言的语法分析程序
LEXERS = [item for item in get_all_lexers() if item[1]]

# 提取除了'pyments' 支持的所有语言列表
LANGUAGE_CHOICES = sorted([(item[1][0], item[0]) for item in LEXERS])

# 提取出'pyment' 支持的所有格式化风格列表
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=120)
    style = models.CharField(choices=STYLE_CHOICES, default='friendly', max_length=120)
    owner = models.ForeignKey('auth.User', related_name='snippets', on_delete=models.CASCADE) # 新增
    highlighted = models.TextField() #新增

    class Meta:
        ordering = ('-created',)

我们还需要确保在保存模型时候,使用pygments代码的高亮库来填充highlighted字段

我们需要导入额外模块,

from pygments.formatters.html import HtmlFormatter
from pygments import highlight
from pygments.lexers import get_lexer_by_name

然后再类中添加一个方法.save()

    def save(self, *args, **kwargs):
        # 使用 pygment 库创建一个高亮显示html表示的Snippet代码

        lexer = get_lexer_by_name(self.language)
        linenos = 'table' if self.linenos else False
        options = {'title': self.title} if self.title else {}
        formatter = HtmlFormatter(style=self.style, linenos=linenos, full=True, **options)
        self.highlighted = highlight(self.code, lexer, formatter)
        super(Snippet, self).save(*args, **kwargs)

做完这些工作,同创做法是会场街一个数据库迁移来实现这一点,但是现在让我们删掉数据库,重新开始。

(env) AdministratordeiMac:tutorial administrator$ ls
db.sqlite3	env		manage.py	quickstart  tutorial
(env) AdministratordeiMac:tutorial administrator$ rm -f db.sqlite3
(env) AdministratordeiMac:tutorial administrator$ rm -r quickstart/migrations

(env) AdministratordeiMac:tutorial administrator$ python manage.py makemigrations quickstart
(env) AdministratordeiMac:tutorial administrator$ python manage.py migrate quickstart

你可以创建几个不同的用户,用于测试不同的API,最快捷的方式就是createsuperuser命令。

pyhton manage.py createsuperuser #然后按照提示填写用户名和密码就可以了

为我们的用户模型添加端点

我们现在有一些用户可以使用,我们最好将这些用户添加到我们的API中。创建一个新的序列化器,在serializer.py我呢间中添加:

# quickstart/serializer.py

from rest_framework import serializers
from .models import Snippet
from django.contrib.auth.models import User


class SnippetSerializer(serializers.ModelSerializer):

    class Meta:
        model = Snippet
        fields = ('id', 'title', 'code', 'linenos', 'language', 'style')


#新增
class UserSerializer(serializers.ModelSerializer): 

    snippets = serializers.PrimaryKeyRelatedField(many=True, queryset=Snippet.objects.all())

    class Meta:
        model = User
        fields = ('id', 'username', 'snippets')

由于snippets在用户模型上是反向关系,当使用ModelSerializer类时候,它将不会被默认包含,我们需要为他添加一个显式字段。

我们还会在view.py中添加几个视图。我们想要使用用户,表示只读视图,所以我们将使用ListAPIViewRetrieveAPIView通用的基于类的视图。

#quictstart/view.py

from rest_framework import generics
from .models import Snippet
from .serializers import SnippetSerializer, UserSerializer #新增导入
from django.contrib.auth.models import User


class SnippetList(generics.ListCreateAPIView):
    queryset = Snippet.objects.all()
    serializer_class = SnippetSerializer


class SnippetDetail(generics.RetrieveUpdateDestroyAPIView):
    queryset = Snippet.objects.all()
    serializer_class = SnippetSerializer

# 新增
class UserList(generics.ListAPIView):
    queryset = User.objects.all()
    serializer_class = UserSerializer
    
    
class UserDetail(generics.RetrieveAPIView):
    queryset = User.objects.all()
    serializer_class = UserSerializer

最后我们需要通过URL conf中引用这些视图来将这些视图添加到API中,修改如下:

from django.conf.urls import url
from rest_framework.urlpatterns import format_suffix_patterns
from .views import SnippetList, SnippetDetail, UserList, UserDetail #新增导入

urlpatterns = [
    url('^quickstart/$', SnippetList.as_view()),
    url('^quickstart/(?P<pk>[0-9]+)/$', SnippetDetail.as_view()),
    url('^users/$',UserList.as_view()) # 新增
    url('users/(?P<pk>[0-9]+)/$', UserDetail.as_view()), # 新增
]

urlpatterns = format_suffix_patterns(urlpatterns)

将Snippets和用户关联

现在,如果我们创建了一个Snippet代码,那么无法将创建Snippet的用户与Snippet实例关联起来。用户不是作为序列化表示的一部分发送的,而是作为请求的属性传入。
我们处理方式是,在我们的Snippet视图上覆盖,.perform_create()方法,这中管理方式,允许我们修改实例保存,并处理传入请求,或者请求URL中隐含的任何信息。
SnippetList视图类中添加一下方法:

class SnippetList(generics.ListCreateAPIView):
    queryset = Snippet.objects.all()
    serializer_class = SnippetSerializer
    
    # 新增
    def perform_create(self, serializer):
        serializer.save(owner=self.request.user)

现在我们的序列化器create()方法将被传递一个owner字段以及来自请求的验证数据。

更新序列化器

现在Snippet与创建他们的用户相关联了,让我们更新我们的SnippetSerializer以体现这一点。将下面内容添加到serializers.py文件中:

# quickstart/serializers.py

class SnippetSerializer(serializers.ModelSerializer):

    owner = serializers.ReadOnlyField(source='owner.username') # 新增

    class Meta:
        model = Snippet
        fields = ('id', 'title', 'code', 'linenos', 'language', 'style', 'owner')

注意:确保将owner添加到了内部的Meta类的字段列表中。

这个字段中,source参数控制哪个属性用于填充字段,并且可以指向序列化实例的任何属性。它也可以采用如上所示的点符号(.),在这种情况下,他将与Django模板语言相似的方式遍历给定的属性。

我们添加的字段是无类型的ReadOnlyField类,与其他类型的字段相反,如CharField,BooleanField等无类型的,但ReadOnlyField始终是只读的,只能用于序列化表示,但不能用于在反序列化实时更新模型实例。我们这里可以用CharField(read_only=True).

添加视图所需的权限

现在,Snippet和用户相关联,我们希望确保只要经过身份认证的用户才能创建、更新、和删除snippets。
REST framework包含许多权限类,我们可以用来限制访问权限。在这种情况下,我们需要的是IsAuthenticatedOrReadOnly类,它将确保身份验证的请求获得读写访问权限。未经身份验证的请求获得只读访问权限。

在quickstart/view.py文件中,添加如下

# quickstart/view.py 

from rest_framework import permissions #新增


class SnippetList(generics.ListCreateAPIView):
    queryset = Snippet.objects.all()
    serializer_class = SnippetSerializer
    permission_classes = (permissions.IsAuthenticatedOrReadOnly,) #新增

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


class SnippetDetail(generics.RetrieveUpdateDestroyAPIView):
    queryset = Snippet.objects.all()
    serializer_class = SnippetSerializer
    permission_classes = (permissions.IsAuthenticatedOrReadOnly,) #新增

增加查看API权限,添加登录

如果你现在打开浏览器,并导航到浏览器的到API,那么你将发现无法再创建新的snippet。因为需要增加权限。
我们可以通过编辑url.py文件中的URLconf来添加可浏览API的登录视图。
在文件顶部,我们导入:

	from django.conf.urls import include

并在文件末尾添加一个模式以包括可浏览的API的登录或者注销视图。

	urlpatterns = [
    url('^quickstart/$', SnippetList.as_view()),
    url('^quickstart/(?P<pk>[0-9]+)/$', SnippetDetail.as_view()),
    url('^users/$',UserList.as_view()),
    url('users/(?P<pk>[0-9]+)/$', UserDetail.as_view()),
    url('^api-auth/', include('rest_framework.urls')), # 新增
]

模式api-auth/部分实际上可以是你想使用的任何URL。

现在再次打开浏览器刷新页面,则会在右上角看到一个登录链接。如果你要用前面的创建用户登录,就可以再次创建snippets。如下图:

一旦创建了一些snippets后,导航到/users/端点,并注意到每个用户的snippets字段中包含与每个用户相关联的snippet id列表。

对象级权限

实际上我们希望所有人都可以看到snippets,但是只要创建snippet的用户才权限CRUD。
为此,我们需要创建一个自定义的权限。
在quickstart应用中,创建一个新的文件permission.py.

# quickstrat/permissions.py

from rest_framework import permissions

class IsOwnerOrReadOnly(permissions.BasePermission):
    # 自定义权限只允许队形的所有者去编辑它

    def has_object_permission(self, request, view, obj):
        # 读取权限被允许用于任何请求
        # 所以我们始终允许GET, HEAD 或者OPTIONS请求。

        if request.method in permissions.SAFE_METHODS:
            return True

        # 写入权限只允许给snippet的所有者
        return obj.owner == request.user

现在我们可以通过编辑SnippetDetail视图中的permission_classes属性将自定义权限添加到我们snippet实例中:

#  quickstart/view.py

from .permissions import IsOwnerOrReadOnly # 新增

class SnippetDetail(generics.RetrieveUpdateDestroyAPIView):
    queryset = Snippet.objects.all()
    serializer_class = SnippetSerializer
    permission_classes = (permissions.IsAuthenticatedOrReadOnly, IsOwnerOrReadOnly) # 新增


现在再次打开浏览器,你会发现"DELETE"和'PUT'操作只会出现在以创建者身份登录的snippet实例端点上。

使用API进行身份验证

因为现在我们在API上有一组权限,如果我们想要编辑任何snippet,我们需要先验证我们的请求。我们还没设置任何认证类,因此当前应用默认的SessionAuthenticationBasicAuthentication.

但我们通过web浏览器与API进行交互的时候,我们可以登录,然后浏览器会话将为请求提供所需的身份验证。

如果我们以百年城的方式与API进行交互,那么我们需要在每个请求上明确提供身份验证凭据。

如果我们尝试建立一个没身份验证的snippet,我们会得到一个错误:

我们可以通过添加之前的用户名和密码来发送成功请求:

我们已经在我们的webAPI上获得了一套精细的权限组合,下一节我们将介绍如何通过为高亮显示 snippet 创建 HTML 端点来将所有内容联结在一起,并通过对系统内的关系使用超链接来提高 API 的凝聚力。

posted on 2019-06-10 14:33  寒江雪_独钓  阅读(205)  评论(0编辑  收藏  举报

导航