[关闭]
@hpp157 2016-08-25T14:42:09.000000Z 字数 18811 阅读 3893

(译)第四章 构建一个REST API

django python


看本教程之前,需要对http协议有清晰的认识和了解,推荐一篇阮一峰大大的文章HTTP协议入门,看完文章后再看本教程

自从Roy Fielding博士,一个美国的计算机科学家以及http协议的主要作者之一,在他的博士论文中提出表述性状态转换(REST)这个架构模式以来,已经有十多年了。这些年来,REST 对web服务的开发产生了深远的影响

REST通过无状态stateless,以及创建的唯一标识符来允许存储,分层,和可伸缩,REST APIs 使用已经在的HTTP动词GET,POST,PUT,DELETE来生成,修改以及删除新的资源。REST这个术语常被大众误解为是可以让任意一个URL返回JSON对象而不是以前的html格式。许多人没有意识到:一个基于RESTful API的web服务必须要满足一些正式的约束和规范才可以。尤其是应用必须被分离成client和server这样的模型,并且服务端必须是完全的statless无状态。
没有客户端属性的上下文(no client context)将会存储在服务端,并且所有的资源都会被统一的用唯一标识符标记,客户端用链接和响应的资源中的头信息(metadata)来操作api以及传递状态。客户端不应假设(should not assume)存在资源或动作(action),除了一些固定的入口点。比如API的root位置等。

本章将告诉你怎样使用django实现一个REST APIs的架构设计

context :总体来说Context就是一个描述应用环境的上下文,在Context中你可以得到你想要的应用的资源和类。No client context 就是只有服务端环境。
context其实说白了,和文章的上下文是一个意思,在通俗一点,我觉得叫环境更好
比如说现在的总统啊,主席啦什么的放古代就是皇帝,这个‘古代’就是context,没有context,结论就不成立
3在一个多层结构的应用中,我们需要传递一些上下文的信息在各层之间传递,这些信息就是context
知乎上有关于什么是上下文的介绍,可以看下:编程中什么是context?

django和REST

在django社区中,最流行的两个用于构建REST的框架是django-tastpie1django-rest-framework。两者都支持从orm和non-ORM data中创建资源,权限和验证都是可插拔的设计,并且支持各种序列化方法,包括json,xml,yaml以及html。
本章我们会充分使用django-rest-framework来帮助我们构建我们的API结构,这个可复用的小框架的最大的特点就是支持自己elf-docementing以及web-browsable APIs,能够将数据从API作出响应到客户端接收到的过程可视化.

我们知道url中包含query parameters,在把它传递给ORM数据库之前,我们是要进行过滤的。django-tastpie1django-rest-framework两个框架都还没有提供过滤转换功能,但是django-rest-framework是可插拔的机制,整合一个django-filter进入这个框架非常容易

我们以安装一些必要的依赖项来开始我们的项目,当人 要看API数据的时候,Markdown可以帮助把注释转换成页面。

  1. $ sudo pip install django-rest-framework Markdown django-filter

Scrum Board Data Map(数据映射)

对于任何项目来说,创建数据模型都是第一步要做的事情,让我们花费一点时间来罗列一下创建模型时要考虑的重点:

初始化项目

我们通过startproject命令来初始化项目,并且初始化一个叫做 board的单应用:

注意:项目project和app的区别,project是app组成的

  1. $ django-admin.py startproject scrum
  2. $ cd scrum
  3. $ python manage.py startapp board

项目设置

