@hpp157
2016-08-25T14:42:09.000000Z
字数 18811
阅读 3893
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-tastpie1
和django-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-tastpie1
和django-rest-framework
两个框架都还没有提供过滤转换功能,但是django-rest-framework
是可插拔的机制,整合一个django-filter
进入这个框架非常容易
我们以安装一些必要的依赖项来开始我们的项目,当人 要看API数据的时候,Markdown可以帮助把注释转换成页面。
$ sudo pip install django-rest-framework Markdown django-filter
对于任何项目来说,创建数据模型都是第一步要做的事情,让我们花费一点时间来罗列一下创建模型时要考虑的重点:
in progress
和in testing
completed
的状态。 我们通过startproject
命令来初始化项目,并且初始化一个叫做 board
的单应用:
注意:项目project和app的区别,project是app组成的
$ django-admin.py startproject scrum
$ cd scrum
$ python manage.py startapp board
当我们创建新项目的时候,需要去更新默认生成的项目设置文件(scrum/settings.py
)把django-reset-framework
包含到设置里来,同时也去除一些用不到的设置。也由于server端不会存储客户端的state信息,引用的contrib-session
这个模块也可以去掉,这样的话就不能使用django admin
了,所以它也可以去掉了。
INSTALLED_APPS = [
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.staticfiles',
#third party apps
'rest_framework',
'rest_framework.authtoken',
#internal apps
'board',
]
MIDDLEWARE_CLASSES = [
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
]
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.psycopg2',
'NAME': 'scrum',
'USER': 'root',
'PASSWORD': '',
'HOST': 'localhost',
'PORT': '5432',
}
}
我们使用Postgresql
数据库而不再使用django默认自带的sqlite3
,这需要你的机器上安装该数据库以及python使用的postgresql
驱动psycopg2
,最好是百度下怎么安装,下面的命令供参考:
$ sudo pip install psycopg2#安装驱动
$ createdb -E UTF-8 scrum #创建数据库
如果你是苹果系统用户的化,建议你通过
homebrew
来安装,它是苹果系统上的一个包管理工具,安装请参考http://brew.sh/
既然django admin
从设置里去掉了,那么urls文件中关于django admin
的部分也可以删掉了
from django.conf.urls import url,include
from rest_framework.authtoken.views import obtain_auth_token
urlpatterns = [
url(r'^api/token/', obtain_auth_token,name='api-token'),
]
如你看到的,所以已经存在的patterns已经被移走并且替换成了一个单一的url。from rest_framework.authtoken.views import obtain_auth_token
。这是一个用于交换用户名和密码组合的一个接口的视图,我们会在项目的下一步使用include来导入设置。
一些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
中编写模型:
from django.conf import settings
from django.db import models
from django.utils.translation import ugettext_lazy as _
# Create your models here.
class Sprint(models.Model):
name=models.CharField(max_length=100,blank=True,default='')
description=models.TextField(blank=True,default='')
end =models.DateField(unique=True)
def __str__(self):
return self.name or _('sprint ending %s') % self.end
我们也需要一个task模型,通过给出的print
列出任务,task
有个name,可选的description
,关联一个sprint(外键,和其它表联系的),给它们添加一个user assigned(也是外键),包括start,end, due date.
我们也需要用状态信息表示task:
STATUS_CHOICEE
的列表添加到数据模型中(board/models.py)
from django.conf import settings
from django.db import models
from django.utils.translation import ugettext_lazy as _
...
class Task(models.Model):
STATUS_TODO=1
STATUS_IN_PROCESS=2
STATUS_TESTING=3
SATAUS_DONE=4
STATUS_CHOICES=(
(STATUS_TODO,_('Not started')) ,
(STATUS_IN_PROCESS,_('In process')),
(STATUS_TESTING, _('Testing')),
(SATAUS_DONE, _('Done')),
)
name=models.CharField(max_length=100)
description=models.TextField(blank=True,default='')
sprint=models.ForeignKey(Sprint,blank=True,null=True)
status =models.SmallIntegerField(choices=STATUS_CHOICES,default=STATUS_TODO)
order =models.SmallIntegerField(default=0)
assigned=models.ForeignKey(settings.AUTH_USER_MODEL,null=True,blank=True)
satrted=models.DateField(blank=True,null=True)
due=models.DateField(blank=True,null=True)
completed=models.DateField(blank=True,null=True)
def __str__(self):
return self.name
我们已经有了连个项目所需要的模型,但是你可以看到在数据模型中有明显的局限性。 states信息在task模型中,让status无法在sprint模型中的使用,还有就是task workflow 没法自定义,这些局限性在单个项目中是可以接受的,但是很明显想要把这个任务板as a sofrware as a service
把软件看作一项服务(SaaS)是不行的。
有了模型我们在命令行运行makemigrations
和migrate
命令来更新数据库。
$ python manage.py makemigrations board
....
-create model sprint
-create model task
$ python manage.py migrate
.....
接下来我们要创建一个超级用户,自己选择一个用户名,使用的是createsuperuser
命令。将来的例子中要使用这个超级用户
$ python manage.py createsuperuser
(leave blank to use 'username': demo
Email address:demo@example.com
Password:
Password:(again)
Superuser cteated successfully
随着模型的就位[in place]
,现在我们把注意力移回到API
本身上。就url
而言,我们设计出来的api看起来像这样:
/api/
/sprints/
/<id>/
/tasks/
/<id>/
/users/
/username/
很重要的一点,考虑一下客户端如何操纵这些API
。的。客户端会向API
的根目录/api/发出(issue
)一个GET
请求,然后去/api/的下面。到这儿,客户端会看到拿到一份sprints,tasks,和users清单,就像在客户端重新制作了一份清单一样。当客户端深入sprint里面看完它的清单后,就会去和它并列的task看task的清单,再然后是user。资源之间的超链接使得客户端可以操纵API 。
我们已经选择了在URL后面不使用版本号。尽管许多非常受欢迎的API 采用了它,因为我们觉得RESTful api实现版本控制的最佳实践就是为不同类型的内容使用不同版本的API 。但是要是你实在想加的话,就自由的加上吧。
通过ViewSet的来把创建的资源和django-rest-framework
的模型绑定起来是很容易的事。我们应该描述模型model是应该怎样被API序列化和反序列化的。这个由board目录下面的serializers.py
中的序列化器处理的。序列化器是从rest_framework
中引入的。
from rest_framework import serializers
from .models import Sprint
class SprintSerializer(serializers.models):
class Meta:
model =Sprint
fields=('id','name','description','end')
在这个简单的例子中,所有的字段通过API暴露了出来.
让我们在board/views.py
中创建ViewSet。首先会引入框架的viewsets
from rest_framework import viewsets
from .models import Sprint
from .serializers import SprintSerializer
class SprintViewSet(viewsets.ModelViewSet):
queryset = Sprint.object.order_by('end')
serializers_class =SprintSerializer
正如你看到的,ModelViewSet
使用HTTP可以理解的动词提供了CURD所需要的 脚手架(scaffolding)。默认的验证,权限,分页以及过滤如果在自己的视图里没有设置的话,就会受rest_framework
的设置影响。
我们这里的view会给自己明确设置这些,因为剩下的其他view也会使用,因此使用一个mixin
类(board/views.py)来实现.
from rest_framework import viewsets
from .models import Sprint
from .serializers import SprintSerializer
#自己设置的
class DefaultsMixin(object):
authentication_class=(
authentication.BaseAuthentication,
authentication.TokenAuthentication
)
permissions_classes=(
permissions.IsAuthenticated
)
paginate=25
paginate_by_param='page_size'
max_paginage_by =100
class SprintViewSet(viewsets.ModelViewSet):
queryset = Sprint.object.order_by('end')
serializers_class =SprintSerializer
现在,让我们给用户权限添加一些验证,验证使用http基本认证 或者使用基于口令的验证。使用基本验证的话,可以方便地使web浏览器(客户端)浏览API。我们这个例子没有那么多细致的权限,唯一的权限要求就是user
是被验证过的permissions.IsAuthenticated
我们需要task在它们各自的末尾暴露出来,就和上边的sprint末尾一样,我们要以一个serializer开始。然后是ViewSet。首先,在board/serializers.py中创建一个序列器。
from rest_framework import serializers
from .models import Sprint ,Task
#task's serializer
class TaskSerializer(serializers.models):
class Meta:
model=Task
fields=(
'id','name','description','sprint',
'status','order','assigned','started',
'due','completed',
)
#Sprint's serializer
class SprintSerializer(serializers.models):
...
看上去还不错,但是这样写系列器是有点问题的。status
显示的是数字而不是数字代表的状态,数字本身并没有什么语义,单数字我们还是不知具体的状态,所以我们要通过添加一个字段status_display
来显示数字所代表的状态。
from rest_framework import serializers
from .models import Sprint,Task#导入两个模型
class TaskSerializer(serializers.ModelSerializer):
status_display=serializers.SerializerMethodField('get_status_display')
class Meta:
model =Task
fields=('id','name','description','sprint','status',
'status_display','order','assigned','started',
'due','completed',)
def get_status_display(self,obj):
return obj.get_status_display()
#Sprint's serializer
class SprintSerializer(serializers.models):
status_display 是一个要被序列化的只读字段,它返回的是在serializer中get_status_display()方法的返回值。
我们序列器的第二个问题assigned是一个User模型的外键。它显示的是user的主键,但是我们的url结构希望通过用户名来找到user。我们通过使用序列器的SlugRelatedField
方法来修改它。`
from rest_framework import serializers
from .models import Sprint, Task # 导入两个模型
class TaskSerializer(serializers.ModelSerializer):
#添加的slugRelatedField方法
assgined = serializers.SlugRelatedField(slug_field=User.USERNAME_FIELD, required=False)
status_display = serializers.SerializerMethodField('get_status_display')
class Meta:
model = Task
fields = ('id', 'name', 'description', 'sprint', 'status',
'status_display', 'order', 'assigned', 'started',
'due', 'completed',)
def get_status_display(self, obj):
return obj.get_status_display()
最后,我们还要为User模型创建一个序列器, 先想一下,我们的User模型应该可以和另一个模型交换出去(swapped out to another)意图就是使我们的应用尽可能的复用。我们用django
工具get_user_model
来满足这个需求。board/serializers.py
from django.contrib.auth import get_user_model# 引入这个方法
from rest_framework import serializers
from .models import Sprint, Task
User =get_user_model()#把方法赋值给模型
class UserSerializer(serializers.ModelSerializer)
full_name=serializers.CharField(source='get_full_name',read_only=True)
class Meta:
model=User
fields=(
'id',User.USERNAME_FIELD,
'full_name',
'is_active',
)
如果一个自定义的用户模型被使用了,这个序列器就会继承django.contrib.auth.models.CustomUser
对象,获得USERNAME_FIELD
属性,get_full_name
方法以及is_active
这个属性。要注意,既然 gei_full_name
是一个方法,这个字段在序列器中就会被标记为read-only只读。
序列器编写完成后,接着就需要创建task和users的ViewSets了。board/views.py
from django.contrib.auth import get_user_model
from rest_framework import authentication, permissions, viewsets
from .models import Sprint , Task
from .serializers import SprintSerializer, TaskSerializer, UserSerializer
User = get_user_model()
....
class DefaultsMixin(object):
...
class TaskViewSet(DefaultsMixin,viewsets.ModelViewSet):
queryset = Task.objects.all()
serializer_class = TaskSerializer
class UserViewSet(DefaultsMixin,viewsets.ModelViewSet):
lookup_field = User.USERNAME_FIELD
lookup_url_kwarg = User.USERNAME_FIELD
queryset = User.objects.order_by(User.USERNAME_FIELD)
serializer_class = UserSerializer
这连个vieset都和sprintViewSet很相似,但是这儿有一点差异,在Userviewset
中,是从ReadOnlyModelViewSet
中继承来的。正如名字暗示的一样,这样不会暴露通过api创建新的用户以及修改用户的action动作, UserviewSe
t会把通过id查找用户名的方法改为通过设置一个lookup_field
来查找。注意下lookup_url_kwarg
为了一致性,也已经改了。
到这儿,board应用有了基本的数据模型和视图逻辑,但是还没有和URL路由系统
连接起来django-rest-framework
有自己的路由系统扩展来处理ViewSet
,每一个ViewSet
使用给出的URL前缀在路由中注册。这次,我们在board/urls.py
中添加一个新file
from rest_framework.routers import DefaultRouter
from . import views
router=DefaultRouter()
router.register(r'sprints',views.SprintViewSet)
router.register(r'task',views.TaskViewSet)
router.register(r'user',views.UserViewSet)
最后,这个路由需要被included到根目录中的URL配置文件中,scrum/urls.py
from django.conf.urls import patterns, include, url
from rest_framework.authtoken.views import obtain_auth_token
from board.urls import router
urlpatterns = patterns('',
# Examples:
# url(r'^$', 'scrum.views.home', name='home'),
# url(r'^blog/', include('blog.urls')),
url(r'^api/token', obtain_auth_token, name='api-token'),
url(r'api',include(router.urls))
)
现在我们已经有基本模型,视图,和我们的Scrum board应用的URL结构了,现在我们可以创建我们RESTful 应用的剩下部分了(remainder of our RESTful application
)
RESTful
应用最重要的规范就是以超文本作为应用状态的引擎(HATEOAS),(意思就是必须以超文本驱动),在这个约束下,RESTful的客户端应该能够同服务器产生的超媒体响应互动起来.也就是说,客户端应该只注意到服务器上的几个固定端点.通过这些固定的端点,客户端应该通过使用描述性资源消息发现服务器上可用的资源。客户端必须能够解读服务器响应,并且从元数据中分离资源数据,比如资源的链接。(such as links to resources
)
这些如何转换为我们的资源呢(意思是我们怎样使用上边的理论),在这些资源中,服务器能提供什么样的有用链接?首先,每个资源应该知道它自己的URL
。Sprint
提供链接给它有联系的task
和`backlog。分配给用户的任务——提供link给User资源,Users提供一种获取这个用户分配的所有任务的途径。这些都做好之后,当操作api时,api客户端就能够响应大多数的问题。
为了给客户端一个看能够看到这些链接的统一的地方,每个资源将在响应中包含相关链接一个链接部分,开始的最方便的方法就是把资源的链接连回到自己身上。和board/serializers.py
中展示的一样
from django.contrib.auth import get_user_model
from rest_framework import serializers
from rest_framework.reverse import reverse # 新导入的
from .models import Sprint, Task
User = get_user_model()
class UserSerializer(serializers.ModelSerializer):
full_name = serializers.CharField(source='get_full_name', read_only=True)
links = serializers.SerializerMethodField()
class Meta:
model = User
fields = (
'id', User.USERNAME_FIELD,
'full_name',
'is_active',
)
def get_links(self, obj):
request = self.context['requset']
username = obj.get_username()
return {
'self': reverse('user-detail', kwargs={User.USERNAME_FIELD: username}, request=request),
}
class SprintSerializer(serializers.ModelSerializer):
links = serializers.SerializerMethodField()
class Meta:
model = Sprint
fields = ('id', 'name', 'description', 'end')
def get_links(self, obj):
request = self.context['request']
return {
'self': reverse('sprint-detail', kwargs={'pk': obj.pk}, request=request),
}
# task's serializer
class TaskSerializer(serializers.ModelSerializer):
assigned = serializers.SlugRelatedField(slug_field=User.USERNAME_FIELD, required=False)
status_display = serializers.SerializerMethodField()
links = serializers.SerializerMethodField()
class Meta:
model = Task
fields = ('id', 'name', 'description', 'sprint', 'status',
'status_display', 'order', 'assigned', 'started',
'due', 'completed',)
def get_status_display(self, obj):
return obj.get_status_display()
def get_links(self, obj):
request = self.context['request']
return {
'self': reverse('task-detail', kwargs={'pk': obj.pk}, request=request),
}
11和31行,每个序列器的响应体中都有一个新的只读字段links。
为了查找links的值,每个序列器都有get_links方法来创建关联的links
每个资源现在都有一个links字段,是通过get_links
方法返回的一个字典,里边只有一个键key
,叫做self
,它指向的是资源的细节。get_links
没有使用django自带的reverse模块,而是内置在django-rest-framework
的reverse
的一个修改版本。不像django
中的reverse
,它将随着路径返回完整的uri(统一资源地址,每个URI代表一种资源
),包括主机名(hostname
)和协议(protocol
)比如protocol="http:"
。这里,当我们需要使用标准的ViewSet的时候,reverse需要当前的请求,该请求就是被默认的传入序列器context中的那个,
分配给sprint的一个任务应该指回sprint。如果有一个用户被分配了任务,你也可以通过反转该链接,从一个任务task反向链接到其被分配用户,就像下面这样:
...
class TaskSerializer(serializers.ModelSerializer):
...
def get_links(self, obj):
request = self.context['request']
links= {
'self': reverse('task-detail', kwargs={'pk': obj.pk}, read_only=True,request=request),
'sprint': None,
'assigned': None,
}
if obj.sprint_id:
links['sprint'] = reverse('sprint-detail', kwargs={'pk': obj.pk}, request=request)
if obj.assigned:
links['assined'] = reverse('user-datail',
kwargs={User.USERNAME_FIELD: obj.assigned}, request=request)
return links
从一个sprint
或者user
链接到task需要使用过滤器,会在稍后章节中加入。
serializers.SerializerMethodField()中不用加上参数,比如status_display = serializers.SerializerMethodField()不用再括号里加get_status_display()这个方法
api
的view
和模型写好之后,该测试API了,我们先用浏览器来查看,然后再使用python shell
django-rest-framework
的最重要的特性之一就是有一个可以在浏览器查看Api的可视化工具,当然你也可以在产品系统中废弃这个功能。它是浏览API的一种很好的方法,特别是,它可以让你考虑如何通过响应的资源的链接来操作API。
$ 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验证,你可以用先前创建的用户名和密码
HTTP 200 OK
Allow: GET, HEAD, OPTIONS
Content-Type: application/json
Vary: Accept
{
"sprints": "http://127.0.0.1:8000/api/sprints/",
"tasks": "http://127.0.0.1:8000/api/tasks/",
"users": "http://127.0.0.1:8000/api/users/"
}
点击sprints
会发现是一个空的列表,因为都还没有创建,所以响应的信息是空的。但是页面底部的表单允许我们创建一个新的sprint
,用name"something sprint",description"test"
还有enddate
创建一个新的sprint
,日期必须是国际通用格式,YYY-MM-dd
,点击提交按钮,会有一个成功的响应以及201的状态码
前文提及过,django-rest-framework
支持使用各种各样的过滤器。我们现在通过给所有的资源添加DefaultsMixin来使用过滤器,board/views.py
...
from rest_framework import authentication, permissions, viewsets, filters
...
class DefaultsMixin(object):
authentication_class = (
authentication.BaseAuthentication,
authentication.TokenAuthentication
)
permissions_classes = (
permissions.IsAuthenticated
)
paginate = 25
paginate_by_param = 'page_size'
max_paginage_by = 100
filter_backends=(
filters.DjangoFilterBackend,
filters.SearchFilter,
filters.OrderingFilter,
)
2行,导入filters
16行,定义了一个可以获取的filter_bakends,这样所有的viewset都可以使用
我们通过给每个viewset添加一个search_fields属性来设置SearchFilter,通过添加字段列表来设置OrderingFilter,让过滤器对添加的字段列表进行排序,代码演示如下
class TaskViewSet(DefaultsMixin,viewsets.ModelViewSet):
queryset = Task.objects.all()
serializer_class = TaskSerializer
search_fields=('name','description')
ordering_fields=('name','order','started','due','completed')
class UserViewSet(DefaultsMixin,viewsets.ModelViewSet):
lookup_field = User.USERNAME_FIELD
lookup_url_kwarg = User.USERNAME_FIELD
queryset = User.objects.order_by(User.USERNAME_FIELD)
serializer_class = UserSerializer
search_fields = (User.USERNAME_FIELD)
class SprintViewSet(viewsets.ModelViewSet):
""" api endpoint for listing and creating sprints"""
queryset = Sprint.objects.order_by('end')
serializer_class = SprintSerializer
search_fields=('name')
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
import django_filters
from .models import Task
class TaskFilter(django_filters.FilterSet):
class Meta:
model= Task
fields=('sprint','status','assigned',)
这是django-filter的最基本的用法, 它基于模型完成了一个过滤器的设置。在TaskViewSet中顶一个的每一个字段都会被转换成客户端用来过滤结果集的查询参数(query parameter),首先,它必须和TaskViewSet关联起来
...
from .forms import TaskFilter
from .models import Sprint , Task
from .serializers import SprintSerializer, TaskSerializer, UserSerializer
...
class TaskViewSet(DefaultsMixin,viewsets.ModelViewSet):
queryset = Task.objects.all()
serializer_class = TaskSerializer
#把TaskFilter赋值给TaskViewSet对象的filter_class属性
filter_class = TaskFilter
search_fields=('name','description')
ordering_fields=('name','order','started','due','completed')
这些做好之后,客户端可以过滤sprint,status,assined user,可是对于一个task来说,sprint不是必须的(task模型/序列器中有sprint字段),但是这个过滤器不会允许在没有sprint的情况下搜索task,在我们目前的数据模型中,当前没有分配sprint的task会被当成blacklog积压任务,为了处理这个问题,我们为taskViewSet添加一个新字段 board/forms.py
import django_filters
from .models import Task
class NullFilter(django_filters.BooleanFilter):
def filter(self, qs, value):
if value is not None:
return qs.filter(**{'%s_isnull' % self.name.value})
return qs
class TaskFilter(django_filters.FilterSet):
blacklog = NullFilter(name='sprint')
class Meta:
model= Task
fields=('sprint','status','assigned','backlog')
这样你访问http://localhost:8000/api/tasks/?backlog=True 的时候会返回所有没有分配sprint的task。另一个TaskFilter的问题是,被assign的user是通过主键pk引用的,但是剩下的api使用username作为唯一标识。我们可以通过修改字段(被重要的ModelChoiceField使用的)
import django_filters
from .models import Task
from django.contrib.auth import get_user_model
User = get_user_model()
class NullFilter(django_filters.BooleanFilter):
def filter(self, qs, value):
if value is not None:
return qs.filter(**{'%s_isnull' % self.name: value})
return qs
class TaskFilter(django_filters.FilterSet):
blacklog = NullFilter(name='sprint')
class Meta:
model = Task
fields = ('sprint', 'status', 'assigned', 'backlog')
def __init__(self, **args, **kwargs):
super().__init__(*args, **kwargs)
self.filters['assigned'].extra.update({
'to_field_name': User.USENAME_FIELD
})
把assigned更新到User.USERNAME_FIELD字段
这样我们查询一个task被分配给了zhangsan用户的时候,就可以用http://localhost:8000/api/tasks/?assigned=zhangsan,而不是用http://localhost:8000/api/tasks/?assigned=1