当我们创建新项目的时候,需要去更新默认生成的项目设置文件(scrum/settings.py)把django-reset-framework包含到设置里来,同时也去除一些用不到的设置。也由于server端不会存储客户端的state信息,引用的contrib-session这个模块也可以去掉,这样的话就不能使用django admin了,所以它也可以去掉了。

  1. INSTALLED_APPS = [
  2. 'django.contrib.auth',
  3. 'django.contrib.contenttypes',
  4. 'django.contrib.staticfiles',
  5. #third party apps
  6. 'rest_framework',
  7. 'rest_framework.authtoken',
  8. #internal apps
  9. 'board',
  10. ]
  11. MIDDLEWARE_CLASSES = [
  12. 'django.middleware.common.CommonMiddleware',
  13. 'django.middleware.csrf.CsrfViewMiddleware',
  14. 'django.middleware.clickjacking.XFrameOptionsMiddleware',
  15. ]
  16. DATABASES = {
  17. 'default': {
  18. 'ENGINE': 'django.db.backends.psycopg2',
  19. 'NAME': 'scrum',
  20. 'USER': 'root',
  21. 'PASSWORD': '',
  22. 'HOST': 'localhost',
  23. 'PORT': '5432',
  24. }
  25. }

我们使用Postgresql数据库而不再使用django默认自带的sqlite3,这需要你的机器上安装该数据库以及python使用的postgresql驱动psycopg2,最好是百度下怎么安装,下面的命令供参考:

  1. $ sudo pip install psycopg2#安装驱动
  2. $ createdb -E UTF-8 scrum #创建数据库

如果你是苹果系统用户的化,建议你通过homebrew来安装,它是苹果系统上的一个包管理工具,安装请参考http://brew.sh/

既然django admin从设置里去掉了,那么urls文件中关于django admin的部分也可以删掉了

  1. from django.conf.urls import url,include
  2. from rest_framework.authtoken.views import obtain_auth_token
  3. urlpatterns = [
  4. url(r'^api/token/', obtain_auth_token,name='api-token'),
  5. ]

如你看到的,所以已经存在的patterns已经被移走并且替换成了一个单一的url。from rest_framework.authtoken.views import obtain_auth_token。这是一个用于交换用户名和密码组合的一个接口的视图,我们会在项目的下一步使用include来导入设置。

不用django admin 了么?

一些Django的开发者可能对于去掉django admin还有点不理解,在没有它的情况下样怎么管理项目。在这个项目中删掉了是因为API不使用它,或者api并不需要session管理,django-rest-framework提供的browsalbe API可以作为一个简单的替代。
你会发现你仍就需要django admin由于你的其他的项目依赖它,如果有这种情况,你可以保留django admin,并且做两个简单的设置,一个给API 用,另一个给admin用。admin的设置中包括session,message还有在中间件里边加入authentication这个中间件。当然你还需要对root urls设置,让它包括admin的url,通过这些设置,一些服务端既能服务admin又能服务API。考虑到大型的应用很可能有不同的扩展需求,这样的话结构比较清晰。

模型

我们现在可以设计模型了,首先就是要把任务tasks分散成sprints。每个sprints有可选的名字,描述,以及一个单独的截至日期。在board/models.py中编写模型:

  1. from django.conf import settings
  2. from django.db import models
  3. from django.utils.translation import ugettext_lazy as _
  4. # Create your models here.
  5. class Sprint(models.Model):
  6. name=models.CharField(max_length=100,blank=True,default='')
  7. description=models.TextField(blank=True,default='')
  8. end =models.DateField(unique=True)
  9. def __str__(self):
  10. return self.name or _('sprint ending %s') % self.end

我们也需要一个task模型,通过给出的print列出任务,task有个name,可选的description,关联一个sprint(外键,和其它表联系的),给它们添加一个user assigned(也是外键),包括start,end, due date.
我们也需要用状态信息表示task:

  1. from django.conf import settings
  2. from django.db import models
  3. from django.utils.translation import ugettext_lazy as _
  4. ...
  5. class Task(models.Model):
  6. STATUS_TODO=1
  7. STATUS_IN_PROCESS=2
  8. STATUS_TESTING=3
  9. SATAUS_DONE=4
  10. STATUS_CHOICES=(
  11. (STATUS_TODO,_('Not started')) ,
  12. (STATUS_IN_PROCESS,_('In process')),
  13. (STATUS_TESTING, _('Testing')),
  14. (SATAUS_DONE, _('Done')),
  15. )
  16. name=models.CharField(max_length=100)
  17. description=models.TextField(blank=True,default='')
  18. sprint=models.ForeignKey(Sprint,blank=True,null=True)
  19. status =models.SmallIntegerField(choices=STATUS_CHOICES,default=STATUS_TODO)
  20. order =models.SmallIntegerField(default=0)
  21. assigned=models.ForeignKey(settings.AUTH_USER_MODEL,null=True,blank=True)
  22. satrted=models.DateField(blank=True,null=True)
  23. due=models.DateField(blank=True,null=True)
  24. completed=models.DateField(blank=True,null=True)
  25. def __str__(self):
  26. return self.name

我们已经有了连个项目所需要的模型,但是你可以看到在数据模型中有明显的局限性。 states信息在task模型中,让status无法在sprint模型中的使用,还有就是task workflow 没法自定义,这些局限性在单个项目中是可以接受的,但是很明显想要把这个任务板as a sofrware as a service把软件看作一项服务(SaaS)是不行的。

有了模型我们在命令行运行makemigrationsmigrate命令来更新数据库。

  1. $ python manage.py makemigrations board
  2. ....
  3. -create model sprint
  4. -create model task
  5. $ python manage.py migrate
  6. .....

接下来我们要创建一个超级用户,自己选择一个用户名,使用的是createsuperuser命令。将来的例子中要使用这个超级用户

  1. $ python manage.py createsuperuser
  2. (leave blank to use 'username': demo
  3. Email address:demo@example.com
  4. Password:
  5. Password:(again)
  6. Superuser cteated successfully

设计API

随着模型的就位[in place],现在我们把注意力移回到API本身上。就url而言,我们设计出来的api看起来像这样:

  1. /api/
  2. /sprints/
  3. /<id>/
  4. /tasks/
  5. /<id>/
  6. /users/
  7. /username/

很重要的一点,考虑一下客户端如何操纵这些API。的。客户端会向API的根目录/api/发出(issue)一个GET请求,然后去/api/的下面。到这儿,客户端会看到拿到一份sprints,tasks,和users清单,就像在客户端重新制作了一份清单一样。当客户端深入sprint里面看完它的清单后,就会去和它并列的task看task的清单,再然后是user。资源之间的超链接使得客户端可以操纵API 。

我们已经选择了在URL后面不使用版本号。尽管许多非常受欢迎的API 采用了它,因为我们觉得RESTful api实现版本控制的最佳实践就是为不同类型的内容使用不同版本的API 。但是要是你实在想加的话,就自由的加上吧。

sprint的端点(endpoint)

通过ViewSet的来把创建的资源和django-rest-framework的模型绑定起来是很容易的事。我们应该描述模型model是应该怎样被API序列化和反序列化的。这个由board目录下面的serializers.py中的序列化器处理的。序列化器是从rest_framework中引入的。

  1. from rest_framework import serializers
  2. from .models import Sprint
  3. class SprintSerializer(serializers.models):
  4. class Meta:
  5. model =Sprint
  6. fields=('id','name','description','end')

在这个简单的例子中,所有的字段通过API暴露了出来.
让我们在board/views.py中创建ViewSet。首先会引入框架的viewsets

  1. from rest_framework import viewsets
  2. from .models import Sprint
  3. from .serializers import SprintSerializer
  4. class SprintViewSet(viewsets.ModelViewSet):
  5. queryset = Sprint.object.order_by('end')
  6. serializers_class =SprintSerializer

正如你看到的,ModelViewSet使用HTTP可以理解的动词提供了CURD所需要的 脚手架(scaffolding)。默认的验证,权限,分页以及过滤如果在自己的视图里没有设置的话,就会受rest_framework的设置影响。
我们这里的view会给自己明确设置这些,因为剩下的其他view也会使用,因此使用一个mixin类(board/views.py)来实现.

  1. from rest_framework import viewsets
  2. from .models import Sprint
  3. from .serializers import SprintSerializer
  4. #自己设置的
  5. class DefaultsMixin(object):
  6. authentication_class=(
  7. authentication.BaseAuthentication,
  8. authentication.TokenAuthentication
  9. )
  10. permissions_classes=(
  11. permissions.IsAuthenticated
  12. )
  13. paginate=25
  14. paginate_by_param='page_size'
  15. max_paginage_by =100
  16. class SprintViewSet(viewsets.ModelViewSet):
  17. queryset = Sprint.object.order_by('end')
  18. serializers_class =SprintSerializer

现在,让我们给用户权限添加一些验证,验证使用http基本认证 或者使用基于口令的验证。使用基本验证的话,可以方便地使web浏览器(客户端)浏览API。我们这个例子没有那么多细致的权限,唯一的权限要求就是user是被验证过的permissions.IsAuthenticated

task和user的端点(endpoint)

我们需要task在它们各自的末尾暴露出来,就和上边的sprint末尾一样,我们要以一个serializer开始。然后是ViewSet。首先,在board/serializers.py中创建一个序列器。

  1. from rest_framework import serializers
  2. from .models import Sprint ,Task
  3. #task's serializer
  4. class TaskSerializer(serializers.models):
  5. class Meta:
  6. model=Task
  7. fields=(
  8. 'id','name','description','sprint',
  9. 'status','order','assigned','started',
  10. 'due','completed',
  11. )
  12. #Sprint's serializer
  13. class SprintSerializer(serializers.models):
  14. ...

看上去还不错,但是这样写系列器是有点问题的。status显示的是数字而不是数字代表的状态,数字本身并没有什么语义,单数字我们还是不知具体的状态,所以我们要通过添加一个字段status_display来显示数字所代表的状态。

  1. from rest_framework import serializers
  2. from .models import Sprint,Task#导入两个模型
  3. class TaskSerializer(serializers.ModelSerializer):
  4. status_display=serializers.SerializerMethodField('get_status_display')
  5. class Meta:
  6. model =Task
  7. fields=('id','name','description','sprint','status',
  8. 'status_display','order','assigned','started',
  9. 'due','completed',)
  10. def get_status_display(self,obj):
  11. return obj.get_status_display()
  12. #Sprint's serializer
  13. class SprintSerializer(serializers.models):

status_display 是一个要被序列化的只读字段,它返回的是在serializer中get_status_display()方法的返回值。

我们序列器的第二个问题assigned是一个User模型的外键。它显示的是user的主键,但是我们的url结构希望通过用户名来找到user。我们通过使用序列器的SlugRelatedField方法来修改它。`

  1. from rest_framework import serializers
  2. from .models import Sprint, Task # 导入两个模型
  3. class TaskSerializer(serializers.ModelSerializer):
  4. #添加的slugRelatedField方法
  5. assgined = serializers.SlugRelatedField(slug_field=User.USERNAME_FIELD, required=False)
  6. status_display = serializers.SerializerMethodField('get_status_display')
  7. class Meta:
  8. model = Task
  9. fields = ('id', 'name', 'description', 'sprint', 'status',
  10. 'status_display', 'order', 'assigned', 'started',
  11. 'due', 'completed',)
  12. def get_status_display(self, obj):
  13. return obj.get_status_display()

最后,我们还要为User模型创建一个序列器, 先想一下,我们的User模型应该可以和另一个模型交换出去(swapped out to another)意图就是使我们的应用尽可能的复用。我们用django工具get_user_model来满足这个需求。board/serializers.py

  1. from django.contrib.auth import get_user_model# 引入这个方法
  2. from rest_framework import serializers
  3. from .models import Sprint, Task
  4. User =get_user_model()#把方法赋值给模型
  5. class UserSerializer(serializers.ModelSerializer)
  6. full_name=serializers.CharField(source='get_full_name',read_only=True)
  7. class Meta:
  8. model=User
  9. fields=(
  10. 'id',User.USERNAME_FIELD,
  11. 'full_name',
  12. 'is_active',
  13. )

如果一个自定义的用户模型被使用了,这个序列器就会继承django.contrib.auth.models.CustomUser对象,获得USERNAME_FIELD属性,get_full_name方法以及is_active这个属性。要注意,既然 gei_full_name是一个方法,这个字段在序列器中就会被标记为read-only只读。

序列器编写完成后,接着就需要创建task和users的ViewSets了。board/views.py

  1. from django.contrib.auth import get_user_model
  2. from rest_framework import authentication, permissions, viewsets
  3. from .models import Sprint , Task
  4. from .serializers import SprintSerializer, TaskSerializer, UserSerializer
  5. User = get_user_model()
  6. ....
  7. class DefaultsMixin(object):
  8. ...
  9. class TaskViewSet(DefaultsMixin,viewsets.ModelViewSet):
  10. queryset = Task.objects.all()
  11. serializer_class = TaskSerializer
  12. class UserViewSet(DefaultsMixin,viewsets.ModelViewSet):
  13. lookup_field = User.USERNAME_FIELD
  14. lookup_url_kwarg = User.USERNAME_FIELD
  15. queryset = User.objects.order_by(User.USERNAME_FIELD)
  16. serializer_class = UserSerializer

这连个vieset都和sprintViewSet很相似,但是这儿有一点差异,在Userviewset中,是从ReadOnlyModelViewSet中继承来的。正如名字暗示的一样,这样不会暴露通过api创建新的用户以及修改用户的action动作, UserviewSet会把通过id查找用户名的方法改为通过设置一个lookup_field来查找。注意下lookup_url_kwarg为了一致性,也已经改了。

连接路由

到这儿,board应用有了基本的数据模型和视图逻辑,但是还没有和URL路由系统连接起来django-rest-framework有自己的路由系统扩展来处理ViewSet,每一个ViewSet使用给出的URL前缀在路由中注册。这次,我们在board/urls.py中添加一个新file

  1. from rest_framework.routers import DefaultRouter
  2. from . import views
  3. router=DefaultRouter()
  4. router.register(r'sprints',views.SprintViewSet)
  5. router.register(r'task',views.TaskViewSet)
  6. router.register(r'user',views.UserViewSet)

最后,这个路由需要被included到根目录中的URL配置文件中,scrum/urls.py

  1. from django.conf.urls import patterns, include, url
  2. from rest_framework.authtoken.views import obtain_auth_token
  3. from board.urls import router
  4. urlpatterns = patterns('',
  5. # Examples:
  6. # url(r'^$', 'scrum.views.home', name='home'),
  7. # url(r'^blog/', include('blog.urls')),
  8. url(r'^api/token', obtain_auth_token, name='api-token'),
  9. url(r'api',include(router.urls))
  10. )

现在我们已经有基本模型,视图,和我们的Scrum board应用的URL结构了,现在我们可以创建我们RESTful 应用的剩下部分了(remainder of our RESTful application)

资源的链接

RESTful应用最重要的规范就是以超文本作为应用状态的引擎(HATEOAS),(意思就是必须以超文本驱动),在这个约束下,RESTful的客户端应该能够同服务器产生的超媒体响应互动起来.也就是说,客户端应该只注意到服务器上的几个固定端点.通过这些固定的端点,客户端应该通过使用描述性资源消息发现服务器上可用的资源。客户端必须能够解读服务器响应,并且从元数据中分离资源数据,比如资源的链接。(such as links to resources)

这些如何转换为我们的资源呢(意思是我们怎样使用上边的理论),在这些资源中,服务器能提供什么样的有用链接?首先,每个资源应该知道它自己的URLSprint提供链接给它有联系的task和`backlog。分配给用户的任务——提供link给User资源,Users提供一种获取这个用户分配的所有任务的途径。这些都做好之后,当操作api时,api客户端就能够响应大多数的问题。

为了给客户端一个看能够看到这些链接的统一的地方,每个资源将在响应中包含相关链接一个链接部分,开始的最方便的方法就是把资源的链接连回到自己身上。和board/serializers.py中展示的一样

  1. from django.contrib.auth import get_user_model
  2. from rest_framework import serializers
  3. from rest_framework.reverse import reverse # 新导入的
  4. from .models import Sprint, Task
  5. User = get_user_model()
  6. class UserSerializer(serializers.ModelSerializer):
  7. full_name = serializers.CharField(source='get_full_name', read_only=True)
  8. links = serializers.SerializerMethodField()
  9. class Meta:
  10. model = User
  11. fields = (
  12. 'id', User.USERNAME_FIELD,
  13. 'full_name',
  14. 'is_active',
  15. )
  16. def get_links(self, obj):
  17. request = self.context['requset']
  18. username = obj.get_username()
  19. return {
  20. 'self': reverse('user-detail', kwargs={User.USERNAME_FIELD: username}, request=request),
  21. }
  22. class SprintSerializer(serializers.ModelSerializer):
  23. links = serializers.SerializerMethodField()
  24. class Meta:
  25. model = Sprint
  26. fields = ('id', 'name', 'description', 'end')
  27. def get_links(self, obj):
  28. request = self.context['request']
  29. return {
  30. 'self': reverse('sprint-detail', kwargs={'pk': obj.pk}, request=request),
  31. }
  32. # task's serializer
  33. class TaskSerializer(serializers.ModelSerializer):
  34. assigned = serializers.SlugRelatedField(slug_field=User.USERNAME_FIELD, required=False)
  35. status_display = serializers.SerializerMethodField()
  36. links = serializers.SerializerMethodField()
  37. class Meta:
  38. model = Task
  39. fields = ('id', 'name', 'description', 'sprint', 'status',
  40. 'status_display', 'order', 'assigned', 'started',
  41. 'due', 'completed',)
  42. def get_status_display(self, obj):
  43. return obj.get_status_display()
  44. def get_links(self, obj):
  45. request = self.context['request']
  46. return {
  47. 'self': reverse('task-detail', kwargs={'pk': obj.pk}, request=request),
  48. }

11和31行,每个序列器的响应体中都有一个新的只读字段links。
为了查找links的值,每个序列器都有get_links方法来创建关联的links

每个资源现在都有一个links字段,是通过get_links方法返回的一个字典,里边只有一个键key,叫做self,它指向的是资源的细节。get_links没有使用django自带的reverse模块,而是内置在django-rest-frameworkreverse的一个修改版本。不像django中的reverse,它将随着路径返回完整的uri(统一资源地址,每个URI代表一种资源),包括主机名(hostname)和协议(protocol)比如protocol="http:"。这里,当我们需要使用标准的ViewSet的时候,reverse需要当前的请求,该请求就是被默认的传入序列器context中的那个,

分配给sprint的一个任务应该指回sprint。如果有一个用户被分配了任务,你也可以通过反转该链接,从一个任务task反向链接到其被分配用户,就像下面这样:

  1. ...
  2. class TaskSerializer(serializers.ModelSerializer):
  3. ...
  4. def get_links(self, obj):
  5. request = self.context['request']
  6. links= {
  7. 'self': reverse('task-detail', kwargs={'pk': obj.pk}, read_only=True,request=request),
  8. 'sprint': None,
  9. 'assigned': None,
  10. }
  11. if obj.sprint_id:
  12. links['sprint'] = reverse('sprint-detail', kwargs={'pk': obj.pk}, request=request)
  13. if obj.assigned:
  14. links['assined'] = reverse('user-datail',
  15. kwargs={User.USERNAME_FIELD: obj.assigned}, request=request)
  16. return links

从一个sprint或者user链接到task需要使用过滤器,会在稍后章节中加入。

serializers.SerializerMethodField()中不用加上参数,比如status_display = serializers.SerializerMethodField()不用再括号里加get_status_display()这个方法

测试API

apiview和模型写好之后,该测试API了,我们先用浏览器来查看,然后再使用python shell

使用浏览的API

django-rest-framework的最重要的特性之一就是有一个可以在浏览器查看Api的可视化工具,当然你也可以在产品系统中废弃这个功能。它是浏览API的一种很好的方法,特别是,它可以让你考虑如何通过响应的资源的链接来操作API。

  1. $ python manage.py runserver

你现在可以访问http://127.0.0.1:8000/api/

测试过程中好像出了点问题,就把django升级到了1.8版本

和你看到的一样,它提供了一个漂亮的可视化工具来让我们看到新创建的api,它展示了当发送一个GET请求到/api/时,返回的响应内容。这里展现的是api顶层可访问的资源。由于api的根目录不需要验证,它下面所有的子资源也是这样不用验证(all of the subresources do);当你点击链接的时候,浏览器就会需要一个用户名和密码来进行HTTP验证,你可以用先前创建的用户名和密码

  1. HTTP 200 OK
  2. Allow: GET, HEAD, OPTIONS
  3. Content-Type: application/json
  4. Vary: Accept
  5. {
  6. "sprints": "http://127.0.0.1:8000/api/sprints/",
  7. "tasks": "http://127.0.0.1:8000/api/tasks/",
  8. "users": "http://127.0.0.1:8000/api/users/"
  9. }

点击sprints会发现是一个空的列表,因为都还没有创建,所以响应的信息是空的。但是页面底部的表单允许我们创建一个新的sprint,用name"something sprint",description"test"还有enddate创建一个新的sprint,日期必须是国际通用格式,YYY-MM-dd,点击提交按钮,会有一个成功的响应以及201的状态码

添加过滤器

前文提及过,django-rest-framework支持使用各种各样的过滤器。我们现在通过给所有的资源添加DefaultsMixin来使用过滤器,board/views.py

  1. ...
  2. from rest_framework import authentication, permissions, viewsets, filters
  3. ...
  4. class DefaultsMixin(object):
  5. authentication_class = (
  6. authentication.BaseAuthentication,
  7. authentication.TokenAuthentication
  8. )
  9. permissions_classes = (
  10. permissions.IsAuthenticated
  11. )
  12. paginate = 25
  13. paginate_by_param = 'page_size'
  14. max_paginage_by = 100
  15. filter_backends=(
  16. filters.DjangoFilterBackend,
  17. filters.SearchFilter,
  18. filters.OrderingFilter,
  19. )

2行,导入filters
16行,定义了一个可以获取的filter_bakends,这样所有的viewset都可以使用

我们通过给每个viewset添加一个search_fields属性来设置SearchFilter,通过添加字段列表来设置OrderingFilter,让过滤器对添加的字段列表进行排序,代码演示如下

  1. class TaskViewSet(DefaultsMixin,viewsets.ModelViewSet):
  2. queryset = Task.objects.all()
  3. serializer_class = TaskSerializer
  4. search_fields=('name','description')
  5. ordering_fields=('name','order','started','due','completed')
  6. class UserViewSet(DefaultsMixin,viewsets.ModelViewSet):
  7. lookup_field = User.USERNAME_FIELD
  8. lookup_url_kwarg = User.USERNAME_FIELD
  9. queryset = User.objects.order_by(User.USERNAME_FIELD)
  10. serializer_class = UserSerializer
  11. search_fields = (User.USERNAME_FIELD)
  12. class SprintViewSet(viewsets.ModelViewSet):
  13. """ api endpoint for listing and creating sprints"""
  14. queryset = Sprint.objects.order_by('end')
  15. serializer_class = SprintSerializer
  16. search_fields=('name')
  17. ordering_fields=('end','name',)

所有的viewset都被添加search_fields字段,来允许在给出的字段列表中搜索
sprints和tasks在api中可排序通过使用ordering_fields,Users在api响应中,总是被username排序

由于现在只有一个task, 搜索foo通过http://localhost:8000/api/tasks/?search=foo,不会有结果,因为没有这个task,搜索已经存在的task会返回结果
为了处理额外的task过滤器,我们可以使用djangoFilterBackend。它要求在TaskViewSet中定义一个filter_class,这个filter_class属性应是django_filters.FilterSet的子类。这个放在一个新的文件中board/forms.py

  1. import django_filters
  2. from .models import Task
  3. class TaskFilter(django_filters.FilterSet):
  4. class Meta:
  5. model= Task
  6. fields=('sprint','status','assigned',)

这是django-filter的最基本的用法, 它基于模型完成了一个过滤器的设置。在TaskViewSet中顶一个的每一个字段都会被转换成客户端用来过滤结果集的查询参数(query parameter),首先,它必须和TaskViewSet关联起来

  1. ...
  2. from .forms import TaskFilter
  3. from .models import Sprint , Task
  4. from .serializers import SprintSerializer, TaskSerializer, UserSerializer
  5. ...
  6. class TaskViewSet(DefaultsMixin,viewsets.ModelViewSet):
  7. queryset = Task.objects.all()
  8. serializer_class = TaskSerializer
  9. #把TaskFilter赋值给TaskViewSet对象的filter_class属性
  10. filter_class = TaskFilter
  11. search_fields=('name','description')
  12. ordering_fields=('name','order','started','due','completed')

这些做好之后,客户端可以过滤sprint,status,assined user,可是对于一个task来说,sprint不是必须的(task模型/序列器中有sprint字段),但是这个过滤器不会允许在没有sprint的情况下搜索task,在我们目前的数据模型中,当前没有分配sprint的task会被当成blacklog积压任务,为了处理这个问题,我们为taskViewSet添加一个新字段 board/forms.py

  1. import django_filters
  2. from .models import Task
  3. class NullFilter(django_filters.BooleanFilter):
  4. def filter(self, qs, value):
  5. if value is not None:
  6. return qs.filter(**{'%s_isnull' % self.name.value})
  7. return qs
  8. class TaskFilter(django_filters.FilterSet):
  9. blacklog = NullFilter(name='sprint')
  10. class Meta:
  11. model= Task
  12. fields=('sprint','status','assigned','backlog')

这样你访问http://localhost:8000/api/tasks/?backlog=True 的时候会返回所有没有分配sprint的task。另一个TaskFilter的问题是,被assign的user是通过主键pk引用的,但是剩下的api使用username作为唯一标识。我们可以通过修改字段(被重要的ModelChoiceField使用的)

  1. import django_filters
  2. from .models import Task
  3. from django.contrib.auth import get_user_model
  4. User = get_user_model()
  5. class NullFilter(django_filters.BooleanFilter):
  6. def filter(self, qs, value):
  7. if value is not None:
  8. return qs.filter(**{'%s_isnull' % self.name: value})
  9. return qs
  10. class TaskFilter(django_filters.FilterSet):
  11. blacklog = NullFilter(name='sprint')
  12. class Meta:
  13. model = Task
  14. fields = ('sprint', 'status', 'assigned', 'backlog')
  15. def __init__(self, **args, **kwargs):
  16. super().__init__(*args, **kwargs)
  17. self.filters['assigned'].extra.update({
  18. 'to_field_name': User.USENAME_FIELD
  19. })

把assigned更新到User.USERNAME_FIELD字段

这样我们查询一个task被分配给了zhangsan用户的时候,就可以用http://localhost:8000/api/tasks/?assigned=zhangsan,而不是用http://localhost:8000/api/tasks/?assigned=1

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注